use std::process::{Command, Output, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
#[cfg_attr(not(windows), allow(dead_code))]
fn platform_shell_timeout() -> Duration {
std::env::var("WIRE_PLATFORM_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.map(Duration::from_secs)
.unwrap_or_else(|| Duration::from_secs(5))
}
pub fn run_with_timeout(mut cmd: Command, timeout: Duration) -> Option<Output> {
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = cmd.spawn().ok()?;
let pid = child.id();
let (tx, rx) = mpsc::channel::<Output>();
thread::spawn(move || {
if let Ok(out) = child.wait_with_output() {
let _ = tx.send(out);
}
});
match rx.recv_timeout(timeout) {
Ok(out) => Some(out),
Err(_) => {
kill_pid_best_effort(pid);
None
}
}
}
fn kill_pid_best_effort(pid: u32) {
#[cfg(unix)]
{
let _ = Command::new("kill")
.args(["-9", &pid.to_string()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(windows)]
{
let _ = Command::new("taskkill.exe")
.args(["/F", "/T", "/PID", &pid.to_string()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
}
}
pub fn process_alive(pid: u32) -> bool {
#[cfg(target_os = "linux")]
{
std::path::Path::new(&format!("/proc/{pid}")).exists()
}
#[cfg(all(unix, not(target_os = "linux")))]
{
Command::new("kill")
.args(["-0", &pid.to_string()])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(windows)]
{
let mut cmd = Command::new("tasklist.exe");
cmd.args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"]);
match run_with_timeout(cmd, platform_shell_timeout()) {
Some(o) if o.status.success() => {
let s = String::from_utf8_lossy(&o.stdout);
let trimmed = s.trim();
!trimmed.is_empty() && !trimmed.starts_with("INFO:")
}
_ => false,
}
}
}
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn cmdline_role(pattern: &str) -> &str {
pattern.strip_prefix("wire ").unwrap_or(pattern)
}
pub fn find_processes_by_cmdline(pattern: &str) -> Vec<u32> {
#[cfg(unix)]
{
Command::new("pgrep")
.args(["-f", pattern])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.split_whitespace()
.filter_map(|s| s.parse::<u32>().ok())
.collect()
})
.unwrap_or_default()
}
#[cfg(windows)]
{
let role = cmdline_role(pattern);
let escaped = role.replace('\'', "''");
let ps = format!(
"Get-CimInstance Win32_Process | \
Where-Object {{ $_.Name -like 'wire*' -and $_.ProcessId -ne $PID -and $_.CommandLine -like '*{escaped}*' }} | \
Select-Object -ExpandProperty ProcessId"
);
let mut cmd = Command::new("powershell.exe");
cmd.args(["-NoProfile", "-NonInteractive", "-Command", &ps]);
run_with_timeout(cmd, platform_shell_timeout())
.filter(|o| o.status.success())
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.split_whitespace()
.filter_map(|s| s.parse::<u32>().ok())
.collect()
})
.unwrap_or_default()
}
#[cfg(not(any(unix, windows)))]
{
let _ = pattern;
Vec::new()
}
}
pub fn pid_cmdline(pid: u32) -> Option<String> {
#[cfg(target_os = "linux")]
{
let path = format!("/proc/{pid}/cmdline");
let bytes = std::fs::read(&path).ok()?;
let s: String = bytes
.into_iter()
.map(|b| if b == 0 { b' ' } else { b })
.map(|b| b as char)
.collect();
let trimmed = s.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
#[cfg(all(unix, not(target_os = "linux")))]
{
let out = Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "command="])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
}
#[cfg(windows)]
{
let ps = format!(
"Get-CimInstance Win32_Process | \
Where-Object {{ $_.ProcessId -eq {pid} }} | \
Select-Object -ExpandProperty CommandLine"
);
let mut cmd = Command::new("powershell.exe");
cmd.args(["-NoProfile", "-NonInteractive", "-Command", &ps]);
let out = run_with_timeout(cmd, platform_shell_timeout())?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
None
}
}
pub fn parse_session_arg(cmdline: &str) -> Option<&str> {
let parts: Vec<&str> = cmdline.split_whitespace().collect();
let i = parts.iter().position(|p| *p == "--session")?;
parts.get(i + 1).copied()
}
pub fn kill_process(pid: u32, force: bool) -> bool {
#[cfg(unix)]
{
let sig = if force { "-9" } else { "-15" };
Command::new("kill")
.args([sig, &pid.to_string()])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(windows)]
{
let pid_str = pid.to_string();
let mut args: Vec<&str> = vec!["/PID", &pid_str, "/T"];
if force {
args.push("/F");
}
Command::new("taskkill.exe")
.args(&args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(not(any(unix, windows)))]
{
let _ = (pid, force);
false
}
}
pub fn current_exe_resolved() -> std::io::Result<std::path::PathBuf> {
Ok(strip_deleted_suffix(&std::env::current_exe()?))
}
pub fn strip_deleted_suffix(p: &std::path::Path) -> std::path::PathBuf {
match p.to_string_lossy().strip_suffix(" (deleted)") {
Some(stripped) => std::path::PathBuf::from(stripped),
None => p.to_path_buf(),
}
}
pub fn machine_id_raw() -> Option<Vec<u8>> {
#[cfg(target_os = "linux")]
{
for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"] {
if let Ok(s) = std::fs::read_to_string(path) {
let t = s.trim();
if !t.is_empty() {
return Some(t.as_bytes().to_vec());
}
}
}
None
}
#[cfg(target_os = "macos")]
{
let out = Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
parse_ioreg_platform_uuid(&text).map(|s| s.into_bytes())
}
#[cfg(windows)]
{
let mut cmd = Command::new("reg.exe");
cmd.args([
"query",
r"HKLM\SOFTWARE\Microsoft\Cryptography",
"/v",
"MachineGuid",
]);
let out = run_with_timeout(cmd, platform_shell_timeout())?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
parse_reg_machine_guid(&text).map(|s| s.into_bytes())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
None
}
}
pub fn os_user_id_bytes() -> Option<Vec<u8>> {
#[cfg(unix)]
{
let out = Command::new("id").arg("-u").output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() {
None
} else {
Some(s.into_bytes())
}
}
#[cfg(windows)]
{
let mut cmd = Command::new("whoami.exe");
cmd.args(["/user", "/fo", "csv", "/nh"]);
let out = run_with_timeout(cmd, platform_shell_timeout())?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
parse_whoami_sid(&text).map(|s| s.into_bytes())
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn parse_ioreg_platform_uuid(text: &str) -> Option<String> {
let line = text.lines().find(|l| l.contains("IOPlatformUUID"))?;
let after = line.split_once('=')?.1.trim();
let unquoted = after.trim_matches('"').trim();
if unquoted.is_empty() {
None
} else {
Some(unquoted.to_string())
}
}
#[cfg_attr(not(windows), allow(dead_code))]
fn parse_reg_machine_guid(text: &str) -> Option<String> {
let line = text.lines().find(|l| l.contains("MachineGuid"))?;
let after = line.split("REG_SZ").nth(1)?.trim();
if after.is_empty() {
None
} else {
Some(after.to_string())
}
}
#[cfg_attr(not(windows), allow(dead_code))]
fn parse_whoami_sid(text: &str) -> Option<String> {
let row = text.lines().find(|l| l.contains("S-1-"))?;
let sid = row.rsplit(',').next()?.trim().trim_matches('"').trim();
if sid.is_empty() {
None
} else {
Some(sid.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_deleted_suffix_removes_kernel_marker() {
use std::path::Path;
assert_eq!(
strip_deleted_suffix(Path::new("/home/admin/.cargo/bin/wire (deleted)")),
Path::new("/home/admin/.cargo/bin/wire")
);
}
#[test]
fn strip_deleted_suffix_leaves_clean_path_untouched() {
use std::path::Path;
assert_eq!(
strip_deleted_suffix(Path::new("/usr/local/bin/wire")),
Path::new("/usr/local/bin/wire")
);
}
#[test]
fn strip_deleted_suffix_only_strips_exact_trailing_token() {
use std::path::Path;
assert_eq!(
strip_deleted_suffix(Path::new("/opt/wire(deleted)")),
Path::new("/opt/wire(deleted)")
);
}
#[test]
fn cmdline_role_strips_wire_prefix() {
assert_eq!(cmdline_role("wire daemon"), "daemon");
assert_eq!(cmdline_role("wire relay-server"), "relay-server");
assert_eq!(cmdline_role("daemon"), "daemon");
assert_eq!(cmdline_role("relay-server"), "relay-server");
}
#[test]
fn process_alive_returns_true_for_self() {
let me = std::process::id();
assert!(
process_alive(me),
"process_alive should return true for self pid {me}"
);
}
#[test]
fn process_alive_returns_false_for_clearly_dead_pid() {
let dead = 4_000_000_001;
assert!(
!process_alive(dead),
"process_alive should return false for synthetic dead pid {dead}"
);
}
#[test]
fn parse_session_arg_extracts_following_value() {
assert_eq!(
parse_session_arg("wire daemon --session slancha-mesh --interval 5"),
Some("slancha-mesh")
);
assert_eq!(
parse_session_arg("wire daemon --interval 5 --session wire-dev"),
Some("wire-dev")
);
assert_eq!(
parse_session_arg("/path/to/wire daemon --session foo"),
Some("foo")
);
}
#[test]
fn parse_session_arg_returns_none_without_flag() {
assert_eq!(parse_session_arg("wire daemon --interval 5"), None);
assert_eq!(
parse_session_arg("wire daemon --all-sessions --interval 5"),
None
);
assert_eq!(parse_session_arg(""), None);
}
#[test]
fn parse_session_arg_returns_none_when_flag_is_last_token() {
assert_eq!(parse_session_arg("wire daemon --session"), None);
}
#[test]
fn pid_cmdline_returns_something_for_self() {
let me = std::process::id();
let cmd = pid_cmdline(me);
assert!(
cmd.is_some() && !cmd.as_ref().unwrap().is_empty(),
"pid_cmdline(self) should return a non-empty cmdline, got {cmd:?}"
);
}
#[test]
fn pid_cmdline_returns_none_for_dead_pid() {
let dead = 4_000_000_003;
assert_eq!(
pid_cmdline(dead),
None,
"pid_cmdline should return None for synthetic dead pid"
);
}
#[test]
fn kill_process_on_nonexistent_pid_returns_false_or_noop() {
let dead = 4_000_000_002;
let _ = kill_process(dead, false);
}
use std::time::Instant;
#[test]
fn run_with_timeout_returns_some_on_fast_command() {
#[cfg(unix)]
let cmd = {
let mut c = Command::new("echo");
c.arg("hello");
c
};
#[cfg(windows)]
let cmd = {
let mut c = Command::new("cmd.exe");
c.args(["/C", "echo hello"]);
c
};
let out = run_with_timeout(cmd, Duration::from_secs(5));
assert!(out.is_some(), "echo must complete inside 5s");
let out = out.unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(
s.contains("hello"),
"stdout should contain `hello`; got {s:?}"
);
}
#[test]
fn run_with_timeout_returns_none_and_kills_on_slow_command() {
#[cfg(unix)]
let cmd = {
let mut c = Command::new("sleep");
c.arg("60");
c
};
#[cfg(windows)]
let cmd = {
let mut c = Command::new("powershell.exe");
c.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"Start-Sleep -Seconds 60",
]);
c
};
let started = Instant::now();
let out = run_with_timeout(cmd, Duration::from_millis(500));
let elapsed = started.elapsed();
assert!(out.is_none(), "slow command must time out, got {out:?}");
assert!(
elapsed < Duration::from_secs(10),
"must return well inside the wedged child's runtime; elapsed={elapsed:?}"
);
}
#[test]
fn parse_ioreg_platform_uuid_extracts_value() {
let sample = "\
+-o IOPlatformExpertDevice <class IOPlatformExpertDevice>
{
\"IOPlatformUUID\" = \"564D5E2F-AAAA-BBBB-CCCC-0123456789AB\"
\"IOPlatformSerialNumber\" = \"C02XX\"
}
";
assert_eq!(
parse_ioreg_platform_uuid(sample),
Some("564D5E2F-AAAA-BBBB-CCCC-0123456789AB".to_string())
);
assert_eq!(parse_ioreg_platform_uuid("no uuid here"), None);
}
#[test]
fn parse_reg_machine_guid_extracts_value() {
let sample = "\r\n\
HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\r\n\
MachineGuid REG_SZ 11112222-3333-4444-5555-666677778888\r\n";
assert_eq!(
parse_reg_machine_guid(sample),
Some("11112222-3333-4444-5555-666677778888".to_string())
);
assert_eq!(parse_reg_machine_guid("no guid"), None);
}
#[test]
fn parse_whoami_sid_extracts_last_quoted_field() {
let sample = "\"contoso\\\\alice\",\"S-1-5-21-1111111111-2222222222-3333333333-1001\"\r\n";
assert_eq!(
parse_whoami_sid(sample),
Some("S-1-5-21-1111111111-2222222222-3333333333-1001".to_string())
);
assert_eq!(parse_whoami_sid("no sid"), None);
}
#[test]
fn platform_shell_timeout_default_is_5s() {
let prev = std::env::var("WIRE_PLATFORM_TIMEOUT_SECS").ok();
unsafe { std::env::remove_var("WIRE_PLATFORM_TIMEOUT_SECS") };
assert_eq!(platform_shell_timeout(), Duration::from_secs(5));
unsafe { std::env::set_var("WIRE_PLATFORM_TIMEOUT_SECS", "12") };
assert_eq!(platform_shell_timeout(), Duration::from_secs(12));
match prev {
Some(v) => unsafe { std::env::set_var("WIRE_PLATFORM_TIMEOUT_SECS", v) },
None => unsafe { std::env::remove_var("WIRE_PLATFORM_TIMEOUT_SECS") },
}
}
}