use smix_simctl::SimctlClient;
use smix_simctl::registry::SimRegistry;
use std::path::Path;
fn pkill(sig: &str, pattern: &str, label: &str) -> bool {
let hit = std::process::Command::new("pkill")
.args([sig, "-f", pattern])
.status()
.map(|s| s.success())
.unwrap_or(false);
if hit {
println!(" {sig} {label}");
}
hit
}
fn settle_ledgers(root: &Path, leases: &smix_lease::store::LeaseDir) {
let Ok(entries) = std::fs::read_dir(leases.path()) else {
println!(" no device ledgers");
return;
};
let mut ids: Vec<String> = entries
.flatten()
.filter_map(|e| {
e.file_name()
.to_string_lossy()
.strip_suffix(".json")
.map(str::to_string)
})
.collect();
ids.sort();
if ids.is_empty() {
println!(" no device ledgers");
return;
}
for id in ids {
let facts = match smix_lease::store::collect_facts(leases, &id) {
Ok(f) => f,
Err(e) => {
eprintln!(" {id}: ledger unreadable: {e}");
continue;
}
};
let Some(held) = facts.existing else {
println!(" {id}: nothing owed");
continue;
};
let mine = std::process::id();
let holder_alive = held.holder.pid_exists && held.holder.identity_matches;
if holder_alive && held.lease.holder.pid != mine {
println!(
" {id}: held by pid {} ({}) — still alive, left alone",
held.lease.holder.pid, held.lease.holder.cmd
);
continue;
}
let cleanup = smix_lease::plan_cleanup(&held.lease);
if cleanup.is_empty() {
println!(" {id}: nothing owed");
}
let mut all_clean = true;
for outcome in smix_capsule::reconcile::execute(root, &cleanup) {
println!(" {id}: {}", outcome.line());
all_clean &= outcome.is_clean();
}
if all_clean {
if let Err(e) = smix_lease::store::remove(leases, &id) {
eprintln!(" {id}: ledger not updated: {e}");
}
} else {
println!(" {id}: some closes failed — ledger kept so they stay visible");
}
}
}
pub async fn run(root: &Path, runner_port: u16) -> Result<(), String> {
let leases = smix_capsule::runner::machine_leases()?;
println!("=== 1. device ledgers (close what we opened) ===");
settle_ledgers(root, &leases);
println!("=== 2. XCUITest runner ===");
smix_capsule::runner::down(root, runner_port)?;
println!("=== 3. web demo stack ===");
pkill("-TERM", "smix/web/node_modules/.bin/vite", "vite");
pkill("-TERM", "smix-demo-target/debug/smix-server", "smix-server");
let smix_dir = root.join(".smix");
let reg = {
let merged = SimRegistry::open_all(root);
(!merged.registry.sims().is_empty()).then_some(merged.registry)
};
println!("=== 4. orphan recorders / motion loops (registered UDIDs only) ===");
if let Some(reg) = ® {
for sim in reg.sims().values() {
pkill(
"-INT",
&format!("simctl io.*{}.*recordVideo", sim.udid),
&format!("recordVideo ({})", sim.device_name),
);
pkill(
"-TERM",
&format!("simctl launch.*{}.*com.apple.Preferences", sim.udid),
&format!("motion loop ({})", sim.device_name),
);
}
}
println!("=== 5. shutdown registered sims (per-UDID) ===");
if let Some(reg) = ® {
let simctl = SimctlClient::new();
let devices = simctl.list_devices().await.map_err(|e| e.to_string())?;
for (alias, sim) in reg.sims() {
let booted = devices
.iter()
.any(|d| d.udid.eq_ignore_ascii_case(&sim.udid) && d.state == "Booted");
if !booted {
continue;
}
let ledger = smix_lease::store::read(&leases, &sim.udid).ok().flatten();
let ours = smix_lease::may_shut_down(ledger.as_ref());
if !ours {
println!(" {alias} ({}) is up but not ours — left alone", sim.udid);
continue;
}
{
simctl
.shutdown(&sim.udid)
.await
.map_err(|e| format!("shutdown {alias} ({}): {e}", sim.udid))?;
if let Err(e) = smix_lease::store::drop_resource_kind(
&leases,
&sim.udid,
&smix_lease::Resource::Booted { by_us: true },
) {
eprintln!(" {}: boot row not cleared: {e}", sim.udid);
}
println!(" shutdown {alias} ({})", sim.udid);
}
}
} else {
println!(
" no registry at {} — skipping sim shutdown",
smix_dir.display()
);
}
println!("=== 6. residue report ===");
let mut patterns = vec!["xcodebuild.*Smix|smix-server|smix/web.*vite".to_string()];
if let Some(reg) = ® {
for sim in reg.sims().values() {
patterns.push(format!("simctl io.*{}.*recordVideo", sim.udid));
}
}
let mut residue = String::new();
for p in &patterns {
let out = std::process::Command::new("pgrep")
.args(["-fl", p])
.output()
.map_err(|e| format!("pgrep: {e}"))?;
let hits = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !hits.is_empty() {
residue.push_str(&hits);
residue.push('\n');
}
}
if !residue.is_empty() {
return Err(format!("STILL RUNNING (inspect manually):\n{residue}"));
}
println!("clean — no smix residual processes.");
Ok(())
}