diff --git a/runtime/include/aurora_events.h b/runtime/include/aurora_events.h index e10c151aa..b54d81314 100644 --- a/runtime/include/aurora_events.h +++ b/runtime/include/aurora_events.h @@ -2,6 +2,7 @@ #include "settings_overlay.h" #include "runtime_config.h" +#include "platform/host_platform.h" #include #include @@ -59,9 +60,7 @@ inline void Flush(bool force = false) { // fiber re-enters runtime teardown and can fault while the fiber machinery is // still active. A window close is an intentional successful exit, so end the // process directly and do not run the crash/atexit paths. -[[noreturn]] inline void ExitForAuroraWindowClose() noexcept { - settings_overlay::ReleaseControllers(); - WindowPlacementPersistence::Flush(true); +[[noreturn]] inline void ExitAuroraProcess() noexcept { #if defined(_WIN32) ::ExitProcess(0); #else @@ -69,6 +68,19 @@ inline void Flush(bool force = false) { #endif } +[[noreturn]] inline void ExitForAuroraWindowClose() noexcept { + settings_overlay::ReleaseControllers(); + WindowPlacementPersistence::Flush(true); + ExitAuroraProcess(); +} + +// Returns only if the new instance could not be started; the LED is left for the new instance to set. +inline void RestartForAuroraWindowClose() noexcept { + WindowPlacementPersistence::Flush(true); + settings_overlay::ReleaseControllers(false); + if (RuntimePlatform::RelaunchSelf()) ExitAuroraProcess(); +} + // Update cached Aurora window/framebuffer dimensions based on pending events. inline void ProcessAuroraEvents(const AuroraEvent* events) { if (!events) { diff --git a/runtime/include/platform/host_platform.h b/runtime/include/platform/host_platform.h index 1fd961e01..9be073b40 100644 --- a/runtime/include/platform/host_platform.h +++ b/runtime/include/platform/host_platform.h @@ -23,4 +23,7 @@ std::filesystem::path LogDirectory(std::string_view applicationName); uint64_t CurrentProcessId() noexcept; +// Starts a new instance of this executable. False if it could not be started. +bool RelaunchSelf() noexcept; + } // namespace RuntimePlatform diff --git a/runtime/include/settings_overlay.h b/runtime/include/settings_overlay.h index 631eb3a6a..6aa3ace17 100644 --- a/runtime/include/settings_overlay.h +++ b/runtime/include/settings_overlay.h @@ -13,5 +13,5 @@ bool StartupScreenVisible() noexcept; void NotifyStrapInputAccepted() noexcept; void AdvancePresentedFrame() noexcept; // Put host controllers back to a neutral state before the process ends. -void ReleaseControllers() noexcept; +void ReleaseControllers(bool resetLeds = true) noexcept; } // namespace settings_overlay diff --git a/runtime/src/platform/host_platform.cpp b/runtime/src/platform/host_platform.cpp index c0caa95a5..20d48b046 100644 --- a/runtime/src/platform/host_platform.cpp +++ b/runtime/src/platform/host_platform.cpp @@ -9,10 +9,18 @@ #include #include #else +#include +#include +#include #include #endif +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#endif + #if defined(__APPLE__) +#include #include #include #endif @@ -82,4 +90,62 @@ uint64_t CurrentProcessId() noexcept { #endif } +bool RelaunchSelf() noexcept { +#if defined(_WIN32) + std::wstring path(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); + if (length == 0 || length >= path.size()) { + return false; + } + path.resize(length); + std::wstring commandLine = GetCommandLineW(); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(path.c_str(), commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, + &startup, &process)) { + return false; + } + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return true; +#else + std::string path; + // Mark inherited descriptors close-on-exec, or the new instance keeps this one's sockets and devices open. +#if defined(__APPLE__) + uint32_t size = 0; + if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) { + return false; + } + path.resize(size); + if (_NSGetExecutablePath(path.data(), &size) != 0) { + return false; + } + char** const env = *_NSGetEnviron(); + for (int fd = 3, max = getdtablesize(); fd < max; ++fd) { + fcntl(fd, F_SETFD, FD_CLOEXEC); + } +#else + path.resize(PATH_MAX); + const ssize_t length = readlink("/proc/self/exe", path.data(), path.size() - 1); + if (length <= 0) { + return false; + } + path.resize(static_cast(length)); + char** const env = environ; + if (DIR* dir = opendir("/proc/self/fd")) { + while (const dirent* entry = readdir(dir)) { + if (const int fd = std::atoi(entry->d_name); fd > 2 && fd != dirfd(dir)) { + fcntl(fd, F_SETFD, FD_CLOEXEC); + } + } + closedir(dir); + } +#endif + char* const argv[] = {path.data(), nullptr}; + pid_t pid = 0; + return posix_spawn(&pid, path.c_str(), nullptr, nullptr, argv, env) == 0; +#endif +} + } // namespace RuntimePlatform diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index 616d141d8..39606379b 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -1202,10 +1202,20 @@ void DrawStartupScreen() { void DrawExitPrompt() { constexpr const char* kTitle = "Exit"; if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle); - if (!ImGui::BeginPopupModal(kTitle, &g_exitPromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return; + static bool restartFailed = false; + if (!ImGui::BeginPopupModal(kTitle, &g_exitPromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) { + restartFailed = false; + return; + } ImGui::TextUnformatted("Quit the game?"); + if (restartFailed) ImGui::TextDisabled("Could not restart."); if (ImGui::Button("Exit", ImVec2(120.0f, 0.0f))) ExitForAuroraWindowClose(); ImGui::SameLine(); + if (ImGui::Button("Restart", ImVec2(120.0f, 0.0f))) { + RestartForAuroraWindowClose(); + restartFailed = true; + } + ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) g_exitPromptOpen = false; if (!g_exitPromptOpen) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); @@ -1423,7 +1433,7 @@ void HandleEvents(const AuroraEvent* events) noexcept { } } -void ReleaseControllers() noexcept { +void ReleaseControllers(bool resetLeds) noexcept { // Aurora drives the LED white on first PADRead and never clears it, and the // exit paths terminate the process outright, so do it here. bool queued = false; @@ -1431,7 +1441,7 @@ void ReleaseControllers() noexcept { const s32 index = PADGetIndexForPort(port); if (index < 0) continue; if (SDL_Gamepad* pad = PADGetSDLGamepadForIndex(static_cast(index))) { - SDL_SetGamepadLED(pad, 0, 0, 0); + if (resetLeds) SDL_SetGamepadLED(pad, 0, 0, 0); queued = true; } }