use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub use crate::proxy_lock::{LockInfo, LockStatus};
fn relay_dir() -> PathBuf {
crate::proxy_lock::mcpr_dir().join("relay")
}
fn lock_path() -> PathBuf {
relay_dir().join("lock")
}
pub fn config_snapshot_path() -> PathBuf {
relay_dir().join("config.toml")
}
pub fn log_path() -> PathBuf {
relay_dir().join("relay.log")
}
pub fn check_lock() -> LockStatus {
let path = lock_path();
let info = match read_lock_file(&path) {
Some(info) => info,
None => return LockStatus::Free,
};
if crate::proxy_lock::is_process_alive(info.pid) {
LockStatus::Held(info)
} else {
LockStatus::Stale(info)
}
}
pub fn read_lock_info() -> Option<LockInfo> {
read_lock_file(&lock_path())
}
pub fn write_lock(port: u16, config_path: &str) -> std::io::Result<()> {
let dir = relay_dir();
fs::create_dir_all(&dir)?;
let pid = std::process::id();
let started_at = chrono::Utc::now().timestamp();
let content = format!("{pid}\n{port}\n{started_at}\n{config_path}\n");
fs::write(lock_path(), content)
}
pub fn remove_lock() {
let _ = fs::remove_file(lock_path());
}
pub fn snapshot_config(content: &str) -> std::io::Result<()> {
let dir = relay_dir();
fs::create_dir_all(&dir)?;
fs::write(config_snapshot_path(), content)
}
pub fn read_snapshot() -> std::io::Result<String> {
fs::read_to_string(config_snapshot_path())
}
pub fn stop_relay() -> bool {
match check_lock() {
LockStatus::Held(info) => {
crate::proxy_lock::send_sigterm(info.pid);
crate::proxy_lock::wait_for_exit(info.pid, Duration::from_secs(10));
remove_lock();
true
}
LockStatus::Stale(_) => {
remove_lock();
false
}
LockStatus::Free => false,
}
}
#[cfg(unix)]
pub fn redirect_stdio() -> std::io::Result<()> {
use nix::unistd::dup2;
use std::os::unix::io::AsRawFd;
let log = log_path();
if let Some(parent) = log.parent() {
fs::create_dir_all(parent)?;
}
let dev_null = fs::OpenOptions::new().read(true).open("/dev/null")?;
let log_file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log)?;
dup2(dev_null.as_raw_fd(), 0).map_err(|e| std::io::Error::other(format!("dup2 stdin: {e}")))?;
dup2(log_file.as_raw_fd(), 1)
.map_err(|e| std::io::Error::other(format!("dup2 stdout: {e}")))?;
dup2(log_file.as_raw_fd(), 2)
.map_err(|e| std::io::Error::other(format!("dup2 stderr: {e}")))?;
Ok(())
}
fn read_lock_file(path: &Path) -> Option<LockInfo> {
let content = fs::read_to_string(path).ok()?;
let mut lines = content.lines();
let pid: u32 = lines.next()?.parse().ok()?;
let port: u16 = lines.next()?.parse().ok()?;
let started_at: i64 = lines.next()?.parse().ok()?;
let config_path: String = lines.next()?.to_string();
Some(LockInfo {
pid,
port,
started_at,
config_path,
daemon_pid: None,
})
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn read_lock_file__roundtrip() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
let pid = std::process::id();
let ts = chrono::Utc::now().timestamp();
let content = format!("{pid}\n8080\n{ts}\n/tmp/relay.toml\n");
fs::write(&lock, &content).unwrap();
let info = read_lock_file(&lock).unwrap();
assert_eq!(info.pid, pid);
assert_eq!(info.port, 8080);
assert_eq!(info.started_at, ts);
assert_eq!(info.config_path, "/tmp/relay.toml");
assert_eq!(info.daemon_pid, None);
}
#[test]
fn read_lock_file__missing_returns_none() {
let info = read_lock_file(Path::new("/nonexistent/path/lock"));
assert!(info.is_none());
}
#[test]
fn read_lock_file__malformed_returns_none() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
fs::write(&lock, "12345\n8080\n").unwrap();
assert!(read_lock_file(&lock).is_none());
}
#[test]
fn read_lock_file__empty_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
fs::write(&lock, "").unwrap();
assert!(read_lock_file(&lock).is_none());
}
#[test]
fn read_lock_file__non_numeric_pid_returns_none() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
fs::write(&lock, "notapid\n8080\n12345\n/tmp/relay.toml\n").unwrap();
assert!(read_lock_file(&lock).is_none());
}
#[test]
fn read_lock_file__non_numeric_port_returns_none() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
fs::write(&lock, "1234\nnotaport\n12345\n/tmp/relay.toml\n").unwrap();
assert!(read_lock_file(&lock).is_none());
}
#[test]
fn snapshot_config__roundtrip() {
let dir = tempfile::tempdir().unwrap();
let snapshot = dir.path().join("config.toml");
let content = "port = 8080\n[relay]\ndomain = \"tunnel.test\"\n";
fs::write(&snapshot, content).unwrap();
let read_back = fs::read_to_string(&snapshot).unwrap();
assert_eq!(read_back, content);
}
#[test]
fn relay_dir__under_mcpr() {
let d = relay_dir();
assert!(d.ends_with(".mcpr/relay"));
}
#[test]
fn lock_path__under_relay_dir() {
let p = lock_path();
assert!(p.ends_with("relay/lock"));
}
#[test]
fn config_snapshot_path__ends_with_config_toml() {
let p = config_snapshot_path();
assert!(p.ends_with("relay/config.toml"));
}
#[test]
fn log_path__ends_with_relay_log() {
let p = log_path();
assert!(p.ends_with("relay/relay.log"));
}
#[cfg(unix)]
#[test]
fn real_path__lifecycle_roundtrip() {
remove_lock();
let _ = fs::remove_file(config_snapshot_path());
assert!(matches!(check_lock(), LockStatus::Free));
assert!(!stop_relay());
assert!(read_snapshot().is_err());
write_lock(9998, "/tmp/test-relay.toml").unwrap();
let info = read_lock_info().unwrap();
assert_eq!(info.pid, std::process::id());
assert_eq!(info.port, 9998);
assert_eq!(info.config_path, "/tmp/test-relay.toml");
assert!(info.started_at > 0);
assert!(matches!(check_lock(), LockStatus::Held(_)));
remove_lock();
assert!(read_lock_info().is_none());
assert!(matches!(check_lock(), LockStatus::Free));
let dir = relay_dir();
fs::create_dir_all(&dir).unwrap();
let ts = chrono::Utc::now().timestamp();
fs::write(
lock_path(),
format!("99999999\n8080\n{ts}\n/tmp/relay.toml\n"),
)
.unwrap();
assert!(matches!(check_lock(), LockStatus::Stale(_)));
let result = stop_relay();
assert!(!result); assert!(read_lock_info().is_none());
let content = "port = 7777\n[relay]\ndomain = \"test.relay\"\n";
snapshot_config(content).unwrap();
let read_back = read_snapshot().unwrap();
assert_eq!(read_back, content);
let _ = fs::remove_file(config_snapshot_path());
}
#[cfg(unix)]
#[test]
fn read_lock_file__stale_when_pid_dead() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
let ts = chrono::Utc::now().timestamp();
let content = format!("99999999\n8080\n{ts}\n/tmp/relay.toml\n");
fs::write(&lock, &content).unwrap();
let info = read_lock_file(&lock).unwrap();
assert_eq!(info.pid, 99999999);
assert!(!crate::proxy_lock::is_process_alive(99999999));
}
#[cfg(unix)]
#[test]
fn read_lock_file__held_when_pid_alive() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
let pid = std::process::id();
let ts = chrono::Utc::now().timestamp();
let content = format!("{pid}\n8080\n{ts}\n/tmp/relay.toml\n");
fs::write(&lock, &content).unwrap();
let info = read_lock_file(&lock).unwrap();
assert!(crate::proxy_lock::is_process_alive(info.pid));
}
#[test]
fn read_lock_file__overflow_port_returns_none() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("lock");
fs::write(&lock, "1234\n99999\n12345\n/tmp/relay.toml\n").unwrap();
assert!(read_lock_file(&lock).is_none()); }
}