Commit 0b44313

Eric Bower  ·  2026-07-30 11:41:38 -0400 EDT
parent 6171338
docs: daemonize
1 files changed,  +35, -14
+35, -14
 1@@ -118,30 +118,51 @@ pub fn spawnPty(sesh_name: []const u8, cmd: Cmd) !PtyInfo {
 2 }
 3 
 4 /// daemonize is the first fork in a double-fork technique to create a
 5-/// completely disconnected session (group of processes).
 6+/// completely disconnected session (container of process groups).
 7 ///
 8 /// When launching a daemon, you normally set the child process of the fork to
 9-/// be the session leader via setsid() which creates a new session that does
10-/// *not* have a controlling terminal. This is important because we don't want
11-/// a controlling terminal for our daemon or else our daemon could receive
12-/// signals to shutdown when the controlling terminal closes.
13+/// be the session leader via setsid() which creates a new session that removes
14+/// the current controlling terminal but creates the authority to grab a new
15+/// one. This is important because we don't want a controlling terminal for our
16+/// daemon or else our daemon could receive signals to shutdown when the
17+/// controlling terminal closes.
18 ///
19 /// However, if the first fork's child process is also the daemon process, then
20-/// it's technically possible for the daemon to open a terminal device
21-/// (e.g. open("/dev/console", O_RDWR)) and then it would acquire a controlling
22+/// it's technically possible for the daemon to open a terminal device (e.g.
23+/// open("/dev/console", O_RDWR)) and then it would acquire a controlling
24 /// terminal! A controlling terminal would expose the daemon to
25 /// terminal-generated signals (e.g. SIGINT) or SIGHUP from terminal disconnect
26 /// which could kill the daemon.
27 ///
28-/// By forking a second time, the grandchild process (the daemon) is not the
29-/// session leader. Per POSIX, only a process that is the session leader can
30-/// acquire a controlling terminal.
31+/// The first fork isn't arbitrary either: setsid() fails with EPERM if the
32+/// caller is already a process group leader, which a process launched directly
33+/// from a shell typically is. The first fork produces a child guaranteed not
34+/// to be a group leader, so setsid() will succeed.  By forking a second time,
35+/// the grandchild process (the daemon) is not the session leader. Per POSIX,
36+/// only a process that is the session leader can acquire a controlling terminal.
37 ///
38-/// Apparently this is considered "being paranoid" but appears to be a standard
39-/// practice for daemons so we're doing it anyway.
40+/// Apparently this is "a bit paranoid," and on Linux it is arguable since a
41+/// session leader only acquires a controlling terminal under
42+/// implementation-defined conditions. But the double-fork is the portable way
43+/// to guarantee the daemon can never acquire one, regardless of how a given
44+/// POSIX implementation behaves. So we baked it into zmx.
45 ///
46-/// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11_01_03
47-/// https://stackoverflow.com/a/16317668
48+/// PID=42  SID=10  PGID=10  ← original (PG leader, has tty)
49+///     │
50+///     │  fork #1
51+///     ├──────────┐
52+///     │ exit     │ PID=55  SID=10  PGID=10  (not a PG leader)
53+///     ✝          │
54+///                │ setsid()
55+///                ▼
56+///               PID=55  SID=55  PGID=55  (session leader, no tty)
57+///                 │
58+///                 │  fork #2
59+///                 ├──────────┐
60+///                 │ exit     │ PID=73  SID=55  PGID=55
61+///                 ✝          │ PID≠SID → can't get a tty
62+///                            ▼
63+///                          DAEMON ✓
64 pub fn daemonize(sesh_name: []const u8, cmd: Cmd, keep_fds_open: []i32) !PtyInfo {
65     // creates the daemon
66     const pid = try lib_posix.fork();