From 2fa4fc89027be57890e50973fff01a50f956b8b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 14:58:39 -0700 Subject: [PATCH] reload configuration on SIGUSR2 This is done at the apprt-level for a couple reasons. (1) For libghostty, we don't have a way to know what the embedding application is doing, so its risky to create signal handlers that might overwrite the application's signal handlers. (2) It's extremely messy to deal with signals and multi-threading. Apprts have framework access that handles this for us. For GTK, we use g_unix_signal_add. For macOS, we use `DispatchSource.makeSignalSource`. This is an awkward API but made for this purpose. --- macos/Sources/App/macOS/AppDelegate.swift | 34 +++++++++++++++++++++++ src/apprt/gtk/App.zig | 23 +++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 734fcbc20..418005927 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -112,6 +112,9 @@ class AppDelegate: NSObject, /// The observer for the app appearance. private var appearanceObserver: NSKeyValueObservation? = nil + /// Signals + private var signals: [DispatchSourceSignal] = [] + /// The custom app icon image that is currently in use. @Published private(set) var appIcon: NSImage? = nil { didSet { @@ -249,6 +252,9 @@ class AppDelegate: NSObject, // Setup our menu setupMenuImages() + + // Setup signal handlers + setupSignals() } func applicationDidBecomeActive(_ notification: Notification) { @@ -406,6 +412,34 @@ class AppDelegate: NSObject, return dockMenu } + /// Setup signal handlers + private func setupSignals() { + // Register a signal handler for config reloading. It appears that all + // of this is required. I've commented each line because its a bit unclear. + // Warning: signal handlers don't work when run via Xcode. They have to be + // run on a real app bundle. + + // We need to ignore signals we register with makeSignalSource or they + // don't seem to handle. + signal(SIGUSR2, SIG_IGN) + + // Make the signal source and register our event handle. We keep a weak + // ref to ourself so we don't create a retain cycle. + let sigusr2 = DispatchSource.makeSignalSource(signal: SIGUSR2, queue: .main) + sigusr2.setEventHandler { [weak self] in + guard let self else { return } + Ghostty.logger.info("reloading configuration in response to SIGUSR2") + self.ghostty.reloadConfig() + } + + // The signal source starts unactivated, so we have to resume it once + // we setup the event handler. + sigusr2.resume() + + // We need to keep a strong reference to it so it isn't disabled. + signals.append(sigusr2) + } + /// Setup all the images for our menu items. private func setupMenuImages() { // Note: This COULD Be done all in the xib file, but I find it easier to diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 7786f976a..c61254fbd 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -373,6 +373,13 @@ pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void { .{}, ); + // Setup a listener for SIGUSR2 to reload the configuration. + _ = glib.unixSignalAdd( + std.posix.SIG.USR2, + sigusr2, + self, + ); + // We don't use g_application_run, we want to manually control the // loop so we have to do the same things the run function does: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -1508,6 +1515,22 @@ pub fn quitNow(self: *App) void { self.running = false; } +// SIGUSR2 signal handler via g_unix_signal_add +fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int { + const self: *App = @ptrCast(@alignCast(ud orelse + return @intFromBool(glib.SOURCE_CONTINUE))); + + log.info("received SIGUSR2, reloading configuration", .{}); + self.reloadConfig(.app, .{ .soft = false }) catch |err| { + log.err( + "error reloading configuration for SIGUSR2: {}", + .{err}, + ); + }; + + return @intFromBool(glib.SOURCE_CONTINUE); +} + /// This is called by the `activate` signal. This is sent on program startup and /// also when a secondary instance launches and requests a new window. fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {