use super::*;
#[derive(Debug, Clone)]
pub(crate) struct RegistryRow {
pub(crate) pid: Option<u32>,
pub(crate) status: Option<String>,
pub(crate) status_updated_at_ms: Option<i64>,
pub(crate) proc_start: Option<String>,
pub(crate) pid_domain: Option<String>,
}
pub(crate) fn registry_row_for(session_id: &str) -> Result<Option<RegistryRow>> {
let dir = crate::path::claude_home()?.join("sessions");
if !dir.is_dir() {
return Ok(None);
}
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let p = entry.path();
if p.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let Ok(raw) = std::fs::read_to_string(&p) else {
continue; };
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
continue;
};
if v.get("sessionId").and_then(serde_json::Value::as_str) != Some(session_id) {
continue;
}
let str_field = |k: &str| {
v.get(k)
.and_then(serde_json::Value::as_str)
.map(str::to_string)
};
return Ok(Some(RegistryRow {
pid: v
.get("pid")
.and_then(serde_json::Value::as_u64)
.and_then(|n| u32::try_from(n).ok()),
status: str_field("status"),
status_updated_at_ms: v.get("statusUpdatedAt").and_then(serde_json::Value::as_i64),
proc_start: str_field("procStartFt").or_else(|| str_field("procStart")),
pid_domain: str_field("pidDomain"),
}));
}
Ok(None)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PidLiveness {
Alive { reuse_guard: ReuseGuard },
Dead,
Reused,
ForeignDomain(String),
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReuseGuard {
Checked,
Skipped,
}
#[must_use]
pub(crate) fn local_pid_domain() -> &'static str {
if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(target_os = "linux") {
"linux"
} else if cfg!(windows) {
"win32"
} else {
"unknown"
}
}
pub(crate) fn probe_pid(
pid: u32,
proc_start: Option<&str>,
pid_domain: Option<&str>,
) -> PidLiveness {
if let Some(d) = pid_domain {
let head = d.split(':').next().unwrap_or(d);
if head != local_pid_domain() {
return PidLiveness::ForeignDomain(d.to_string());
}
}
match ps_probe(pid) {
PsProbe::Unavailable => PidLiveness::Unavailable,
PsProbe::NoProcess => PidLiveness::Dead,
PsProbe::Alive(actual) => match (proc_start.and_then(parse_registry_proc_start), actual) {
(Some(reg), Some(act)) => {
if (reg.as_second() - act.as_second()).abs() <= 2 {
PidLiveness::Alive {
reuse_guard: ReuseGuard::Checked,
}
} else {
PidLiveness::Reused
}
}
_ => PidLiveness::Alive {
reuse_guard: ReuseGuard::Skipped,
},
},
}
}
pub(crate) enum PsProbe {
NoProcess,
Alive(Option<jiff::Timestamp>),
#[cfg_attr(unix, allow(dead_code))]
Unavailable,
}
const FILETIME_UNIX_OFFSET_SECS: i64 = 11_644_473_600;
pub(crate) fn parse_registry_proc_start(s: &str) -> Option<jiff::Timestamp> {
let s = s.trim();
if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
return filetime_to_timestamp(s.parse::<u64>().ok()?);
}
let bd = jiff::fmt::strtime::parse("%a %b %e %H:%M:%S %Y", s).ok()?;
let dt = bd.to_datetime().ok()?;
dt.to_zoned(jiff::tz::TimeZone::UTC)
.ok()
.map(|z| z.timestamp())
}
pub(crate) fn filetime_to_timestamp(ticks: u64) -> Option<jiff::Timestamp> {
let secs = i64::try_from(ticks / 10_000_000).ok()? - FILETIME_UNIX_OFFSET_SECS;
if secs < 0 {
return None;
}
let nanos = i32::try_from((ticks % 10_000_000) * 100).ok()?;
jiff::Timestamp::new(secs, nanos).ok()
}
#[cfg(unix)]
pub(crate) fn ps_probe(pid: u32) -> PsProbe {
let Ok(out) = std::process::Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "lstart="])
.output()
else {
return PsProbe::Alive(None); };
let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !out.status.success() || text.is_empty() {
if std::path::Path::new(&format!("/proc/{pid}")).is_dir() {
return PsProbe::Alive(None);
}
return PsProbe::NoProcess;
}
let local = crate::timez::local_tz();
for fmt in ["%a %b %e %H:%M:%S %Y", "%a %e %b %H:%M:%S %Y"] {
if let Ok(bd) = jiff::fmt::strtime::parse(fmt, &text) {
if let Ok(dt) = bd.to_datetime() {
if let Ok(z) = dt.to_zoned(local.clone()) {
return PsProbe::Alive(Some(z.timestamp()));
}
}
}
}
PsProbe::Alive(None)
}
#[cfg(windows)]
pub(crate) fn ps_probe(pid: u32) -> PsProbe {
let script = format!(
"$ErrorActionPreference='Stop'; try {{ $p = Get-Process -Id {pid} }} catch {{ exit 3 }}; \
try {{ [Console]::Out.Write($p.StartTime.ToFileTimeUtc()) }} catch {{ [Console]::Out.Write('NOSTART') }}"
);
if let Ok(out) = std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-Command", &script])
.output()
{
let text = String::from_utf8_lossy(&out.stdout).trim().to_string();
if out.status.code() == Some(3) {
return PsProbe::NoProcess;
}
if out.status.success() {
if text.bytes().all(|b| b.is_ascii_digit()) && !text.is_empty() {
return PsProbe::Alive(text.parse::<u64>().ok().and_then(filetime_to_timestamp));
}
return PsProbe::Alive(None);
}
}
let Ok(out) = std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
.output()
else {
return PsProbe::Unavailable;
};
let text = String::from_utf8_lossy(&out.stdout);
if text.lines().any(|l| l.trim_start().starts_with('"')) {
PsProbe::Alive(None)
} else {
PsProbe::NoProcess
}
}
#[cfg(not(any(unix, windows)))]
pub(crate) fn ps_probe(_pid: u32) -> PsProbe {
PsProbe::Unavailable
}