From d3f49af80caf468ff24bb6c6322015110a26be24 Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Fri, 9 Oct 2026 23:29:55 +0800 Subject: [PATCH 1/2] windows/macos: orderly embedded-backend shutdown on session end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linux adapter has shipped InstallShutdownHook since #128 and the scaffold wires it since #168; Windows and macOS scaffolds had no signal or session-end path, so logoff/shutdown killed the process with the backend mid-flight. - Windows system adapter: InstallShutdownHook over SetConsoleCtrlHandler (CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, CTRL_SHUTDOWN_ EVENT — session logoff and OS shutdown reach GUI processes through the last two). Registration failure throws; the callback contract stays synchronous and allocation-light like the Linux hook. - Windows scaffold: MainWindow funnels window close (Window.Closed) and the console-control callback into StopBackendOrderly, which reads the shared backend through std::atomic_load and calls the thread-safe stop() — the control callback runs on its own thread and never touches XAML state. - macOS scaffold: NSApplicationDelegateAdaptor with applicationShouldTerminate runs the registered backend stop for every termination path (Cmd-Q, window close, logout, shutdown). Fixes #120. --- CHANGELOG.md | 8 ++++++ .../host/Sources/RivetHost/RivetHostApp.swift | 18 ++++++++++++ platform/windows/host/MainWindow.xaml.cpp | 17 +++++++++++ platform/windows/host/MainWindow.xaml.h | 4 +++ platform/windows/system/system_services.cpp | 28 +++++++++++++++++++ platform/windows/system/system_services.hpp | 10 +++++++ tests/codegen.rkt | 11 ++++++++ 7 files changed, 96 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5322722..c3e726bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- Windows and macOS hosts now shut the embedded backend down orderly on + session end. The Windows system adapter gains InstallShutdownHook + (SetConsoleCtrlHandler: CTRL_C/CLOSE/LOGOFF/SHUTDOWN) and the scaffold + wires it plus the window-close path to a thread-safe backend stop; the + macOS scaffold installs an NSApplicationDelegate whose + applicationShouldTerminate stops the backend for Cmd-Q, logout, and OS + shutdown. Mirrors the Linux InstallShutdownHook shipped by #128 and + wired into the scaffold by #168. Fixes #120. - Linux hosts now terminate on SIGTERM/SIGINT. The host scaffold and the Taskboard example install Rivet's shutdown hook so a termination signal runs the same orderly backend shutdown as the GTK shutdown signal and diff --git a/platform/macos/host/Sources/RivetHost/RivetHostApp.swift b/platform/macos/host/Sources/RivetHost/RivetHostApp.swift index 94703256..c2dccfa0 100644 --- a/platform/macos/host/Sources/RivetHost/RivetHostApp.swift +++ b/platform/macos/host/Sources/RivetHost/RivetHostApp.swift @@ -5,6 +5,8 @@ import RivetSystem @main struct RivetHostApp: App { + @NSApplicationDelegateAdaptor(ApplicationDelegate.self) + private var applicationDelegate @StateObject private var model = AppModel() private let activationRouter = RivetActivationRouter() @@ -23,6 +25,19 @@ struct RivetHostApp: App { } } +/// Every termination path — Cmd-Q, window close, logout, and OS shutdown — +/// passes through applicationShouldTerminate before the process exits, so +/// the embedded backend gets one orderly stop hook. The AppModel registers +/// its backend shutdown once startup succeeds. +final class ApplicationDelegate: NSObject, NSApplicationDelegate { + static var orderlyShutdown: (() -> Void)? + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + Self.orderlyShutdown?() + return .terminateNow + } +} + @MainActor final class AppModel: ObservableObject { @Published var status = "Starting embedded Racket CS…" @@ -41,6 +56,9 @@ final class AppModel: ObservableObject { ) let backend = EmbeddedRacketBackend(configuration: config) self.backend = backend + ApplicationDelegate.orderlyShutdown = { [weak backend] in + backend?.stop() + } Task.detached { [backend] in do { diff --git a/platform/windows/host/MainWindow.xaml.cpp b/platform/windows/host/MainWindow.xaml.cpp index c6773485..2dc65843 100644 --- a/platform/windows/host/MainWindow.xaml.cpp +++ b/platform/windows/host/MainWindow.xaml.cpp @@ -64,9 +64,26 @@ rivet::windows::RacketRuntimeConfig runtime_config() { MainWindow::MainWindow() { InitializeComponent(); Title(L"Rivet — Racket + WinUI 3"); + // Ordinary window close and OS session end (logoff/shutdown) both funnel + // into the same orderly backend stop. The console-control callback runs + // on its own thread, so it only touches the thread-safe stop() — never + // XAML state. + Closed([this](auto const&, auto const&) { StopBackendOrderly(); }); + try { + rivet::system::InstallShutdownHook([this] { StopBackendOrderly(); }); + } catch (std::exception const&) { + // Signal coverage is platform policy and never blocks startup; the + // window-close path above still runs the orderly shutdown. + } InitializeBackendAsync(); } +void MainWindow::StopBackendOrderly() { + if (auto backend = std::atomic_load(&backend_); backend != nullptr) { + backend->stop(); + } +} + winrt::fire_and_forget MainWindow::InitializeBackendAsync() { auto const dispatcher = DispatcherQueue(); auto const weak = get_weak(); diff --git a/platform/windows/host/MainWindow.xaml.h b/platform/windows/host/MainWindow.xaml.h index 85c9b274..05540bea 100644 --- a/platform/windows/host/MainWindow.xaml.h +++ b/platform/windows/host/MainWindow.xaml.h @@ -15,6 +15,10 @@ struct MainWindow : MainWindowT { return rivet::system::NativeWindowHandle(*this); } + // Thread-safe orderly backend stop shared by the window-close path and + // the console-control shutdown hook (logoff/CTRL events). + void StopBackendOrderly(); + void Increment_Click(winrt::Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& args); diff --git a/platform/windows/system/system_services.cpp b/platform/windows/system/system_services.cpp index d7076dc4..98f573a4 100644 --- a/platform/windows/system/system_services.cpp +++ b/platform/windows/system/system_services.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -178,4 +179,31 @@ std::vector ActivationArguments() { return result; } +namespace { + +std::function g_shutdown_callback; + +BOOL WINAPI ConsoleCtrlHandler(DWORD event_type) { + switch (event_type) { + case CTRL_C_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_SHUTDOWN_EVENT: + if (g_shutdown_callback != nullptr) g_shutdown_callback(); + return TRUE; + default: + return FALSE; + } +} + +} // namespace + +void InstallShutdownHook(std::function callback) { + g_shutdown_callback = std::move(callback); + if (::SetConsoleCtrlHandler(&ConsoleCtrlHandler, TRUE) == 0) { + g_shutdown_callback = nullptr; + ThrowLastError("SetConsoleCtrlHandler"); + } +} + } // namespace rivet::system diff --git a/platform/windows/system/system_services.hpp b/platform/windows/system/system_services.hpp index 12de7b9e..1c260100 100644 --- a/platform/windows/system/system_services.hpp +++ b/platform/windows/system/system_services.hpp @@ -78,6 +78,16 @@ using CrashCallback = void (*)(EXCEPTION_POINTERS const*) noexcept; void InstallCrashHook(CrashCallback callback, std::wstring const& restart_arguments = L""); +// Mirrors the Linux adapter's InstallShutdownHook: the callback runs on a +// console-control thread for CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_ +// EVENT, and CTRL_SHUTDOWN_EVENT (session logoff and OS shutdown reach GUI +// processes without a console through the last two). Keep it synchronous +// and allocation-light — the OS terminates the process shortly after the +// handler returns TRUE — and touch only thread-safe state such as the +// backend's stop(). Throws std::system_error when the handler cannot be +// registered. +void InstallShutdownHook(std::function callback); + // Windows delivers URL scheme and file-association activations through the // process command line for unpackaged/MSI apps. Registration belongs to the // installer; this helper normalizes the received payload for application code. diff --git a/tests/codegen.rkt b/tests/codegen.rkt index 1cdda45a..d05cfd1c 100644 --- a/tests/codegen.rkt +++ b/tests/codegen.rkt @@ -24,6 +24,10 @@ (file->string (build-path project-root "windows" "RivetHost.vcxproj"))) (define windows-main-header (file->string (build-path project-root "windows" "MainWindow.xaml.h"))) + (define windows-main-source + (file->string (build-path project-root "windows" "MainWindow.xaml.cpp"))) + (define macos-app-source + (file->string (build-path project-root "macos-host" "Sources" "RivetHost" "RivetHostApp.swift"))) (define macos-package (file->string (build-path project-root "macos-host" "Package.swift"))) (define linux-cmake @@ -46,6 +50,13 @@ (check-true (regexp-match? #rx"rivet::system::NativeWindowHandle" windows-main-header)) (check-true (regexp-match? #rx"HWND WindowHandle" windows-main-header)) + (check-true + (regexp-match? #rx"rivet::system::InstallShutdownHook" windows-main-source)) + (check-true (regexp-match? #rx"StopBackendOrderly" windows-main-header)) + (check-true + (regexp-match? #rx"applicationShouldTerminate" macos-app-source)) + (check-true + (regexp-match? #rx"orderlyShutdown" macos-app-source)) (check-false (regexp-match? #rx"10\\.0\\.19041\\.0" windows-project)) From 888193aceec075219ebbbc1266e8f1637334d32b Mon Sep 17 00:00:00 2001 From: "ren.ji" Date: Sat, 10 Oct 2026 01:12:33 +0800 Subject: [PATCH 2/2] macos scaffold: isolate the shutdown closure to the main actor Swift strict concurrency rejects a nonisolated static var; AppKit delivers applicationShouldTerminate on the main thread, so MainActor plus assumeIsolated keeps registration (from the MainActor AppModel) and the callback on one isolation domain. --- platform/macos/host/Sources/RivetHost/RivetHostApp.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/platform/macos/host/Sources/RivetHost/RivetHostApp.swift b/platform/macos/host/Sources/RivetHost/RivetHostApp.swift index c2dccfa0..9579b565 100644 --- a/platform/macos/host/Sources/RivetHost/RivetHostApp.swift +++ b/platform/macos/host/Sources/RivetHost/RivetHostApp.swift @@ -29,11 +29,14 @@ struct RivetHostApp: App { /// passes through applicationShouldTerminate before the process exits, so /// the embedded backend gets one orderly stop hook. The AppModel registers /// its backend shutdown once startup succeeds. +@MainActor final class ApplicationDelegate: NSObject, NSApplicationDelegate { static var orderlyShutdown: (() -> Void)? - func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - Self.orderlyShutdown?() + nonisolated func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + // AppKit delivers this callback on the main thread, where the + // registered shutdown closure lives. + MainActor.assumeIsolated { Self.orderlyShutdown?() } return .terminateNow } }