use smix_capsule::runner_view::{self, Seen};
use smix_lease::store::{self, CheckoutLedgers, LeaseDir};
pub fn run(leases: &LeaseDir, prune: bool) -> Result<u8, crate::CliError> {
let machine = ledgers_in(leases);
let checkout_dir = std::env::current_dir()
.ok()
.and_then(|cwd| CheckoutLedgers::discover(&cwd));
let checkout: Vec<(String, smix_lease::Lease)> = match &checkout_dir {
Some(c) => c
.device_ids()
.into_iter()
.filter_map(|id| c.read(&id).ok().flatten().map(|l| (id, l)))
.collect(),
None => Vec::new(),
};
let listeners = runner_view::listeners();
let live_pids: std::collections::HashSet<u32> = machine
.iter()
.flat_map(|(_, lease)| lease.known_resources())
.filter_map(|r| match r {
smix_lease::Resource::AndroidRunner { proc, .. } => Some(proc),
_ => None,
})
.filter(|proc| {
let p = smix_lease::store::probe(proc);
p.pid_exists && p.identity_matches
})
.map(|proc| proc.pid)
.collect();
let rows = runner_view::attribute(&machine, &listeners, &checkout, &live_pids);
let mut pruned = 0usize;
let mut failed_prunes = 0usize;
if rows.is_empty() {
println!(
"no runner ledgers under {leases}, and nothing on this machine is \
listening as a smix runner"
);
return Ok(0);
}
for row in &rows {
match &row.seen {
Seen::Both {
app_pid,
ledger_session_pid,
live_session_pid,
} => {
println!(
":{:<6} {:<40} both app pid {app_pid}, ledger session pid \
{ledger_session_pid}",
row.port, row.device_id
);
match live_session_pid {
Some(live) if live != ledger_session_pid => println!(
"{:>8} the session driving it now is pid {live}, not the one \
recorded",
""
),
None => println!(
"{:>8} no xcodebuild session here answers for it — it keeps \
serving after the session that started it exits",
""
),
_ => {}
}
}
Seen::LedgerOnly { ledger_pid } => {
println!(
":{:<6} {:<40} ledger-only recorded at pid {ledger_pid}; nothing is \
listening there",
row.port, row.device_id
);
}
Seen::ProcessGone { ledger_pid } => {
println!(
":{:<6} {:<40} process-gone recorded at pid {ledger_pid}; that \
process is no longer running",
row.port, row.device_id
);
if prune {
match smix_lease::store::drop_resource_kind(
leases,
&row.device_id,
&smix_lease::Resource::AndroidRunner {
port: row.port,
serial: row.device_id.clone(),
proc: smix_lease::store::identify_self(),
},
) {
Ok(()) => {
pruned += 1;
println!("{:>8} pruned.", "");
}
Err(e) => {
println!("{:>8} could not prune it: {e}", "");
failed_prunes += 1;
}
}
} else {
println!(
"{:>8} nothing to stop. `smix runner list --prune` clears \
rows like this one.",
""
);
}
}
Seen::ProcessOnly {
app_pid,
session_pid,
named_by,
} => {
println!(
":{:<6} {:<40} process-only app pid {app_pid}{}",
row.port,
row.device_id,
match session_pid {
Some(p) => format!(", session pid {p}"),
None => String::new(),
}
);
println!("{:>8} this machine's ledgers have no record of it.", "");
if let (Some(c), Some(_)) = (&checkout_dir, named_by) {
println!(
"{:>8} {c} names this device — that book is evidence about \
where to look, not authority.",
""
);
}
}
Seen::NotProbed { ledger_pid, why } => {
println!(
":{:<6} {:<40} not-probed recorded at pid {ledger_pid} — {why}",
row.port, row.device_id
);
}
}
}
if prune {
println!();
if failed_prunes > 0 {
println!(
"pruned {pruned} row(s); {failed_prunes} would not go — \
the ledger still holds them and the next run will say so again"
);
return Ok(1);
}
println!(
"pruned {pruned} row(s) whose recorded process had ended. \
Boot rows and claims are untouched: they answer who may switch a \
device off, which this command has not looked at."
);
}
Ok(0)
}
fn ledgers_in(leases: &LeaseDir) -> Vec<(String, smix_lease::Lease)> {
let Ok(entries) = std::fs::read_dir(leases.path()) else {
return Vec::new();
};
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();
ids.into_iter()
.filter_map(|id| store::read(leases, &id).ok().flatten().map(|l| (id, l)))
.collect()
}