Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

systemd-notify.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

systemd-notify.tsBlame111 lines · 1 contributor
f764c07Claude1/**
2 * BLOCK N2 — systemd sd_notify(READY=1) helper.
3 *
4 * When the service unit is `Type=notify`, systemd blocks `systemctl restart`
5 * until the new process sends `READY=1` over the AF_UNIX datagram socket
6 * whose path is exposed via the `NOTIFY_SOCKET` env var. Once we send that,
7 * the unit is considered started and the old process is reaped.
8 *
9 * This is what eliminates the "sleep + curl /healthz with retries" loop on
10 * the deploy box: systemd itself knows when the new process is serving HTTP,
11 * so the deploy workflow can move on the instant Bun.serve() returns.
12 *
13 * Defensive guarantees (load-bearing — boot must never fail on this):
14 * - If `NOTIFY_SOCKET` is unset (dev, local test, non-systemd hosts), the
15 * helper is a silent no-op.
16 * - All socket errors are swallowed. A failed notify NEVER throws out into
17 * `src/index.ts` boot path.
18 * - No external dependency. Uses `Bun.udpSocket` (datagram support shipped
19 * in Bun 1.1+).
20 *
21 * Reference:
22 * - https://www.freedesktop.org/software/systemd/man/sd_notify.html
23 * - https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
24 */
25
26/**
27 * Factory for the datagram socket used to talk to systemd. Pulled out so
28 * tests can inject a mock without touching Bun globals.
29 */
30export type DatagramOpener = (
31 socketPath: string
32) => Promise<{
33 send(data: string | Uint8Array, port: number, hostname: string): unknown;
34 close(): void;
35}>;
36
37const defaultOpener: DatagramOpener = async (socketPath: string) => {
38 // Bun's udpSocket factory. We intentionally don't bind to a port — we
39 // only need to send a single datagram and then close. The `connect`
40 // option targets the unix datagram path exposed by systemd.
41 //
42 // Note: at the time of writing Bun's udpSocket primarily exposes UDP/IP;
43 // we pass `connect` with the path field as a best-effort. If the runtime
44 // rejects the unix path, the resulting throw is caught by `notifySystemdReady`
45 // and we fall back gracefully (systemd will eventually time out and fall
46 // back to its normal start sequence — boot is unaffected).
47 // eslint-disable-next-line @typescript-eslint/no-explicit-any
48 const socket = await (Bun as any).udpSocket({
49 socket: {
50 data() {
51 /* unused */
52 },
53 drain() {
54 /* unused */
55 },
56 error() {
57 /* swallow */
58 },
59 },
60 connect: { hostname: socketPath, port: 0 },
61 });
62 return socket as ReturnType<DatagramOpener> extends Promise<infer T>
63 ? T
64 : never;
65};
66
67/**
68 * Send `READY=1\n` to systemd's notification socket if one is configured.
69 *
70 * - No-op when `NOTIFY_SOCKET` is unset (dev / non-systemd).
71 * - Never throws. Errors are intentionally swallowed.
72 *
73 * @param opts.openDatagram Optional injected socket factory (tests).
74 * @param opts.env Optional env-bag (defaults to `process.env`).
75 */
76export async function notifySystemdReady(
77 opts: {
78 openDatagram?: DatagramOpener;
79 env?: NodeJS.ProcessEnv;
80 } = {}
81): Promise<void> {
82 const env = opts.env ?? process.env;
83 const socketPath = env.NOTIFY_SOCKET;
84 if (!socketPath || socketPath.length === 0) {
85 // Dev / non-systemd host. Silent no-op.
86 return;
87 }
88
89 const opener = opts.openDatagram ?? defaultOpener;
90
91 try {
92 const socket = await opener(socketPath);
93 try {
94 socket.send("READY=1\n", 0, socketPath);
95 } catch {
96 // ignore — best effort
97 }
98 try {
99 socket.close();
100 } catch {
101 // ignore — best effort
102 }
103 } catch {
104 // Socket couldn't be opened. Most likely the runtime doesn't support
105 // AF_UNIX datagrams via Bun.udpSocket on this host, or the socket path
106 // is stale. Either way, boot must continue.
107 return;
108 }
109}
110
111export const __test = { defaultOpener };