use std::ffi::{CString, OsString};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
const ADOPT_FD: &str = "BORG_LISTEN_FD";
const BUSY_TRIES: u32 = 20;
const RESTARTED: &str = "BORG_RESTARTED";
const PROBE: &str = "BORG_RESTART_PROBE";
const PROBED: &str = "takes a restart in place";
const PROBE_WAIT: std::time::Duration = std::time::Duration::from_secs(10);
const ALONE_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Id {
dev: u64,
ino: u64,
size: u64,
mtime_ns: i128,
}
fn id_of(m: &std::fs::Metadata) -> Id {
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()) }
}
fn exec_id(p: &Path) -> Option<Id> {
let m = std::fs::metadata(p).ok()?;
(m.is_file() && m.mode() & 0o111 != 0).then(|| id_of(&m))
}
pub(crate) fn exe_path() -> PathBuf {
static P: OnceLock<PathBuf> = OnceLock::new();
P.get_or_init(|| from_argv0(std::env::args_os().next(), std::env::var_os("PATH")).unwrap_or_else(running)).clone()
}
fn from_argv0(arg0: Option<OsString>, path: Option<OsString>) -> Option<PathBuf> {
let a = PathBuf::from(arg0.filter(|a| !a.is_empty())?);
if a.as_os_str().as_bytes().contains(&b'/') {
return Some(if a.is_absolute() { a } else { std::env::current_dir().ok()?.join(a) });
}
std::env::split_paths(&path?).filter(|d| d.is_absolute()).map(|d| d.join(&a)).find(|p| exec_id(p).is_some())
}
fn running() -> PathBuf {
std::env::current_exe().map_or_else(|_| PathBuf::from("borg"), |e| undeleted(&e))
}
fn undeleted(p: &Path) -> PathBuf {
let b = p.as_os_str().as_bytes();
PathBuf::from(std::ffi::OsStr::from_bytes(b.strip_suffix(b" (deleted)").unwrap_or(b)))
}
#[derive(Debug)]
pub enum ReexecError {
Dup(std::io::Error),
Exec(PathBuf, std::io::Error),
}
impl std::fmt::Display for ReexecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReexecError::Dup(e) => write!(f, "the lock could not be passed on: {e}"),
ReexecError::Exec(path, e) => write!(f, "{}: {e}", path.display()),
}
}
}
impl std::error::Error for ReexecError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ReexecError::Dup(e) | ReexecError::Exec(_, e) => Some(e),
}
}
}
pub struct Image {
path: PathBuf,
base: Option<Id>,
running: Option<std::fs::File>,
failed: Vec<Option<Id>>,
}
impl Image {
pub fn now() -> Image {
if cfg!(test) {
return Image::of(PathBuf::new(), None);
}
Image::of(exe_path(), std::fs::File::open("/proc/self/exe").ok())
}
fn of(path: PathBuf, running: Option<std::fs::File>) -> Image {
let base = exec_id(&path);
Image { path, base, running, failed: Vec::new() }
}
pub fn stale(&self) -> bool {
exec_id(&self.path) != self.base || self.running.as_ref().is_some_and(|f| f.metadata().is_ok_and(|m| m.nlink() == 0))
}
pub fn due(&self) -> bool {
let now = exec_id(&self.path);
now.is_some() && !self.failed.contains(&now) && self.stale()
}
pub fn reexec(&mut self, lock: &std::fs::File, stopped: impl Fn() -> bool) -> Result<(), ReexecError> {
use std::io::Write;
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
let target = exec_id(&self.path);
let set = stop_set();
unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &raw const set, std::ptr::null_mut()) };
let unblock = || {
unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const set, std::ptr::null_mut()) };
};
if stopped() {
unblock();
return Ok(());
}
let dup = unsafe { libc::fcntl(lock.as_raw_fd(), libc::F_DUPFD, 3) };
let err = if dup < 0 {
ReexecError::Dup(std::io::Error::last_os_error())
} else {
let e = execve_retrying(&self.path, ADOPT_FD, &dup.to_string());
unsafe { libc::close(dup) };
ReexecError::Exec(self.path.clone(), e)
};
unblock();
self.failed.push(target);
if stopped() { Ok(()) } else { Err(err) }
}
pub fn restart(&mut self) -> Result<(), String> {
use std::io::Write;
let _ = std::io::stdout().flush();
let _ = std::io::stderr().flush();
let target = exec_id(&self.path);
let set = stop_set();
unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()) };
let unblock = || {
unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &set, std::ptr::null_mut()) };
};
let takes = takes_restart(&self.path) && exec_id(&self.path) == target;
if takes {
alone();
}
if stopped() {
unblock();
return Ok(());
}
let err = takes.then(|| execve_retrying(&self.path, RESTARTED, "1"));
unblock();
self.failed.push(target);
match err {
_ if stopped() => Ok(()),
None => Err(format!(
"{} 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",
self.path.display()
)),
Some(why) => {
Err(format!("could not run the reinstalled borg-mesh ({}: {why}): install it again, then start `borg mesh serve`", self.path.display()))
}
}
}
}
fn execve_retrying(path: &Path, var: &str, value: &str) -> std::io::Error {
let cstr = |b: &[u8]| CString::new(b).ok();
let file = cstr(path.as_os_str().as_bytes());
let argv: Option<Vec<CString>> = std::env::args_os().map(|a| cstr(a.as_bytes())).collect();
let envp: Option<Vec<CString>> = std::env::vars_os()
.filter(|(k, _)| k != var)
.map(|(k, v)| cstr(&[k.as_bytes(), b"=", v.as_bytes()].concat()))
.chain([cstr(format!("{var}={value}").as_bytes())])
.collect();
let (Some(file), Some(argv), Some(envp)) = (file, argv, envp) else { return std::io::Error::from(std::io::ErrorKind::InvalidInput) };
let ptrs = |v: &[CString]| v.iter().map(|c| c.as_ptr()).chain([std::ptr::null()]).collect::<Vec<_>>();
let (argv, envp) = (ptrs(&argv), ptrs(&envp));
let mut tries = 0;
loop {
unsafe { libc::execve(file.as_ptr(), argv.as_ptr(), envp.as_ptr()) };
let e = std::io::Error::last_os_error();
tries += 1;
if e.raw_os_error() != Some(libc::ETXTBSY) || tries == BUSY_TRIES {
return e;
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
fn takes_restart(path: &Path) -> bool {
use std::process::{Command, Stdio};
let spawn = || Command::new(path).arg("--help").env(PROBE, "1").stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn();
let mut tries = 1;
let mut child = loop {
match spawn() {
Ok(c) => break c,
Err(e) if e.raw_os_error() == Some(libc::ETXTBSY) && tries < BUSY_TRIES => {
tries += 1;
std::thread::sleep(std::time::Duration::from_millis(25));
}
Err(_) => return false,
}
};
let t0 = std::time::Instant::now();
let done = loop {
match child.try_wait() {
Ok(Some(status)) => break status.success(),
Ok(None) if t0.elapsed() < PROBE_WAIT => std::thread::sleep(std::time::Duration::from_millis(10)),
_ => {
let _ = child.kill();
let _ = child.wait();
break false;
}
}
};
let mut out = String::new();
done && child.stdout.take().is_some_and(|mut o| std::io::Read::read_to_string(&mut o, &mut out).is_ok()) && out.trim() == PROBED
}
pub fn probed() -> bool {
if std::env::var_os(PROBE).is_none() {
return false;
}
println!("{PROBED}");
true
}
fn alone() {
let t0 = std::time::Instant::now();
let threads = || std::fs::read_dir("/proc/self/task").map_or(1, Iterator::count);
while threads() > 1 && t0.elapsed() < ALONE_WAIT {
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
static STOPPED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
extern "C" fn on_stop(_: libc::c_int) {
STOPPED.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub fn trap_stops() {
on_signals(&[libc::SIGINT, libc::SIGTERM], on_stop);
if std::env::var_os(RESTARTED).is_some() {
unsafe { std::env::remove_var(RESTARTED) };
unblock_stops();
}
}
pub fn on_signals(sigs: &[libc::c_int], handler: extern "C" fn(libc::c_int)) {
for &sig in sigs {
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = handler as libc::sighandler_t;
sa.sa_flags = libc::SA_RESTART;
libc::sigemptyset(&raw mut sa.sa_mask);
libc::sigaction(sig, &raw const sa, std::ptr::null_mut());
}
}
}
fn unblock_stops() {
let set = stop_set();
unsafe { libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const set, std::ptr::null_mut()) };
}
pub fn stopped() -> bool {
STOPPED.load(std::sync::atomic::Ordering::SeqCst)
}
fn stop_set() -> libc::sigset_t {
unsafe {
let mut set: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut set);
for s in [libc::SIGINT, libc::SIGHUP, libc::SIGTERM] {
libc::sigaddset(&mut set, s);
}
set
}
}
pub fn inherited() -> Option<RawFd> {
let fd = std::env::var_os(ADOPT_FD)?;
unsafe { std::env::remove_var(ADOPT_FD) };
unblock_stops();
fd.to_str().and_then(|s| s.parse::<RawFd>().ok()).filter(|fd| *fd > 2)
}
pub fn adopt(fd: RawFd, path: &Path) -> Option<std::fs::File> {
if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
return None;
}
let f = unsafe { std::fs::File::from_raw_fd(fd) };
let same = match (f.metadata(), std::fs::metadata(path)) {
(Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
_ => false,
};
if !same || f.try_lock().is_err() {
return None;
}
unsafe { libc::fcntl(f.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) };
Some(f)
}
#[cfg(test)]
mod tests {
use super::*;
fn temp(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("borg-reexec-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn install(p: &Path, body: &str) {
use std::os::unix::fs::PermissionsExt;
let part = p.with_extension("part");
std::fs::write(&part, body).unwrap();
std::fs::set_permissions(&part, std::fs::Permissions::from_mode(0o755)).unwrap();
std::fs::rename(&part, p).unwrap();
}
#[test]
fn a_shim_is_not_replaced_and_a_rename_over_it_is() {
let d = temp("shim");
let p = d.join("borg");
install(&p, "#!/bin/sh\nexec /bin/true\n");
let mut img = Image::of(p.clone(), std::fs::File::open("/bin/sh").ok());
assert!(!img.stale() && !img.due(), "the shim and the running binary differ: that is no reinstall");
install(&p, "#!/bin/sh\nexec /bin/true\n");
assert!(img.stale() && img.due(), "a rename over the path");
img.failed.push(exec_id(&p));
assert!(img.stale() && !img.due(), "gated, not tried again");
install(&p, "#!/bin/sh\nexec /bin/false\n");
assert!(img.due(), "a new file is tried");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn an_image_in_a_unit_test_never_follows_the_test_binary() {
let img = Image::now();
assert!(exec_id(&img.path).is_none() && !img.stale() && !img.due());
assert!(exec_id(&exe_path()).is_some(), "the test binary itself is a file: the seam, not the path, keeps it out");
}
#[test]
fn a_deleted_running_binary_is_stale() {
let d = temp("unlinked");
let p = d.join("borg");
install(&p, "#!/bin/sh\n");
let old = d.join("old");
install(&old, "#!/bin/sh\n");
let img = Image::of(p, std::fs::File::open(&old).ok());
assert!(!img.stale());
std::fs::remove_file(&old).unwrap();
assert!(img.stale() && img.due(), "the path holds an executable to re-exec");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn the_path_is_argv0_resolved_without_the_deleted_suffix() {
assert_eq!(undeleted(Path::new("/x/bin/borg (deleted)")), PathBuf::from("/x/bin/borg"));
assert_eq!(undeleted(Path::new("/x/bin/borg")), PathBuf::from("/x/bin/borg"));
let d = temp("argv0");
install(&d.join("borg"), "#!/bin/sh\n");
let path = std::env::join_paths([Path::new("/nonexistent"), &d]).unwrap();
assert_eq!(from_argv0(Some("borg".into()), Some(path.clone())), Some(d.join("borg")), "a bare word on PATH");
assert_eq!(from_argv0(Some("/a/b/borg".into()), Some(path)), Some(PathBuf::from("/a/b/borg")), "as given, never canonicalized");
assert_eq!(from_argv0(Some("nope".into()), Some(d.as_os_str().into())), None, "the running binary then");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn an_inherited_fd_of_another_file_is_not_adopted() {
use std::os::unix::io::IntoRawFd;
let d = temp("adopt");
let (lock, other) = (d.join("w.lock"), d.join("other"));
std::fs::write(&lock, "").unwrap();
std::fs::write(&other, "").unwrap();
let fd = |p: &Path| std::fs::OpenOptions::new().write(true).open(p).unwrap();
assert!(adopt(fd(&other).into_raw_fd(), &lock).is_none(), "another file");
let held = fd(&lock);
held.lock().unwrap();
assert!(adopt(fd(&lock).into_raw_fd(), &lock).is_none(), "another description, the lock held by it");
drop(held);
let mine = fd(&lock);
mine.lock().unwrap();
assert!(adopt(mine.into_raw_fd(), &lock).is_some(), "the description that holds the lock");
assert!(adopt(9999, &lock).is_none(), "no open fd");
let _ = std::fs::remove_dir_all(d);
}
}