anvilsign in

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
793793 dependencies = [
794794 "futures-util",
795795 "getrandom 0.4.3",
796+ "libc",
796797 "portable-pty",
797798 "rcgen",
798799 "serde",
⋯ 326 unchanged lines
modifieddaemon/Cargo.toml+1 −0
⋯ 5 unchanged lines
66 [dependencies]
77 futures-util = { version = "0.3.34", default-features = false, features = ["std", "sink"] }
88 getrandom = "0.4.3"
9+libc = "0.2.189"
910 portable-pty = "0.9.0"
1011 rcgen = { version = "0.14.9", default-features = false, features = ["pem", "ring"] }
1112 serde = { version = "1.0.229", features = ["derive"] }
⋯ 9 unchanged lines
modifieddaemon/src/activation.rs+21 −0
⋯ 37 unchanged lines
3838 TcpListener::from_raw_fd(LISTEN_FDS_START)
3939 };
4040
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+
4149 // Same invariant the self-bound path asserts. A unit file with
4250 // `ListenStream=0.0.0.0:7681` would otherwise silently expose a shell to
4351 // the network, and the unit is a file the user can edit.
⋯ 40 unchanged lines
8492 std::io::Error::new(std::io::ErrorKind::InvalidInput, msg)
8593 }
8694
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+
87108 #[cfg(test)]
88109 mod tests {
89110 use super::*;
⋯ 39 unchanged lines
modifieddaemon/src/install.rs+7 −5
⋯ 141 unchanged lines
142142 # Stopping this unit must stop this daemon and nothing else. On a\n\
143143 # machine with no tmux server running, *we* are what starts one, and a\n\
144144 # 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",
150152 exe = exe.display(),
151153 idle = opts.idle_secs,
152154 )
⋯ 294 unchanged lines