diff --git a/Cargo.lock b/Cargo.lock index 7c713629b..1ef5afd06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5312,6 +5312,7 @@ dependencies = [ "flate2", "http 1.4.0", "http-body-util", + "libc", "md-5 0.10.6", "parking_lot", "serde", diff --git a/crates/fakecloud-codebuild/src/runtime.rs b/crates/fakecloud-codebuild/src/runtime.rs index 7dfd4a3d9..8a5e88f6a 100644 --- a/crates/fakecloud-codebuild/src/runtime.rs +++ b/crates/fakecloud-codebuild/src/runtime.rs @@ -226,11 +226,12 @@ fn current_instance_label() -> String { /// build containers survive: `reconcile_builds` flips the persisted /// `IN_PROGRESS` builds to `FAILED`, but nothing reaps the containers, so the /// daemon slowly leaks them. Sweep every container tagged `fakecloud-codebuild` -/// whose `fakecloud-instance` label is not the current process. Best-effort: -/// any daemon error is ignored (the backend stays usable). Containers owned by -/// the live process are left untouched so a concurrent build is never killed. +/// whose owning process (its `fakecloud-instance` label) is gone. Containers +/// owned by any *live* fakecloud process are left untouched: several can share +/// one daemon, and removing another running server's build container fails +/// that build mid-provisioning. Best-effort: any daemon error is ignored (the +/// backend stays usable). pub async fn sweep_orphan_containers(cli: &str) { - let current = current_instance_label(); let out = Command::new(cli) .args([ "ps", @@ -254,7 +255,10 @@ pub async fn sweep_orphan_containers(cli: &str) { continue; }; let instance = parts.next().unwrap_or(""); - if instance != current { + if fakecloud_core::container_net::owned_by_dead_process( + instance, + fakecloud_core::container_net::pid_alive, + ) { orphans.push(id.to_string()); } } diff --git a/crates/fakecloud-core/Cargo.toml b/crates/fakecloud-core/Cargo.toml index d52efa7ab..0b27c2438 100644 --- a/crates/fakecloud-core/Cargo.toml +++ b/crates/fakecloud-core/Cargo.toml @@ -29,3 +29,6 @@ tokio = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } + +[target.'cfg(unix)'.dependencies] +libc = "0.2" diff --git a/crates/fakecloud-core/src/container_net.rs b/crates/fakecloud-core/src/container_net.rs index f4bf39be7..6d099f498 100644 --- a/crates/fakecloud-core/src/container_net.rs +++ b/crates/fakecloud-core/src/container_net.rs @@ -158,6 +158,47 @@ pub fn bounded_status(cli: &str, args: &[String]) -> bool { wait_bounded(&mut child) && child.wait().map(|s| s.success()).unwrap_or(false) } +/// True if the given PID is a live process on this host. +/// +/// On Unix this is `kill(pid, 0)`: it returns 0 if the process exists +/// (including zombies), or sets `errno` to `ESRCH` if not. On non-Unix +/// platforms it conservatively returns `true`, so a caller never removes a +/// resource it can't prove is orphaned. +#[cfg(unix)] +pub fn pid_alive(pid: u32) -> bool { + // SAFETY: `kill` with signal 0 is a liveness probe; it does not + // actually deliver a signal. Any PID value is safe to pass. + let rc = unsafe { libc::kill(pid as libc::pid_t, 0) }; + if rc == 0 { + return true; + } + // errno == EPERM means the process exists but we can't signal it — + // still alive from our perspective. + std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM) +} + +#[cfg(not(unix))] +pub fn pid_alive(_pid: u32) -> bool { + true +} + +/// Whether a container or network labelled `fakecloud-instance=