use anyhow::{Context, Result, bail};
use semver::Version;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
#[cfg(feature = "tui")]
use std::os::unix::fs::OpenOptionsExt;
#[cfg(feature = "tui")]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
const CRATES_IO_SOURCE: &str = "registry+https://github.com/rust-lang/crates.io-index";
pub const RUN_NAMESPACE: &str = "stage-v2";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StageRun {
LegacyPid(u32),
LeasedRun { pid: u32 },
}
const NONCE_HEX_LEN: usize = 16;
#[must_use]
pub fn parse_run_dir(name: &str) -> Option<StageRun> {
fn pid(s: &str) -> Option<u32> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
if let Some(p) = pid(name) {
return Some(StageRun::LegacyPid(p));
}
let (p, nonce) = name.split_once('-')?;
let p = pid(p)?;
if nonce.len() == NONCE_HEX_LEN
&& nonce
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
Some(StageRun::LeasedRun { pid: p })
} else {
None
}
}
const LEASE_FILE: &str = ".lease";
const LEASE_PENDING: &str = ".lease.pending";
struct UnpublishedRun<'a> {
run_dir: &'a Path,
armed: bool,
}
impl Drop for UnpublishedRun<'_> {
fn drop(&mut self) {
if self.armed {
let _ = fs::remove_dir_all(self.run_dir);
}
}
}
pub struct Lease {
#[cfg_attr(
not(test),
expect(dead_code, reason = "the field's work is keeping the descriptor open")
)]
file: fs::File,
}
impl Lease {
pub fn acquire(run_dir: &Path) -> Result<Self> {
use std::os::fd::AsRawFd;
if let Some(parent) = run_dir.parent() {
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
}
fs::create_dir(run_dir).with_context(|| format!("creating {}", run_dir.display()))?;
let mut guard = UnpublishedRun {
run_dir,
armed: true,
};
let pending = run_dir.join(LEASE_PENDING);
let file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&pending)
.with_context(|| format!("creating {}", pending.display()))?;
let fd = file.as_raw_fd();
if unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) } < 0 {
return Err(std::io::Error::last_os_error())
.with_context(|| format!("locking {}", pending.display()));
}
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
return Err(std::io::Error::last_os_error())
.with_context(|| format!("reading descriptor flags of {}", pending.display()));
}
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0 {
return Err(std::io::Error::last_os_error())
.with_context(|| format!("clearing close-on-exec on {}", pending.display()));
}
let published = run_dir.join(LEASE_FILE);
fs::rename(&pending, &published)
.with_context(|| format!("publishing {}", published.display()))?;
guard.armed = false;
Ok(Self { file })
}
}
fn require_real_run_dir(run_dir: &Path) -> std::io::Result<()> {
let meta = fs::symlink_metadata(run_dir)?;
if meta.file_type().is_dir() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotADirectory,
"run path is not a real directory",
))
}
}
pub fn take_lease_for_removal(run_dir: &Path) -> std::io::Result<Option<Lease>> {
use std::os::fd::AsRawFd;
require_real_run_dir(run_dir)?;
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(run_dir.join(LEASE_FILE))?;
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0 {
return Ok(Some(Lease { file }));
}
let e = std::io::Error::last_os_error();
if e.kind() == std::io::ErrorKind::WouldBlock {
Ok(None)
} else {
Err(e)
}
}
pub fn remove_leased_run(run_dir: &Path) -> std::io::Result<()> {
require_real_run_dir(run_dir)?;
for entry in fs::read_dir(run_dir)? {
let entry = entry?;
if entry.file_name() == LEASE_FILE {
continue;
}
if entry.file_type()?.is_dir() {
fs::remove_dir_all(entry.path())?;
} else {
fs::remove_file(entry.path())?;
}
}
fs::remove_file(run_dir.join(LEASE_FILE))?;
fs::remove_dir(run_dir)
}
pub fn release_and_remove_run(lease: Lease, run_dir: &Path) {
drop(lease);
if let Ok(Some(_held)) = take_lease_for_removal(run_dir) {
let _ = remove_leased_run(run_dir);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LeaseState {
Held,
Released,
Unknown,
}
#[must_use]
pub fn probe_lease(run_dir: &Path) -> LeaseState {
use std::os::fd::AsRawFd;
if require_real_run_dir(run_dir).is_err() {
return LeaseState::Unknown;
}
let Ok(file) = fs::File::open(run_dir.join(LEASE_FILE)) else {
return LeaseState::Unknown;
};
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_SH | libc::LOCK_NB) } == 0 {
LeaseState::Released
} else if std::io::Error::last_os_error().kind() == std::io::ErrorKind::WouldBlock {
LeaseState::Held
} else {
LeaseState::Unknown
}
}
pub fn new_run_dir_name() -> std::io::Result<String> {
use std::fmt::Write as _;
let mut nonce = [0u8; NONCE_HEX_LEN / 2];
let mut filled = 0usize;
while filled < nonce.len() {
let n = unsafe {
libc::getrandom(nonce[filled..].as_mut_ptr().cast(), nonce.len() - filled, 0)
};
if n < 0 {
let e = std::io::Error::last_os_error();
if e.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(e);
}
filled += usize::try_from(n).unwrap_or(0);
}
let mut name = format!("{}-", std::process::id());
for b in nonce {
write!(name, "{b:02x}").expect("writing to a String cannot fail");
}
Ok(name)
}
#[derive(Debug)]
pub struct Built {
pub version: Version,
pub bins: Vec<String>,
pub bin_paths: Vec<PathBuf>,
}
#[derive(Deserialize)]
struct Crates2 {
installs: BTreeMap<String, InstallInfo>,
}
#[derive(Deserialize)]
struct InstallInfo {
bins: Vec<String>,
}
#[cfg(test)]
static CARGO_PROGRAM: std::sync::RwLock<Option<PathBuf>> = std::sync::RwLock::new(None);
#[cfg(test)]
static FAKE_CARGO_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) struct FakeCargo {
_serial: std::sync::MutexGuard<'static, ()>,
}
#[cfg(test)]
impl FakeCargo {
pub(crate) fn install(script: &Path) -> Self {
let serial = FAKE_CARGO_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*CARGO_PROGRAM.write().unwrap() = Some(script.to_path_buf());
Self { _serial: serial }
}
}
#[cfg(test)]
impl Drop for FakeCargo {
fn drop(&mut self) {
*CARGO_PROGRAM.write().unwrap() = None;
}
}
#[cfg(test)]
pub(crate) fn released_run_fixture(run_dir: &Path) {
fs::create_dir_all(run_dir).unwrap();
fs::write(run_dir.join(LEASE_FILE), b"").unwrap();
}
#[cfg(test)]
pub(crate) fn eventually(deadline: std::time::Duration, mut attempt: impl FnMut() -> bool) -> bool {
let end = std::time::Instant::now() + deadline;
loop {
if attempt() {
return true;
}
if std::time::Instant::now() >= end {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
#[cfg(test)]
pub(crate) fn no_spawned_children() -> std::sync::MutexGuard<'static, ()> {
FAKE_CARGO_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn cargo_program() -> std::ffi::OsString {
#[cfg(test)]
if let Some(p) = CARGO_PROGRAM.read().unwrap().clone() {
return p.into_os_string();
}
std::ffi::OsString::from("cargo")
}
fn command(name: &str, version: Option<&Version>, locked: bool, stage: &Path) -> Command {
let mut cmd = Command::new(cargo_program());
cmd.arg("install").arg(name).arg("--root").arg(stage);
if let Some(version) = version {
cmd.arg("--version").arg(format!("={version}"));
}
if locked {
cmd.arg("--locked");
}
if let Some(path) = std::env::var_os("PATH") {
let mut dirs: Vec<PathBuf> = std::env::split_paths(&path).collect();
dirs.push(stage.join("bin"));
if let Ok(joined) = std::env::join_paths(dirs) {
cmd.env("PATH", joined);
}
}
cmd
}
#[cfg(test)]
fn retry_while_text_file_busy<T>(
mut attempt: impl FnMut() -> std::io::Result<T>,
) -> std::io::Result<T> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
match attempt() {
Err(e)
if e.raw_os_error() == Some(libc::ETXTBSY)
&& std::time::Instant::now() < deadline =>
{
std::thread::sleep(std::time::Duration::from_millis(20));
}
other => return other,
}
}
}
fn spawn_cargo(cmd: &mut Command) -> std::io::Result<std::process::Child> {
#[cfg(test)]
return retry_while_text_file_busy(|| cmd.spawn());
#[cfg(not(test))]
cmd.spawn()
}
pub fn build(name: &str, version: Option<&Version>, locked: bool, stage: &Path) -> Result<Built> {
fs::create_dir_all(stage).with_context(|| format!("creating {}", stage.display()))?;
let status = spawn_cargo(&mut command(name, version, locked, stage))
.and_then(|mut child| child.wait())
.context("failed to spawn cargo")?;
if !status.success() {
bail!("cargo install {name} failed with {status}");
}
verified_info(name, version, stage)
}
#[cfg(feature = "tui")]
fn poll_readable(fd: std::os::fd::RawFd) -> std::io::Result<bool> {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let n = unsafe { libc::poll(&raw mut pfd, 1, 100) };
match n {
0 => Ok(false),
1.. => Ok(true),
_ => {
let e = std::io::Error::last_os_error();
if e.kind() == std::io::ErrorKind::Interrupted {
Ok(false)
} else {
Err(e)
}
}
}
}
#[cfg(feature = "tui")]
fn drain_stderr(
reader: &mut std::io::BufReader<std::process::ChildStderr>,
child: &mut std::process::Child,
control: &crate::BuildControl,
pgid: Option<i32>,
on_line: &mut dyn FnMut(&str),
lines: &mut Vec<String>,
) -> Option<std::io::Error> {
let mut buf: Vec<u8> = Vec::new();
let mut swept = false;
loop {
if control.cancelled() && !swept && matches!(child.try_wait(), Ok(Some(_))) {
swept = true;
if let Some(pgid) = pgid {
unsafe {
libc::kill(-pgid, libc::SIGKILL);
}
}
}
if reader.buffer().is_empty() {
match poll_readable(std::os::fd::AsRawFd::as_raw_fd(reader.get_ref())) {
Ok(true) => {}
Ok(false) => continue,
Err(e) => return Some(e),
}
}
buf.clear();
match std::io::BufRead::read_until(reader, b'\n', &mut buf) {
Ok(0) => return None,
Ok(_) => {
while matches!(buf.last(), Some(b'\n' | b'\r')) {
buf.pop();
}
let line = crate::text::sanitize(&String::from_utf8_lossy(&buf));
on_line(&line);
lines.push(line);
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Some(e),
}
}
}
#[cfg(feature = "tui")]
pub fn build_captured(
name: &str,
version: Option<&Version>,
locked: bool,
stage: &Path,
log_dir: &Path,
on_line: &mut dyn FnMut(&str),
control: &crate::BuildControl,
) -> Result<Built> {
fs::create_dir_all(stage).with_context(|| format!("creating {}", stage.display()))?;
let mut cmd = command(name, version, locked, stage);
cmd.env("CARGO_TERM_COLOR", "never");
std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
let mut child = spawn_cargo(&mut cmd).context("failed to spawn cargo")?;
let pgid = i32::try_from(child.id()).ok();
if let Some(pgid) = pgid {
control.spawned(pgid);
}
let stderr = child
.stderr
.take()
.context("cargo spawned without a stderr pipe")?;
let mut reader = std::io::BufReader::new(stderr);
let mut lines: Vec<String> = Vec::new();
let read_error = drain_stderr(&mut reader, &mut child, control, pgid, on_line, &mut lines);
if read_error.is_some() {
if let Ok(pgid) = i32::try_from(child.id()) {
unsafe {
libc::kill(-pgid, libc::SIGKILL);
}
}
}
let status = child.wait();
if control.cancelled()
&& let Some(pgid) = pgid
{
unsafe {
libc::kill(-pgid, libc::SIGKILL);
}
}
control.reaped();
let status = status.context("waiting for cargo")?;
if let Some(e) = read_error {
return Err(failure_with_log(
log_dir,
name,
&lines,
format!("reading cargo output failed: {e}"),
&tail_from(&lines, lines.len().saturating_sub(TAIL_LINES)),
));
}
if !status.success() {
use std::os::unix::process::ExitStatusExt;
if control.cancelled() && status.signal().is_some() {
return Err(anyhow::Error::new(crate::BuildCancelled));
}
let start = lines
.iter()
.position(|l| {
matches!(
crate::progress::parse_line(l),
crate::progress::BuildEvent::Error
)
})
.unwrap_or(lines.len().saturating_sub(TAIL_LINES));
return Err(failure_with_log(
log_dir,
name,
&lines,
format!("cargo install {name} failed with {status}"),
&tail_from(&lines, start),
));
}
verified_info(name, version, stage).map_err(|e| {
failure_with_log(
log_dir,
name,
&lines,
format!("cargo exited successfully, but: {e:#}"),
&tail_from(&lines, lines.len().saturating_sub(TAIL_LINES)),
)
})
}
#[cfg(feature = "tui")]
fn tail_from(lines: &[String], start: usize) -> Vec<&str> {
lines[start..]
.iter()
.take(TAIL_LINES)
.map(String::as_str)
.collect()
}
#[cfg(feature = "tui")]
fn failure_with_log(
log_dir: &Path,
name: &str,
lines: &[String],
headline: String,
tail: &[&str],
) -> anyhow::Error {
let log = write_build_log(log_dir, name, lines);
let mut msg = headline;
match log {
Ok(path) => {
msg.push_str("\nfull log: ");
msg.push_str(&path.display().to_string());
}
Err(e) => {
use std::fmt::Write as _;
let _ = write!(msg, "\n(could not write the full log: {e:#})");
}
}
for l in tail {
msg.push_str("\n ");
msg.push_str(l);
}
anyhow::anyhow!(msg)
}
#[cfg(feature = "tui")]
const TAIL_LINES: usize = 12;
#[cfg(feature = "tui")]
fn write_build_log(log_dir: &Path, name: &str, lines: &[String]) -> Result<PathBuf> {
fs::create_dir_all(log_dir)
.with_context(|| format!("creating log directory {}", log_dir.display()))?;
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let path = log_dir.join(format!("build-{name}-{}-{stamp}.log", std::process::id()));
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&path)
.with_context(|| format!("creating build log {}", path.display()))?;
file.set_permissions(fs::Permissions::from_mode(0o600))
.with_context(|| format!("setting permissions on build log {}", path.display()))?;
let mut body = lines.join("\n");
body.push('\n');
std::io::Write::write_all(&mut file, body.as_bytes())
.with_context(|| format!("writing build log {}", path.display()))?;
Ok(path)
}
fn verified_info(name: &str, version: Option<&Version>, stage: &Path) -> Result<Built> {
let built = staged_info(name, stage)?;
if let Some(version) = version
&& built.version != *version
{
bail!(
"asked for {name} {version} but the stage holds {}",
built.version
);
}
Ok(built)
}
fn staged_info(name: &str, stage: &Path) -> Result<Built> {
let path = stage.join(".crates2.json");
let raw = fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let parsed: Crates2 =
serde_json::from_str(&raw).with_context(|| format!("parsing {}", path.display()))?;
let mut best: Option<Built> = None;
for (key, info) in &parsed.installs {
let mut parts = key.split_whitespace();
let (Some(key_name), Some(key_version), Some(key_source)) =
(parts.next(), parts.next(), parts.next())
else {
continue;
};
if key_name != name || !key_source.contains(CRATES_IO_SOURCE) {
continue;
}
let version = Version::parse(key_version)
.with_context(|| format!("unparsable staged version `{key_version}`"))?;
let replace = match &best {
Some(b) => version > b.version,
None => true,
};
if replace {
crate::validate::validate_bin_list(&info.bins)
.with_context(|| format!("stage bookkeeping for `{name}`"))?;
let bin_dir = stage.join("bin");
best = Some(Built {
bin_paths: info.bins.iter().map(|b| bin_dir.join(b)).collect(),
bins: info.bins.clone(),
version,
});
}
}
best.with_context(|| format!("`{name}` missing from stage bookkeeping after build"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn staged_info_parses_crates2() {
let dir = std::env::temp_dir().join("cargo-lbin-test-stage");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join(".crates2.json"),
r#"{"installs":{
"hexyl 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)":
{"bins":["hexyl"]},
"other 1.0.0 (git+https://example.com/other#abc)":
{"bins":["other"]}
}}"#,
)
.unwrap();
let built = staged_info("hexyl", &dir).unwrap();
assert_eq!(built.version, Version::parse("0.14.0").unwrap());
assert_eq!(built.bins, vec!["hexyl"]);
assert!(
staged_info("other", &dir).is_err(),
"git source must not match"
);
assert!(staged_info("absent", &dir).is_err());
fs::write(
dir.join(".crates2.json"),
r#"{"installs":{
"dupes 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)":
{"bins":["foo","foo"]}
}}"#,
)
.unwrap();
let err = format!("{:#}", staged_info("dupes", &dir).unwrap_err());
assert!(err.contains("listed twice"), "{err}");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(feature = "tui")]
#[test]
fn captured_build_forwards_lines_and_logs_failures() {
use std::os::unix::fs::PermissionsExt;
let root = std::env::temp_dir().join("cargo-lbin-test-captured");
let _ = fs::remove_dir_all(&root);
let fake_bin = root.join("bin");
let stage = root.join("stage");
let logs = root.join("logs");
fs::create_dir_all(&fake_bin).unwrap();
let script = fake_bin.join("cargo");
fs::write(
&script,
"#!/bin/sh\n\
echo ' Compiling one v1.0.0' >&2\n\
echo ' Compiling two v2.0.0' >&2\n\
echo 'error[E0308]: mismatched types' >&2\n\
echo 'note: expected u8' >&2\n\
exit 101\n",
)
.unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
let _fake = FakeCargo::install(&script);
let mut seen: Vec<String> = Vec::new();
let control = crate::BuildControl::new();
let err = build_captured(
"boomcrate",
None,
false,
&stage,
&logs,
&mut |l| {
seen.push(l.to_owned());
},
&control,
)
.unwrap_err();
let msg = format!("{err:#}");
assert_eq!(seen.len(), 4, "every stderr line reaches the frontend");
assert!(
msg.contains("error[E0308]") && msg.contains("note: expected u8"),
"tail starts at the first compiler error: {msg}"
);
assert!(
!msg.contains("Compiling one"),
"successful units stay out of the tail: {msg}"
);
assert!(msg.contains("full log:"), "log path travels in the error");
let log = fs::read_dir(&logs).unwrap().next().unwrap().unwrap().path();
let mode = fs::metadata(&log).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "the build log is private to the user");
let full = fs::read_to_string(&log).unwrap();
assert!(
full.contains("Compiling one") && full.contains("note: expected u8"),
"the log holds everything the tail dropped"
);
let _ = fs::remove_dir_all(&root);
}
#[cfg(feature = "tui")]
#[test]
fn captured_build_streams_success_and_verifies_the_stage() {
use std::os::unix::fs::PermissionsExt;
let root = std::env::temp_dir().join("cargo-lbin-test-captured-ok");
let _ = fs::remove_dir_all(&root);
let fake_bin = root.join("bin");
let stage = root.join("stage");
let logs = root.join("logs");
fs::create_dir_all(&fake_bin).unwrap();
let script = fake_bin.join("cargo");
fs::write(&script, "#!/bin/sh\nexit 1\n").unwrap();
fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
let _fake = FakeCargo::install(&script);
fs::write(
&script,
"#!/bin/sh\n\
echo ' Compiling okcrate v0.1.0' >&2\n\
echo ' Finished release [optimized]' >&2\n\
mkdir -p \"$4\"\n\
printf '%s' '{\"installs\":{\"okcrate 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)\":{\"bins\":[\"okcrate\"]}}}' > \"$4/.crates2.json\"\n\
exit 0\n",
)
.unwrap();
let mut count = 0usize;
let built = build_captured(
"okcrate",
None,
false,
&stage,
&logs,
&mut |l| {
if matches!(
crate::progress::parse_line(l),
crate::progress::BuildEvent::Compiling { .. }
) {
count += 1;
}
},
&crate::BuildControl::new(),
)
.unwrap();
assert_eq!(count, 1);
assert_eq!(built.bins, vec!["okcrate"]);
fs::write(&script, "#!/bin/sh\nexit 0\n").unwrap();
let _ = fs::remove_dir_all(&logs);
let err = build_captured(
"ghost",
None,
false,
&stage,
&logs,
&mut |_| {},
&crate::BuildControl::new(),
)
.unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("cargo exited successfully, but:"),
"verification failure names itself: {msg}"
);
assert!(
msg.contains("full log:"),
"verification failure still writes and names the log: {msg}"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn run_dir_parsing_is_strict_on_both_layouts() {
assert_eq!(parse_run_dir("1234"), Some(StageRun::LegacyPid(1234)));
assert_eq!(
parse_run_dir("1234-0123456789abcdef"),
Some(StageRun::LeasedRun { pid: 1234 })
);
for junk in [
"",
"-",
"12x",
"+7", " 7",
"1234-",
"-0123456789abcdef",
"1234-0123456789abcde", "1234-0123456789abcdef0", "1234-0123456789ABCDEF", "1234-0123456789abcdeg",
"12x4-0123456789abcdef",
"1234-0123456789abcdef-0", ] {
assert_eq!(parse_run_dir(junk), None, "accepted junk: {junk:?}");
}
}
#[test]
fn a_handled_acquire_error_leaves_no_unknown_run_behind() {
let root = std::env::temp_dir().join("cargo-lbin-test-lease-guard");
let _ = fs::remove_dir_all(&root);
let mut run = root.clone();
while run.as_os_str().len() + 201 < 4090 {
run = run.join("x".repeat(200));
}
let pad = 4090_usize.saturating_sub(run.as_os_str().len() + 1);
run = run.join("x".repeat(pad.clamp(1, 200)));
let Err(err) = Lease::acquire(&run) else {
panic!("a path past PATH_MAX must not acquire")
};
assert!(
format!("{err:#}").contains(".lease.pending"),
"the failure is the pending file's, past create_dir: {err:#}"
);
assert!(
!run.exists(),
"a handled error demolishes the half-made run"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn remove_leased_run_clears_payload_and_spares_symlink_targets() {
let _serial = no_spawned_children();
let root = std::env::temp_dir().join("cargo-lbin-test-lease-remove");
let _ = fs::remove_dir_all(&root);
let outside = root.join("outside");
fs::create_dir_all(outside.join("keep")).unwrap();
let run = root.join("42-00000000000000ff");
released_run_fixture(&run);
fs::create_dir_all(run.join("somecrate").join("bin")).unwrap();
fs::write(run.join("somecrate").join("bin").join("tool"), b"x").unwrap();
fs::write(run.join("stray-file"), b"y").unwrap();
std::os::unix::fs::symlink(&outside, run.join("link-out")).unwrap();
let mut held = None;
assert!(
eventually(std::time::Duration::from_secs(10), || {
held = take_lease_for_removal(&run).unwrap();
held.is_some()
}),
"no writer left: the taker owns the run"
);
remove_leased_run(&run).unwrap();
assert!(!run.exists(), "the run is wholly gone, lease included");
assert!(
outside.join("keep").exists(),
"a payload symlink is unlinked, never followed"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn a_symlinked_run_path_is_never_followed() {
let root = std::env::temp_dir().join("cargo-lbin-test-lease-toplink");
let _ = fs::remove_dir_all(&root);
let outside = root.join("outside");
fs::create_dir_all(outside.join("keep")).unwrap();
fs::write(outside.join(LEASE_FILE), b"").unwrap();
let link = root.join("55-00000000000000ab");
std::os::unix::fs::symlink(&outside, &link).unwrap();
let take = take_lease_for_removal(&link);
assert!(
take.is_err(),
"a symlinked run path must refuse the taker outright"
);
let removal = remove_leased_run(&link);
assert!(
removal.is_err(),
"a symlinked run path must refuse the remover outright"
);
assert!(
outside.join("keep").exists() && outside.join(LEASE_FILE).exists(),
"zero traversal: the link's target is untouched"
);
assert!(
link.exists() || fs::symlink_metadata(&link).is_ok(),
"the link itself is left where it was found"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn creator_cleanup_defers_to_a_surviving_inheritor() {
let _serial = no_spawned_children();
let root = std::env::temp_dir().join("cargo-lbin-test-lease-veto");
let _ = fs::remove_dir_all(&root);
let run = root.join("777-00000000000000dd");
let stop = root.join("stop");
let lease = Lease::acquire(&run).unwrap();
fs::create_dir_all(run.join("somecrate")).unwrap();
let mut child = Command::new("sh")
.arg("-c")
.arg(format!(
"n=0; while [ ! -e \"{stop}\" ] && [ \"$n\" -lt 400 ]; do n=$((n+1)); sleep 0.05; done",
stop = stop.display()
))
.spawn()
.unwrap();
release_and_remove_run(lease, &run);
assert!(
run.exists(),
"a surviving inheritor vetoes the creator's own cleanup"
);
assert!(
take_lease_for_removal(&run).unwrap().is_none(),
"the veto is the inheritor's lock, nothing softer: a re-ask is refused too"
);
fs::write(&stop, b"").unwrap();
child.wait().unwrap();
assert!(
eventually(std::time::Duration::from_secs(10), || {
take_lease_for_removal(&run).unwrap().is_some()
}),
"the inheritor's exit is the release; the ordinary ownerless path takes over"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn a_lease_is_held_exactly_as_long_as_its_holder_lives() {
use std::os::fd::AsRawFd;
let _serial = no_spawned_children();
let root = std::env::temp_dir().join("cargo-lbin-test-lease-lifetime");
let _ = fs::remove_dir_all(&root);
let run = root.join("12345-0123456789abcdef");
let lease = Lease::acquire(&run).unwrap();
assert!(
!run.join(LEASE_PENDING).exists(),
"acquire publishes by rename: the pending name must not outlive it"
);
assert!(
run.join(LEASE_FILE).exists(),
"from the first instant .lease exists, it is held"
);
let probe = fs::File::open(run.join(LEASE_FILE)).unwrap();
let contended = unsafe { libc::flock(probe.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
assert_eq!(contended, -1, "a held lease must refuse a second owner");
assert_eq!(
std::io::Error::last_os_error().kind(),
std::io::ErrorKind::WouldBlock,
"the refusal is contention, not some other failure"
);
drop(lease);
assert!(
eventually(std::time::Duration::from_secs(10), || {
unsafe { libc::flock(probe.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) == 0 }
}),
"a dropped lease must become takeable"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn a_lease_descriptor_is_inheritable_across_exec() {
use std::os::fd::AsRawFd;
let root = std::env::temp_dir().join("cargo-lbin-test-lease-cloexec");
let _ = fs::remove_dir_all(&root);
let run = root.join("12345-fedcba9876543210");
let lease = Lease::acquire(&run).unwrap();
let flags = unsafe { libc::fcntl(lease.file.as_raw_fd(), libc::F_GETFD) };
assert!(flags >= 0);
assert_eq!(
flags & libc::FD_CLOEXEC,
0,
"a close-on-exec lease dies with this process — the one lifetime it must outlive"
);
drop(lease);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn probing_answers_unknown_held_released_and_readers_coexist() {
use std::os::fd::AsRawFd;
let _serial = no_spawned_children();
let root = std::env::temp_dir().join("cargo-lbin-test-lease-probe");
let _ = fs::remove_dir_all(&root);
let window = root.join("12345-00000000000000ee");
fs::create_dir_all(&window).unwrap();
assert_eq!(probe_lease(&window), LeaseState::Unknown);
let run = root.join("12345-00000000000000cc");
let lease = Lease::acquire(&run).unwrap();
assert_eq!(probe_lease(&run), LeaseState::Held);
drop(lease);
let run = root.join("12345-00000000000000cd");
released_run_fixture(&run);
let reader = fs::File::open(run.join(LEASE_FILE)).unwrap();
assert_eq!(
unsafe { libc::flock(reader.as_raw_fd(), libc::LOCK_SH | libc::LOCK_NB) },
0,
"no writer left: the reader's shared lock goes through"
);
assert_eq!(
probe_lease(&run),
LeaseState::Released,
"a fellow reader must never register as an owner"
);
let taker = fs::File::open(run.join(LEASE_FILE)).unwrap();
assert_eq!(
unsafe { libc::flock(taker.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) },
-1,
"a reader inside refuses the exclusive taker"
);
drop(reader);
assert_eq!(probe_lease(&run), LeaseState::Released);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn run_dir_names_round_trip_and_differ() {
let a = new_run_dir_name().unwrap();
let b = new_run_dir_name().unwrap();
assert_ne!(a, b, "two runs, one name: the nonce failed its job");
for name in [&a, &b] {
assert_eq!(
parse_run_dir(name),
Some(StageRun::LeasedRun {
pid: std::process::id()
}),
"the generator wrote a name the parser rejects: {name}"
);
}
}
}