use crate::relay_lock;
#[derive(Debug)]
pub enum StopResult {
Stopped { pid: u32 },
StaleCleaned,
}
#[derive(Debug)]
pub struct RelayStatusInfo {
pub pid: u32,
pub port: u16,
pub started_at: i64,
}
pub fn stop_relay() -> Result<StopResult, String> {
match relay_lock::check_lock() {
relay_lock::LockStatus::Held(info) => {
let pid = info.pid;
relay_lock::stop_relay();
Ok(StopResult::Stopped { pid })
}
relay_lock::LockStatus::Stale(_) => {
relay_lock::remove_lock();
Ok(StopResult::StaleCleaned)
}
relay_lock::LockStatus::Free => Err("relay is not running.".to_string()),
}
}
pub fn relay_status() -> Result<RelayStatusInfo, String> {
match relay_lock::check_lock() {
relay_lock::LockStatus::Held(info) => Ok(RelayStatusInfo {
pid: info.pid,
port: info.port,
started_at: info.started_at,
}),
relay_lock::LockStatus::Stale(_) => {
relay_lock::remove_lock();
Err("relay is not running (stale lock cleaned up).".to_string())
}
relay_lock::LockStatus::Free => Err("relay is not running.".to_string()),
}
}
pub fn start_relay_from_snapshot() -> Result<(), String> {
relay_lock::read_snapshot().map_err(|e| format!("no config snapshot for relay: {e}"))?;
let config_path = relay_lock::config_snapshot_path().display().to_string();
let exe = std::env::current_exe().map_err(|e| format!("cannot find mcpr binary: {e}"))?;
let status = std::process::Command::new(exe)
.args(["relay", "start", &config_path])
.status()
.map_err(|e| format!("failed to spawn relay: {e}"))?;
if !status.success() {
return Err("relay failed to start".to_string());
}
Ok(())
}
pub fn restart_relay() -> Result<(), String> {
let _ = stop_relay();
start_relay_from_snapshot()
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn lifecycle__all_paths() {
relay_lock::remove_lock();
let result = stop_relay();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not running"));
let result = relay_status();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not running"));
relay_lock::write_lock(7777, "/tmp/test-relay.toml").unwrap();
let result = relay_status();
assert!(result.is_ok());
let info = result.unwrap();
assert_eq!(info.pid, std::process::id());
assert_eq!(info.port, 7777);
assert!(info.started_at > 0);
relay_lock::remove_lock();
let dir = relay_lock::config_snapshot_path()
.parent()
.unwrap()
.to_path_buf();
std::fs::create_dir_all(&dir).unwrap();
let ts = chrono::Utc::now().timestamp();
std::fs::write(
dir.join("lock"),
format!("99999999\n8080\n{ts}\n/tmp/relay.toml\n"),
)
.unwrap();
let result = relay_status();
assert!(result.is_err());
assert!(result.unwrap_err().contains("stale"));
assert!(relay_lock::read_lock_info().is_none());
std::fs::write(
dir.join("lock"),
format!("99999999\n8080\n{ts}\n/tmp/relay.toml\n"),
)
.unwrap();
let result = stop_relay();
assert!(result.is_ok());
assert!(matches!(result.unwrap(), StopResult::StaleCleaned));
assert!(relay_lock::read_lock_info().is_none());
}
#[test]
fn start_relay_from_snapshot__fails_without_snapshot() {
let _ = std::fs::remove_file(relay_lock::config_snapshot_path());
let result = start_relay_from_snapshot();
assert!(result.is_err());
assert!(result.unwrap_err().contains("no config snapshot"));
}
}