use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const DAEMON_PID_SCHEMA: &str = "wire-daemon-pid-v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DaemonPid {
pub schema: String,
pub pid: u32,
pub bin_path: String,
pub version: String,
pub started_at: String,
pub did: Option<String>,
pub relay_url: Option<String>,
}
#[derive(Debug, Clone)]
pub enum PidRecord {
Json(DaemonPid),
LegacyInt(u32),
Missing,
Corrupt(String),
}
impl PidRecord {
pub fn pid(&self) -> Option<u32> {
match self {
PidRecord::Json(d) => Some(d.pid),
PidRecord::LegacyInt(p) => Some(*p),
_ => None,
}
}
}
pub fn ensure_daemon_running() -> Result<bool> {
ensure_background("daemon", &["daemon", "--interval", "5"])
}
pub fn ensure_notify_running() -> Result<bool> {
ensure_background("notify", &["notify", "--interval", "2"])
}
fn pid_file(name: &str) -> Result<PathBuf> {
Ok(crate::config::state_dir()?.join(format!("{name}.pid")))
}
pub fn read_pid_record(name: &str) -> PidRecord {
let path = match pid_file(name) {
Ok(p) => p,
Err(_) => return PidRecord::Missing,
};
let body = match std::fs::read_to_string(&path) {
Ok(b) => b,
Err(_) => return PidRecord::Missing,
};
let trimmed = body.trim();
if trimmed.is_empty() {
return PidRecord::Missing;
}
if trimmed.starts_with('{') {
match serde_json::from_str::<DaemonPid>(trimmed) {
Ok(d) => return PidRecord::Json(d),
Err(e) => return PidRecord::Corrupt(format!("JSON parse: {e}")),
}
}
match trimmed.parse::<u32>() {
Ok(pid) => PidRecord::LegacyInt(pid),
Err(e) => PidRecord::Corrupt(format!("expected int or JSON: {e}")),
}
}
fn write_pid_record(name: &str, record: &DaemonPid) -> Result<()> {
let path = pid_file(name)?;
let body = serde_json::to_vec_pretty(record)?;
std::fs::write(&path, body)?;
Ok(())
}
fn build_pid_record(pid: u32) -> DaemonPid {
let bin_path = std::env::current_exe()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let version = env!("CARGO_PKG_VERSION").to_string();
let started_at = time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
let (did, relay_url) = identity_for_pid_record();
DaemonPid {
schema: DAEMON_PID_SCHEMA.to_string(),
pid,
bin_path,
version,
started_at,
did,
relay_url,
}
}
fn identity_for_pid_record() -> (Option<String>, Option<String>) {
let did = crate::config::read_agent_card()
.ok()
.and_then(|card| {
card.get("did")
.and_then(Value::as_str)
.map(str::to_string)
});
let relay_url = crate::config::read_relay_state()
.ok()
.and_then(|state| {
state
.get("self")
.and_then(|s| s.get("relay_url"))
.and_then(Value::as_str)
.map(str::to_string)
});
(did, relay_url)
}
fn wait_until_alive(pid: u32, budget: Duration) -> bool {
let deadline = Instant::now() + budget;
while Instant::now() < deadline {
if process_alive(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(10));
}
process_alive(pid)
}
fn ensure_background(name: &str, args: &[&str]) -> Result<bool> {
if std::env::var("WIRE_MCP_SKIP_AUTO_UP").is_ok() {
return Ok(false);
}
if let Some(pid) = read_pid_record(name).pid()
&& process_alive(pid)
{
return Ok(false);
}
crate::config::ensure_dirs()?;
let exe = std::env::current_exe()?;
let child = Command::new(&exe)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = child.id();
if !wait_until_alive(pid, Duration::from_millis(500)) {
anyhow::bail!(
"spawned `wire {}` (pid {pid}) did not appear alive within 500ms",
args.join(" ")
);
}
let record = build_pid_record(pid);
write_pid_record(name, &record)?;
Ok(true)
}
pub fn daemon_version_mismatch() -> Option<String> {
let record = read_pid_record("daemon");
let pid = record.pid()?;
if !process_alive(pid) {
return None;
}
match record {
PidRecord::Json(d) => {
if d.version != env!("CARGO_PKG_VERSION") {
Some(d.version)
} else {
None
}
}
PidRecord::LegacyInt(_) => {
Some("<pre-0.5.11>".to_string())
}
_ => None,
}
}
#[cfg(target_os = "linux")]
fn process_alive(pid: u32) -> bool {
std::path::Path::new(&format!("/proc/{pid}")).exists()
}
#[cfg(not(target_os = "linux"))]
fn process_alive(pid: u32) -> bool {
Command::new("kill")
.args(["-0", &pid.to_string()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_alive_self() {
assert!(process_alive(std::process::id()));
}
#[test]
fn process_alive_zero_is_false_or_self() {
assert!(!process_alive(99_999_999));
}
#[test]
fn pid_record_round_trips_via_json_form() {
crate::config::test_support::with_temp_home(|| {
crate::config::ensure_dirs().unwrap();
let record = DaemonPid {
schema: DAEMON_PID_SCHEMA.to_string(),
pid: 12345,
bin_path: "/usr/local/bin/wire".to_string(),
version: "0.5.11".to_string(),
started_at: "2026-05-16T01:23:45Z".to_string(),
did: Some("did:wire:paul-mac".to_string()),
relay_url: Some("https://wireup.net".to_string()),
};
write_pid_record("daemon", &record).unwrap();
let read = read_pid_record("daemon");
match read {
PidRecord::Json(d) => assert_eq!(d, record),
other => panic!("expected JSON record, got {other:?}"),
}
});
}
#[test]
fn pid_record_tolerates_legacy_int_form() {
crate::config::test_support::with_temp_home(|| {
crate::config::ensure_dirs().unwrap();
let path = super::pid_file("daemon").unwrap();
std::fs::write(&path, "98765").unwrap();
let read = read_pid_record("daemon");
match read {
PidRecord::LegacyInt(pid) => assert_eq!(pid, 98765),
other => panic!("expected LegacyInt, got {other:?}"),
}
});
}
#[test]
fn pid_record_corrupt_reports_corrupt_not_panic() {
crate::config::test_support::with_temp_home(|| {
crate::config::ensure_dirs().unwrap();
let path = super::pid_file("daemon").unwrap();
std::fs::write(&path, "not-a-pid-or-json {{{").unwrap();
let read = read_pid_record("daemon");
assert!(matches!(read, PidRecord::Corrupt(_)), "got {read:?}");
});
}
#[test]
fn daemon_version_mismatch_returns_none_when_no_pidfile() {
crate::config::test_support::with_temp_home(|| {
assert_eq!(daemon_version_mismatch(), None);
});
}
}