1use 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
31const ADOPT_FD: &str = "BORG_LISTEN_FD";
34
35const BUSY_TRIES: u32 = 20;
39
40const RESTARTED: &str = "BORG_RESTARTED";
43
44const PROBE: &str = "BORG_RESTART_PROBE";
47const PROBED: &str = "takes a restart in place";
48
49const PROBE_WAIT: std::time::Duration = std::time::Duration::from_secs(10);
52
53const ALONE_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
57
58#[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
71fn 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
77pub(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
99fn 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#[derive(Debug)]
107pub enum ReexecError {
108 Dup(std::io::Error),
110 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
131pub struct Image {
134 path: PathBuf,
135 base: Option<Id>,
136 running: Option<std::fs::File>,
138 failed: Vec<Option<Id>>,
140}
141
142impl Image {
143 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 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 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 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 unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &raw const set, std::ptr::null_mut()) };
186 let unblock = || {
187 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 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 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 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 unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()) };
224 let unblock = || {
225 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 self.failed.push(target);
240 match err {
241 _ 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
255fn 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 unsafe { libc::execve(file.as_ptr(), argv.as_ptr(), envp.as_ptr()) };
275 let e = std::io::Error::last_os_error();
276 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
286fn 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 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
321pub fn probed() -> bool {
324 if std::env::var_os(PROBE).is_none() {
325 return false;
326 }
327 println!("{PROBED}");
328 true
329}
330
331fn 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
340static 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
347pub fn trap_stops() {
352 on_signals(&[libc::SIGINT, libc::SIGTERM], on_stop);
353 if std::env::var_os(RESTARTED).is_some() {
354 unsafe { std::env::remove_var(RESTARTED) };
357 unblock_stops();
358 }
359}
360
361pub fn on_signals(sigs: &[libc::c_int], handler: extern "C" fn(libc::c_int)) {
364 for &sig in sigs {
365 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
377fn unblock_stops() {
379 let set = stop_set();
380 unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const set, std::ptr::null_mut()) };
382}
383
384pub fn stopped() -> bool {
386 STOPPED.load(std::sync::atomic::Ordering::SeqCst)
387}
388
389fn stop_set() -> libc::sigset_t {
390 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
401pub fn inherited() -> Option<RawFd> {
406 let fd = std::env::var_os(ADOPT_FD)?;
407 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
413pub fn adopt(fd: RawFd, path: &Path) -> Option<std::fs::File> {
417 if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
419 return None;
420 }
421 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 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 #[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 #[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 #[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 #[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}