Commit 0b44313
Eric Bower
·
2026-07-30 11:41:38 -0400 EDT
parent 6171338
docs: daemonize
1 files changed,
+35,
-14
+35,
-14
| ... | ... | @@ -118,30 +118,51 @@ pub fn spawnPty(sesh_name: []const u8, cmd: Cmd) !PtyInfo { | |
| 118 | 118 | } | |
| 119 | 119 | ||
| 120 | 120 | /// daemonize is the first fork in a double-fork technique to create a | |
| 121 | - | /// completely disconnected session (group of processes). | |
| 121 | + | /// completely disconnected session (container of process groups). | |
| 122 | 122 | /// | |
| 123 | 123 | /// When launching a daemon, you normally set the child process of the fork to | |
| 124 | - | /// be the session leader via setsid() which creates a new session that does | |
| 125 | - | /// *not* have a controlling terminal. This is important because we don't want | |
| 126 | - | /// a controlling terminal for our daemon or else our daemon could receive | |
| 127 | - | /// signals to shutdown when the controlling terminal closes. | |
| 124 | + | /// be the session leader via setsid() which creates a new session that removes | |
| 125 | + | /// the current controlling terminal but creates the authority to grab a new | |
| 126 | + | /// one. This is important because we don't want a controlling terminal for our | |
| 127 | + | /// daemon or else our daemon could receive signals to shutdown when the | |
| 128 | + | /// controlling terminal closes. | |
| 128 | 129 | /// | |
| 129 | 130 | /// However, if the first fork's child process is also the daemon process, then | |
| 130 | - | /// it's technically possible for the daemon to open a terminal device | |
| 131 | - | /// (e.g. open("/dev/console", O_RDWR)) and then it would acquire a controlling | |
| 131 | + | /// it's technically possible for the daemon to open a terminal device (e.g. | |
| 132 | + | /// open("/dev/console", O_RDWR)) and then it would acquire a controlling | |
| 132 | 133 | /// terminal! A controlling terminal would expose the daemon to | |
| 133 | 134 | /// terminal-generated signals (e.g. SIGINT) or SIGHUP from terminal disconnect | |
| 134 | 135 | /// which could kill the daemon. | |
| 135 | 136 | /// | |
| 136 | - | /// By forking a second time, the grandchild process (the daemon) is not the | |
| 137 | - | /// session leader. Per POSIX, only a process that is the session leader can | |
| 138 | - | /// acquire a controlling terminal. | |
| 137 | + | /// The first fork isn't arbitrary either: setsid() fails with EPERM if the | |
| 138 | + | /// caller is already a process group leader, which a process launched directly | |
| 139 | + | /// from a shell typically is. The first fork produces a child guaranteed not | |
| 140 | + | /// to be a group leader, so setsid() will succeed. By forking a second time, | |
| 141 | + | /// the grandchild process (the daemon) is not the session leader. Per POSIX, | |
| 142 | + | /// only a process that is the session leader can acquire a controlling terminal. | |
| 139 | 143 | /// | |
| 140 | - | /// Apparently this is considered "being paranoid" but appears to be a standard | |
| 141 | - | /// practice for daemons so we're doing it anyway. | |
| 144 | + | /// Apparently this is "a bit paranoid," and on Linux it is arguable since a | |
| 145 | + | /// session leader only acquires a controlling terminal under | |
| 146 | + | /// implementation-defined conditions. But the double-fork is the portable way | |
| 147 | + | /// to guarantee the daemon can never acquire one, regardless of how a given | |
| 148 | + | /// POSIX implementation behaves. So we baked it into zmx. | |
| 142 | 149 | /// | |
| 143 | - | /// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11_01_03 | |
| 144 | - | /// https://stackoverflow.com/a/16317668 | |
| 150 | + | /// PID=42 SID=10 PGID=10 ← original (PG leader, has tty) | |
| 151 | + | /// │ | |
| 152 | + | /// │ fork #1 | |
| 153 | + | /// ├──────────┐ | |
| 154 | + | /// │ exit │ PID=55 SID=10 PGID=10 (not a PG leader) | |
| 155 | + | /// ✝ │ | |
| 156 | + | /// │ setsid() | |
| 157 | + | /// ▼ | |
| 158 | + | /// PID=55 SID=55 PGID=55 (session leader, no tty) | |
| 159 | + | /// │ | |
| 160 | + | /// │ fork #2 | |
| 161 | + | /// ├──────────┐ | |
| 162 | + | /// │ exit │ PID=73 SID=55 PGID=55 | |
| 163 | + | /// ✝ │ PID≠SID → can't get a tty | |
| 164 | + | /// ▼ | |
| 165 | + | /// DAEMON ✓ | |
| 145 | 166 | pub fn daemonize(sesh_name: []const u8, cmd: Cmd, keep_fds_open: []i32) !PtyInfo { | |
| 146 | 167 | // creates the daemon | |
| 147 | 168 | const pid = try lib_posix.fork(); |