Implement pause/resume commands w/ APOLLO_APP_STATUS envvar

This commit is contained in:
Yukino Song
2025-06-05 01:57:41 +08:00
parent 2795e34e16
commit 3e0cbaf2c2
13 changed files with 312 additions and 119 deletions

View File

@@ -594,6 +594,7 @@ namespace config {
false, // notify_pre_releases
false, // legacy_ordering
{}, // prep commands
{}, // state commands
{}, // server commands
};
@@ -1211,6 +1212,7 @@ namespace config {
string_f(vars, "external_ip", nvhttp.external_ip);
list_prep_cmd_f(vars, "global_prep_cmd", config::sunshine.prep_cmds);
list_prep_cmd_f(vars, "global_state_cmd", config::sunshine.state_cmds);
list_server_cmd_f(vars, "server_cmd", config::sunshine.server_cmds);
string_f(vars, "audio_sink", audio.sink);

View File

@@ -282,6 +282,7 @@ namespace config {
bool notify_pre_releases;
bool legacy_ordering;
std::vector<prep_cmd_t> prep_cmds;
std::vector<prep_cmd_t> state_cmds;
std::vector<server_cmd_t> server_cmds;
};

View File

@@ -1285,6 +1285,8 @@ namespace confighttp {
print_req(request);
proc::proc.terminate();
// We may not return from this call
platf::restart();
}
@@ -1304,6 +1306,9 @@ namespace confighttp {
print_req(request);
BOOST_LOG(warning) << "Requested quit from config page!"sv;
proc::proc.terminate();
#ifdef _WIN32
if (GetConsoleWindow() == NULL) {
lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true);

View File

@@ -268,6 +268,9 @@ int main(int argc, char *argv[]) {
logging::log_flush();
lifetime::debug_trap();
};
proc::proc.terminate();
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
shutdown_event->raise(true);

View File

@@ -127,7 +127,7 @@ namespace proc {
}
}
boost::filesystem::path find_working_directory(const std::string &cmd, boost::process::v1::environment &env) {
boost::filesystem::path find_working_directory(const std::string &cmd, const 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);
@@ -375,6 +375,7 @@ namespace proc {
_env["APOLLO_APP_ID"] = _app.id;
_env["APOLLO_APP_NAME"] = _app.name;
_env["APOLLO_APP_UUID"] = _app.uuid;
_env["APOLLO_APP_STATUS"] = "STARTING";
_env["APOLLO_CLIENT_UUID"] = launch_session->unique_id;
_env["APOLLO_CLIENT_NAME"] = launch_session->device_name;
_env["APOLLO_CLIENT_WIDTH"] = std::to_string(render_width);
@@ -457,6 +458,8 @@ namespace proc {
}
}
_env["APOLLO_APP_STATUS"] = "RUNNING";
for (auto &cmd : _app.detached) {
boost::filesystem::path working_dir = _app.working_dir.empty() ?
find_working_directory(cmd, _env) :
@@ -593,16 +596,108 @@ namespace proc {
return 0;
}
void proc_t::pause() {
if (_app.terminate_on_pause) {
terminate();
} else {
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_pausing(proc::proc.get_last_run_app_name());
#endif
void proc_t::resume() {
BOOST_LOG(info) << "Session resuming for app [" << _app_name << "].";
if (!_app.state_cmds.empty()) {
auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable {
_env["APOLLO_APP_STATUS"] = "RESUMING";
std::error_code ec;
auto _state_resume_it = std::begin(cmd_list);
for (; _state_resume_it != std::end(cmd_list); ++_state_resume_it) {
auto &cmd = *_state_resume_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_LOG(info) << "Executing Resume Cmd: ["sv << cmd.do_cmd << "] elevated: " << cmd.elevated;
auto child = platf::run_command(cmd.elevated, true, cmd.do_cmd, working_dir, _env, nullptr, ec, nullptr);
if (ec) {
BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message();
break;
}
child.wait();
auto ret = child.exit_code();
if (ret != 0 && ec != std::errc::permission_denied) {
BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']';
break;
}
}
});
exec_thread.detach();
}
}
void proc_t::pause() {
if (!running()) {
BOOST_LOG(info) << "Session already stopped, do not run pause commands.";
return;
}
if (_app.terminate_on_pause) {
BOOST_LOG(info) << "Terminating app [" << _app_name << "] when all clients are disconnected. Pause commands are skipped.";
terminate();
return;
}
BOOST_LOG(info) << "Session pausing for app [" << _app_name << "].";
if (!_app.state_cmds.empty()) {
auto exec_thread = std::thread([cmd_list = _app.state_cmds, app_working_dir = _app.working_dir, _env = _env]() mutable {
_env["APOLLO_APP_STATUS"] = "PAUSING";
std::error_code ec;
auto _state_pause_it = std::begin(cmd_list);
for (; _state_pause_it != std::end(cmd_list); ++_state_pause_it) {
auto &cmd = *_state_pause_it;
// Skip empty commands
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_LOG(info) << "Executing Pause Cmd: ["sv << cmd.undo_cmd << "] elevated: " << cmd.elevated;
auto child = platf::run_command(cmd.elevated, true, cmd.undo_cmd, working_dir, _env, nullptr, ec, nullptr);
if (ec) {
BOOST_LOG(error) << "Couldn't run ["sv << cmd.undo_cmd << "]: System: "sv << ec.message();
break;
}
child.wait();
auto ret = child.exit_code();
if (ret != 0 && ec != std::errc::permission_denied) {
BOOST_LOG(error) << '[' << cmd.undo_cmd << "] failed with code ["sv << ret << ']';
break;
}
}
});
exec_thread.detach();
}
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
system_tray::update_tray_pausing(proc::proc.get_last_run_app_name());
#endif
}
void proc_t::terminate(bool immediate, bool needs_refresh) {
std::error_code ec;
placebo = false;
@@ -614,6 +709,8 @@ namespace proc {
_process = boost::process::v1::child();
_process_group = boost::process::v1::group();
_env["APOLLO_APP_STATUS"] = "TERMINATING";
for (; _app_prep_it != _app_prep_begin; --_app_prep_it) {
auto &cmd = *(_app_prep_it - 1);
@@ -1195,6 +1292,34 @@ namespace proc {
}
}
// Build the list of pause/resume commands.
std::vector<proc::cmd_t> state_cmds;
bool exclude_global_state_cmds = app_node.value("exclude-global-state-cmd", false);
if (!exclude_global_state_cmds) {
state_cmds.reserve(config::sunshine.state_cmds.size());
for (auto &state_cmd : config::sunshine.state_cmds) {
auto do_cmd = parse_env_val(this_env, state_cmd.do_cmd);
auto undo_cmd = parse_env_val(this_env, state_cmd.undo_cmd);
state_cmds.emplace_back(
std::move(do_cmd),
std::move(undo_cmd),
std::move(state_cmd.elevated)
);
}
}
if (app_node.contains("state-cmd") && app_node["state-cmd"].is_array()) {
for (auto &prep_node : app_node["state-cmd"]) {
std::string do_cmd = parse_env_val(this_env, prep_node.value("do", ""));
std::string undo_cmd = parse_env_val(this_env, prep_node.value("undo", ""));
bool elevated = prep_node.value("elevated", false);
state_cmds.emplace_back(
std::move(do_cmd),
std::move(undo_cmd),
std::move(elevated)
);
}
}
// Build the list of detached commands.
std::vector<std::string> detached;
if (app_node.contains("detached") && app_node["detached"].is_array()) {
@@ -1243,6 +1368,7 @@ namespace proc {
ctx.name = std::move(name);
ctx.prep_cmds = std::move(prep_cmds);
ctx.state_cmds = std::move(state_cmds);
ctx.detached = std::move(detached);
apps.emplace_back(std::move(ctx));

View File

@@ -62,6 +62,7 @@ namespace proc {
*/
struct ctx_t {
std::vector<cmd_t> prep_cmds;
std::vector<cmd_t> state_cmds;
/**
* Some applications, such as Steam, either exit quickly, or keep running indefinitely.
@@ -136,6 +137,7 @@ namespace proc {
std::string get_last_run_app_name();
std::string get_running_app_uuid();
boost::process::v1::environment get_env();
void resume();
void pause();
void terminate(bool immediate = false, bool needs_refresh = true);
@@ -164,7 +166,7 @@ namespace proc {
};
boost::filesystem::path
find_working_directory(const std::string &cmd, boost::process::v1::environment &env);
find_working_directory(const std::string &cmd, const boost::process::v1::environment &env);
/**
* @brief Calculate a stable id based on name and image data

View File

@@ -2120,6 +2120,7 @@ namespace stream {
// If this is the first session, invoke the platform callbacks
if (++running_sessions == 1) {
platf::streaming_will_start();
proc::proc.resume();
}
if (!session.do_cmds.empty()) {

View File

@@ -79,12 +79,15 @@ namespace system_tray {
void tray_restart_cb(struct tray_menu *item) {
BOOST_LOG(info) << "Restarting from system tray"sv;
proc::proc.terminate();
platf::restart();
}
void tray_quit_cb(struct tray_menu *item) {
BOOST_LOG(info) << "Quitting from system tray"sv;
proc::proc.terminate();
#ifdef _WIN32
// If we're running in a service, return a special status to
// tell it to terminate too, otherwise it will just respawn us.