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