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()); + } }