atagen

roundabout

public
Star0
Watch0

Commit 61c6e94355d9

tarball

atagen <boss@atagen.co>2026-09-27 03:59 UTC

2 files changed+49-9
daemon: take a socket from systemd and never outlive a wiped runtime dir

Socket activation lets systemd hold the socket across daemon restarts, so a client knocking while the daemon idles out just waits instead of racing a respawn. The idle exit also needs the spawn lock, and it opened that without create, so deleting the runtime dir left a daemon idling forever with no way out. Config falls back to /etc/rabt/config.toml so a system can set defaults for every user.
Modified file: src/daemon.rs+39 additions−5 deletions
@@ -1,10 +1,16 @@
11 use std::{
22 collections::BTreeMap,
33 fs, io,
4- os::unix::net::{UnixListener, UnixStream},
4+ os::{
5+ fd::FromRawFd,
6+ unix::net::{UnixListener, UnixStream},
7+ },
58 path::PathBuf,
69 process,
7- sync::{Arc, Mutex, MutexGuard},
10+ sync::{
11+ Arc, Mutex, MutexGuard,
12+ atomic::{AtomicBool, Ordering},
13+ },
814 thread,
915 time::{Duration, Instant},
1016 };
@@ -489,6 +495,11 @@ fn tick(shared: &Shared) {
489495 st.fire_overdue();
490496 if st.conns == 0 && st.leases.is_empty() && st.idle_since.elapsed() >= st.cfg.queue.linger.0
491497 {
498+ // systemd owns the socket and queues whoever knocks while we're
499+ // leaving, so there's nothing to tidy
500+ if ACTIVATED.load(Ordering::Relaxed) {
501+ process::exit(0);
502+ }
492503 // a spawner holds this while it clears a stale socket, so taking it
493504 // here stops us unlinking our successor's socket on the way out
494505 if let Some(_guard) = try_spawn_lock() {
@@ -558,15 +569,30 @@ fn watch_config(shared: &Shared) {
558569 }
559570 }
560571
572+/// creates the file if it's gone, else a wiped runtime dir leaves us idling
573+/// forever unable to take the lock we need to exit
561574 fn try_spawn_lock() -> Option<client::Flock> {
562575 let file = fs::OpenOptions::new()
576+ .create(true)
577+ .truncate(false)
563578 .write(true)
564579 .open(paths::spawn_lock())
565580 .ok()?;
566581 client::Flock::try_exclusive(file)
567582 }
568583
569-pub fn main(cfg: Config) -> ! {
584+static ACTIVATED: AtomicBool = AtomicBool::new(false);
585+
586+fn inherited() -> Option<UnixListener> {
587+ let ours = std::env::var("LISTEN_PID").ok()?.parse::<u32>().ok()? == process::id();
588+ if !ours || std::env::var("LISTEN_FDS").ok()? != "1" {
589+ return None;
590+ }
591+ // SAFETY: sd_listen_fds protocol, fd 3 is the listening socket
592+ Some(unsafe { UnixListener::from_raw_fd(3) })
593+}
594+
595+fn bind() -> UnixListener {
570596 // fork once more so the client that spawned us can reap its child now
571597 // instead of carrying a zombie for the length of its build
572598 match unsafe { libc::fork() } {
@@ -577,8 +603,7 @@ pub fn main(cfg: Config) -> ! {
577603 0 => {}
578604 _ => process::exit(0),
579605 }
580-
581- let listener = match UnixListener::bind(paths::socket()) {
606+ match UnixListener::bind(paths::socket()) {
582607 Ok(l) => l,
583608 Err(e) if e.kind() == io::ErrorKind::AddrInUse && client::connect().is_some() => {
584609 process::exit(0)
@@ -587,6 +612,15 @@ pub fn main(cfg: Config) -> ! {
587612 eprintln!("roundabout: bind: {e}");
588613 process::exit(1);
589614 }
615+ }
616+}
617+
618+pub fn main(cfg: Config) -> ! {
619+ let listener = if let Some(l) = inherited() {
620+ ACTIVATED.store(true, Ordering::Relaxed);
621+ l
622+ } else {
623+ bind()
590624 };
591625
592626 let shared: Shared = Arc::new(Mutex::new(State::new(cfg)));
Modified file: src/paths.rs+10 additions−4 deletions
@@ -16,10 +16,16 @@ fn xdg(var: &str, fallback: &str) -> PathBuf {
1616 }
1717
1818 pub fn config_file() -> PathBuf {
19- env::var_os("ROUNDABOUT_CONFIG").map_or_else(
20- || xdg("XDG_CONFIG_HOME", ".config").join("roundabout/config.toml"),
21- PathBuf::from,
22- )
19+ if let Some(p) = env::var_os("ROUNDABOUT_CONFIG") {
20+ return p.into();
21+ }
22+ let user = xdg("XDG_CONFIG_HOME", ".config").join("roundabout/config.toml");
23+ let system = PathBuf::from("/etc/roundabout/config.toml");
24+ if !user.exists() && system.exists() {
25+ system
26+ } else {
27+ user
28+ }
2329 }
2430
2531 pub fn state_dir() -> PathBuf {