mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-01-29 09:32:39 +00:00
Implement graceful termination and group-based app tracking
This commit is contained in:
parent
d05a67166e
commit
593e170da8
@ -550,6 +550,8 @@ Application List
|
||||
- ``name`` - The name of the application/game
|
||||
- ``output`` - The file where the output of the command is stored
|
||||
- ``auto-detach`` - Specifies whether the app should be treated as detached if it exits quickly
|
||||
- ``wait-all`` - Specifies whether to wait for all processes to terminate rather than just the initial process
|
||||
- ``exit-timeout`` - Specifies how long to wait in seconds for the process to gracefully exit (default: 5 seconds)
|
||||
- ``prep-cmd`` - A list of commands to be run before/after the application
|
||||
|
||||
- If any of the prep-commands fail, starting the application is aborted
|
||||
|
@ -618,6 +618,22 @@ namespace platf {
|
||||
void
|
||||
open_url(const std::string &url);
|
||||
|
||||
/**
|
||||
* @brief Attempt to gracefully terminate a process group.
|
||||
* @param native_handle The native handle of the process group.
|
||||
* @return true if termination was successfully requested.
|
||||
*/
|
||||
bool
|
||||
request_process_group_exit(std::uintptr_t native_handle);
|
||||
|
||||
/**
|
||||
* @brief Checks if a process group still has running children.
|
||||
* @param native_handle The native handle of the process group.
|
||||
* @return true if processes are still running.
|
||||
*/
|
||||
bool
|
||||
process_group_running(std::uintptr_t native_handle);
|
||||
|
||||
input_t
|
||||
input();
|
||||
void
|
||||
|
@ -249,6 +249,33 @@ namespace platf {
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attempt to gracefully terminate a process group.
|
||||
* @param native_handle The process group ID.
|
||||
* @return true if termination was successfully requested.
|
||||
*/
|
||||
bool
|
||||
request_process_group_exit(std::uintptr_t native_handle) {
|
||||
if (kill(-((pid_t) native_handle), SIGTERM) == 0 || errno == ESRCH) {
|
||||
BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if a process group still has running children.
|
||||
* @param native_handle The process group ID.
|
||||
* @return true if processes are still running.
|
||||
*/
|
||||
bool
|
||||
process_group_running(std::uintptr_t native_handle) {
|
||||
return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0;
|
||||
}
|
||||
|
||||
struct sockaddr_in
|
||||
to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {
|
||||
struct sockaddr_in saddr_v4 = {};
|
||||
|
@ -252,6 +252,33 @@ namespace platf {
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attempt to gracefully terminate a process group.
|
||||
* @param native_handle The process group ID.
|
||||
* @return true if termination was successfully requested.
|
||||
*/
|
||||
bool
|
||||
request_process_group_exit(std::uintptr_t native_handle) {
|
||||
if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) {
|
||||
BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if a process group still has running children.
|
||||
* @param native_handle The process group ID.
|
||||
* @return true if processes are still running.
|
||||
*/
|
||||
bool
|
||||
process_group_running(std::uintptr_t native_handle) {
|
||||
return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0;
|
||||
}
|
||||
|
||||
struct sockaddr_in
|
||||
to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {
|
||||
struct sockaddr_in saddr_v4 = {};
|
||||
|
@ -6,6 +6,7 @@
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@ -904,6 +905,106 @@ namespace platf {
|
||||
lifetime::exit_sunshine(0, true);
|
||||
}
|
||||
|
||||
struct enum_wnd_context_t {
|
||||
std::set<DWORD> process_ids;
|
||||
bool requested_exit;
|
||||
};
|
||||
|
||||
static BOOL CALLBACK
|
||||
prgrp_enum_windows(HWND hwnd, LPARAM lParam) {
|
||||
auto enum_ctx = (enum_wnd_context_t *) lParam;
|
||||
|
||||
// Find the owner PID of this window
|
||||
DWORD wnd_process_id;
|
||||
if (!GetWindowThreadProcessId(hwnd, &wnd_process_id)) {
|
||||
// Continue enumeration
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Check if this window is owned by a process we want to terminate
|
||||
if (enum_ctx->process_ids.find(wnd_process_id) != enum_ctx->process_ids.end()) {
|
||||
// Send an async WM_CLOSE message to this window
|
||||
if (SendNotifyMessageW(hwnd, WM_CLOSE, 0, 0)) {
|
||||
BOOST_LOG(debug) << "Sent WM_CLOSE to PID: "sv << wnd_process_id;
|
||||
enum_ctx->requested_exit = true;
|
||||
}
|
||||
else {
|
||||
auto error = GetLastError();
|
||||
BOOST_LOG(warning) << "Failed to send WM_CLOSE to PID ["sv << wnd_process_id << "]: " << error;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue enumeration
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Attempt to gracefully terminate a process group.
|
||||
* @param native_handle The job object handle.
|
||||
* @return true if termination was successfully requested.
|
||||
*/
|
||||
bool
|
||||
request_process_group_exit(std::uintptr_t native_handle) {
|
||||
auto job_handle = (HANDLE) native_handle;
|
||||
|
||||
// Get list of all processes in our job object
|
||||
bool success;
|
||||
DWORD required_length = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST);
|
||||
auto process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length);
|
||||
auto fg = util::fail_guard([&process_id_list]() {
|
||||
free(process_id_list);
|
||||
});
|
||||
while (!(success = QueryInformationJobObject(job_handle, JobObjectBasicProcessIdList,
|
||||
process_id_list, required_length, &required_length)) &&
|
||||
GetLastError() == ERROR_MORE_DATA) {
|
||||
free(process_id_list);
|
||||
process_id_list = (PJOBOBJECT_BASIC_PROCESS_ID_LIST) calloc(1, required_length);
|
||||
if (!process_id_list) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(warning) << "Failed to enumerate processes in group: "sv << err;
|
||||
return false;
|
||||
}
|
||||
else if (process_id_list->NumberOfProcessIdsInList == 0) {
|
||||
// If all processes are already dead, treat it as a success
|
||||
return true;
|
||||
}
|
||||
|
||||
enum_wnd_context_t enum_ctx = {};
|
||||
enum_ctx.requested_exit = false;
|
||||
for (DWORD i = 0; i < process_id_list->NumberOfProcessIdsInList; i++) {
|
||||
enum_ctx.process_ids.emplace(process_id_list->ProcessIdList[i]);
|
||||
}
|
||||
|
||||
// Enumerate all windows belonging to processes in the list
|
||||
EnumWindows(prgrp_enum_windows, (LPARAM) &enum_ctx);
|
||||
|
||||
// Return success if we told at least one window to close
|
||||
return enum_ctx.requested_exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks if a process group still has running children.
|
||||
* @param native_handle The job object handle.
|
||||
* @return true if processes are still running.
|
||||
*/
|
||||
bool
|
||||
process_group_running(std::uintptr_t native_handle) {
|
||||
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting_info;
|
||||
|
||||
if (!QueryInformationJobObject((HANDLE) native_handle, JobObjectBasicAccountingInformation, &accounting_info, sizeof(accounting_info), nullptr)) {
|
||||
auto err = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to get job accounting info: "sv << err;
|
||||
return false;
|
||||
}
|
||||
|
||||
return accounting_info.ActiveProcesses != 0;
|
||||
}
|
||||
|
||||
SOCKADDR_IN
|
||||
to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {
|
||||
SOCKADDR_IN saddr_v4 = {};
|
||||
|
@ -61,17 +61,46 @@ namespace proc {
|
||||
* @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.
|
||||
*/
|
||||
void
|
||||
terminate_process_group(bp::child &proc, bp::group &group) {
|
||||
if (group.valid()) {
|
||||
BOOST_LOG(debug) << "Terminating child processes"sv;
|
||||
terminate_process_group(bp::child &proc, bp::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.
|
||||
group.terminate();
|
||||
group.detach();
|
||||
}
|
||||
|
||||
if (proc.valid()) {
|
||||
// avoid zombie process
|
||||
proc.wait();
|
||||
proc.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,7 +270,15 @@ namespace proc {
|
||||
|
||||
int
|
||||
proc_t::running() {
|
||||
if (placebo || _process.running()) {
|
||||
if (placebo) {
|
||||
return _app_id;
|
||||
}
|
||||
else if (_app.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 &&
|
||||
@ -265,7 +302,7 @@ namespace proc {
|
||||
proc_t::terminate() {
|
||||
std::error_code ec;
|
||||
placebo = false;
|
||||
terminate_process_group(_process, _process_group);
|
||||
terminate_process_group(_process, _process_group, _app.exit_timeout);
|
||||
_process = bp::child();
|
||||
_process_group = bp::group();
|
||||
|
||||
@ -566,6 +603,8 @@ namespace proc {
|
||||
auto working_dir = app_node.get_optional<std::string>("working-dir"s);
|
||||
auto elevated = app_node.get_optional<bool>("elevated"s);
|
||||
auto auto_detach = app_node.get_optional<bool>("auto-detach"s);
|
||||
auto wait_all = app_node.get_optional<bool>("wait-all"s);
|
||||
auto exit_timeout = app_node.get_optional<int>("exit-timeout"s);
|
||||
|
||||
std::vector<proc::cmd_t> prep_cmds;
|
||||
if (!exclude_global_prep.value_or(false)) {
|
||||
@ -625,6 +664,8 @@ namespace proc {
|
||||
|
||||
ctx.elevated = elevated.value_or(false);
|
||||
ctx.auto_detach = auto_detach.value_or(true);
|
||||
ctx.wait_all = wait_all.value_or(true);
|
||||
ctx.exit_timeout = std::chrono::seconds { exit_timeout.value_or(5) };
|
||||
|
||||
auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
|
||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
|
@ -38,10 +38,16 @@ namespace proc {
|
||||
std::vector<cmd_t> prep_cmds;
|
||||
|
||||
/**
|
||||
* Some applications, such as Steam,
|
||||
* either exit quickly, or keep running indefinitely.
|
||||
* Steam.exe is one such application.
|
||||
* That is why some applications need be run and forgotten about
|
||||
* Some applications, such as Steam, either exit quickly, or keep running indefinitely.
|
||||
*
|
||||
* Apps that launch normal child processes and terminate will be handled by the process
|
||||
* grouping logic (wait_all). However, apps that launch child processes indirectly or
|
||||
* into another process group (such as UWP apps) can only be handled by the auto-detach
|
||||
* heuristic which catches processes that exit 0 very quickly, but we won't have proper
|
||||
* process tracking for those.
|
||||
*
|
||||
* For cases where users just want to kick off a background process and never manage the
|
||||
* lifetime of that process, they can use detached commands for that.
|
||||
*/
|
||||
std::vector<std::string> detached;
|
||||
|
||||
@ -53,6 +59,8 @@ namespace proc {
|
||||
std::string id;
|
||||
bool elevated;
|
||||
bool auto_detach;
|
||||
bool wait_all;
|
||||
std::chrono::seconds exit_timeout;
|
||||
};
|
||||
|
||||
class proc_t {
|
||||
|
@ -243,6 +243,28 @@
|
||||
a launcher-type app is detected, it is treated as a detached app.
|
||||
</div>
|
||||
</div>
|
||||
<!-- wait for all processes -->
|
||||
<div class="mb-3 form-check">
|
||||
<label for="waitAll" class="form-check-label">Continue streaming until all app processes exit</label>
|
||||
<input type="checkbox" class="form-check-input" id="waitAll" v-model="editForm['wait-all']"
|
||||
true-value="true" false-value="false" />
|
||||
<div class="form-text">
|
||||
This will continue streaming until all processes started by the app have terminated.
|
||||
When unchecked, streaming will stop when the initial app process exits, even if other
|
||||
app processes are still running.
|
||||
</div>
|
||||
</div>
|
||||
<!-- exit timeout -->
|
||||
<div class="mb-3">
|
||||
<label for="exitTimeout" class="form-label">Exit Timeout</label>
|
||||
<input type="text" class="form-control monospace" id="exitTimeout" aria-describedby="exitTimeoutHelp"
|
||||
v-model="editForm['exit-timeout']" />
|
||||
<div id="exitTimeoutHelp" class="form-text">
|
||||
Number of seconds to wait for all app processes to gracefully exit when requested to quit.<br>
|
||||
If unset, the default is to wait up to 5 seconds. If set to zero or a negative value,
|
||||
the app will be immediately terminated.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="appImagePath" class="form-label">Image</label>
|
||||
<div class="input-group dropup">
|
||||
@ -412,6 +434,8 @@
|
||||
"exclude-global-prep-cmd": false,
|
||||
elevated: false,
|
||||
"auto-detach": true,
|
||||
"wait-all": true,
|
||||
"exit-timeout": 5,
|
||||
"prep-cmd": [],
|
||||
detached: [],
|
||||
"image-path": ""
|
||||
@ -434,6 +458,12 @@
|
||||
if (this.editForm["auto-detach"] === undefined) {
|
||||
this.editForm["auto-detach"] = true;
|
||||
}
|
||||
if (this.editForm["wait-all"] === undefined) {
|
||||
this.editForm["wait-all"] = true;
|
||||
}
|
||||
if (this.editForm["exit-timeout"] === undefined) {
|
||||
this.editForm["exit-timeout"] = 5;
|
||||
}
|
||||
this.showEditForm = true;
|
||||
},
|
||||
showDeleteForm(id) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user