Implement restart support for all platforms

This commit is contained in:
Cameron Gutman 2023-04-23 18:09:13 -05:00
parent 4668ff59e5
commit 50f689ff80
10 changed files with 128 additions and 82 deletions

View File

@ -544,7 +544,6 @@ namespace confighttp {
outputTree.put("status", "true"); outputTree.put("status", "true");
outputTree.put("platform", SUNSHINE_PLATFORM); outputTree.put("platform", SUNSHINE_PLATFORM);
outputTree.put("version", PROJECT_VER); outputTree.put("version", PROJECT_VER);
outputTree.put("restart_supported", platf::restart_supported());
auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str())); auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str()));
@ -595,30 +594,8 @@ namespace confighttp {
print_req(request); print_req(request);
std::stringstream ss; // We may not return from this call
std::stringstream configStream; platf::restart();
ss << request->content.rdbuf();
pt::ptree outputTree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_json(data, outputTree);
response->write(data.str());
});
if (!platf::restart_supported()) {
outputTree.put("status", false);
outputTree.put("error", "Restart is not currently supported on this platform");
return;
}
if (!platf::restart()) {
outputTree.put("status", false);
outputTree.put("error", "Restart failed");
return;
}
outputTree.put("status", true);
} }
void void

View File

@ -108,6 +108,7 @@ namespace version {
} // namespace version } // namespace version
namespace lifetime { namespace lifetime {
static char **argv;
static std::atomic_int desired_exit_code; static std::atomic_int desired_exit_code;
/** /**
@ -130,6 +131,14 @@ namespace lifetime {
std::this_thread::sleep_for(1s); std::this_thread::sleep_for(1s);
} }
} }
/**
* @brief Gets the argv array passed to main()
*/
char **
get_argv() {
return argv;
}
} // namespace lifetime } // namespace lifetime
/** /**
@ -207,6 +216,8 @@ SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
*/ */
int int
main(int argc, char *argv[]) { main(int argc, char *argv[]) {
lifetime::argv = argv;
task_pool_util::TaskPool::task_id_t force_shutdown = nullptr; task_pool_util::TaskPool::task_id_t force_shutdown = nullptr;
#ifdef _WIN32 #ifdef _WIN32

View File

@ -69,6 +69,8 @@ namespace mail {
namespace lifetime { namespace lifetime {
void void
exit_sunshine(int exit_code, bool async); exit_sunshine(int exit_code, bool async);
char **
get_argv();
} // namespace lifetime } // namespace lifetime
#endif // SUNSHINE_MAIN_H #endif // SUNSHINE_MAIN_H

View File

@ -399,9 +399,7 @@ namespace platf {
void void
streaming_will_stop(); streaming_will_stop();
bool void
restart_supported();
bool
restart(); restart();
struct batched_send_info_t { struct batched_send_info_t {

View File

@ -193,16 +193,34 @@ namespace platf {
// Nothing to do // Nothing to do
} }
bool void
restart_supported() { restart_on_exit() {
// Restart not supported yet char executable[PATH_MAX];
return false; ssize_t len = readlink("/proc/self/exe", executable, PATH_MAX - 1);
if (len == -1) {
BOOST_LOG(fatal) << "readlink() failed: "sv << errno;
return;
}
executable[len] = '\0';
// ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves
int openmax = (int) sysconf(_SC_OPEN_MAX);
for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
close(fd);
}
// Re-exec ourselves with the same arguments
if (execv(executable, lifetime::get_argv()) < 0) {
BOOST_LOG(fatal) << "execv() failed: "sv << errno;
return;
}
} }
bool void
restart() { restart() {
// Restart not supported yet // Gracefully clean up and restart ourselves instead of exiting
return false; atexit(restart_on_exit);
lifetime::exit_sunshine(0, true);
} }
bool bool

View File

@ -3,6 +3,7 @@
#include <dlfcn.h> #include <dlfcn.h>
#include <fcntl.h> #include <fcntl.h>
#include <ifaddrs.h> #include <ifaddrs.h>
#include <mach-o/dyld.h>
#include <net/if_dl.h> #include <net/if_dl.h>
#include <pwd.h> #include <pwd.h>
@ -192,16 +193,33 @@ namespace platf {
// Nothing to do // Nothing to do
} }
bool void
restart_supported() { restart_on_exit() {
// Restart not supported yet char executable[2048];
return false; uint32_t size = sizeof(executable);
if (_NSGetExecutablePath(executable, &size) < 0) {
BOOST_LOG(fatal) << "NSGetExecutablePath() failed: "sv << errno;
return;
}
// ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves
int openmax = (int) sysconf(_SC_OPEN_MAX);
for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
close(fd);
}
// Re-exec ourselves with the same arguments
if (execv(executable, lifetime::get_argv()) < 0) {
BOOST_LOG(fatal) << "execv() failed: "sv << errno;
return;
}
} }
bool void
restart() { restart() {
// Restart not supported yet // Gracefully clean up and restart ourselves instead of exiting
return false; atexit(restart_on_exit);
lifetime::exit_sunshine(0, true);
} }
bool bool

View File

@ -809,19 +809,49 @@ namespace platf {
} }
} }
bool void
restart_supported() { restart_on_exit() {
// Restart is supported if we're running from the service STARTUPINFOEXW startup_info {};
return (GetConsoleWindow() == NULL); startup_info.StartupInfo.cb = sizeof(startup_info);
WCHAR executable[MAX_PATH];
if (GetModuleFileNameW(NULL, executable, ARRAYSIZE(executable)) == 0) {
auto winerr = GetLastError();
BOOST_LOG(fatal) << "Failed to get Sunshine path: "sv << winerr;
return;
}
PROCESS_INFORMATION process_info;
if (!CreateProcessW(executable,
GetCommandLineW(),
nullptr,
nullptr,
false,
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT,
nullptr,
nullptr,
(LPSTARTUPINFOW) &startup_info,
&process_info)) {
auto winerr = GetLastError();
BOOST_LOG(fatal) << "Unable to restart Sunshine: "sv << winerr;
return;
}
CloseHandle(process_info.hProcess);
CloseHandle(process_info.hThread);
} }
bool void
restart() { restart() {
// Gracefully exit. The service will restart us in a few seconds. // If we're running standalone, we have to respawn ourselves via CreateProcess().
// We use an async exit call here because we can't block the // If we're running from the service, we should just exit and let it respawn us.
// HTTP thread or we'll hang shutdown. if (GetConsoleWindow() != NULL) {
// Avoid racing with the new process by waiting until we're exiting to start it.
atexit(restart_on_exit);
}
// We use an async exit call here because we can't block the HTTP thread or we'll hang shutdown.
lifetime::exit_sunshine(0, true); lifetime::exit_sunshine(0, true);
return true;
} }
SOCKADDR_IN SOCKADDR_IN

