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;
37
38const RESTARTED: &str = "BORG_RESTARTED";
41
42const PROBE: &str = "BORG_RESTART_PROBE";
45const PROBED: &str = "takes a restart in place";
46
47const PROBE_WAIT: std::time::Duration = std::time::Duration::from_secs(10);
49
50const ALONE_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
53
54#[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
67fn 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
73pub(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
95fn 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#[derive(Debug)]
103pub enum ReexecError {
104 Dup(std::io::Error),
106 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
127pub struct Image {
130 path: PathBuf,
131 base: Option<Id>,
132 running: Option<std::fs::File>,
134 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 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 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 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 unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &raw const set, std::ptr::null_mut()) };
176 let unblock = || {
177 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 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 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 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 unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()) };
214 let unblock = || {
215 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 self.failed.push(target);
230 match err {
231 _ 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
245fn 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 unsafe { libc::execve(file.as_ptr(), argv.as_ptr(), envp.as_ptr()) };
265 let e = std::io::Error::last_os_error();
266 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
276fn 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 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
311pub fn probed() -> bool {
314 if std::env::var_os(PROBE).is_none() {
315 return false;
316 }
317 println!("{PROBED}");
318 true
319}
320
321fn 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
330static 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
337pub fn trap_stops() {
342 on_signals(&[libc::SIGINT, libc::SIGTERM], on_stop);
343 if std::env::var_os(RESTARTED).is_some() {
344 unsafe { std::env::remove_var(RESTARTED) };
347 unblock_stops();
348 }
349}
350
351pub fn on_signals(sigs: &[libc::c_int], handler: extern "C" fn(libc::c_int)) {
354 for &sig in sigs {
355 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
367fn unblock_stops() {
369 let set = stop_set();
370 unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const set, std::ptr::null_mut()) };
372}
373
374pub fn stopped() -> bool {
376 STOPPED.load(std::sync::atomic::Ordering::SeqCst)
377}
378
379fn stop_set() -> libc::sigset_t {
380 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
391pub fn inherited() -> Option<RawFd> {
396 let fd = std::env::var_os(ADOPT_FD)?;
397 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
403pub fn adopt(fd: RawFd, path: &Path) -> Option<std::fs::File> {
407 if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
409 return None;
410 }
411 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 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 #[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 #[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 #[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}