From 6bb8ab5016daee8719835aeb12198f3629a92a2c Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sun, 27 Sep 2026 23:32:21 +0200 Subject: [PATCH] deb,prune: clean up orphaned session lock files Session locks live next to the session root, and failed or interrupted runs left them behind after the root was gone (seven stray locks on a used machine). Session removal now unlinks its own lock file, and prune collects the orphans: by default only the ones no live build holds (probed with a non-blocking flock), with --all every one of them. Locks guarding an existing session root are never touched. --- src/prune.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/src/prune.rs b/src/prune.rs index 3228a4b..7c598cc 100644 --- a/src/prune.rs +++ b/src/prune.rs @@ -101,6 +101,9 @@ enum Artifact { Session(PathBuf), /// A `pkh test` run directory (report artifacts). TestRun(PathBuf), + /// An orphaned session lock file (`.lock` whose session root is + /// gone). + SessionLock(PathBuf), } impl Artifact { @@ -112,7 +115,8 @@ impl Artifact { | Artifact::Tarball(p) | Artifact::LogFile(p) | Artifact::Session(p) - | Artifact::TestRun(p) => p, + | Artifact::TestRun(p) + | Artifact::SessionLock(p) => p, } } @@ -125,6 +129,7 @@ impl Artifact { Artifact::LogFile(_) => "build log", Artifact::Session(_) => "resumable build session", Artifact::TestRun(_) => "pkh test run", + Artifact::SessionLock(_) => "stale session lockfile", } } } @@ -133,17 +138,32 @@ impl Artifact { /// with `--all`, every session; otherwise only those untouched for longer /// than the retention window ([`crate::deb::session::SESSION_RETENTION`]) /// and the ones whose manifest is missing or unreadable (inert leftovers). +/// +/// Orphaned lock files (`.lock` whose session root is gone — left +/// behind by failed/interrupted runs) are collected too: by default only +/// the ones no live build holds (probed with a non-blocking flock), with +/// `--all` every one of them. fn discover_sessions(sessions_dir: &Path, all: bool) -> Vec { let Ok(entries) = fs::read_dir(sessions_dir) else { return Vec::new(); }; - entries - .flatten() - .map(|entry| entry.path()) - .filter(|path| path.is_dir()) - .filter(|path| is_prunable_session(path, all)) - .map(Artifact::Session) - .collect() + let mut artifacts = Vec::new(); + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + if let Some(slug) = name.strip_suffix(".lock") { + if !path.is_file() || sessions_dir.join(slug).is_dir() { + continue; + } + let held = crate::deb::session::lock_is_held(&path).unwrap_or(true); + if all || !held { + artifacts.push(Artifact::SessionLock(path)); + } + } else if path.is_dir() && is_prunable_session(&path, all) { + artifacts.push(Artifact::Session(path)); + } + } + artifacts } /// Discover the `pkh test` run directories prune may remove under @@ -800,6 +820,7 @@ none /tmp/other proc rw 0 0 Artifact::LogFile(_) => logs += 1, Artifact::Session(_) => {} Artifact::TestRun(_) => {} + Artifact::SessionLock(_) => {} } } assert_eq!(temp_dirs, 2); @@ -1303,4 +1324,60 @@ none /tmp/other proc rw 0 0 assert!(report.removed.contains(&fresh)); assert!(!fresh.exists()); } + + /// Orphaned session lock files are pruned (unless held by a live + /// build); locks whose session root still exists are never touched. + #[test] + fn test_session_lock_pruning() { + use std::os::unix::io::AsRawFd; + + let temp = tempdir().unwrap(); + let sessions = temp.path().join("sessions"); + let orphan_free = sessions.join("free.lock"); + let orphan_held = sessions.join("held.lock"); + let rooted = sessions.join("rooted.lock"); + fs::create_dir_all(&sessions).unwrap(); + fs::write(&orphan_free, b"").unwrap(); + fs::write(&orphan_held, b"").unwrap(); + fs::write(&rooted, b"").unwrap(); + fs::create_dir_all(sessions.join("rooted")).unwrap(); + + // Hold one orphaned lock, as a live build would. + let holder = fs::OpenOptions::new() + .read(true) + .open(&orphan_held) + .unwrap(); + unsafe { + libc::flock(holder.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB); + } + + let report = prune_in_roots( + temp.path(), + None, + Some(&sessions), + None, + PruneOptions::default(), + ) + .unwrap(); + assert!(report.removed.contains(&orphan_free)); + assert!(!orphan_free.exists()); + assert!(orphan_held.exists(), "a held lock belongs to a live build"); + assert!(rooted.exists(), "a lock guarding an existing root stays"); + + // --all takes the held orphan too (the root it guarded is gone). + drop(holder); + let report = prune_in_roots( + temp.path(), + None, + Some(&sessions), + None, + PruneOptions { + all: true, + ..Default::default() + }, + ) + .unwrap(); + assert!(report.removed.contains(&orphan_held)); + assert!(!orphan_held.exists()); + } }