summaryrefslogtreecommitdiff
path: root/src/os/systemd.zig
blob: 1a9a10ee711b5c996d45c90e09e4f34dba858f8f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
const std = @import("std");
const builtin = @import("builtin");

const log = std.log.scoped(.systemd);

/// Returns true if the program was launched as a systemd service.
///
/// On Linux, this returns true if the program was launched as a systemd
/// service. It will return false if Ghostty was launched any other way.
///
/// For other platforms and app runtimes, this returns false.
pub fn launchedBySystemd() bool {
    return switch (builtin.os.tag) {
        .linux => linux: {
            // On Linux, systemd sets the `INVOCATION_ID` (v232+) and the
            // `JOURNAL_STREAM` (v231+) environment variables. If these
            // environment variables are not present we were not launched by
            // systemd.
            if (std.posix.getenv("INVOCATION_ID") == null) break :linux false;
            if (std.posix.getenv("JOURNAL_STREAM") == null) break :linux false;

            // If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure
            // that our parent process is actually `systemd`, not some other terminal
            // emulator that doesn't clean up those environment variables.
            const ppid = std.os.linux.getppid();
            if (ppid == 1) break :linux true;

            // If the parent PID is not 1 we need to check to see if we were launched by
            // a user systemd daemon. Do that by checking the `/proc/<ppid>/comm`
            // to see if it ends with `systemd`.
            var comm_path_buf: [std.fs.max_path_bytes]u8 = undefined;
            const comm_path = std.fmt.bufPrint(&comm_path_buf, "/proc/{d}/comm", .{ppid}) catch {
                log.err("unable to format comm path for pid {d}", .{ppid});
                break :linux false;
            };
            const comm_file = std.fs.openFileAbsolute(comm_path, .{ .mode = .read_only }) catch {
                log.err("unable to open '{s}' for reading", .{comm_path});
                break :linux false;
            };
            defer comm_file.close();

            // The maximum length of the command name is defined by
            // `TASK_COMM_LEN` in the Linux kernel. This is usually 16
            // bytes at the time of writing (Jun 2025) so its set to that.
            // Also, since we only care to compare to "systemd", anything
            // longer can be assumed to not be systemd.
            const TASK_COMM_LEN = 16;
            var comm_data_buf: [TASK_COMM_LEN]u8 = undefined;
            const comm_size = comm_file.readAll(&comm_data_buf) catch {
                log.err("problems reading from '{s}'", .{comm_path});
                break :linux false;
            };
            const comm_data = comm_data_buf[0..comm_size];

            break :linux std.mem.eql(
                u8,
                std.mem.trimRight(u8, comm_data, "\n"),
                "systemd",
            );
        },

        // No other system supports systemd so always return false.
        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);
    }
};