use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CryoState {
pub session_number: u32,
pub pid: Option<u32>,
#[serde(default)]
pub retry_count: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_override: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_retries_override: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_session_duration_override: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_wake: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_report_time: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_index: Option<usize>,
}
pub fn state_path(dir: &Path) -> PathBuf {
dir.join("timer.json")
}
pub fn save_state(path: &Path, state: &CryoState) -> Result<()> {
let json = serde_json::to_string_pretty(state)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load_state(path: &Path) -> Result<Option<CryoState>> {
if !path.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(path)?;
if contents.trim().is_empty() {
return Ok(None);
}
let state: CryoState = serde_json::from_str(&contents)?;
Ok(Some(state))
}
pub fn is_locked(state: &CryoState) -> bool {
if let Some(pid) = state.pid {
let ret = unsafe { libc::kill(pid as i32, 0) };
if ret == 0 {
return true;
}
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
errno == libc::EPERM
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_empty_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("timer.json");
std::fs::write(&path, "").unwrap();
let result = load_state(&path).unwrap();
assert!(result.is_none(), "Empty file should return None");
}
#[test]
fn test_load_corrupted_json() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("timer.json");
std::fs::write(&path, "{broken").unwrap();
let result = load_state(&path);
assert!(result.is_err(), "Corrupted JSON should return error");
}
#[test]
fn test_load_minimal_json() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("timer.json");
std::fs::write(&path, r#"{"session_number": 5}"#).unwrap();
let state = load_state(&path).unwrap().unwrap();
assert_eq!(state.session_number, 5);
assert!(state.pid.is_none(), "pid should default to None");
assert_eq!(state.retry_count, 0, "retry_count should default to 0");
assert!(state.agent_override.is_none());
}
#[test]
fn test_is_locked_stale_pid() {
let mut child = std::process::Command::new("true").spawn().unwrap();
let pid = child.id();
child.wait().unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
let state = CryoState {
session_number: 1,
pid: Some(pid),
retry_count: 0,
next_wake: None,
agent_override: None,
max_retries_override: None,
max_session_duration_override: None,
last_report_time: None,
provider_index: None,
};
assert!(!is_locked(&state), "Dead PID should not be locked");
}
#[test]
fn test_is_locked_no_pid() {
let state = CryoState {
session_number: 1,
pid: None,
retry_count: 0,
next_wake: None,
agent_override: None,
max_retries_override: None,
max_session_duration_override: None,
last_report_time: None,
provider_index: None,
};
assert!(!is_locked(&state), "No PID should not be locked");
}
#[test]
fn test_is_locked_own_pid() {
let state = CryoState {
session_number: 1,
pid: Some(std::process::id()),
retry_count: 0,
next_wake: None,
agent_override: None,
max_retries_override: None,
max_session_duration_override: None,
last_report_time: None,
provider_index: None,
};
assert!(is_locked(&state), "Own PID should be locked");
}
}