#![allow(dead_code)]
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use serde::Serialize;
use signal_hook::consts::signal::{SIGCHLD, SIGHUP, SIGTERM, SIGUSR1};
use signal_hook::iterator::Signals;
#[derive(Debug, Default)]
pub struct SignalState {
pub sigterm: AtomicBool,
pub sighup: AtomicBool,
pub sigchld: AtomicBool,
pub sigusr1: AtomicBool,
}
pub fn install() -> std::io::Result<Arc<SignalState>> {
let state = Arc::new(SignalState::default());
let state_for_thread = Arc::clone(&state);
let mut signals = Signals::new([SIGTERM, SIGHUP, SIGCHLD, SIGUSR1])?;
thread::Builder::new()
.name("ezpn-signals".to_string())
.spawn(move || {
for sig in signals.forever() {
match sig {
SIGTERM => state_for_thread.sigterm.store(true, Ordering::SeqCst),
SIGHUP => state_for_thread.sighup.store(true, Ordering::SeqCst),
SIGCHLD => state_for_thread.sigchld.store(true, Ordering::SeqCst),
SIGUSR1 => state_for_thread.sigusr1.store(true, Ordering::SeqCst),
_ => {
continue;
}
}
crate::pane::wake_main_loop();
}
})?;
Ok(state)
}
pub fn dump_session_state<T: Serialize>(state: &T) -> std::io::Result<PathBuf> {
let dir = state_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"neither XDG_STATE_HOME nor HOME is set",
)
})?;
std::fs::create_dir_all(&dir)?;
let pid = std::process::id();
let unix_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let path = dir.join(format!("dump-{pid}-{unix_secs}.json"));
let file = std::fs::File::create(&path)?;
serde_json::to_writer_pretty(file, state).map_err(std::io::Error::other)?;
Ok(path)
}
fn state_dir() -> Option<PathBuf> {
if let Ok(state) = std::env::var("XDG_STATE_HOME") {
if !state.is_empty() {
return Some(PathBuf::from(state).join("ezpn"));
}
}
let home = std::env::var("HOME").ok()?;
if home.is_empty() {
return None;
}
Some(
PathBuf::from(home)
.join(".local")
.join("state")
.join("ezpn"),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_state_default_is_all_false() {
let s = SignalState::default();
assert!(!s.sigterm.load(Ordering::SeqCst));
assert!(!s.sighup.load(Ordering::SeqCst));
assert!(!s.sigchld.load(Ordering::SeqCst));
assert!(!s.sigusr1.load(Ordering::SeqCst));
}
#[test]
fn install_returns_arc_with_writable_flags() {
let state = install().expect("install handlers");
assert!(!state.sigterm.load(Ordering::SeqCst));
state.sigterm.store(true, Ordering::SeqCst);
assert!(state.sigterm.load(Ordering::SeqCst));
let cloned = Arc::clone(&state);
cloned.sighup.store(true, Ordering::SeqCst);
assert!(state.sighup.load(Ordering::SeqCst));
}
#[test]
fn dump_session_state_writes_json_to_xdg_state_home() {
let tmp = std::env::temp_dir().join(format!(
"ezpn-signals-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&tmp).expect("create tmp");
let prev = std::env::var("XDG_STATE_HOME").ok();
std::env::set_var("XDG_STATE_HOME", &tmp);
#[derive(serde::Serialize)]
struct Probe {
kind: &'static str,
n: u32,
}
let probe = Probe { kind: "test", n: 7 };
let path = dump_session_state(&probe).expect("dump");
assert!(path.exists(), "dump file should exist at {path:?}");
assert!(
path.starts_with(tmp.join("ezpn")),
"dump path {path:?} should be under XDG_STATE_HOME/ezpn"
);
let body = std::fs::read_to_string(&path).expect("read dump");
assert!(body.contains("\"kind\""));
assert!(body.contains("\"test\""));
assert!(body.contains("\"n\""));
assert!(body.contains("7"));
match prev {
Some(v) => std::env::set_var("XDG_STATE_HOME", v),
None => std::env::remove_var("XDG_STATE_HOME"),
}
let _ = std::fs::remove_dir_all(&tmp);
}
}