use std::path::Path;
use std::time::{Duration, Instant};
use crate::ensure::{
Endpoint, EnsureDaemonOptions, EnsureDaemonStatus, ensure_daemon, probe_endpoint,
spawn_under_lock,
};
use crate::pidfile;
use crate::proc_lock::{self, TryLockResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplaceOutcome {
NoDaemonRunning,
UpToDate { version: String },
Replaced { old: String, new: String },
Skipped { reason: String },
}
#[must_use]
pub fn is_stale(running: &str, installed: &str) -> bool {
match (parse3(running), parse3(installed)) {
(Some(r), Some(i)) => r < i,
_ => true,
}
}
#[must_use]
pub fn should_replace(stale: bool, force: bool) -> bool {
stale || force
}
fn parse3(v: &str) -> Option<(u64, u64, u64)> {
let core = v.trim().trim_start_matches('v');
let mut it = core.split('.').map(|s| s.split('-').next().unwrap_or(s));
let a = it.next()?.parse().ok()?;
let b = it.next()?.parse().ok()?;
let c = it.next().unwrap_or("0").parse().ok()?;
Some((a, b, c))
}
fn endpoint_string(ep: &Endpoint) -> String {
match ep {
Endpoint::UnixSocket { path } => path.display().to_string(),
Endpoint::WindowsPipe { name } => name.clone(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HardKillOutcome {
Reaped,
Forced,
IdentitySkipped,
}
pub fn hard_kill(pid: u32, state_dir: &Path) -> std::io::Result<HardKillOutcome> {
#[cfg(unix)]
{
Ok(hard_kill_unix(
pid,
Duration::from_millis(800),
pidfile::pid_alive,
|p| pid_belongs_to_daemon(p, state_dir),
|p| {
let _ = std::process::Command::new("kill")
.args(["-TERM", &p.to_string()])
.status();
},
|p| {
let _ = std::process::Command::new("kill")
.args(["-KILL", &p.to_string()])
.status();
},
))
}
#[cfg(windows)]
{
if !pid_belongs_to_daemon(pid, state_dir) {
return Ok(HardKillOutcome::IdentitySkipped);
}
windows_native::terminate_process(pid)?;
Ok(HardKillOutcome::Forced)
}
}
#[cfg(unix)]
fn hard_kill_unix(
pid: u32,
grace: Duration,
is_alive: impl Fn(u32) -> bool,
still_ours: impl Fn(u32) -> bool,
term: impl Fn(u32),
kill: impl Fn(u32),
) -> HardKillOutcome {
term(pid);
std::thread::sleep(grace);
if !is_alive(pid) {
return HardKillOutcome::Reaped;
}
if !still_ours(pid) {
return HardKillOutcome::IdentitySkipped;
}
kill(pid);
HardKillOutcome::Forced
}
#[must_use]
pub fn pid_belongs_to_daemon(pid: u32, state_dir: &Path) -> bool {
#[cfg(windows)]
{
let _ = state_dir;
windows_native::pid_image_is_daemon(pid)
}
#[cfg(unix)]
{
match read_proc_cmdline(pid) {
Some(argv) => unix_argv_is_our_daemon(&argv, state_dir),
None => false,
}
}
}
#[cfg_attr(not(test), allow(dead_code))]
fn cmdline_is_our_daemon(args: &str, state_dir: &Path) -> bool {
if !args.contains("terminal-commanderd") {
return false;
}
let needle = state_dir.to_string_lossy();
contains_path_arg(args, needle.as_ref())
}
#[cfg_attr(not(test), allow(dead_code))]
fn contains_path_arg(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return false;
}
let bytes = haystack.as_bytes();
let nlen = needle.len();
let mut from = 0;
while let Some(rel) = haystack[from..].find(needle) {
let i = from + rel;
let end = i + nlen;
let before_ok = i == 0 || is_arg_boundary(bytes[i - 1]);
let after_ok = end == bytes.len() || is_arg_boundary(bytes[end]);
if before_ok && after_ok {
return true;
}
from = i + 1;
}
false
}
#[cfg_attr(not(test), allow(dead_code))]
fn is_arg_boundary(b: u8) -> bool {
b.is_ascii_whitespace() || b == b'=' || b == b'"' || b == b'\''
}
#[cfg(unix)]
fn read_proc_cmdline(pid: u32) -> Option<Vec<String>> {
let raw = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
if raw.is_empty() {
return None;
}
let raw = raw.strip_suffix(b"\0").unwrap_or(&raw);
let argv: Vec<String> = raw
.split(|&b| b == 0)
.map(|seg| String::from_utf8_lossy(seg).into_owned())
.collect();
if argv.is_empty() {
return None;
}
Some(argv)
}
#[cfg(unix)]
fn unix_argv_is_our_daemon<S: AsRef<str>>(argv: &[S], state_dir: &Path) -> bool {
const DAEMON_BIN: &str = "terminal-commanderd";
const DATA_DIR_FLAG: &str = "--data-dir";
const DATA_DIR_EQ: &str = "--data-dir=";
let exe = match argv.first() {
Some(first) => first.as_ref(),
None => return false,
};
if std::path::Path::new(exe)
.file_name()
.and_then(|n| n.to_str())
!= Some(DAEMON_BIN)
{
return false;
}
let mut iter = argv.iter().skip(1);
while let Some(arg) = iter.next() {
let arg = arg.as_ref();
let value = if arg == DATA_DIR_FLAG {
iter.next().map(AsRef::as_ref)
} else {
arg.strip_prefix(DATA_DIR_EQ)
};
if let Some(value) = value {
return std::path::Path::new(value) == state_dir;
}
}
true
}
#[must_use]
pub fn find_daemon_pid_os(state_dir: &Path) -> Option<u32> {
#[cfg(windows)]
{
let _ = state_dir;
windows_native::find_first_daemon_pid()
}
#[cfg(unix)]
{
let out = std::process::Command::new("pgrep")
.args(["-f", "terminal-commanderd"])
.output()
.ok()?;
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| line.trim().parse::<u32>().ok())
.find(|&pid| pid_belongs_to_daemon(pid, state_dir))
}
}
#[cfg(windows)]
mod windows_native {
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
};
use windows::Win32::System::Threading::{
GetCurrentProcessId, OpenProcess, PROCESS_NAME_FORMAT, PROCESS_QUERY_LIMITED_INFORMATION,
PROCESS_TERMINATE, QueryFullProcessImageNameW, TerminateProcess,
};
const DAEMON_IMAGE: &str = "terminal-commanderd.exe";
struct OwnedHandle(HANDLE);
impl Drop for OwnedHandle {
fn drop(&mut self) {
unsafe {
let _ = CloseHandle(self.0);
}
}
}
fn pid_image_file_name(pid: u32) -> Option<String> {
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()? };
let proc = OwnedHandle(handle);
let mut buf = [0u16; 1024];
let mut len = u32::try_from(buf.len()).expect("image-path buffer length fits in u32");
let ok = unsafe {
QueryFullProcessImageNameW(
proc.0,
PROCESS_NAME_FORMAT(0),
windows::core::PWSTR(buf.as_mut_ptr()),
&raw mut len,
)
.is_ok()
};
if !ok || len == 0 {
return None;
}
let path = String::from_utf16_lossy(&buf[..len as usize]);
std::path::Path::new(&path)
.file_name()
.and_then(|n| n.to_str())
.map(str::to_ascii_lowercase)
}
pub(super) fn pid_image_is_daemon(pid: u32) -> bool {
pid_image_file_name(pid).is_some_and(|name| name == DAEMON_IMAGE)
}
pub(super) fn terminate_process(pid: u32) -> std::io::Result<()> {
let access = PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION;
let handle = unsafe { OpenProcess(access, false, pid) }
.map_err(|e| std::io::Error::other(e.to_string()))?;
let proc = OwnedHandle(handle);
unsafe { TerminateProcess(proc.0, 1) }.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(())
}
pub(super) fn find_first_daemon_pid() -> Option<u32> {
let snapshot =
OwnedHandle(unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?);
let current_pid = unsafe { GetCurrentProcessId() };
let mut entry = PROCESSENTRY32W {
dwSize: u32::try_from(std::mem::size_of::<PROCESSENTRY32W>())
.expect("PROCESSENTRY32W size fits in u32"),
..Default::default()
};
let mut has_entry = unsafe { Process32FirstW(snapshot.0, &raw mut entry).is_ok() };
while has_entry {
let pid = entry.th32ProcessID;
if pid != current_pid && exe_name_from_entry(&entry).eq_ignore_ascii_case(DAEMON_IMAGE)
{
if pid_image_is_daemon(pid) {
return Some(pid);
}
}
has_entry = unsafe { Process32NextW(snapshot.0, &raw mut entry).is_ok() };
}
None
}
fn exe_name_from_entry(entry: &PROCESSENTRY32W) -> String {
let end = entry
.szExeFile
.iter()
.position(|c| *c == 0)
.unwrap_or(entry.szExeFile.len());
String::from_utf16_lossy(&entry.szExeFile[..end])
}
}
pub async fn replace_if_stale(
opts: &EnsureDaemonOptions,
installed_version: &str,
force: bool,
) -> ReplaceOutcome {
if !probe_endpoint(&opts.endpoint).await {
return ReplaceOutcome::NoDaemonRunning;
}
let ep_str = endpoint_string(&opts.endpoint);
let (old_version, pid) = match pidfile::read_pidfile(&opts.state_dir) {
Some(rec) => {
if !should_replace(is_stale(&rec.version, installed_version), force) {
return ReplaceOutcome::UpToDate {
version: rec.version,
};
}
if rec.endpoint != ep_str {
return ReplaceOutcome::Skipped {
reason: format!(
"pidfile endpoint {:?} != target {:?}; refusing to kill",
rec.endpoint, ep_str
),
};
}
(rec.version, rec.pid)
}
None => {
match find_daemon_pid_os(&opts.state_dir) {
Some(pid) => ("pre-pidfile".to_owned(), pid),
None => {
return ReplaceOutcome::Skipped {
reason: "reachable daemon, no pidfile, no killable pid found".to_owned(),
};
}
}
}
};
if !pid_belongs_to_daemon(pid, &opts.state_dir) {
return ReplaceOutcome::Skipped {
reason: format!(
"pid {pid} no longer a terminal-commanderd bound to {:?}; \
refusing to kill (pid may have been recycled)",
opts.state_dir
),
};
}
if let Err(e) = hard_kill(pid, &opts.state_dir) {
return ReplaceOutcome::Skipped {
reason: format!("hard-kill pid {pid} failed: {e}"),
};
}
for _ in 0..30 {
if !probe_endpoint(&opts.endpoint).await {
pidfile::remove_pidfile(&opts.state_dir);
return ReplaceOutcome::Replaced {
old: old_version,
new: installed_version.to_owned(),
};
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
ReplaceOutcome::Skipped {
reason: format!("killed pid {pid} but endpoint still reachable after 3s"),
}
}
pub async fn ensure_or_replace(
opts: &EnsureDaemonOptions,
version: &str,
force: bool,
) -> EnsureDaemonStatus {
let start = Instant::now();
if !force
&& probe_endpoint(&opts.endpoint).await
&& let Some(rec) = pidfile::read_pidfile(&opts.state_dir)
&& !is_stale(&rec.version, version)
{
return EnsureDaemonStatus::AlreadyRunning {
endpoint: opts.endpoint.clone(),
pid: Some(rec.pid),
};
}
let _ = std::fs::create_dir_all(&opts.state_dir);
let lock_path = pidfile::lock_path(&opts.state_dir);
match proc_lock::try_acquire(&lock_path) {
Ok(TryLockResult::Acquired(guard)) => {
let outcome = replace_if_stale(opts, version, force).await;
tracing::debug!("ensure_or_replace: replace outcome = {outcome:?}");
if probe_endpoint(&opts.endpoint).await {
let pid = pidfile::read_pidfile(&opts.state_dir).map(|r| r.pid);
return EnsureDaemonStatus::AlreadyRunning {
endpoint: opts.endpoint.clone(),
pid,
};
}
spawn_under_lock(opts.clone(), start, &guard).await
}
Ok(TryLockResult::Contended) => {
let deadline = start + opts.startup_timeout;
while Instant::now() < deadline {
if probe_endpoint(&opts.endpoint).await {
let pid = pidfile::read_pidfile(&opts.state_dir).map(|r| r.pid);
return EnsureDaemonStatus::AlreadyRunning {
endpoint: opts.endpoint.clone(),
pid,
};
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
ensure_daemon(opts.clone()).await
}
Err(e) => {
tracing::warn!("bring-up lock unavailable ({e}); replace+ensure without single-flight");
let _ = replace_if_stale(opts, version, force).await;
ensure_daemon(opts.clone()).await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stale_compare() {
assert!(is_stale("0.1.13", "0.1.14"));
assert!(is_stale("0.1.13", "0.2.0"));
assert!(!is_stale("0.1.14", "0.1.14"));
assert!(!is_stale("0.2.0", "0.1.14"));
assert!(
is_stale("garbage", "0.1.14"),
"unparseable running => stale"
);
assert!(!is_stale("v0.1.14", "0.1.14"), "v-prefix tolerated");
}
#[test]
fn force_replaces_even_when_versions_match() {
assert!(!is_stale("0.1.18", "0.1.18"));
assert!(should_replace(
false, true
));
assert!(should_replace(true, false));
assert!(should_replace(true, true));
assert!(!should_replace(false, false));
}
#[test]
fn recycled_or_unrelated_pid_is_refused_at_kill() {
let unrelated_live_pid = std::process::id();
assert!(
!pid_belongs_to_daemon(
unrelated_live_pid,
std::path::Path::new("/tmp/tc-m4-not-a-daemon")
),
"a live pid that is not our daemon must be refused (no force-kill of a recycled pid)"
);
assert!(
!pid_belongs_to_daemon(0xFFFF_FFF0, std::path::Path::new("/tmp/tc-m4-not-a-daemon")),
"a dead/absent pid must be refused"
);
}
#[cfg(unix)]
#[test]
fn sigkill_withheld_when_pid_recycled_during_grace() {
use std::cell::Cell;
let killed = Cell::new(false);
let outcome = hard_kill_unix(
4242,
Duration::from_millis(0),
|_| true, |_| false, |_| {}, |_| killed.set(true),
);
assert_eq!(outcome, HardKillOutcome::IdentitySkipped);
assert!(
!killed.get(),
"SIGKILL must NOT be sent to a recycled pid (kill-leg identity gate)"
);
}
#[cfg(unix)]
#[test]
fn sigkill_sent_when_still_our_daemon_after_grace() {
use std::cell::Cell;
let killed = Cell::new(false);
let outcome = hard_kill_unix(
4242,
Duration::from_millis(0),
|_| true, |_| true, |_| {},
|_| killed.set(true),
);
assert_eq!(outcome, HardKillOutcome::Forced);
assert!(
killed.get(),
"a live, still-ours daemon must be force-killed"
);
}
#[cfg(unix)]
#[test]
fn no_force_signal_when_graceful_reaped_it() {
use std::cell::Cell;
let killed = Cell::new(false);
let outcome = hard_kill_unix(
4242,
Duration::from_millis(0),
|_| false, |_| panic!("identity must not be probed once the pid is already gone"),
|_| {},
|_| killed.set(true),
);
assert_eq!(outcome, HardKillOutcome::Reaped);
assert!(!killed.get());
}
#[cfg(unix)]
#[test]
fn cmdline_match_is_literal_not_regex() {
let dir = std::path::Path::new("/tmp/tc (run)+[v1]/state.d");
let cmd = format!("terminal-commanderd --data-dir {}", dir.display());
assert!(
cmdline_is_our_daemon(&cmd, dir),
"a state_dir with regex metacharacters must match the cmdline verbatim"
);
assert!(!cmdline_is_our_daemon(
&cmd,
std::path::Path::new("/tmp/other")
));
assert!(
!cmdline_is_our_daemon(&format!("cat {}", dir.display()), dir),
"the daemon binary name is required, not just the path"
);
assert!(
!cmdline_is_our_daemon("terminal-commanderd --data-dir /tmp/elsewhere", dir),
"the exact state_dir is required, not just the binary name"
);
}
#[test]
fn cmdline_match_rejects_path_prefix_of_another_session() {
let base = std::path::Path::new("/home/u/.local/share/terminal-commanderd/state");
let seeded_cmd = "terminal-commanderd --data-dir /home/u/.local/share/terminal-commanderd/state/agent-1 start";
assert!(
!cmdline_is_our_daemon(seeded_cmd, base),
"a seeded session's daemon (state/agent-1) must not match the base session (state)"
);
let base_cmd =
"terminal-commanderd --data-dir /home/u/.local/share/terminal-commanderd/state start";
assert!(cmdline_is_our_daemon(base_cmd, base));
let seeded = base.join("agent-1");
assert!(!cmdline_is_our_daemon(base_cmd, &seeded));
}
#[test]
fn cmdline_match_handles_apostrophe_and_equals_forms() {
let dir = std::path::Path::new("/home/OBrien'X/state");
assert!(cmdline_is_our_daemon(
"terminal-commanderd --data-dir /home/OBrien'X/state start",
dir
));
assert!(cmdline_is_our_daemon(
"terminal-commanderd --data-dir=/home/OBrien'X/state",
dir
));
assert!(!cmdline_is_our_daemon(
"terminal-commanderd --data-dir /home/OBrien'X/state-2 start",
dir
));
}
#[cfg(unix)]
#[test]
fn unix_argv_matches_the_real_daemon() {
let dir = std::path::Path::new("/home/u/.local/share/terminal-commanderd/state");
let argv = vec![
"/opt/tc/terminal-commanderd",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
"start",
"--mode",
"ipc-server",
];
assert!(
unix_argv_is_our_daemon(&argv, dir),
"the real daemon (argv[0]=terminal-commanderd, --data-dir=<our dir>) must match"
);
let joined = vec![
"terminal-commanderd",
"--data-dir=/home/u/.local/share/terminal-commanderd/state",
"start",
];
assert!(
unix_argv_is_our_daemon(&joined, dir),
"the --data-dir=<value> form of the real daemon must match"
);
let bare = vec![
"terminal-commanderd",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
];
assert!(unix_argv_is_our_daemon(&bare, dir));
}
#[cfg(unix)]
#[test]
fn unix_argv_rejects_non_daemon_impostors() {
let dir = std::path::Path::new("/home/u/.local/share/terminal-commanderd/state");
let vim = vec![
"vim",
"/home/u/.local/share/terminal-commanderd/notes.txt",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
];
assert!(
!unix_argv_is_our_daemon(&vim, dir),
"a non-daemon process (vim) must NEVER be matched, even when its argv \
mentions the daemon string and our state_dir"
);
let grep = vec![
"grep",
"-rn",
"terminal-commanderd",
"/home/u/.local/share/terminal-commanderd/state",
];
assert!(
!unix_argv_is_our_daemon(&grep, dir),
"grep over the daemon dir must NEVER be matched"
);
let tail_log = vec![
"tail",
"-f",
"/home/u/.local/share/terminal-commanderd/logs/terminal-commanderd.log",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
];
assert!(!unix_argv_is_our_daemon(&tail_log, dir));
let wrong_bin = vec![
"/opt/tc/my-terminal-commanderd",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
];
assert!(
!unix_argv_is_our_daemon(&wrong_bin, dir),
"a different binary whose name merely contains the daemon string must reject"
);
let log_as_argv0 = vec![
"/home/u/.local/share/terminal-commanderd/logs/terminal-commanderd.log",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
];
assert!(
!unix_argv_is_our_daemon(&log_as_argv0, dir),
"argv[0]=...terminal-commanderd.log must reject (basename != terminal-commanderd)"
);
let other_session = vec![
"terminal-commanderd",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state/agent-1",
"start",
];
assert!(
!unix_argv_is_our_daemon(&other_session, dir),
"the daemon of a sibling session (different --data-dir) must NEVER be matched"
);
let parent_dir = std::path::Path::new("/home/u/.local/share/terminal-commanderd");
assert!(
!unix_argv_is_our_daemon(
&[
"terminal-commanderd",
"--data-dir",
"/home/u/.local/share/terminal-commanderd/state",
],
parent_dir
),
"a daemon under <dir>/state must not match a request for <dir>"
);
let no_flag = vec!["terminal-commanderd", "start", "--mode", "ipc-server"];
assert!(
unix_argv_is_our_daemon(&no_flag, dir),
"the env-configured (no --data-dir) daemon launch shape must be \
recognized as ours — the per-state_dir pidfile already scoped it"
);
let empty: Vec<&str> = vec![];
assert!(!unix_argv_is_our_daemon(&empty, dir));
}
}
#[cfg(all(test, windows))]
mod windows_tests {
use super::*;
fn spawn_fake_daemon() -> (tempfile::TempDir, std::process::Child) {
let dir = tempfile::tempdir().expect("tempdir");
let fake = dir.path().join("terminal-commanderd.exe");
let ping = std::path::Path::new(r"C:\Windows\System32\PING.EXE");
std::fs::copy(ping, &fake).expect("copy ping.exe to terminal-commanderd.exe");
let child = std::process::Command::new(&fake)
.args(["-n", "30", "127.0.0.1"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn fake daemon");
(dir, child)
}
#[test]
fn pid_belongs_to_daemon_rejects_non_daemon_and_absent() {
let self_pid = std::process::id();
assert!(
!pid_belongs_to_daemon(self_pid, std::path::Path::new(r"C:\tc-not-a-daemon")),
"the test runner's own pid (image != terminal-commanderd.exe) must be refused"
);
assert!(
!pid_belongs_to_daemon(0xFFFF_FFF0, std::path::Path::new(r"C:\tc-not-a-daemon")),
"an absent pid must be refused"
);
}
#[test]
fn pid_belongs_to_daemon_true_only_when_image_matches() {
let (_dir, mut child) = spawn_fake_daemon();
let pid = child.id();
let belongs = pid_belongs_to_daemon(pid, std::path::Path::new(r"C:\any\state"));
let _ = child.kill();
let _ = child.wait();
assert!(
belongs,
"a process whose image file name is terminal-commanderd.exe must be accepted"
);
}
#[test]
fn hard_kill_terminates_a_spawned_daemon_image() {
let (_dir, mut child) = spawn_fake_daemon();
let pid = child.id();
let outcome = hard_kill(pid, std::path::Path::new(r"C:\any\state")).expect("hard_kill");
assert_eq!(
outcome,
HardKillOutcome::Forced,
"an identity-matching live process must be force-terminated"
);
let status = child.wait().expect("wait on terminated child");
assert!(
!status.success(),
"TerminateProcess(exit=1) must make the child exit non-zero"
);
let after = hard_kill(pid, std::path::Path::new(r"C:\any\state")).expect("hard_kill again");
assert_eq!(
after,
HardKillOutcome::IdentitySkipped,
"a dead/absent pid must NOT be force-killed (identity gate)"
);
}
#[test]
fn hard_kill_withholds_force_for_non_daemon_pid() {
let self_pid = std::process::id();
let outcome =
hard_kill(self_pid, std::path::Path::new(r"C:\any\state")).expect("hard_kill self");
assert_eq!(
outcome,
HardKillOutcome::IdentitySkipped,
"the test runner (non-daemon image) must NEVER be force-killed"
);
}
#[test]
fn find_daemon_pid_os_enumerates_running_daemon_image() {
let (_dir, mut child) = spawn_fake_daemon();
let pid = child.id();
let found = find_daemon_pid_os(std::path::Path::new(r"C:\any\state"));
let _ = child.kill();
let _ = child.wait();
assert!(
found.is_some(),
"find_daemon_pid_os must enumerate the running daemon-image process (spawned pid {pid})"
);
}
}