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),
Missing,
Corrupt(String),
}
impl PidRecord {
pub fn pid(&self) -> Option<u32> {
match self {
PidRecord::Json(d) => Some(d.pid),
_ => 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")))
}
#[derive(Debug, Clone)]
pub struct DaemonLiveness {
pub pidfile_pid: Option<u32>,
pub pidfile_alive: bool,
pub pgrep_pids: Vec<u32>,
pub orphan_pids: Vec<u32>,
pub record: PidRecord,
}
pub fn pid_is_alive(pid: u32) -> bool {
crate::platform::process_alive(pid)
}
pub fn daemon_liveness() -> DaemonLiveness {
let record = read_pid_record("daemon");
let pidfile_pid = record.pid();
let pidfile_alive = pidfile_pid.map(pid_is_alive).unwrap_or(false);
let pgrep_pids: Vec<u32> = crate::platform::find_processes_by_cmdline("wire daemon");
let known_session_pids: std::collections::HashSet<u32> = crate::session::list_sessions()
.map(|sessions| {
sessions
.iter()
.filter_map(|s| crate::session::session_daemon_pid(&s.home_dir))
.collect()
})
.unwrap_or_default();
let supervisor_pid: Option<u32> = crate::session::sessions_root()
.ok()
.map(|root| root.join("supervisor.pid"))
.filter(|p| p.exists())
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| s.trim().parse::<u32>().ok())
.filter(|p| pid_is_alive(*p));
let orphan_pids: Vec<u32> = pgrep_pids
.iter()
.filter(|p| {
Some(**p) != pidfile_pid
&& !known_session_pids.contains(*p)
&& Some(**p) != supervisor_pid
})
.copied()
.collect();
DaemonLiveness {
pidfile_pid,
pidfile_alive,
pgrep_pids,
orphan_pids,
record,
}
}
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;
}
match serde_json::from_str::<DaemonPid>(trimmed) {
Ok(d) => PidRecord::Json(d),
Err(e) => PidRecord::Corrupt(format!("JSON parse: {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(())
}
pub fn write_self_daemon_pid() -> Result<()> {
let path = pid_file("daemon")?;
let my_pid = std::process::id();
if path.exists()
&& let Ok(s) = std::fs::read_to_string(&path)
&& let Ok(rec) = serde_json::from_str::<DaemonPid>(s.trim())
&& rec.pid == my_pid
{
return Ok(());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
write_pid_record("daemon", &build_pid_record(my_pid))
}
pub const LAST_SYNC_FILE_SCHEMA: &str = "wire-daemon-last-sync-v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LastSyncRecord {
pub schema: String,
pub ts: String,
pub push_n: usize,
pub pull_n: usize,
pub rejected_n: usize,
}
fn last_sync_file() -> Result<PathBuf> {
Ok(crate::config::state_dir()?.join("last_sync.json"))
}
pub fn write_last_sync_record(push_n: usize, pull_n: usize, rejected_n: usize) {
let record = LastSyncRecord {
schema: LAST_SYNC_FILE_SCHEMA.to_string(),
ts: time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default(),
push_n,
pull_n,
rejected_n,
};
let _ = (|| -> Result<()> {
let path = last_sync_file()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = serde_json::to_vec_pretty(&record)?;
std::fs::write(&path, body)?;
Ok(())
})()
.map_err(|e| eprintln!("daemon: last-sync persist error (non-fatal): {e:#}"));
}
pub fn read_last_sync_record() -> Option<LastSyncRecord> {
let path = last_sync_file().ok()?;
let body = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&body).ok()
}
pub fn last_sync_age_seconds() -> Option<u64> {
let rec = read_last_sync_record()?;
let parsed =
time::OffsetDateTime::parse(&rec.ts, &time::format_description::well_known::Rfc3339)
.ok()?;
let delta = time::OffsetDateTime::now_utc() - parsed;
let secs = delta.whole_seconds();
Some(secs.max(0) as u64)
}
pub fn daemon_singleton_holder() -> Option<u32> {
match read_pid_record("daemon").pid() {
Some(pid) if pid_is_alive(pid) => Some(pid),
_ => None,
}
}
pub fn claim_daemon_singleton() -> Result<DaemonPidGuard> {
crate::config::ensure_dirs()?;
let pid = std::process::id();
let record = build_pid_record(pid);
write_pid_record("daemon", &record)?;
let path = pid_file("daemon")?;
Ok(DaemonPidGuard {
path,
owned_pid: pid,
})
}
pub struct DaemonPidGuard {
path: PathBuf,
owned_pid: u32,
}
impl Drop for DaemonPidGuard {
fn drop(&mut self) {
if let Ok(body) = std::fs::read_to_string(&self.path) {
let still_ours = serde_json::from_str::<DaemonPid>(body.trim())
.map(|d| d.pid == self.owned_pid)
.unwrap_or_else(|_| {
body.trim()
.parse::<u32>()
.map(|p| p == self.owned_pid)
.unwrap_or(false)
});
if still_ours {
let _ = std::fs::remove_file(&self.path);
}
}
}
}
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
}
}
_ => None,
}
}
fn process_alive(pid: u32) -> bool {
crate::platform::process_alive(pid)
}
#[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_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);
});
}
}