diff --git a/README.md b/README.md index 8a478ab2..f63e6c41 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ It's licensed under the terms of the GNU General Public License, version 3 or la * Local Hiscores integration with hi2txt * And much more! +## Runtime Signals +- Linux/macOS: + - `SIGHUP`: graceful restart (re-read configuration) + - `SIGINT` / `SIGTERM`: graceful shutdown + - `SIGUSR1`: live layout XML hot-reload +- Windows (console mode): + - `Ctrl+C`, `Ctrl+Break`, console close/logoff/shutdown events: graceful shutdown + ## System Requirements * OS * Windows (10 or higher) diff --git a/RetroFE/Source/Execute/Launcher.cpp b/RetroFE/Source/Execute/Launcher.cpp index 4dd2d326..dcff3015 100644 --- a/RetroFE/Source/Execute/Launcher.cpp +++ b/RetroFE/Source/Execute/Launcher.cpp @@ -367,7 +367,7 @@ bool Launcher::run(std::string collection, Item* collectionItem, Page* currentPa break; } case PreWaitMode::UpToMs: { - // IMPORTANT: use simpleLaunch() so job-close doesn’t kill it + // IMPORTANT: use simpleLaunch() so job-close doesn�t kill it LOG_INFO("Launcher", std::string("Waiting up to ") + std::to_string(preWaitMs) + " ms for pre-hook (non-blocking)..."); if (!preMgr->simpleLaunch(pExe.string(), preArgs, pCwd.string())) { LOG_ERROR("Launcher", "Pre-hook failed to start: " + pExe.string()); @@ -381,14 +381,14 @@ bool Launcher::run(std::string collection, Item* collectionItem, Page* currentPa // ~30fps std::this_thread::sleep_for(std::chrono::milliseconds(33)); } - // We can’t know exit code in this mode (no handle by design) + // We can�t know exit code in this mode (no handle by design) LOG_INFO("Launcher", std::string("Pre-hook still running after ") + std::to_string(preWaitMs) + " ms; continuing."); break; } case PreWaitMode::NoWait: default: { - // IMPORTANT: use simpleLaunch() so job-close doesn’t kill it + // IMPORTANT: use simpleLaunch() so job-close doesn�t kill it if (!preMgr->simpleLaunch(pExe.string(), preArgs, pCwd.string())) { LOG_ERROR("Launcher", "Pre-hook failed to start: " + pExe.string()); return false; @@ -452,6 +452,10 @@ bool Launcher::run(std::string collection, Item* collectionItem, Page* currentPa int timeout = 30; config_.getProperty(OPTION_ATTRACTMODELAUNCHRUNTIME, timeout); auto attractModeInputCheck = [&inputMonitor]() { + // Check for shutdown signal first + if (RetroFE::isShutdownRequested()) { + return true; + } return inputMonitor.checkInputEvents() != InputDetectionResult::NoInput; }; WaitResult result = processManager->wait(timeout, attractModeInputCheck, onFrameTick); @@ -490,6 +494,12 @@ bool Launcher::run(std::string collection, Item* collectionItem, Page* currentPa else { // Normal mode LOG_INFO("Launcher", "Waiting for launched process to complete. Press quit combo to force quit."); auto quitCheck = [&inputMonitor, launchingNestedRetroFE, quitComboEnabled]() { + // Check for shutdown signal first + if (RetroFE::isShutdownRequested()) { + LOG_INFO("Launcher", "Shutdown signal received. Terminating launched process."); + return true; + } + // Nested RetroFE always owns quit regardless of config if (launchingNestedRetroFE) return false; diff --git a/RetroFE/Source/RetroFE.cpp b/RetroFE/Source/RetroFE.cpp index bfb3ebd9..6bc25068 100644 --- a/RetroFE/Source/RetroFE.cpp +++ b/RetroFE/Source/RetroFE.cpp @@ -79,6 +79,7 @@ std::atomic RetroFE::reloadRequested_{false}; std::atomic RetroFE::sighupReceived_{false}; +std::atomic RetroFE::shutdownRequested_{false}; void RetroFE::handleSigusr1(int) { reloadRequested_.store(true); @@ -88,6 +89,24 @@ void RetroFE::handleSighup(int) { sighupReceived_.store(true); } +void RetroFE::handleSigterm(int) { + shutdownRequested_.store(true); +} + +bool RetroFE::isShutdownRequested() { + return shutdownRequested_.load(); +} + +#ifdef WIN32 +BOOL WINAPI RetroFE::consoleCtrlHandler(DWORD dwCtrlType) { + if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT || dwCtrlType == CTRL_CLOSE_EVENT) { + shutdownRequested_.store(true); + return TRUE; // Indicate that the signal was handled + } + return FALSE; // Let the system handle other signals +} +#endif + RetroFE::RetroFE(Configuration& c) : initialized(false), initializeError(false), initializeThread(NULL), config_(c), db_(NULL), metadb_(NULL), input_(config_), currentPage_(NULL), keyInputDisable_(0), currentTime_(0), lastLaunchReturnTime_(0), @@ -811,6 +830,13 @@ bool RetroFE::run() { signal(SIGUSR1, RetroFE::handleSigusr1); // SIGHUP triggers a clean restart (re-reads all configuration) signal(SIGHUP, RetroFE::handleSighup); + // SIGTERM triggers graceful shutdown + signal(SIGTERM, RetroFE::handleSigterm); +#endif + +#ifdef WIN32 + // Set console control handler for graceful shutdown on Windows + SetConsoleCtrlHandler(RetroFE::consoleCtrlHandler, TRUE); #endif config_.getProperty(OPTION_ATTRACTMODETIME, attractModeTime); @@ -978,6 +1004,15 @@ bool RetroFE::run() { setState(RETROFE_QUIT_REQUEST); } + // Check for SIGTERM or Windows shutdown signal: trigger graceful shutdown. + // Wait until we are out of splash mode and not already shutting down. + if (!splashMode && shutdownRequested_.exchange(false) + && state_ != RETROFE_QUIT_REQUEST && state_ != RETROFE_QUIT) + { + LOG_INFO("RetroFE", "Shutdown signal received – exiting gracefully"); + setState(RETROFE_QUIT_REQUEST); + } + if (!splashMode && state_accepts_input(state_)) { RETROFE_STATE inputState = processUserInput(currentPage_); if (inputState != RETROFE_IDLE) { diff --git a/RetroFE/Source/RetroFE.h b/RetroFE/Source/RetroFE.h index bdce34f3..fddd8d72 100644 --- a/RetroFE/Source/RetroFE.h +++ b/RetroFE/Source/RetroFE.h @@ -69,6 +69,14 @@ class RetroFE MetadataDatabase* getMetaDb(); uint64_t freq_ = SDL_GetPerformanceFrequency(); + static void handleSigusr1(int sig); + static void handleSighup(int sig); + static void handleSigterm(int sig); +#ifdef WIN32 + static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType); +#endif + static bool isShutdownRequested(); + private: #ifdef WIN32 HWND hwnd; @@ -190,10 +198,9 @@ class RetroFE void handleMusicControls(UserInput::KeyCode_E input); Page *loadPage(const std::string& collectionName); Page *loadSplashPage( ); - static void handleSigusr1(int sig); - static void handleSighup(int sig); static std::atomic reloadRequested_; static std::atomic sighupReceived_; + static std::atomic shutdownRequested_; std::vector collectionCycle_; std::vector::iterator collectionCycleIt_;