use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Duration;
pub const MARKER_SCHEMA: &str = "wire-retired-v1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetiredMarker {
pub schema: String,
pub retired_at_unix: u64,
#[serde(default)]
pub reason: String,
}
pub fn marker_path(home: &Path) -> PathBuf {
home.join("state").join("wire").join("retired.json")
}
pub fn is_retired(home: &Path) -> bool {
marker_path(home).exists()
}
pub fn read_marker(home: &Path) -> Option<RetiredMarker> {
let bytes = std::fs::read(marker_path(home)).ok()?;
serde_json::from_slice(&bytes).ok()
}
fn write_marker(home: &Path, reason: &str, now_unix: u64) -> Result<()> {
let dir = home.join("state").join("wire");
std::fs::create_dir_all(&dir)?;
let m = RetiredMarker {
schema: MARKER_SCHEMA.to_string(),
retired_at_unix: now_unix,
reason: reason.to_string(),
};
let tmp = dir.join("retired.json.tmp");
std::fs::write(&tmp, serde_json::to_vec_pretty(&m)?)?;
std::fs::rename(&tmp, marker_path(home))?; Ok(())
}
fn remove_marker(home: &Path) -> Result<()> {
let p = marker_path(home);
if p.exists() {
std::fs::remove_file(&p)?;
}
Ok(())
}
pub fn retire_session<F>(home: &Path, reason: &str, now_unix: u64, stop: F) -> Result<Option<u32>>
where
F: Fn(u32) -> bool,
{
write_marker(home, reason, now_unix)?;
let pid = crate::session::session_daemon_pid(home);
if let Some(p) = pid {
stop(p);
}
Ok(pid)
}
pub fn revive_session(home: &Path) -> Result<()> {
remove_marker(home)
}
pub fn stop_daemon_graceful_then_force(pid: u32) -> bool {
crate::platform::kill_process(pid, false);
for _ in 0..10 {
if !crate::platform::process_alive(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(100));
}
crate::platform::kill_process(pid, true);
for _ in 0..6 {
if !crate::platform::process_alive(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(100));
}
!crate::platform::process_alive(pid)
}
pub fn current_home() -> Option<PathBuf> {
let cfg = crate::config::config_dir().ok()?; let home = cfg.parent()?.parent()?; if !home
.join("config")
.join("wire")
.join("private.key")
.exists()
{
return None;
}
Some(std::fs::canonicalize(home).unwrap_or_else(|_| home.to_path_buf()))
}
pub fn is_current(home: &Path) -> bool {
match current_home() {
Some(cur) => {
let h = std::fs::canonicalize(home).unwrap_or_else(|_| home.to_path_buf());
h == cur
}
None => false,
}
}
pub fn has_pending_inbound(home: &Path) -> bool {
let dir = home
.join("state")
.join("wire")
.join("pending-inbound-pairs");
match std::fs::read_dir(&dir) {
Ok(mut entries) => entries.any(|e| {
e.ok()
.and_then(|e| e.path().extension().map(|x| x == "json"))
.unwrap_or(false)
}),
Err(_) => false,
}
}
pub fn identity_age_s(home: &Path) -> Option<u64> {
let p = home.join("config").join("wire").join("private.key");
let mtime = std::fs::metadata(&p).ok()?.modified().ok()?;
mtime.elapsed().ok().map(|d| d.as_secs())
}
pub fn resolve_target(arg: &str) -> Result<crate::session::SessionInfo> {
let a = arg.trim();
if a.is_empty() {
bail!("empty identity — pass a handle, fingerprint, or session key");
}
let sessions = crate::session::list_sessions()?;
let matched: Vec<crate::session::SessionInfo> = sessions
.into_iter()
.filter(|s| {
s.handle.as_deref() == Some(a)
|| s.name == a
|| s.did
.as_deref()
.and_then(crate::dash::fingerprint_from_did)
.as_deref()
== Some(a)
})
.collect();
match matched.len() {
0 => bail!("no wire identity matches '{a}' (try a handle, fingerprint, or `wire dash`)"),
1 => Ok(matched.into_iter().next().unwrap()),
n => {
let names: Vec<String> = matched
.iter()
.map(|s| s.handle.clone().unwrap_or_else(|| s.name.clone()))
.collect();
bail!(
"'{a}' is ambiguous — {n} identities match: {}",
names.join(", ")
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn marker_roundtrip_and_is_retired() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
assert!(!is_retired(home));
write_marker(home, "test", 1_700_000_000).unwrap();
assert!(is_retired(home), "marker present ⇒ retired");
let m = read_marker(home).unwrap();
assert_eq!(m.schema, MARKER_SCHEMA);
assert_eq!(m.retired_at_unix, 1_700_000_000);
assert_eq!(m.reason, "test");
remove_marker(home).unwrap();
assert!(!is_retired(home), "marker removed ⇒ not retired");
}
#[test]
fn is_retired_is_pure_existence_not_content() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
std::fs::create_dir_all(home.join("state").join("wire")).unwrap();
std::fs::write(marker_path(home), b"{ this is not json").unwrap();
assert!(
is_retired(home),
"corrupt marker must still read as retired"
);
assert!(
read_marker(home).is_none(),
"corrupt body → None, but still retired"
);
}
#[test]
fn retire_writes_marker_before_kill() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().to_path_buf();
let called = std::cell::Cell::new(false);
let pid = retire_session(&home, "r", 1, |_p| {
called.set(true);
true
})
.unwrap();
assert_eq!(pid, None, "no pid file ⇒ nothing to stop");
assert!(!called.get());
assert!(is_retired(&home), "marker written even with no daemon");
}
#[test]
fn revive_is_noop_when_not_retired() {
let dir = tempfile::tempdir().unwrap();
revive_session(dir.path()).unwrap();
assert!(!is_retired(dir.path()));
}
}