diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 4ba09b88a5a..9efbaa99003 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -584,7 +584,7 @@ namespace confighttp { }); file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4)); - proc::refresh(config::stream.file_apps); + proc::proc.refresh(config::stream.file_apps); output_tree["status"] = true; send_response(response, output_tree); @@ -656,7 +656,7 @@ namespace confighttp { file_tree["apps"] = new_apps; file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4)); - proc::refresh(config::stream.file_apps); + proc::proc.refresh(config::stream.file_apps); output_tree["status"] = true; output_tree["result"] = "application " + std::to_string(index) + " deleted"; diff --git a/src/main.cpp b/src/main.cpp index 04b080c5b88..e93a7e542d8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -276,7 +276,7 @@ int main(int argc, char *argv[]) { SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE); #endif - proc::refresh(config::stream.file_apps); + proc::proc.refresh(config::stream.file_apps); // If any of the following fail, we log an error and continue event though sunshine will not function correctly. // This allows access to the UI to fix configuration problems or view the logs. diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 6b518bf91e9..6311c3a1750 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -753,10 +753,10 @@ namespace nvhttp { } tree.put("root.ServerCodecModeSupport", codec_mode_flags); - auto current_appid = proc::proc.running(); + auto current_appid = proc::proc.get_running_app_id(); tree.put("root.PairStatus", pair_status); - tree.put("root.currentgame", current_appid); - tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); + tree.put("root.currentgame", current_appid.value_or(0)); + tree.put("root.state", current_appid ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); std::ostringstream data; @@ -837,10 +837,7 @@ namespace nvhttp { return; } - auto appid = util::from_view(get_arg(args, "appid")); - - auto current_appid = proc::proc.running(); - if (current_appid > 0) { + if (proc::proc.get_running_app_id()) { tree.put("root.resume", 0); tree.put("root..status_code", 400); tree.put("root..status_message", "An app is already running on this host"); @@ -884,6 +881,7 @@ namespace nvhttp { return; } + auto appid = util::from_view(get_arg(args, "appid")); if (appid > 0) { auto err = proc::proc.execute(appid, launch_session); if (err) { @@ -917,8 +915,7 @@ namespace nvhttp { response->close_connection_after_response = true; }); - auto current_appid = proc::proc.running(); - if (current_appid == 0) { + if (!proc::proc.get_running_app_id()) { tree.put("root.resume", 0); tree.put("root..status_code", 503); tree.put("root..status_message", "No running app to resume"); @@ -1000,12 +997,9 @@ namespace nvhttp { tree.put("root..status_code", 200); rtsp_stream::terminate_sessions(); + proc::proc.terminate(); - if (proc::proc.running() > 0) { - proc::proc.terminate(); - } - - // The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not. + // The config needs to be reverted regardless of whether "proc::proc.terminate()" actually terminated app or not. display_device::revert_configuration(); } diff --git a/src/process.cpp b/src/process.cpp index c009fda3ca1..67ad219bf72 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -41,124 +41,63 @@ #define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png" namespace proc { - using namespace std::literals; - namespace pt = boost::property_tree; - - proc_t proc; - - class deinit_t: public platf::deinit_t { - public: - ~deinit_t() { - proc.terminate(); - } - }; - - std::unique_ptr init() { - return std::make_unique(); - } - - void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) { - if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) { - if (exit_timeout.count() > 0) { - // Request processes in the group to exit gracefully - if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) { - // If the request was successful, wait for a little while for them to exit. - BOOST_LOG(info) << "Successfully requested the app to exit. Waiting up to "sv << exit_timeout.count() << " seconds for it to close."sv; - - // group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop - while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) { - std::this_thread::sleep_for(1s); - } - - if (exit_timeout.count() < 0) { - BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv; - } else { - BOOST_LOG(info) << "All app processes have successfully exited."sv; - } - } else { - BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv; + namespace { + std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) { + int stack = 0; + + --begin; + do { + ++begin; + switch (*begin) { + case '(': + ++stack; + break; + case ')': + --stack; } - } else { - BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv; - } - - // We always call terminate() even if we waited successfully for all processes above. - // This ensures the process group state is consistent with the OS in boost. - std::error_code ec; - group.terminate(ec); - group.detach(); - } - - if (proc.valid()) { - // avoid zombie process - proc.detach(); - } - } + } while (begin != end && stack != 0); - boost::filesystem::path find_working_directory(const std::string &cmd, boost::process::v1::environment &env) { - // Parse the raw command string into parts to get the actual command portion -#ifdef _WIN32 - auto parts = boost::program_options::split_winmain(cmd); -#else - auto parts = boost::program_options::split_unix(cmd); -#endif - if (parts.empty()) { - BOOST_LOG(error) << "Unable to parse command: "sv << cmd; - return boost::filesystem::path(); + if (begin == end) { + throw std::out_of_range("Missing closing bracket \')\'"); + } + return begin; } + } // namespace - BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + // The global :/ + proc_t proc; - // If the target is a URL, don't parse any further here - if (parts.at(0).find("://") != std::string::npos) { - return boost::filesystem::path(); - } + std::variant, int> app_t::start(ctx_t app_context, int app_id, boost::process::v1::environment env, const rtsp_stream::launch_session_t &launch_session) { + // This is a simple trick for using "std::make_unique" with private constructor + class app_constructor_t: public app_t { + using app_t::app_t; + }; - // If the cmd path is not an absolute path, resolve it using our PATH variable - boost::filesystem::path cmd_path(parts.at(0)); - if (!cmd_path.is_absolute()) { - cmd_path = boost::process::v1::search_path(parts.at(0)); - if (cmd_path.empty()) { - BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; - return boost::filesystem::path(); - } + auto app {std::make_unique()}; + if (int return_code {app->execute(std::move(app_context), app_id, std::move(env), launch_session)}; return_code != 0) { + return return_code; } - BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; - - // Now that we have a complete path, we can just use parent_path() - return cmd_path.parent_path(); + return app; } - int proc_t::execute(int app_id, std::shared_ptr launch_session) { - // Ensure starting from a clean slate - terminate(); - - auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { - return app.id == std::to_string(app_id); - }); - - if (iter == _apps.end()) { - BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; - return 404; - } - + int app_t::execute(ctx_t app_context, int app_id, boost::process::v1::environment env, const rtsp_stream::launch_session_t &launch_session) { _app_id = app_id; - _app = *iter; - _app_prep_begin = std::begin(_app.prep_cmds); - _app_prep_it = _app_prep_begin; + _env = std::move(env); + _context = std::move(app_context); + _prep_cmd_it = std::begin(_context.prep_cmds); // Add Stream-specific environment variables _env["SUNSHINE_APP_ID"] = std::to_string(_app_id); - _env["SUNSHINE_APP_NAME"] = _app.name; - _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session->width); - _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session->height); - _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); - _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; - _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); - _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; - _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; - int channelCount = launch_session->surround_info & (65535); + _env["SUNSHINE_APP_NAME"] = _context.name; + _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(launch_session.width); + _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(launch_session.height); + _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session.fps); + _env["SUNSHINE_CLIENT_HDR"] = launch_session.enable_hdr ? "true" : "false"; + _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session.gcmap); + _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session.host_audio ? "true" : "false"; + _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session.enable_sops ? "true" : "false"; + int channelCount = launch_session.surround_info & (65535); switch (channelCount) { case 2: _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; @@ -170,49 +109,51 @@ namespace proc { _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; break; } - _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; + _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session.surround_params; - if (!_app.output.empty() && _app.output != "null"sv) { + if (!_context.output.empty() && _context.output != "null"sv) { #ifdef _WIN32 // fopen() interprets the filename as an ANSI string on Windows, so we must convert it // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. - auto woutput = platf::from_utf8(_app.output); + auto woutput = platf::from_utf8(_context.output); // Use _SH_DENYNO to allow us to open this log file again for writing even if it is // still open from a previous execution. This is required to handle the case of a // detached process executing again while the previous process is still running. - _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); + _output_pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); #else - _pipe.reset(fopen(_app.output.c_str(), "a")); + _output_pipe.reset(fopen(_context.output.c_str(), "a")); #endif } - std::error_code ec; // Executed when returning from function auto fg = util::fail_guard([&]() { terminate(); }); - for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { - auto &cmd = *_app_prep_it; + // Execute the "do" commands + for (; _prep_cmd_it != std::end(_context.prep_cmds); ++_prep_cmd_it) { + auto &cmd = *_prep_cmd_it; // Skip empty commands if (cmd.do_cmd.empty()) { continue; } - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd.do_cmd, _env) : - boost::filesystem::path(_app.working_dir); + boost::filesystem::path working_dir = _context.working_dir.empty() ? + find_working_directory(cmd.do_cmd) : + boost::filesystem::path(_context.working_dir); BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']'; - auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr); + + std::error_code ec; + auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, _output_pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); // We don't want any prep commands failing launch of the desktop. // This is to prevent the issue where users reboot their PC and need to log in with Sunshine. // permission_denied is typically returned when the user impersonation fails, which can happen when user is not signed in yet. - if (!(_app.cmd.empty() && ec == std::errc::permission_denied)) { + if (!(_context.cmd.empty() && ec == std::errc::permission_denied)) { return -1; } } @@ -225,12 +166,16 @@ namespace proc { } } - for (auto &cmd : _app.detached) { - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : - boost::filesystem::path(_app.working_dir); + // Execute the "detached" commands + for (auto &cmd : _context.detached) { + boost::filesystem::path working_dir = _context.working_dir.empty() ? + find_working_directory(cmd) : + boost::filesystem::path(_context.working_dir); BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; - auto child = platf::run_command(_app.elevated, true, cmd, working_dir, _env, _pipe.get(), ec, nullptr); + + std::error_code ec; + auto child = platf::run_command(_context.elevated, true, cmd, working_dir, _env, _output_pipe.get(), ec, nullptr); + if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } else { @@ -238,29 +183,39 @@ namespace proc { } } - if (_app.cmd.empty()) { + // Execute the main command if available + if (_context.cmd.empty()) { BOOST_LOG(info) << "Executing [Desktop]"sv; - placebo = true; + _is_detached = true; } else { - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(_app.cmd, _env) : - boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; - _process = platf::run_command(_app.elevated, true, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_group); + boost::filesystem::path working_dir = _context.working_dir.empty() ? + find_working_directory(_context.cmd) : + boost::filesystem::path(_context.working_dir); + BOOST_LOG(info) << "Executing: ["sv << _context.cmd << "] in ["sv << working_dir << ']'; + + std::error_code ec; + _process = platf::run_command(_context.elevated, true, _context.cmd, working_dir, _env, _output_pipe.get(), ec, &_process_group); + if (ec) { - BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); + BOOST_LOG(warning) << "Couldn't run ["sv << _context.cmd << "]: System: "sv << ec.message(); return -1; } } - _app_launch_time = std::chrono::steady_clock::now(); - + _launch_time = std::chrono::steady_clock::now(); fg.disable(); - return 0; } - int proc_t::running() { + app_t::~app_t() { + try { + terminate(); + } catch (const std::exception &e) { + BOOST_LOG(fatal) << "Exception in `app_t` destructor: " << e.what(); + } + } + + std::optional app_t::get_app_id() { #ifndef _WIN32 // On POSIX OSes, we must periodically wait for our children to avoid // them becoming zombies. This must be synchronized carefully with @@ -271,19 +226,19 @@ namespace proc { }); #endif - if (placebo) { + if (_is_detached) { return _app_id; - } else if (_app.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { + } else if (_context.wait_all && _process_group && platf::process_group_running((std::uintptr_t) _process_group.native_handle())) { // The app is still running if any process in the group is still running return _app_id; } else if (_process.running()) { // The app is still running only if the initial process launched is still running return _app_id; - } else if (_app.auto_detach && _process.native_exit_code() == 0 && - std::chrono::steady_clock::now() - _app_launch_time < 5s) { + } else if (_context.auto_detach && _process.native_exit_code() == 0 && + std::chrono::steady_clock::now() - _launch_time < 5s) { BOOST_LOG(info) << "App exited gracefully within 5 seconds of launch. Treating the app as a detached command."sv; BOOST_LOG(info) << "Adjust this behavior in the Applications tab or apps.json if this is not what you want."sv; - placebo = true; + _is_detached = true; return _app_id; } @@ -293,28 +248,30 @@ namespace proc { terminate(); } - return 0; + return std::nullopt; } - void proc_t::terminate() { - std::error_code ec; - placebo = false; - terminate_process_group(_process, _process_group, _app.exit_timeout); - _process = boost::process::v1::child(); - _process_group = boost::process::v1::group(); + void app_t::terminate() { + // Perform cleanup for the actual process first + terminate_process_group(_process, _process_group, _context.exit_timeout); + _process = {}; + _process_group = {}; - for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { - auto &cmd = *(_app_prep_it - 1); + // Execute undo commands + for (; _prep_cmd_it != std::begin(_context.prep_cmds); --_prep_cmd_it) { + const auto &cmd = *std::prev(_prep_cmd_it); if (cmd.undo_cmd.empty()) { continue; } - boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd.undo_cmd, _env) : - boost::filesystem::path(_app.working_dir); + boost::filesystem::path working_dir = _context.working_dir.empty() ? + find_working_directory(cmd.undo_cmd) : + boost::filesystem::path(_context.working_dir); BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']'; - auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr); + + std::error_code ec; + auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, _output_pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "System: "sv << ec.message(); @@ -328,37 +285,126 @@ namespace proc { } } - _pipe.reset(); + // Close the output pipe + _output_pipe.reset(); + } + + void app_t::terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout) { + if (group.valid() && platf::process_group_running((std::uintptr_t) group.native_handle())) { + if (exit_timeout.count() > 0) { + // Request processes in the group to exit gracefully + if (platf::request_process_group_exit((std::uintptr_t) group.native_handle())) { + // If the request was successful, wait for a little while for them to exit. + BOOST_LOG(info) << "Successfully requested the app to exit. Waiting up to "sv << exit_timeout.count() << " seconds for it to close."sv; + + // group::wait_for() and similar functions are broken and deprecated, so we use a simple polling loop + while (platf::process_group_running((std::uintptr_t) group.native_handle()) && (--exit_timeout).count() >= 0) { + std::this_thread::sleep_for(1s); + } + + if (exit_timeout.count() < 0) { + BOOST_LOG(warning) << "App did not fully exit within the timeout. Terminating the app's remaining processes."sv; + } else { + BOOST_LOG(info) << "All app processes have successfully exited."sv; + } + } else { + BOOST_LOG(info) << "App did not respond to a graceful termination request. Forcefully terminating the app's processes."sv; + } + } else { + BOOST_LOG(info) << "No graceful exit timeout was specified for this app. Forcefully terminating the app's processes."sv; + } + + // We always call terminate() even if we waited successfully for all processes above. + // This ensures the process group state is consistent with the OS in boost. + std::error_code ec; + group.terminate(ec); + group.detach(); + } + + if (proc.valid()) { + // avoid zombie process + proc.detach(); + } + } + + 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, + // we return a deinit_t to main() to handle termination when we're exiting. + // Once we reach this point here, termination must have already happened. + assert(!_started_app); + } + + int proc_t::execute(int app_id, std::shared_ptr launch_session) { + std::lock_guard lock {_mutex}; + + // Ensure starting from a clean slate + terminate(); + if (!launch_session) { + BOOST_LOG(error) << "nullptr given to proc_t::execute!"sv; + return -1; + } + + auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto &app) { + return app.id == std::to_string(app_id); + }); + + if (iter == _apps.end()) { + BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; + return 404; + } + + // Keep the last run app name regardless of whether we manage to start it or not. + _last_run_app_name = iter->name; + + auto app_or_exit_code {app_t::start(*iter, app_id, _env, *launch_session)}; + if (auto *app = std::get_if<0>(&app_or_exit_code); app) { + // Save the started app and return exit code 0. + _started_app = std::move(*app); + return 0; + } + + // Exit code of the failed start + return std::get<1>(app_or_exit_code); + } - bool has_run = _app_id > 0; + std::optional proc_t::get_running_app_id() { + std::lock_guard lock {_mutex}; - // Only show the Stopped notification if we actually have an app to stop - // Since terminate() is always run when a new app has started - if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { + if (_started_app) { + return _started_app->get_app_id(); + } + + return std::nullopt; + } + + void proc_t::terminate() { + std::lock_guard lock {_mutex}; + + if (_started_app) { #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 - system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); + // Only show the Stopped notification if we actually have an app to stop + // Since terminate() is always run when a new app has started + if (!get_last_run_app_name().empty()) { + system_tray::update_tray_stopped(get_last_run_app_name()); + } #endif display_device::revert_configuration(); } - _app_id = -1; + _started_app = nullptr; } - const std::vector &proc_t::get_apps() const { + std::vector proc_t::get_apps() const { + std::lock_guard lock {_mutex}; return _apps; } - std::vector &proc_t::get_apps() { - return _apps; - } + std::string proc_t::get_app_image(int app_id) const { + std::lock_guard lock {_mutex}; - // Gets application image from application list. - // Returns image from assets directory if found there. - // Returns default image if image configuration is not set. - // Returns http content-type header compatible image type. - std::string proc_t::get_app_image(int app_id) { - auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { + auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto &app) { return app.id == std::to_string(app_id); }); auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path; @@ -366,41 +412,70 @@ namespace proc { return validate_app_image_path(app_image_path); } - std::string proc_t::get_last_run_app_name() { - return _app.name; + std::string proc_t::get_last_run_app_name() const { + std::lock_guard lock {_mutex}; + return _last_run_app_name; } - 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, - // we return a deinit_t to main() to handle termination when we're exiting. - // Once we reach this point here, termination must have already happened. - assert(!placebo); - assert(!_process.running()); + void proc_t::refresh(const std::string &file_name) { + std::lock_guard lock {_mutex}; + + auto opt_env_and_apps {parse(file_name)}; + if (opt_env_and_apps) { + auto [env, apps] = *opt_env_and_apps; + + _env = std::move(env); + _apps = std::move(apps); + } } - std::string_view::iterator find_match(std::string_view::iterator begin, std::string_view::iterator end) { - int stack = 0; - - --begin; - do { - ++begin; - switch (*begin) { - case '(': - ++stack; - break; - case ')': - --stack; + std::unique_ptr init() { + class deinit_t: public platf::deinit_t { + public: + ~deinit_t() override { + proc.terminate(); } - } while (begin != end && stack != 0); + }; + + return std::make_unique(); + } - if (begin == end) { - throw std::out_of_range("Missing closing bracket \')\'"); + boost::filesystem::path find_working_directory(const std::string &cmd) { + // Parse the raw command string into parts to get the actual command portion +#ifdef _WIN32 + auto parts = boost::program_options::split_winmain(cmd); +#else + auto parts = boost::program_options::split_unix(cmd); +#endif + if (parts.empty()) { + BOOST_LOG(error) << "Unable to parse command: "sv << cmd; + return {}; + } + + BOOST_LOG(debug) << "Parsed target ["sv << parts.at(0) << "] from command ["sv << cmd << ']'; + + // If the target is a URL, don't parse any further here + if (parts.at(0).find("://") != std::string::npos) { + return {}; } - return begin; + + // If the cmd path is not an absolute path, resolve it using our PATH variable + boost::filesystem::path cmd_path(parts.at(0)); + if (!cmd_path.is_absolute()) { + cmd_path = boost::process::v1::search_path(parts.at(0)); + if (cmd_path.empty()) { + BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv; + return {}; + } + } + + BOOST_LOG(debug) << "Resolved target ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']'; + + // Now that we have a complete path, we can just use parent_path() + return cmd_path.parent_path(); } - std::string parse_env_val(boost::process::v1::native_environment &env, const std::string_view &val_raw) { + std::string parse_env_val(const boost::process::v1::native_environment &env, std::string_view val_raw) { auto pos = std::begin(val_raw); auto dollar = std::find(pos, std::end(val_raw), '$'); @@ -418,19 +493,23 @@ namespace proc { auto var_name = std::string {var_begin, var_end}; #ifdef _WIN32 - // Windows treats environment variable names in a case-insensitive manner, - // so we look for a case-insensitive match here. This is critical for - // correctly appending to PATH on Windows. - auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { - return boost::iequals(e.get_name(), var_name); - }); - if (itr != env.cend()) { - // Use an existing case-insensitive match - var_name = itr->get_name(); + { + // Windows treats environment variable names in a case-insensitive manner, + // so we look for a case-insensitive match here. This is critical for + // correctly appending to PATH on Windows. + auto itr = std::find_if(env.cbegin(), env.cend(), [&](const auto &e) { + return boost::iequals(e.get_name(), var_name); + }); + if (itr != env.cend()) { + // Use an existing case-insensitive match + var_name = itr->get_name(); + } } #endif - ss << env[var_name].to_string(); + if (auto env_it = env.find(var_name); env_it != env.end()) { + ss << env_it->to_string(); + } pos = var_end + 1; next = var_end; @@ -455,7 +534,7 @@ namespace proc { return ss.str(); } - std::string validate_app_image_path(std::string app_image_path) { + std::string validate_app_image_path(const std::string &app_image_path) { if (app_image_path.empty()) { return DEFAULT_APP_IMAGE_PATH; } @@ -491,7 +570,7 @@ namespace proc { return app_image_path; } - std::optional calculate_sha256(const std::string &filename) { + std::optional calculate_sha256(const std::string &file_name) { crypto::md_ctx_t ctx {EVP_MD_CTX_create()}; if (!ctx) { return std::nullopt; @@ -503,7 +582,7 @@ namespace proc { // Read file and update calculated SHA char buf[1024 * 16]; - std::ifstream file(filename, std::ifstream::binary); + std::ifstream file(file_name, std::ifstream::binary); while (file.good()) { file.read(buf, sizeof(buf)); if (!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) { @@ -532,7 +611,7 @@ namespace proc { return result.checksum(); } - std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { + std::tuple calculate_app_id(const std::string &app_name, const std::string &app_image_path, int app_index) { // Generate id by hashing name with image data if present std::vector to_hash; to_hash.push_back(app_name); @@ -553,7 +632,7 @@ namespace proc { ss << s; }); auto input_no_index = ss.str(); - ss << index; + ss << app_index; auto input_with_index = ss.str(); // CRC32 then truncate to signed 32-bit range due to client limitations @@ -563,10 +642,11 @@ namespace proc { return std::make_tuple(id_no_index, id_with_index); } - std::optional parse(const std::string &file_name) { - pt::ptree tree; + std::optional>> parse(const std::string &file_name) { + namespace pt = boost::property_tree; try { + pt::ptree tree; pt::read_json(file_name, tree); auto &apps_node = tree.get_child("apps"s); @@ -579,10 +659,10 @@ namespace proc { } std::set ids; - std::vector apps; + std::vector apps; int i = 0; for (auto &[_, app_node] : apps_node) { - proc::ctx_t ctx; + ctx_t ctx; auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s); auto detached_nodes_opt = app_node.get_child_optional("detached"s); @@ -597,7 +677,7 @@ namespace proc { auto wait_all = app_node.get_optional("wait-all"s); auto exit_timeout = app_node.get_optional("exit-timeout"s); - std::vector prep_cmds; + std::vector prep_cmds; if (!exclude_global_prep.value_or(false)) { prep_cmds.reserve(config::sunshine.prep_cmds.size()); for (auto &prep_cmd : config::sunshine.prep_cmds) { @@ -683,22 +763,11 @@ namespace proc { apps.emplace_back(std::move(ctx)); } - return proc::proc_t { - std::move(this_env), - std::move(apps) - }; + return std::make_tuple(std::move(this_env), std::move(apps)); } catch (std::exception &e) { BOOST_LOG(error) << e.what(); } return std::nullopt; } - - void refresh(const std::string &file_name) { - auto proc_opt = proc::parse(file_name); - - if (proc_opt) { - proc = std::move(*proc_opt); - } - } } // namespace proc diff --git a/src/process.h b/src/process.h index f5a81e900c6..3b4782d976c 100644 --- a/src/process.h +++ b/src/process.h @@ -23,8 +23,7 @@ namespace proc { using file_t = util::safe_ptr_v2; - - typedef config::prep_cmd_t cmd_t; + using cmd_t = config::prep_cmd_t; /** * pre_cmds -- guaranteed to be executed unless any of the commands fail. @@ -67,76 +66,211 @@ namespace proc { std::chrono::seconds exit_timeout; }; - class proc_t { + /** + * @brief App class that contains all of what is necessary for executing and terminating app. + */ + class app_t { public: - KITTY_DEFAULT_CONSTR_MOVE_THROW(proc_t) + /** + * @brief Start the app based on the provided context, env and session settings. + * @param app_context Data required for starting the app. + * @param app_id Requested app id for starting the app. + * @param env Environment for the app processes. + * @param launch_session Additional parameters provided during stream start/resume. + * @returns An instance of the `app_t` class or a "return code" in case the app could not be started. + */ + static std::variant, int> start(ctx_t app_context, int app_id, boost::process::v1::environment env, const rtsp_stream::launch_session_t &launch_session); - proc_t( - boost::process::v1::environment &&env, - std::vector &&apps - ): - _app_id(0), - _env(std::move(env)), - _apps(std::move(apps)) { - } + /** + * @brief Destructor that automatically cleans up after the app, terminating processes if needed. + */ + virtual ~app_t(); - int execute(int app_id, std::shared_ptr launch_session); + /** + * @brief Get id of the app if it's still running. + * + * It is not necessary for any kind of process to be actually running, for the + * app to be considered "running". App can be of a detached type - without any process. + * + * @note This method pools for the process state every time it is called and may perform early cleanup. + * @return App id that was provided during start if the app is still running, empty optional otherwise + */ + std::optional get_app_id(); + private: /** - * @return `_app_id` if a process is running, otherwise returns `0` + * @brief Private constructor to enforce the usage of `start` method. */ - int running(); + explicit app_t() = default; - ~proc_t(); + /** + * @brief Execute the app based on the provided context, env and session settings. + * @param app_context Data required for starting the app. + * @param app_id Requested app id for starting the app. + * @param env Environment for the app processes. + * @param launch_session Additional parameters provided during stream start/resume. + * @returns "Return code" - `0` on success and other values on failure. + */ + int execute(ctx_t app_context, int app_id, boost::process::v1::environment env, const rtsp_stream::launch_session_t &launch_session); - const std::vector &get_apps() const; - std::vector &get_apps(); - std::string get_app_image(int app_id); - std::string get_last_run_app_name(); + /** + * @brief Terminate the currently running process (if any) and run undo commands. + */ void terminate(); - private: - int _app_id; - - boost::process::v1::environment _env; - std::vector _apps; - ctx_t _app; - std::chrono::steady_clock::time_point _app_launch_time; + /** + * @brief Terminate all child processes in a process group. + * @param proc The child process itself. + * @param group The group of all children in the process tree. + * @param exit_timeout The timeout to wait for the process group to gracefully exit. + */ + static void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout); - // If no command associated with _app_id, yet it's still running - bool placebo {}; + int _app_id {0}; ///< A hash representing the app. + bool _is_detached {false}; ///< App has no cmd, or it has successfully terminated withing 5s (configurable) as is now considered as detached. - boost::process::v1::child _process; - boost::process::v1::group _process_group; + ctx_t _context; ///< Parsed app context from settings. + std::vector::const_iterator _prep_cmd_it; ///< Prep cmd iterator used as a starting point when terminating app if execution failed and not all undo commands need to be executed. + std::chrono::steady_clock::time_point _launch_time; ///< Time at which the app was executed. - file_t _pipe; - std::vector::const_iterator _app_prep_it; - std::vector::const_iterator _app_prep_begin; + boost::process::v1::environment _env; ///< Environment used during execution and termination. + boost::process::v1::child _process; ///< Process handle. + boost::process::v1::group _process_group; ///< Process group handle. + file_t _output_pipe; ///< Pipe to an optional output file. }; /** - * @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. + * @brief Thread-safe wrapper-like class around the app list for handling the execution and termination of the app. */ - std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index); + class proc_t { + public: + /** + * @brief Destructor that may assert in debug mode if the app was not terminated at this point. + */ + virtual ~proc_t(); - std::string validate_app_image_path(std::string app_image_path); - void refresh(const std::string &file_name); - std::optional parse(const std::string &file_name); + /** + * @brief Execute the app based on the app id and session settings. + * @param app_id Requested app id for starting the app. + * @param launch_session Additional parameters provided during stream start/resume. + * @returns "Return code" - `0` on success and other values on failure. + * @note The apps list for `app_id` is populated by calling `refresh`. + */ + int execute(int app_id, std::shared_ptr launch_session); + + /** + * @brief Get id of the app if it's still running. + * @return App id that was provided during execution if the app is still running, empty optional otherwise + */ + std::optional get_running_app_id(); + + /** + * @brief Terminate the currently running process. + */ + void terminate(); + + /** + * @brief Get the list of applications. + * @return A list of applications. + */ + std::vector get_apps() const; + + /** + * @brief Get the image path of the application with the given app_id. + * + * - returns image from assets directory if found there. + * - returns default image if image configuration is not set. + * - returns http content-type header compatible image type. + * + * @param app_id The id of the application. + * @return The image path of the application. + */ + std::string get_app_image(int app_id) const; + + /** + * @brief Get the name of the last run application (not necessarily successfully started). + * @return The name of the last run application. + */ + std::string get_last_run_app_name() const; + + /** + * @brief Refresh the app list and env to be used for the next execution of the app. + * @param file_name File to be parsed for the app list. + */ + void refresh(const std::string &file_name); + + private: + mutable std::recursive_mutex _mutex; ///< Mutex to sync access from multiple threads. + std::unique_ptr _started_app; ///< Successfully started app that may or may no longer be running. + boost::process::v1::environment _env; ///< Environment to be used for a new app execution. + std::vector _apps; ///< App contexts for starting new apps. + std::string _last_run_app_name; ///< Name of the last executed application. + }; + + /// The legendary global of the proc_t + extern proc_t proc; /** - * @brief Initialize proc functions - * @return Unique pointer to `deinit_t` to manage cleanup + * @brief Initialize proc functions. + * @return Unique pointer to `deinit_t` to manage cleanup. */ std::unique_ptr init(); /** - * @brief Terminates all child processes in a process group. - * @param proc The child process itself. - * @param group The group of all children in the process tree. - * @param exit_timeout The timeout to wait for the process group to gracefully exit. + * @brief Get the working directory for the file path. + * @param file_path File path to be parsed. + * @return Working directory. */ - void terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout); + boost::filesystem::path find_working_directory(const std::string &file_path); - extern proc_t proc; + /** + * @brief Parse the string and replace any "$(...)" patterns with a value from env. + * @param env Environment to be used as a source for replacement. + * @param val_raw Raw string to be parsed. + * @returns Strings with replacements made (if any) with values from env. + * @warning This function throws if the `val_raw` is ill-formed. + */ + std::string parse_env_val(const boost::process::v1::native_environment &env, std::string_view val_raw); + + /** + * @brief Validate the image path. + * @param app_image_path File path to validate. + * + * Requirements: + * - images must be of `.png` file ending + * - image file must exist (can be relative to the `assets` directory). + * + * @returns Validated image path on success, default image path on failure. + */ + std::string validate_app_image_path(const std::string &app_image_path); + + /** + * @brief Calculate SHA256 from the file contents. + * @param file_name File to be used for calculation. + * @returns Calculated hash string or empty optional if hash could not be calculated. + */ + std::optional calculate_sha256(const std::string &file_name); + + /** + * @brief Calculate CRC32 for the input string. + * @param input String to calculate hash for. + * @returns Hash for the input string. + */ + uint32_t calculate_crc32(const std::string &input); + + /** + * @brief Calculate a stable id based on name and image data (and an app index if required). + * @param app_name App name. + * @param app_image_path App image path. + * @param app_index App index in the app list. + * @return Tuple of id calculated without index (for use if no collision) and one with. + */ + std::tuple calculate_app_id(const std::string &app_name, const std::string &app_image_path, int app_index); + + /** + * @brief Parse the app list file. + * @param file_name The app list file location. + * @returns A tuple of parsed apps list and the environment used for parsing, empty optional if parsing has failed. + */ + std::optional>> parse(const std::string &file_name); } // namespace proc diff --git a/src/stream.cpp b/src/stream.cpp index b42bd3049f5..5d928dd196a 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -1093,7 +1093,7 @@ namespace stream { } // Don't break until any pending sessions either expire or connect - if (proc::proc.running() == 0 && !has_session_awaiting_peer) { + if (!proc::proc.get_running_app_id() && !has_session_awaiting_peer) { BOOST_LOG(info) << "Process terminated"sv; break; } @@ -1883,7 +1883,7 @@ namespace stream { // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { bool revert_display_config {config::video.dd.config_revert_on_disconnect}; - if (proc::proc.running()) { + if (proc::proc.get_running_app_id()) { #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); #endif