Skip to main content

borg/
reexec.rs

1//! A long-lived process follows a reinstall in place: `cargo install` renames a new file over the old,
2//! and an old image would go on running the code it was started with.
3//!
4//! - A session's waiter (listen.rs) re-execs its own path (`Image::reexec`), same pid, same stdout (a
5//!   plugin's JSON line protocol and Claude Code's background task survive), its lock's open file
6//!   description passed on (`ADOPT_FD`), so the lock is never free between the images and no second
7//!   waiter starts meanwhile. SIGINT, SIGHUP and SIGTERM are blocked across the exec and seen by the new
8//!   image once it trapped them (`inherited`): none is lost. Its only other threads (runner.rs
9//!   `run_bounded`'s readers) start with them blocked too.
10//!
11//! The mesh daemon (borg-mesh main.rs `serve`) follows a reinstall of borg-mesh in place: it lives as
12//! long as the machine serves, and `cargo install` renames a new file over the old. Once its image is
13//! stale (`Image::due`) it stops as on Ctrl-C, and `Image::restart` execs the new borg-mesh in the same
14//! process (`trap_stops`, `stopped`: a stop during it is held for the new image, which ends on it before
15//! it serves). Only one that says it takes such a restart (`probed`) is exec'd: one built before it
16//! could (a downgrade) would never unblock the stops, so no Ctrl-C or SIGTERM would ever end it. The
17//! daemon exits 1 with a message then, to be started again.
18//!
19//! The identity is that of the path the process was started by (argv[0] resolved as execvp does), not
20//! of `/proc/self/exe`: a wrapper or shim is the file that is exec'd, so comparing it with the binary it
21//! starts would differ forever and re-exec in a loop. A running binary deleted from under a path that
22//! stays (a symlinked install whose old target went) counts too: its link count is 0.
23
24use std::ffi::{CString, OsString};
25use std::os::unix::ffi::OsStrExt;
26use std::os::unix::fs::MetadataExt;
27use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
28use std::path::{Path, PathBuf};
29use std::sync::OnceLock;
30
31/// The lock fd a re-exec'd waiter inherits from its previous image (`Image::reexec`, `adopt`). Never set
32/// by an engineer: removed from the environment first thing, so no child sees it.
33const ADOPT_FD: &str = "BORG_LISTEN_FD";
34
35/// How often an exec of a busy file (`ETXTBSY`) is tried, 25 ms apart, the stop signals held meanwhile.
36/// Guard: half a second for an installer still closing the new binary, then the old image keeps
37/// running rather than retrying forever with Ctrl-C held off.
38const BUSY_TRIES: u32 = 20;
39
40/// Set in the environment of a process `Image::restart` exec'd: its stops are blocked until
41/// `trap_stops` trapped them. Removed there, so no child sees it.
42const RESTARTED: &str = "BORG_RESTARTED";
43
44/// Set in the environment of `restart`'s probe of the file it would exec (run as `<path> --help`, which
45/// every borg-mesh answers at once): one that takes a restart prints `PROBED` instead (`probed`).
46const PROBE: &str = "BORG_RESTART_PROBE";
47const PROBED: &str = "takes a restart in place";
48
49/// How long `restart` waits for its probe's answer. Guard: a new file that hangs on `--help` is not
50/// exec'd, and the running image goes on.
51const PROBE_WAIT: std::time::Duration = std::time::Duration::from_secs(10);
52
53/// How long `restart` waits for the process's other threads to end (Linux), so a stop can reach
54/// no thread but the one that holds it blocked. Guard: a thread that never ends delays the restart
55/// by this much, never blocks it.
56const ALONE_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
57
58/// What makes one executable file this one: a rename, a copy or a rebuild changes one of them.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60struct Id {
61    dev: u64,
62    ino: u64,
63    size: u64,
64    mtime_ns: i128,
65}
66
67fn id_of(m: &std::fs::Metadata) -> Id {
68    Id { dev: m.dev(), ino: m.ino(), size: m.size(), mtime_ns: i128::from(m.mtime()) * 1_000_000_000 + i128::from(m.mtime_nsec()) }
69}
70
71/// The identity of the regular executable at `p` (symlinks followed); None for anything else.
72fn exec_id(p: &Path) -> Option<Id> {
73    let m = std::fs::metadata(p).ok()?;
74    (m.is_file() && m.mode() & 0o111 != 0).then(|| id_of(&m))
75}
76
77/// The path this process was started by: argv[0] as execvp resolves it (a word with a `/` against the
78/// working directory, a bare word on PATH), never canonicalized, so a wrapper or a symlinked install is
79/// the file named; else the running binary with Linux's ` (deleted)` cut off; else `borg`. What a
80/// long-lived process starts borg as and prints commands with: `current_exe()` of a replaced binary
81/// reads `<path> (deleted)`.
82pub(crate) fn exe_path() -> PathBuf {
83    static P: OnceLock<PathBuf> = OnceLock::new();
84    P.get_or_init(|| from_argv0(std::env::args_os().next(), std::env::var_os("PATH")).unwrap_or_else(running)).clone()
85}
86
87fn from_argv0(arg0: Option<OsString>, path: Option<OsString>) -> Option<PathBuf> {
88    let a = PathBuf::from(arg0.filter(|a| !a.is_empty())?);
89    if a.as_os_str().as_bytes().contains(&b'/') {
90        return Some(if a.is_absolute() { a } else { std::env::current_dir().ok()?.join(a) });
91    }
92    std::env::split_paths(&path?).filter(|d| d.is_absolute()).map(|d| d.join(&a)).find(|p| exec_id(p).is_some())
93}
94
95fn running() -> PathBuf {
96    std::env::current_exe().map_or_else(|_| PathBuf::from("borg"), |e| undeleted(&e))
97}
98
99/// `p` without the ` (deleted)` Linux appends to the link of a running binary that was replaced.
100fn undeleted(p: &Path) -> PathBuf {
101    let b = p.as_os_str().as_bytes();
102    PathBuf::from(std::ffi::OsStr::from_bytes(b.strip_suffix(b" (deleted)").unwrap_or(b)))
103}
104
105/// Why `Image::reexec` did not replace the image.
106#[derive(Debug)]
107pub enum ReexecError {
108    /// The lock's fd could not be duplicated for the new image.
109    Dup(std::io::Error),
110    /// The exec of this path failed.
111    Exec(PathBuf, std::io::Error),
112}
113
114impl std::fmt::Display for ReexecError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            ReexecError::Dup(e) => write!(f, "the lock could not be passed on: {e}"),
118            ReexecError::Exec(path, e) => write!(f, "{}: {e}", path.display()),
119        }
120    }
121}
122
123impl std::error::Error for ReexecError {
124    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
125        match self {
126            ReexecError::Dup(e) | ReexecError::Exec(_, e) => Some(e),
127        }
128    }
129}
130
131/// What a long-lived process's image was started from (`now`, at its start): whether it is the binary
132/// installed now.
133pub struct Image {
134    path: PathBuf,
135    base: Option<Id>,
136    /// The running binary, opened at the start (Linux): its link count 0 says it was deleted.
137    running: Option<std::fs::File>,
138    /// The identities of the path an exec failed on: not tried again until the path changes.
139    failed: Vec<Option<Id>>,
140}
141
142impl Image {
143    /// This process's image. In this crate's unit tests an image of no file, never stale or due: their
144    /// binary is the shared target's, which another `cargo test` may rebuild while a waiter test runs
145    /// (listen.rs `Waiter`), and a re-exec would replace the test runner with it.
146    pub fn now() -> Image {
147        if cfg!(test) {
148            return Image::of(PathBuf::new(), None);
149        }
150        Image::of(exe_path(), std::fs::File::open("/proc/self/exe").ok())
151    }
152
153    fn of(path: PathBuf, running: Option<std::fs::File>) -> Image {
154        let base = exec_id(&path);
155        Image { path, base, running, failed: Vec::new() }
156    }
157
158    /// The path names another file than at the start (or none), or the running binary was deleted: this
159    /// image is old code.
160    pub fn stale(&self) -> bool {
161        exec_id(&self.path) != self.base || self.running.as_ref().is_some_and(|f| f.metadata().is_ok_and(|m| m.nlink() == 0))
162    }
163
164    /// Stale, and the path holds an executable no exec failed on: a re-exec is worth trying.
165    pub fn due(&self) -> bool {
166        let now = exec_id(&self.path);
167        now.is_some() && !self.failed.contains(&now) && self.stale()
168    }
169
170    /// For a waiter (listen.rs): replaces this process's image by the file at its path, same pid, argv
171    /// and environment, stdout and stderr flushed, the stop signals blocked across (`stopped` looked at
172    /// once more after they are), `lock`'s open file description passed on as a duplicate without
173    /// close-on-exec (`ADOPT_FD`). Returns only when it did not happen: Ok when a stop came (the caller
174    /// ends as stopped), Err with why the exec failed (the path is not tried again until it changes).
175    ///
176    /// # Errors
177    /// The dup or the exec failed (`ReexecError`).
178    pub fn reexec(&mut self, lock: &std::fs::File, stopped: impl Fn() -> bool) -> Result<(), ReexecError> {
179        use std::io::Write;
180        let _ = std::io::stdout().flush();
181        let _ = std::io::stderr().flush();
182        let target = exec_id(&self.path);
183        let set = stop_set();
184        // SAFETY: pthread_sigmask(3) on a set built by sigemptyset/sigaddset.
185        unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &raw const set, std::ptr::null_mut()) };
186        let unblock = || {
187            // SAFETY: as above.
188            unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const set, std::ptr::null_mut()) };
189        };
190        if stopped() {
191            unblock();
192            return Ok(());
193        }
194        // SAFETY: fcntl(2) F_DUPFD on the lock's own open fd: a new fd, close-on-exec clear.
195        let dup = unsafe { libc::fcntl(lock.as_raw_fd(), libc::F_DUPFD, 3) };
196        let err = if dup < 0 {
197            ReexecError::Dup(std::io::Error::last_os_error())
198        } else {
199            let e = execve_retrying(&self.path, ADOPT_FD, &dup.to_string());
200            // SAFETY: close(2) of the duplicate made above, which nothing else owns.
201            unsafe { libc::close(dup) };
202            ReexecError::Exec(self.path.clone(), e)
203        };
204        unblock();
205        self.failed.push(target);
206        if stopped() { Ok(()) } else { Err(err) }
207    }
208
209    /// For the mesh daemon, once it stopped serving: replaces its image by the file at its path, stdout
210    /// and stderr flushed, same pid, argv and environment, with SIGINT, SIGHUP and SIGTERM blocked
211    /// across; a stop `trap_stops` saw first returns Ok (the caller ends as stopped), one that comes
212    /// later stays pending for the new image (`trap_stops` there). Only a
213    /// file whose probe says it unblocks them there (`takes_restart`) is exec'd. On Linux it first
214    /// waits up to `ALONE_WAIT` for its other threads to end, so none can take a stop meanwhile (the
215    /// daemon's are gone once its runtime shut down). Err with what the engineer does now.
216    pub fn restart(&mut self) -> Result<(), String> {
217        use std::io::Write;
218        let _ = std::io::stdout().flush();
219        let _ = std::io::stderr().flush();
220        let target = exec_id(&self.path);
221        let set = stop_set();
222        // SAFETY: pthread_sigmask(3) on a set built by sigemptyset/sigaddset.
223        unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()) };
224        let unblock = || {
225            // SAFETY: as above.
226            unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &set, std::ptr::null_mut()) };
227        };
228        let takes = takes_restart(&self.path) && exec_id(&self.path) == target;
229        if takes {
230            alone();
231        }
232        if stopped() {
233            unblock();
234            return Ok(());
235        }
236        let err = takes.then(|| execve_retrying(&self.path, RESTARTED, "1"));
237        unblock();
238        // The exec did not happen: the path is not tried again until it changes.
239        self.failed.push(target);
240        match err {
241            // A stop held across the probe (a Ctrl-C at the terminal reached the probe too): the
242            // caller ends as stopped.
243            _ if stopped() => Ok(()),
244            None => Err(format!(
245                "{} cannot take over a running `borg mesh serve` (a borg-mesh built before it could, or one that changed meanwhile): start `borg mesh serve` again",
246                self.path.display()
247            )),
248            Some(why) => {
249                Err(format!("could not run the reinstalled borg-mesh ({}: {why}): install it again, then start `borg mesh serve`", self.path.display()))
250            }
251        }
252    }
253}
254
255/// execve(2) of `path` with this process's argv and environment, `var` set to `value` in it (any value
256/// it had dropped); a busy file (`ETXTBSY`) tried again `BUSY_TRIES` times. Returns only on failure,
257/// with why.
258fn execve_retrying(path: &Path, var: &str, value: &str) -> std::io::Error {
259    let cstr = |b: &[u8]| CString::new(b).ok();
260    let file = cstr(path.as_os_str().as_bytes());
261    let argv: Option<Vec<CString>> = std::env::args_os().map(|a| cstr(a.as_bytes())).collect();
262    let envp: Option<Vec<CString>> = std::env::vars_os()
263        .filter(|(k, _)| k != var)
264        .map(|(k, v)| cstr(&[k.as_bytes(), b"=", v.as_bytes()].concat()))
265        .chain([cstr(format!("{var}={value}").as_bytes())])
266        .collect();
267    let (Some(file), Some(argv), Some(envp)) = (file, argv, envp) else { return std::io::Error::from(std::io::ErrorKind::InvalidInput) };
268    let ptrs = |v: &[CString]| v.iter().map(|c| c.as_ptr()).chain([std::ptr::null()]).collect::<Vec<_>>();
269    let (argv, envp) = (ptrs(&argv), ptrs(&envp));
270    let mut tries = 0;
271    loop {
272        // SAFETY: execve(2) with NUL-terminated strings and NULL-terminated arrays that outlive the
273        // call; it returns only on failure.
274        unsafe { libc::execve(file.as_ptr(), argv.as_ptr(), envp.as_ptr()) };
275        let e = std::io::Error::last_os_error();
276        // A file still open for writing somewhere (a fork that has not exec'd yet, a copy not closed
277        // yet) is busy for a moment only.
278        tries += 1;
279        if e.raw_os_error() != Some(libc::ETXTBSY) || tries == BUSY_TRIES {
280            return e;
281        }
282        std::thread::sleep(std::time::Duration::from_millis(25));
283    }
284}
285
286/// Whether the file at `path`, run as `<path> --help` with `PROBE` set, says `PROBED` within
287/// `PROBE_WAIT`: it is a borg-mesh that unblocks the stops `restart` holds across its exec. A borg-mesh
288/// built before it could prints its help; a shim passes the probe on to what it runs.
289fn takes_restart(path: &Path) -> bool {
290    use std::process::{Command, Stdio};
291    let spawn = || Command::new(path).arg("--help").env(PROBE, "1").stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn();
292    // A busy file (`ETXTBSY`, as for the exec after) is tried again; the child's signal mask is
293    // reset by std, so the stops `restart` holds blocked reach it.
294    let mut tries = 1;
295    let mut child = loop {
296        match spawn() {
297            Ok(c) => break c,
298            Err(e) if e.raw_os_error() == Some(libc::ETXTBSY) && tries < BUSY_TRIES => {
299                tries += 1;
300                std::thread::sleep(std::time::Duration::from_millis(25));
301            }
302            Err(_) => return false,
303        }
304    };
305    let t0 = std::time::Instant::now();
306    let done = loop {
307        match child.try_wait() {
308            Ok(Some(status)) => break status.success(),
309            Ok(None) if t0.elapsed() < PROBE_WAIT => std::thread::sleep(std::time::Duration::from_millis(10)),
310            _ => {
311                let _ = child.kill();
312                let _ = child.wait();
313                break false;
314            }
315        }
316    };
317    let mut out = String::new();
318    done && child.stdout.take().is_some_and(|mut o| std::io::Read::read_to_string(&mut o, &mut out).is_ok()) && out.trim() == PROBED
319}
320
321/// The first step of a process that may be `restart`ed into (borg-mesh `main`): true when this run is
322/// `restart`'s probe, answered here (the caller then ends at once).
323pub fn probed() -> bool {
324    if std::env::var_os(PROBE).is_none() {
325        return false;
326    }
327    println!("{PROBED}");
328    true
329}
330
331/// Waits up to `ALONE_WAIT` until this process runs one thread (Linux; elsewhere at once).
332fn alone() {
333    let t0 = std::time::Instant::now();
334    let threads = || std::fs::read_dir("/proc/self/task").map_or(1, Iterator::count);
335    while threads() > 1 && t0.elapsed() < ALONE_WAIT {
336        std::thread::sleep(std::time::Duration::from_millis(10));
337    }
338}
339
340/// A stop (SIGINT, SIGTERM) a process that `restart`s saw: `trap_stops`'s handler sets it.
341static STOPPED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
342
343extern "C" fn on_stop(_: libc::c_int) {
344    STOPPED.store(true, std::sync::atomic::Ordering::SeqCst);
345}
346
347/// The first step of a process that may `restart` (the mesh daemon), while it runs one thread: SIGINT
348/// and SIGTERM trapped (`stopped` says whether one came), and, when a previous image restarted it,
349/// the stop signals unblocked only then, so a stop that came across the exec is seen now (a SIGHUP
350/// then ends it, as it would have the old one).
351pub fn trap_stops() {
352    on_signals(&[libc::SIGINT, libc::SIGTERM], on_stop);
353    if std::env::var_os(RESTARTED).is_some() {
354        // SAFETY: called while the process runs one thread (the doc above); no other reads the
355        // environment.
356        unsafe { std::env::remove_var(RESTARTED) };
357        unblock_stops();
358    }
359}
360
361/// `handler` for each of `sigs`, by sigaction(2): it stays installed across deliveries, and a call it
362/// interrupts restarts. `handler` must be async-signal-safe (store to an atomic, no more).
363pub fn on_signals(sigs: &[libc::c_int], handler: extern "C" fn(libc::c_int)) {
364    for &sig in sigs {
365        // SAFETY: a zeroed sigaction is valid; only its handler, its empty mask and SA_RESTART are set
366        // before sigaction(2) reads it.
367        unsafe {
368            let mut sa: libc::sigaction = std::mem::zeroed();
369            sa.sa_sigaction = handler as libc::sighandler_t;
370            sa.sa_flags = libc::SA_RESTART;
371            libc::sigemptyset(&raw mut sa.sa_mask);
372            libc::sigaction(sig, &raw const sa, std::ptr::null_mut());
373        }
374    }
375}
376
377/// The stop signals (`stop_set`) unblocked: one held across an exec is delivered now.
378fn unblock_stops() {
379    let set = stop_set();
380    // SAFETY: pthread_sigmask(3) on a set built by sigemptyset/sigaddset.
381    unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const set, std::ptr::null_mut()) };
382}
383
384/// Whether a stop came since `trap_stops`.
385pub fn stopped() -> bool {
386    STOPPED.load(std::sync::atomic::Ordering::SeqCst)
387}
388
389fn stop_set() -> libc::sigset_t {
390    // SAFETY: sigemptyset(3)/sigaddset(3) initialise the zeroed set before it is read.
391    unsafe {
392        let mut set: libc::sigset_t = std::mem::zeroed();
393        libc::sigemptyset(&mut set);
394        for s in [libc::SIGINT, libc::SIGHUP, libc::SIGTERM] {
395            libc::sigaddset(&mut set, s);
396        }
397        set
398    }
399}
400
401/// The first step of a waiter (listen.rs), once it trapped its stop signals: when a previous image
402/// re-exec'd it (`Image::reexec`), the inherited lock fd, its variable gone from the environment, the
403/// stop signals unblocked only now, so a stop that came across the exec is seen; None for a fresh
404/// start. The fd is the waiter's lock only once `adopt` checked it.
405pub fn inherited() -> Option<RawFd> {
406    let fd = std::env::var_os(ADOPT_FD)?;
407    // SAFETY: the waiter is single-threaded this early; no other thread reads the environment.
408    unsafe { std::env::remove_var(ADOPT_FD) };
409    unblock_stops();
410    fd.to_str().and_then(|s| s.parse::<RawFd>().ok()).filter(|fd| *fd > 2)
411}
412
413/// The inherited `fd` as the lock of `path`, when it is that file's open description and holds its lock
414/// (a try_lock on the same description succeeds); else None (a forged or stale variable: the waiter
415/// takes the lock as a fresh one does). Close-on-exec again from here.
416pub fn adopt(fd: RawFd, path: &Path) -> Option<std::fs::File> {
417    // SAFETY: fcntl(2) F_GETFD only asks whether `fd` is open, so an fd that is not is never owned.
418    if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
419        return None;
420    }
421    // SAFETY: `fd` is open and was handed to this process for it alone (`Image::reexec`): owned here.
422    let f = unsafe { std::fs::File::from_raw_fd(fd) };
423    let same = match (f.metadata(), std::fs::metadata(path)) {
424        (Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
425        _ => false,
426    };
427    if !same || f.try_lock().is_err() {
428        return None;
429    }
430    // SAFETY: fcntl(2) F_SETFD on the fd owned above.
431    unsafe { libc::fcntl(f.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) };
432    Some(f)
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    fn temp(name: &str) -> PathBuf {
440        let d = std::env::temp_dir().join(format!("borg-reexec-{name}-{}", std::process::id()));
441        let _ = std::fs::remove_dir_all(&d);
442        std::fs::create_dir_all(&d).unwrap();
443        d
444    }
445
446    fn install(p: &Path, body: &str) {
447        use std::os::unix::fs::PermissionsExt;
448        let part = p.with_extension("part");
449        std::fs::write(&part, body).unwrap();
450        std::fs::set_permissions(&part, std::fs::Permissions::from_mode(0o755)).unwrap();
451        std::fs::rename(&part, p).unwrap();
452    }
453
454    /// A wrapper or shim is the path, the binary it starts another file: never stale for that, so no
455    /// re-exec loop. A rename over the path makes it stale and due; an exec that failed on that file is
456    /// not due again (still stale: gated) until the path changes once more.
457    #[test]
458    fn a_shim_is_not_replaced_and_a_rename_over_it_is() {
459        let d = temp("shim");
460        let p = d.join("borg");
461        install(&p, "#!/bin/sh\nexec /bin/true\n");
462        let mut img = Image::of(p.clone(), std::fs::File::open("/bin/sh").ok());
463        assert!(!img.stale() && !img.due(), "the shim and the running binary differ: that is no reinstall");
464        install(&p, "#!/bin/sh\nexec /bin/true\n");
465        assert!(img.stale() && img.due(), "a rename over the path");
466        img.failed.push(exec_id(&p));
467        assert!(img.stale() && !img.due(), "gated, not tried again");
468        install(&p, "#!/bin/sh\nexec /bin/false\n");
469        assert!(img.due(), "a new file is tried");
470        let _ = std::fs::remove_dir_all(d);
471    }
472
473    /// Guard: the test binary may be rebuilt under a running waiter test; an image taken in a unit test
474    /// names no file, so it is never stale or due.
475    #[test]
476    fn an_image_in_a_unit_test_never_follows_the_test_binary() {
477        let img = Image::now();
478        assert!(exec_id(&img.path).is_none() && !img.stale() && !img.due());
479        assert!(exec_id(&exe_path()).is_some(), "the test binary itself is a file: the seam, not the path, keeps it out");
480    }
481
482    /// A running binary deleted from under a path that stays (a symlinked install's old target) is
483    /// stale: its link count is 0.
484    #[test]
485    fn a_deleted_running_binary_is_stale() {
486        let d = temp("unlinked");
487        let p = d.join("borg");
488        install(&p, "#!/bin/sh\n");
489        let old = d.join("old");
490        install(&old, "#!/bin/sh\n");
491        let img = Image::of(p, std::fs::File::open(&old).ok());
492        assert!(!img.stale());
493        std::fs::remove_file(&old).unwrap();
494        assert!(img.stale() && img.due(), "the path holds an executable to re-exec");
495        let _ = std::fs::remove_dir_all(d);
496    }
497
498    #[test]
499    fn the_path_is_argv0_resolved_without_the_deleted_suffix() {
500        assert_eq!(undeleted(Path::new("/x/bin/borg (deleted)")), PathBuf::from("/x/bin/borg"));
501        assert_eq!(undeleted(Path::new("/x/bin/borg")), PathBuf::from("/x/bin/borg"));
502        let d = temp("argv0");
503        install(&d.join("borg"), "#!/bin/sh\n");
504        let path = std::env::join_paths([Path::new("/nonexistent"), &d]).unwrap();
505        assert_eq!(from_argv0(Some("borg".into()), Some(path.clone())), Some(d.join("borg")), "a bare word on PATH");
506        assert_eq!(from_argv0(Some("/a/b/borg".into()), Some(path)), Some(PathBuf::from("/a/b/borg")), "as given, never canonicalized");
507        assert_eq!(from_argv0(Some("nope".into()), Some(d.as_os_str().into())), None, "the running binary then");
508        let _ = std::fs::remove_dir_all(d);
509    }
510
511    /// An inherited fd is a waiter's lock only when it is that file's description holding its lock.
512    #[test]
513    fn an_inherited_fd_of_another_file_is_not_adopted() {
514        use std::os::unix::io::IntoRawFd;
515        let d = temp("adopt");
516        let (lock, other) = (d.join("w.lock"), d.join("other"));
517        std::fs::write(&lock, "").unwrap();
518        std::fs::write(&other, "").unwrap();
519        let fd = |p: &Path| std::fs::OpenOptions::new().write(true).open(p).unwrap();
520        assert!(adopt(fd(&other).into_raw_fd(), &lock).is_none(), "another file");
521        let held = fd(&lock);
522        held.lock().unwrap();
523        assert!(adopt(fd(&lock).into_raw_fd(), &lock).is_none(), "another description, the lock held by it");
524        drop(held);
525        let mine = fd(&lock);
526        mine.lock().unwrap();
527        assert!(adopt(mine.into_raw_fd(), &lock).is_some(), "the description that holds the lock");
528        assert!(adopt(9999, &lock).is_none(), "no open fd");
529        let _ = std::fs::remove_dir_all(d);
530    }
531}