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.
This commit is contained in:
Mitchell Hashimoto
2025-07-01 14:58:39 -07:00
parent 5c4a30d85f
commit 2fa4fc8902
2 changed files with 57 additions and 0 deletions

View File

@@ -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

View File

@@ -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 {