View File

@ -123,6 +123,17 @@ namespace system_tray {
open_url("https://www.paypal.com/paypalme/ReenigneArcher"); open_url("https://www.paypal.com/paypalme/ReenigneArcher");
} }
/**
* @brief Callback for restarting Sunshine from the system tray.
* @param item The tray menu item.
*/
void
tray_restart_cb(struct tray_menu *item) {
BOOST_LOG(info) << "Restarting from system tray"sv;
platf::restart();
}
/** /**
* @brief Callback for exiting Sunshine from the system tray. * @brief Callback for exiting Sunshine from the system tray.
* @param item The tray menu item. * @param item The tray menu item.
@ -162,6 +173,7 @@ namespace system_tray {
{ .text = "PayPal", .cb = tray_donate_paypal_cb }, { .text = "PayPal", .cb = tray_donate_paypal_cb },
{ .text = nullptr } } }, { .text = nullptr } } },
{ .text = "-" }, { .text = "-" },
{ .text = "Restart", .cb = tray_restart_cb },
{ .text = "Quit", .cb = tray_quit_cb }, { .text = "Quit", .cb = tray_quit_cb },
{ .text = nullptr } }, { .text = nullptr } },
}; };

View File

@ -965,18 +965,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-success my-4" v-if="saved && restart_supported"> <div class="alert alert-success my-4" v-if="saved && !restarted">
<b>Success!</b> Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions. <b>Success!</b> Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.
</div> </div>
<div class="alert alert-success my-4" v-if="saved && !restart_supported">
<b>Success!</b> Restart Sunshine to apply changes.
</div>
<div class="alert alert-success my-4" v-if="restarted"> <div class="alert alert-success my-4" v-if="restarted">
<b>Success!</b> Sunshine is restarting to apply changes. <b>Success!</b> Sunshine is restarting to apply changes.
</div> </div>
<div class="mb-3 buttons"> <div class="mb-3 buttons">
<button class="btn btn-primary" @click="save">Save</button> <button class="btn btn-primary" @click="save">Save</button>
<button class="btn btn-success" @click="apply" v-if="saved && restart_supported && !restarted">Apply</button> <button class="btn btn-success" @click="apply" v-if="saved && !restarted">Apply</button>
</div> </div>
</div> </div>
@ -1023,7 +1020,6 @@
data() { data() {
return { return {
platform: "", platform: "",
restart_supported: false,
saved: false, saved: false,
restarted: false, restarted: false,
config: null, config: null,
@ -1087,7 +1083,6 @@
.then((r) => { .then((r) => {
this.config = r; this.config = r;
this.platform = this.config.platform; this.platform = this.config.platform;
this.restart_supported = (this.config.restart_supported === "true");
var app = document.getElementById("app"); var app = document.getElementById("app");
if (this.platform === "windows") { if (this.platform === "windows") {
@ -1108,7 +1103,6 @@
// remove values we don't want in the config file // remove values we don't want in the config file
delete this.config.platform; delete this.config.platform;
delete this.config.restart_supported;
delete this.config.status; delete this.config.status;
delete this.config.version; delete this.config.version;
//Populate default values if not present in config //Populate default values if not present in config
@ -1197,10 +1191,12 @@
saved.then((result) => { saved.then((result) => {
if (result === true) { if (result === true) {
this.restarted = true;
setTimeout(() => {
this.saved = this.restarted = false;
}, 5000);
fetch("/api/restart", { fetch("/api/restart", {
method: "POST" method: "POST"
}).then((r) => {
if (r.status === 200) this.restarted = true;
}); });
} }
}); });

View File

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<!--Restart Sunshine--> <!--Restart Sunshine-->
<div class="card p-2 my-4" v-if="restartSupported"> <div class="card p-2 my-4">
<div class="card-body"> <div class="card-body">
<h2>Restart Sunshine</h2> <h2>Restart Sunshine</h2>
<br /> <br />
@ -31,12 +31,9 @@
If Sunshine isn't working properly, you can try restarting it. If Sunshine isn't working properly, you can try restarting it.
This will terminate any running sessions. This will terminate any running sessions.
</p> </p>
<div class="alert alert-success" v-if="restartStatus === true"> <div class="alert alert-success" v-if="restartPressed === true">
Sunshine is restarting Sunshine is restarting
</div> </div>
<div class="alert alert-danger" v-if="restartStatus === false">
Error restarting Sunshine
</div>
<div> <div>
<button class="btn btn-warning" :disabled="restartPressed" @click="restart"> <button class="btn btn-warning" :disabled="restartPressed" @click="restart">
Restart Sunshine Restart Sunshine
@ -90,9 +87,7 @@
closeAppStatus: null, closeAppStatus: null,
unpairAllPressed: false, unpairAllPressed: false,
unpairAllStatus: null, unpairAllStatus: null,
restartSupported: false,
restartPressed: false, restartPressed: false,
restartStatus: null,
logs: 'Loading...', logs: 'Loading...',
logFilter: null, logFilter: null,
logInterval: null, logInterval: null,
@ -111,11 +106,6 @@
this.refreshLogs(); this.refreshLogs();
}, 5000); }, 5000);
this.refreshLogs(); this.refreshLogs();
fetch("/api/config")
.then((r) => r.json())
.then((r) => {
this.restartSupported = (r.restart_supported === "true");
});
}, },
beforeDestroy(){ beforeDestroy(){
clearInterval(this.logInterval); clearInterval(this.logInterval);
@ -157,17 +147,11 @@
}, },
restart() { restart() {
this.restartPressed = true; this.restartPressed = true;
setTimeout(() => {
this.restartPressed = false;
}, 5000);
fetch("/api/restart", { fetch("/api/restart", {
method: "POST", method: "POST",
}).then((r) => {
this.restartPressed = false;
// We won't get a response in the success case
this.restartStatus = r.status.toString() !== "false";
setTimeout(() => {
this.restartStatus = null;
}, 5000);
}); });
}, },
}, },