diff --git a/src/config.cpp b/src/config.cpp index 20175da3..2edcc115 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -455,6 +455,7 @@ namespace config { platf::appdata().string() + "/sunshine.log", // log file false, // notify_pre_releases {}, // prep commands + {}, // server commands }; bool @@ -846,6 +847,33 @@ namespace config { } } + void + list_server_cmd_f(std::unordered_map &vars, const std::string &name, std::vector &input) { + std::string string; + string_f(vars, name, string); + + std::stringstream jsonStream; + + // check if string is empty, i.e. when the value doesn't exist in the config file + if (string.empty()) { + return; + } + + // We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it. + jsonStream << "{\"server_cmd\":" << string << "}"; + + boost::property_tree::ptree jsonTree; + boost::property_tree::read_json(jsonStream, jsonTree); + + for (auto &[_, prep_cmd] : jsonTree.get_child("server_cmd"s)) { + auto cmd_name = prep_cmd.get_optional("name"s); + auto cmd_val = prep_cmd.get_optional("cmd"s); + auto elevated = prep_cmd.get_optional("elevated"s); + + input.emplace_back(cmd_name.value_or(""), cmd_val.value_or(""), elevated.value_or(false)); + } + } + void list_int_f(std::unordered_map &vars, const std::string &name, std::vector &input) { std::vector list; @@ -1037,6 +1065,7 @@ namespace config { string_f(vars, "external_ip", nvhttp.external_ip); list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds); + list_server_cmd_f(vars, "server_cmd", config::sunshine.server_cmds); string_f(vars, "audio_sink", audio.sink); string_f(vars, "virtual_sink", audio.virtual_sink); @@ -1283,7 +1312,7 @@ namespace config { // so that service instance will do the work instead. if (!config_loaded && !shortcut_launch) { - BOOST_LOG(fatal) << "To relaunch Sunshine successfully, use the shortcut in the Start Menu. Do not run Sunshine.exe manually."sv; + BOOST_LOG(fatal) << "To relaunch Apollo successfully, use the shortcut in the Start Menu. Do not run sunshine.exe manually."sv; std::this_thread::sleep_for(10s); #else if (!config_loaded) { diff --git a/src/config.h b/src/config.h index 98563296..b6d7a687 100644 --- a/src/config.h +++ b/src/config.h @@ -163,6 +163,14 @@ namespace config { std::string undo_cmd; bool elevated; }; + + struct server_cmd_t { + server_cmd_t(std::string &&cmd_name, std::string &&cmd_val, bool &&elevated): + cmd_name(std::move(cmd_name)), cmd_val(std::move(cmd_val)), elevated(std::move(elevated)) {} + std::string cmd_name; + std::string cmd_val; + bool elevated; + }; struct sunshine_t { bool hide_tray_controls; std::string locale; @@ -188,6 +196,7 @@ namespace config { std::string log_file; bool notify_pre_releases; std::vector prep_cmds; + std::vector server_cmds; }; extern video_t video; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 74d918f5..6afb4f2a 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -755,15 +755,26 @@ namespace nvhttp { tree.put("root.ExternalPort", net::map_port(PORT_HTTP)); tree.put("root.MaxLumaPixelsHEVC", video::active_hevc_mode > 1 ? "1869449984" : "0"); - #ifdef _WIN32 - tree.put("root.VirtualDisplayCapable", true); - tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); - #endif - // Only include the MAC address for requests sent from paired clients over HTTPS. // For HTTP requests, use a placeholder MAC address that Moonlight knows to ignore. if constexpr (std::is_same_v) { tree.put("root.mac", platf::get_mac_address(net::addr_to_normalized_string(local_endpoint.address()))); + + pt::ptree& root_node = tree.get_child("root"); + + if (config::sunshine.server_cmds.size() > 0) { + // Broadcast server_cmds + for (const auto& cmd : config::sunshine.server_cmds) { + pt::ptree cmd_node; + cmd_node.put_value(cmd.cmd_name); + root_node.push_back(std::make_pair("ServerCommand", cmd_node)); + } + } + + #ifdef _WIN32 + tree.put("root.VirtualDisplayCapable", true); + tree.put("root.VirtualDisplayDriverReady", proc::vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK); + #endif } else { tree.put("root.mac", "00:00:00:00:00:00"); diff --git a/src/process.cpp b/src/process.cpp index 27e93d74..3d076fc2 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -554,6 +554,11 @@ namespace proc { return _app.name; } + boost::process::environment + proc_t::get_env() { + return _env; + } + proc_t::~proc_t() { // It's not safe to call terminate() here because our proc_t is a static variable // that may be destroyed after the Boost loggers have been destroyed. Instead, diff --git a/src/process.h b/src/process.h index 377e07dd..80a11cb9 100644 --- a/src/process.h +++ b/src/process.h @@ -83,6 +83,7 @@ namespace proc { bool virtual_display; bool initial_hdr; + proc_t( boost::process::environment &&env, std::vector &&apps): @@ -109,15 +110,17 @@ namespace proc { get_app_image(int app_id); std::string get_last_run_app_name(); + boost::process::environment + get_env(); void terminate(); private: int _app_id; + boost::process::environment _env; std::shared_ptr _launch_session; - boost::process::environment _env; std::vector _apps; ctx_t _app; std::chrono::steady_clock::time_point _app_launch_time; @@ -133,6 +136,9 @@ namespace proc { std::vector::const_iterator _app_prep_begin; }; + boost::filesystem::path + find_working_directory(const std::string &cmd, boost::process::environment &env); + /** * @brief Calculate a stable id based on name and image data * @return Tuple of id calculated without index (for use if no collision) and one with. diff --git a/src/stream.cpp b/src/stream.cpp index 0f6b9463..d55cca66 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -46,6 +46,9 @@ extern "C" { #define IDX_RUMBLE_TRIGGER_DATA 12 #define IDX_SET_MOTION_EVENT 13 #define IDX_SET_RGB_LED 14 +#define IDX_EXEC_SERVER_CMD 15 +#define IDX_SET_CLIPBOARD 16 +#define IDX_FILE_TRANSFER_NONCE_REQUEST 17 static const short packetTypes[] = { 0x0305, // Start A @@ -63,6 +66,9 @@ static const short packetTypes[] = { 0x5500, // Rumble triggers (Sunshine protocol extension) 0x5501, // Set motion event (Sunshine protocol extension) 0x5502, // Set RGB LED (Sunshine protocol extension) + 0x3000, // Execute Server Command (Apollo protocol extension) + 0x3001, // Set Clipboard (Apollo protocol extension) + 0x3002, // File transfer nonce request (Apollo protocol extension) }; namespace asio = boost::asio; @@ -989,6 +995,37 @@ namespace stream { input::passthrough(session->input, std::move(plaintext)); }); + server->map(packetTypes[IDX_EXEC_SERVER_CMD], [server](session_t *session, const std::string_view &payload) { + BOOST_LOG(debug) << "type [IDX_EXEC_SERVER_CMD]: "sv; + uint8_t cmdIndex = *(uint8_t*)payload.data(); + + if (cmdIndex < config::sunshine.server_cmds.size()) { + const auto& cmd = config::sunshine.server_cmds[cmdIndex]; + BOOST_LOG(info) << "Executing server command: " << cmd.cmd_name; + + std::error_code ec; + auto env = proc::proc.get_env(); + boost::filesystem::path working_dir = proc::find_working_directory(cmd.cmd_val, env); + auto child = platf::run_command(cmd.elevated, true, cmd.cmd_val, working_dir, {}, nullptr, ec, nullptr); + + if (ec) { + BOOST_LOG(error) << "Failed to execute server command: " << ec.message(); + } else { + child.detach(); + } + } else { + BOOST_LOG(error) << "Invalid server command index: " << (int)cmdIndex; + } + }); + + server->map(packetTypes[IDX_SET_CLIPBOARD], [server](session_t *session, const std::string_view &payload) { + BOOST_LOG(info) << "type [IDX_SET_CLIPBOARD]: "sv << payload << " size: " << payload.size(); + }); + + server->map(packetTypes[IDX_FILE_TRANSFER_NONCE_REQUEST], [server](session_t *session, const std::string_view &payload) { + BOOST_LOG(info) << "type [IDX_FILE_TRANSFER_NONCE_REQUEST]: "sv << payload << " size: " << payload.size(); + }); + server->map(packetTypes[IDX_ENCRYPTED], [server](session_t *session, const std::string_view &payload) { BOOST_LOG(verbose) << "type [IDX_ENCRYPTED]"sv; diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 4077a52c..5050fc01 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -38,6 +38,7 @@ v-if="currentTab === 'general'" :config="config" :global-prep-cmd="global_prep_cmd" + :server-cmd="server_cmd" :platform="platform"> @@ -134,6 +135,7 @@ currentTab: "general", vdisplayStatus: "1", global_prep_cmd: [], + server_cmd: [], tabs: [ // TODO: Move the options to each Component instead, encapsulate. { id: "general", @@ -143,6 +145,7 @@ "sunshine_name": "", "min_log_level": 2, "global_prep_cmd": "[]", + "server_cmd": "[]", "notify_pre_releases": "disabled", }, }, @@ -328,7 +331,9 @@ }); this.config.global_prep_cmd = this.config.global_prep_cmd || []; + this.config.server_cmd = this.config.server_cmd || []; this.global_prep_cmd = JSON.parse(this.config.global_prep_cmd); + this.server_cmd = JSON.parse(this.config.server_cmd); }); }, methods: { @@ -336,7 +341,8 @@ this.$forceUpdate() }, serialize() { - this.config.global_prep_cmd = JSON.stringify(this.global_prep_cmd); + this.config.global_prep_cmd = JSON.stringify(this.global_prep_cmd.filter(cmd => cmd.do || cmd.undo)); + this.config.server_cmd = JSON.stringify(this.server_cmd.filter(cmd => cmd.name && cmd.cmd)); }, save() { this.saved = false; diff --git a/src_assets/common/assets/web/configs/tabs/General.vue b/src_assets/common/assets/web/configs/tabs/General.vue index 428c689e..df9e9373 100644 --- a/src_assets/common/assets/web/configs/tabs/General.vue +++ b/src_assets/common/assets/web/configs/tabs/General.vue @@ -4,26 +4,35 @@ import { ref } from 'vue' const props = defineProps({ platform: String, config: Object, - globalPrepCmd: Array + globalPrepCmd: Array, + serverCmd: Array }) const config = ref(props.config) const globalPrepCmd = ref(props.globalPrepCmd) +const serverCmd = ref(props.serverCmd) -function addCmd() { - let template = { - do: "", - undo: "", - }; - - if (props.platform === 'windows') { - template = { ...template, elevated: false }; - } - globalPrepCmd.value.push(template); +const prepCmdTemplate = { + do: "", + undo: "", } -function removeCmd(index) { - globalPrepCmd.value.splice(index,1) +const serverCmdTemplate = { + name: "", + cmd: "" +} + +function addCmd(cmdArr, template) { + const _tpl = Object.assign({}, template); + + if (props.platform === 'windows') { + _tpl.elevated = false; + } + cmdArr.push(_tpl); +} + +function removeCmd(cmdArr, index) { + cmdArr.splice(index,1) } @@ -114,17 +123,63 @@ function removeCmd(index) { - - - + + + +
+ +
{{ $t('config.server_cmd_desc') }}
+ + + + + + + + + + + + + + + + + +
{{ $t('_common.cmd_name') }} {{ $t('_common.cmd_val') }} + {{ $t('_common.run_as') }} +
+ + + + +
+ + +
+
+ + +
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 297f0fe0..efc7b7fa 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -5,6 +5,8 @@ "autodetect": "Autodetect (recommended)", "beta": "(beta)", "cancel": "Cancel", + "cmd_name": "Command Name", + "cmd_val": "Command Value", "disabled": "Disabled", "disabled_def": "Disabled (default)", "dismiss": "Dismiss", @@ -308,6 +310,8 @@ "qsv_slow_hevc": "Allow Slow HEVC Encoding", "qsv_slow_hevc_desc": "This can enable HEVC encoding on older Intel GPUs, at the cost of higher GPU usage and worse performance.", "restart_note": "Apollo is restarting to apply changes.", + "server_cmd": "Server Commands", + "server_cmd_desc": "Configure a list of commands to be executed when called from client during streaming.", "sunshine_name": "Apollo Name", "sunshine_name_desc": "The name displayed by Moonlight. If not specified, the PC's hostname is used", "sw_preset": "SW Presets", diff --git a/src_assets/common/assets/web/public/assets/locale/zh.json b/src_assets/common/assets/web/public/assets/locale/zh.json index 02d6a6b9..556756b9 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -5,6 +5,8 @@ "autodetect": "自动检测 (推荐)", "beta": "(测试版)", "cancel": "取消", + "cmd_name": "命令名称", + "cmd_val": "命令值", "disabled": "禁用", "disabled_def": "禁用(默认)", "dismiss": "关闭", @@ -308,6 +310,8 @@ "res_fps_desc": "由 Apollo 通告的显示模式。 某些版本的 Moonlight,如 Moonlight-nx (Switch),依靠这些清单来确保支持所请求的分辨率和 fps。 此设置不会改变屏幕串流送至 Moonlight 的方式。", "resolutions": "通告分辨率", "restart_note": "正在重启 Apollo 以应用更改。", + "server_cmd": "服务端命令", + "server_cmd_desc": "配置一个命令列表,当串流时从客户端调用。", "sunshine_name": "Apollo 主机名称", "sunshine_name_desc": "在 Moonlight 中显示的名称。如果未指定,则使用 PC 的主机名", "sw_preset": "软件编码预设",