collin/browser-terminal-extension · 57d73a66
Stop termbridge reload from killing tmux sessions
Collin Richards · 2026-08-18 07:01 UTC · 57d73a662e4db4dddb85be4f8e708231436a9b11 · parent 5114c62f · browse files
modifieddaemon/Cargo.lock+1 −0
| ⋯ 792 unchanged lines | |||
| 793 | 793 | dependencies = [ | |
| 794 | 794 | "futures-util", | |
| 795 | 795 | "getrandom 0.4.3", | |
| 796 | + | "libc", | |
| 796 | 797 | "portable-pty", | |
| 797 | 798 | "rcgen", | |
| 798 | 799 | "serde", | |
| ⋯ 326 unchanged lines | |||
modifieddaemon/Cargo.toml+1 −0
| ⋯ 5 unchanged lines | |||
| 6 | 6 | [dependencies] | |
| 7 | 7 | futures-util = { version = "0.3.34", default-features = false, features = ["std", "sink"] } | |
| 8 | 8 | getrandom = "0.4.3" | |
| 9 | + | libc = "0.2.189" | |
| 9 | 10 | portable-pty = "0.9.0" | |
| 10 | 11 | rcgen = { version = "0.14.9", default-features = false, features = ["pem", "ring"] } | |
| 11 | 12 | serde = { version = "1.0.229", features = ["derive"] } | |
| ⋯ 9 unchanged lines | |||
modifieddaemon/src/activation.rs+21 −0
| ⋯ 37 unchanged lines | |||
| 38 | 38 | TcpListener::from_raw_fd(LISTEN_FDS_START) | |
| 39 | 39 | }; | |
| 40 | 40 | ||
| 41 | + | // systemd clears FD_CLOEXEC on fds it hands over, so they survive its own | |
| 42 | + | // exec into this binary — but that leaves it cleared here too. Uncleared, | |
| 43 | + | // this fd would survive our exec of tmux the same way, and every process | |
| 44 | + | // tmux ever spawns would inherit a handle on the listening socket. systemd | |
| 45 | + | // could then never rebind the port after this daemon exits, which is | |
| 46 | + | // exactly the "Address already in use" `termbridge reload` must not hit. | |
| 47 | + | set_cloexec(&listener)?; | |
| 48 | + | ||
| 41 | 49 | // Same invariant the self-bound path asserts. A unit file with | |
| 42 | 50 | // `ListenStream=0.0.0.0:7681` would otherwise silently expose a shell to | |
| 43 | 51 | // the network, and the unit is a file the user can edit. | |
| ⋯ 40 unchanged lines | |||
| 84 | 92 | std::io::Error::new(std::io::ErrorKind::InvalidInput, msg) | |
| 85 | 93 | } | |
| 86 | 94 | ||
| 95 | + | fn set_cloexec(listener: &TcpListener) -> std::io::Result<()> { | |
| 96 | + | use std::os::fd::AsRawFd; | |
| 97 | + | let fd = listener.as_raw_fd(); | |
| 98 | + | let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; | |
| 99 | + | if flags < 0 { | |
| 100 | + | return Err(std::io::Error::last_os_error()); | |
| 101 | + | } | |
| 102 | + | if unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 { | |
| 103 | + | return Err(std::io::Error::last_os_error()); | |
| 104 | + | } | |
| 105 | + | Ok(()) | |
| 106 | + | } | |
| 107 | + | ||
| 87 | 108 | #[cfg(test)] | |
| 88 | 109 | mod tests { | |
| 89 | 110 | use super::*; | |
| ⋯ 39 unchanged lines | |||
modifieddaemon/src/install.rs+7 −5
| ⋯ 141 unchanged lines | |||
| 142 | 142 | # Stopping this unit must stop this daemon and nothing else. On a\n\ | |
| 143 | 143 | # machine with no tmux server running, *we* are what starts one, and a\n\ | |
| 144 | 144 | # process forked from here keeps this cgroup for life — reparenting to\n\ | |
| 145 | - | # systemd when tmux daemonises does not move it out. So under the\n\ | |
| 146 | - | # default KillMode=control-group, `termbridge reload` would signal the\n\ | |
| 147 | - | # user's tmux server and every pane in it, which is the one thing this\n\ | |
| 148 | - | # daemon promises never to touch.\n\ | |
| 149 | - | KillMode=mixed\n", | |
| 145 | + | # systemd when tmux daemonises via setsid does not move it out. Under\n\ | |
| 146 | + | # the default KillMode=control-group, and even under KillMode=mixed —\n\ | |
| 147 | + | # which still SIGKILLs every remaining cgroup process the moment this\n\ | |
| 148 | + | # daemon's main process exits, per systemd.kill(5) — `termbridge\n\ | |
| 149 | + | # reload` would kill the user's tmux server and every pane in it. Only\n\ | |
| 150 | + | # KillMode=process leaves the rest of the cgroup alone.\n\ | |
| 151 | + | KillMode=process\n", | |
| 150 | 152 | exe = exe.display(), | |
| 151 | 153 | idle = opts.idle_secs, | |
| 152 | 154 | ) | |
| ⋯ 294 unchanged lines | |||