From e18f16d94de50e4d82ff8a9c83b413374be10abb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 4 Jul 2025 14:57:09 -0500 Subject: [PATCH 1/3] linux: add functions for notifying systemd about process state Functions for notifying systemd that we are ready or have started reloading the configuration. --- src/os/systemd.zig | 133 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/os/systemd.zig b/src/os/systemd.zig index 9b67296d6..1a9a10ee7 100644 --- a/src/os/systemd.zig +++ b/src/os/systemd.zig @@ -63,3 +63,136 @@ pub fn launchedBySystemd() bool { else => false, }; } + +/// systemd notifications. Used by Ghostty to inform systemd of the state of the +/// process. Currently only used to notify systemd that we are ready and that +/// configuration reloading has started. +/// +/// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html +/// +/// These functions were re-implemented in Zig instead of using the `libsystemd` +/// library to avoid the complexity of another external dependency, as well as +/// to take advantage of Zig features like `comptime` to ensure minimal impact +/// on non-Linux systems (like FreeBSD) that will never support `systemd`. +/// +/// Linux systems that do not use `systemd` should not be impacted as they +/// should never start Ghostty with the `NOTIFY_SOCKET` environment variable set +/// and these functions essentially become a no-op. +/// +/// See `systemd`'s [Interface Portability and Stability Promise](https://systemd.io/PORTABILITY_AND_STABILITY/) +/// for assurances that the interfaces used here will be supported and stable for +/// the long term. +pub const notify = struct { + /// Send the given message to the UNIX socket specified in the NOTIFY_SOCKET + /// environment variable. If there NOTIFY_SOCKET environment variable does + /// not exist then no message is sent. + fn send(message: []const u8) void { + // systemd is Linux-only so this is a no-op anywhere else + if (comptime builtin.os.tag != .linux) return; + + // Get the socket address that should receive notifications. + const socket_path = std.posix.getenv("NOTIFY_SOCKET") orelse return; + + // If the socket address is an empty string return. + if (socket_path.len == 0) return; + + // The socket address must be a path or an abstract socket. + if (socket_path[0] != '/' and socket_path[0] != '@') { + log.warn("only AF_UNIX sockets with path or abstract namespace addresses are supported!", .{}); + return; + } + + var socket_address: std.os.linux.sockaddr.un = undefined; + + // Error out if the supplied socket path is too long. + if (socket_address.path.len < socket_path.len) { + log.warn("NOTIFY_SOCKET path is too long!", .{}); + return; + } + + socket_address.family = std.os.linux.AF.UNIX; + + @memcpy(socket_address.path[0..socket_path.len], socket_path); + socket_address.path[socket_path.len] = 0; + + const socket: std.os.linux.socket_t = socket: { + const rc = std.os.linux.socket( + std.os.linux.AF.UNIX, + std.os.linux.SOCK.DGRAM | std.os.linux.SOCK.CLOEXEC, + 0, + ); + switch (std.os.linux.E.init(rc)) { + .SUCCESS => break :socket @intCast(rc), + else => |e| { + log.warn("creating socket failed: {s}", .{@tagName(e)}); + return; + }, + } + }; + + defer _ = std.os.linux.close(socket); + + connect: { + const rc = std.os.linux.connect( + socket, + &socket_address, + @offsetOf(std.os.linux.sockaddr.un, "path") + socket_address.path.len, + ); + switch (std.os.linux.E.init(rc)) { + .SUCCESS => break :connect, + else => |e| { + log.warn("unable to connect to notify socket: {s}", .{@tagName(e)}); + return; + }, + } + } + + write: { + const rc = std.os.linux.write(socket, message.ptr, message.len); + switch (std.os.linux.E.init(rc)) { + .SUCCESS => { + const written = rc; + if (written < message.len) { + log.warn("short write to notify socket: {d} < {d}", .{ rc, message.len }); + return; + } + break :write; + }, + else => |e| { + log.warn("unable to write to notify socket: {s}", .{@tagName(e)}); + return; + }, + } + } + } + + /// Tell systemd that we are ready or that we are finished reloading. + /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#READY=1 + pub fn ready() void { + if (comptime builtin.os.tag != .linux) return; + + send("READY=1"); + } + + /// Tell systemd that we have started reloading our configuration. + /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#RELOADING=1 + /// and: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#MONOTONIC_USEC=%E2%80%A6 + pub fn reloading() void { + if (comptime builtin.os.tag != .linux) return; + + const ts = std.posix.clock_gettime(.MONOTONIC) catch |err| { + log.err("unable to get MONOTONIC clock: {}", .{err}); + return; + }; + + const now = ts.sec * std.time.us_per_s + @divFloor(ts.nsec, std.time.ns_per_us); + + var buffer: [64]u8 = undefined; + const message = std.fmt.bufPrint(&buffer, "RELOADING=1\nMONOTONIC_USEC={d}", .{now}) catch |err| { + log.err("unable to format reloading message: {}", .{err}); + return; + }; + + send(message); + } +}; From c9d0bbefc2a2e5152996113a78315fc723b155ec Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 4 Jul 2025 15:01:54 -0500 Subject: [PATCH 2/3] linux: switch systemd user service to type=notify-reload This allows `systemctl` to send SIGUSR2 to Ghostty to trigger a reload, which is more convenient than scripting `ps` and `kill` to find the Ghostty main PID. --- dist/linux/systemd.service.in | 5 ++++- src/apprt/gtk/App.zig | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index 3ff848ddd..76ccdd3f4 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -1,9 +1,12 @@ [Unit] Description=@NAME@ After=graphical-session.target +After=dbus.socket +Requires=dbus.socket [Service] -Type=dbus +Type=notify-reload +ReloadSignal=SIGUSR2 BusName=@APPID@ ExecStart=@GHOSTTY@ --launched-from=systemd diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 907f3a36d..bdb2f0f24 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -29,6 +29,7 @@ const apprt = @import("../../apprt.zig"); const configpkg = @import("../../config.zig"); const input = @import("../../input.zig"); const internal_os = @import("../../os/main.zig"); +const systemd = @import("../../os/systemd.zig"); const terminal = @import("../../terminal/main.zig"); const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); @@ -1035,6 +1036,12 @@ pub fn reloadConfig( target: apprt.action.Target, opts: apprt.action.ReloadConfig, ) !void { + // Tell systemd that reloading has started. + systemd.notify.reloading(); + + // When we exit this function tell systemd that reloading has finished. + defer systemd.notify.ready(); + if (opts.soft) { switch (target) { .app => try self.core_app.updateConfig(self, &self.config), @@ -1367,6 +1374,9 @@ pub fn run(self: *App) !void { log.warn("error handling configuration changes err={}", .{err}); }; + // Tell systemd that we are ready. + systemd.notify.ready(); + while (self.running) { _ = glib.MainContext.iteration(self.ctx, 1); From 248acbea5b5f8988b3e5cd44538b756025962bf6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 9 Jul 2025 12:29:45 -0500 Subject: [PATCH 3/3] gtk: remove NOTIFY_SOCKET from the inherited environment variables --- src/apprt/gtk/Surface.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5c886e663..d16083d5a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2333,6 +2333,7 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { env.remove("DBUS_STARTER_BUS_TYPE"); env.remove("INVOCATION_ID"); env.remove("JOURNAL_STREAM"); + env.remove("NOTIFY_SOCKET"); // Unset environment varies set by snaps if we're running in a snap. // This allows Ghostty to further launch additional snaps.