use std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::time::Duration;
#[cfg(unix)]
use nix::sys::signal::{self, Signal};
#[cfg(unix)]
use nix::unistd::Pid;
const PID_FILE: &str = "mcprd.pid";
const DAEMON_LOG: &str = "mcprd.log";
#[derive(Debug)]
pub struct DaemonInfo {
pub pid: u32,
pub started_at: i64,
}
pub enum DaemonStatus {
Running(DaemonInfo),
Stale(DaemonInfo),
NotRunning,
}
fn mcpr_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".mcpr")
}
pub fn pid_file_path() -> PathBuf {
mcpr_dir().join(PID_FILE)
}
pub fn daemon_log_path() -> PathBuf {
mcpr_dir().join(DAEMON_LOG)
}
pub fn read_pid_file() -> Option<DaemonInfo> {
let content = fs::read_to_string(pid_file_path()).ok()?;
let mut lines = content.lines();
let pid: u32 = lines.next()?.parse().ok()?;
let started_at: i64 = lines.next()?.parse().ok()?;
Some(DaemonInfo { pid, started_at })
}
pub fn write_pid_file() -> std::io::Result<()> {
let dir = mcpr_dir();
fs::create_dir_all(&dir)?;
let pid = std::process::id();
let started_at = chrono::Utc::now().timestamp();
let content = format!("{pid}\n{started_at}\n");
fs::write(pid_file_path(), content)
}
pub fn remove_pid_file() {
let _ = fs::remove_file(pid_file_path());
}
#[cfg(unix)]
pub fn is_process_alive(pid: u32) -> bool {
signal::kill(Pid::from_raw(pid as i32), None).is_ok()
}
#[cfg(not(unix))]
pub fn is_process_alive(_pid: u32) -> bool {
false
}
pub fn check_status() -> DaemonStatus {
match read_pid_file() {
Some(info) => {
if is_process_alive(info.pid) {
DaemonStatus::Running(info)
} else {
DaemonStatus::Stale(info)
}
}
None => DaemonStatus::NotRunning,
}
}
#[cfg(unix)]
pub fn daemonize(timeout: Duration) -> Result<std::os::unix::io::RawFd, String> {
use nix::unistd::{ForkResult, close, fork, pipe, setsid};
use std::os::unix::io::{IntoRawFd, RawFd};
let (read_owned, write_owned) = pipe().map_err(|e| format!("pipe failed: {e}"))?;
let read_fd: RawFd = read_owned.into_raw_fd();
let write_fd: RawFd = write_owned.into_raw_fd();
match unsafe { fork() }.map_err(|e| format!("fork failed: {e}"))? {
ForkResult::Parent { child: _ } => {
let _ = close(write_fd);
wait_for_readiness(read_fd, timeout);
unreachable!();
}
ForkResult::Child => {
let _ = close(read_fd);
setsid().map_err(|e| format!("setsid failed: {e}"))?;
match unsafe { fork() }.map_err(|e| format!("second fork failed: {e}"))? {
ForkResult::Parent { child: _ } => {
std::process::exit(0);
}
ForkResult::Child => {
redirect_stdio().map_err(|e| format!("failed to redirect stdio: {e}"))?;
Ok(write_fd)
}
}
}
}
}
#[cfg(unix)]
pub fn daemonize_proxy(
proxy_name: &str,
timeout: Duration,
) -> Result<std::os::unix::io::RawFd, String> {
use nix::unistd::{ForkResult, close, fork, pipe, setsid};
use std::os::unix::io::{IntoRawFd, RawFd};
let name = proxy_name.to_string();
let (read_owned, write_owned) = pipe().map_err(|e| format!("pipe failed: {e}"))?;
let read_fd: RawFd = read_owned.into_raw_fd();
let write_fd: RawFd = write_owned.into_raw_fd();
match unsafe { fork() }.map_err(|e| format!("fork failed: {e}"))? {
ForkResult::Parent { child: _ } => {
let _ = close(write_fd);
wait_for_proxy_readiness(read_fd, timeout, &name);
unreachable!();
}
ForkResult::Child => {
let _ = close(read_fd);
setsid().map_err(|e| format!("setsid failed: {e}"))?;
match unsafe { fork() }.map_err(|e| format!("second fork failed: {e}"))? {
ForkResult::Parent { child: _ } => {
std::process::exit(0);
}
ForkResult::Child => {
super::proxy_lock::redirect_stdio(&name)
.map_err(|e| format!("failed to redirect stdio: {e}"))?;
Ok(write_fd)
}
}
}
}
}
#[cfg(unix)]
pub fn daemonize_relay(timeout: Duration) -> Result<std::os::unix::io::RawFd, String> {
use nix::unistd::{ForkResult, close, fork, pipe, setsid};
use std::os::unix::io::{IntoRawFd, RawFd};
let (read_owned, write_owned) = pipe().map_err(|e| format!("pipe failed: {e}"))?;
let read_fd: RawFd = read_owned.into_raw_fd();
let write_fd: RawFd = write_owned.into_raw_fd();
match unsafe { fork() }.map_err(|e| format!("fork failed: {e}"))? {
ForkResult::Parent { child: _ } => {
let _ = close(write_fd);
wait_for_relay_readiness(read_fd, timeout);
unreachable!();
}
ForkResult::Child => {
let _ = close(read_fd);
setsid().map_err(|e| format!("setsid failed: {e}"))?;
match unsafe { fork() }.map_err(|e| format!("second fork failed: {e}"))? {
ForkResult::Parent { child: _ } => {
std::process::exit(0);
}
ForkResult::Child => {
super::relay_lock::redirect_stdio()
.map_err(|e| format!("failed to redirect stdio: {e}"))?;
Ok(write_fd)
}
}
}
}
}
#[cfg(unix)]
fn wait_for_relay_readiness(read_fd: std::os::unix::io::RawFd, _timeout: Duration) {
use std::os::unix::io::FromRawFd;
let mut pipe_read = unsafe { std::fs::File::from_raw_fd(read_fd) };
let mut buf = [0u8; 1];
let handle = std::thread::spawn(move || {
let result = pipe_read.read(&mut buf);
(buf, result)
});
match handle.join() {
Ok((buf, Ok(1))) if buf[0] == b'1' => {
if let Some(info) = super::relay_lock::read_lock_info() {
eprintln!("relay started (PID: {}, port: {})", info.pid, info.port);
} else {
eprintln!("relay started");
}
std::process::exit(0);
}
Ok((_, Ok(_))) => {
eprintln!(
"error: relay failed to start (check {})",
super::relay_lock::log_path().display()
);
std::process::exit(1);
}
Ok((_, Err(e))) => {
eprintln!("error: reading readiness pipe: {e}");
std::process::exit(1);
}
Err(_) => {
eprintln!("error: internal error waiting for relay");
std::process::exit(1);
}
}
}
#[cfg(unix)]
fn wait_for_proxy_readiness(
read_fd: std::os::unix::io::RawFd,
_timeout: Duration,
proxy_name: &str,
) {
use std::os::unix::io::FromRawFd;
let mut pipe_read = unsafe { std::fs::File::from_raw_fd(read_fd) };
let mut buf = [0u8; 1];
let handle = std::thread::spawn(move || {
let result = pipe_read.read(&mut buf);
(buf, result)
});
match handle.join() {
Ok((buf, Ok(1))) if buf[0] == b'1' => {
if let Some(info) = super::proxy_lock::read_lock_info(proxy_name) {
eprintln!(
"proxy \"{}\" started (PID: {}, port: {})",
proxy_name, info.pid, info.port
);
if let Ok(snapshot) = super::proxy_lock::read_snapshot(proxy_name)
&& let Ok(table) = snapshot.parse::<toml::Table>()
{
if let Some(subdomain) = table
.get("tunnel")
.and_then(|t| t.as_table())
.filter(|t| t.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false))
.and_then(|t| t.get("subdomain"))
.and_then(|v| v.as_str())
{
eprintln!(" tunnel: https://{subdomain}.tunnel.mcpr.app");
}
if let Some(server) = table
.get("cloud")
.and_then(|t| t.as_table())
.and_then(|t| t.get("server"))
.and_then(|v| v.as_str())
{
eprintln!(" dashboard: https://cloud.mcpr.app/servers/{server}");
}
}
} else {
eprintln!("proxy \"{}\" started", proxy_name);
}
std::process::exit(0);
}
Ok((_, Ok(_))) => {
eprintln!(
"error: proxy \"{}\" failed to start (check {})",
proxy_name,
super::proxy_lock::log_path(proxy_name).display()
);
std::process::exit(1);
}
Ok((_, Err(e))) => {
eprintln!("error: reading readiness pipe: {e}");
std::process::exit(1);
}
Err(_) => {
eprintln!("error: internal error waiting for proxy");
std::process::exit(1);
}
}
}
#[cfg(unix)]
fn wait_for_readiness(read_fd: std::os::unix::io::RawFd, _timeout: Duration) {
use std::os::unix::io::FromRawFd;
let mut pipe_read = unsafe { std::fs::File::from_raw_fd(read_fd) };
let mut buf = [0u8; 1];
let handle = std::thread::spawn(move || {
let result = pipe_read.read(&mut buf);
(buf, result)
});
match handle.join() {
Ok((buf, Ok(1))) if buf[0] == b'1' => {
if let Some(info) = read_pid_file() {
eprintln!("mcprd started (PID: {})", info.pid);
} else {
eprintln!("mcprd started");
}
std::process::exit(0);
}
Ok((_, Ok(_))) => {
eprintln!("error: daemon failed to start (check daemon.log)");
eprintln!(" log: {}", daemon_log_path().display());
std::process::exit(1);
}
Ok((_, Err(e))) => {
eprintln!("error: reading readiness pipe: {e}");
std::process::exit(1);
}
Err(_) => {
eprintln!("error: internal error waiting for daemon");
std::process::exit(1);
}
}
}
#[cfg(unix)]
fn redirect_stdio() -> std::io::Result<()> {
use nix::unistd::dup2;
use std::os::unix::io::AsRawFd;
let log_path = daemon_log_path();
if let Some(parent) = log_path.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_path)?;
dup2(dev_null.as_raw_fd(), 0).map_err(std::io::Error::other)?; dup2(log_file.as_raw_fd(), 1).map_err(std::io::Error::other)?; dup2(log_file.as_raw_fd(), 2).map_err(std::io::Error::other)?;
Ok(())
}
#[cfg(unix)]
pub fn signal_ready(write_fd: std::os::unix::io::RawFd) {
use std::os::unix::io::FromRawFd;
let mut pipe_write = unsafe { std::fs::File::from_raw_fd(write_fd) };
if let Err(e) = pipe_write.write_all(b"1") {
eprintln!("warning: failed to signal daemon readiness: {e}");
}
}
#[cfg(unix)]
pub async fn run_supervisor(ready_fd: Option<i32>) {
if let Err(e) = write_pid_file() {
eprintln!("error: failed to write PID file: {e}");
std::process::exit(1);
}
if let Some(fd) = ready_fd {
signal_ready(fd);
}
eprintln!("[mcprd] supervisor started (PID: {})", std::process::id());
let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false);
let shutdown_trigger = shutdown_tx.clone();
tokio::spawn(async move {
use tokio::signal::unix::{SignalKind, signal};
let ctrl_c = tokio::signal::ctrl_c();
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM");
tokio::select! {
_ = ctrl_c => {},
_ = sigterm.recv() => {},
}
eprintln!("[mcprd] received shutdown signal");
let _ = shutdown_trigger.send(true);
});
let health_shutdown = shutdown_tx.subscribe();
tokio::spawn(async move {
let mut rx = health_shutdown;
loop {
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {},
_ = rx.changed() => break,
}
let proxies = super::proxy_lock::list_proxies();
for (name, status) in &proxies {
if let super::proxy_lock::LockStatus::Stale(info) = status {
eprintln!(
"[mcprd] proxy \"{}\" (PID: {}) is dead, cleaning up lockfile",
name, info.pid
);
super::proxy_lock::remove_lock(name);
}
}
if let super::relay_lock::LockStatus::Stale(info) = super::relay_lock::check_lock() {
eprintln!(
"[mcprd] relay (PID: {}) is dead, cleaning up lockfile",
info.pid
);
super::relay_lock::remove_lock();
}
}
});
let _ = shutdown_rx.changed().await;
let stopped = super::proxy_lock::stop_all_proxies();
if !stopped.is_empty() {
eprintln!(
"[mcprd] stopped {} proxy(ies): {}",
stopped.len(),
stopped.join(", ")
);
}
if super::relay_lock::stop_relay() {
eprintln!("[mcprd] stopped relay");
}
remove_pid_file();
eprintln!("[mcprd] shutdown complete.");
}
pub fn stop_daemon() {
match check_status() {
DaemonStatus::Running(info) => {
eprintln!("Stopping mcpr daemon (PID: {})...", info.pid);
send_sigterm(info.pid);
wait_for_exit(info.pid, Duration::from_secs(10));
remove_pid_file();
eprintln!("Stopped.");
}
DaemonStatus::Stale(_) => {
eprintln!("Daemon is not running (stale PID file removed).");
remove_pid_file();
}
DaemonStatus::NotRunning => {
eprintln!("No daemon is running.");
std::process::exit(1);
}
}
}
pub fn stop_daemon_if_running() {
match check_status() {
DaemonStatus::Running(info) => {
eprintln!("Stopping mcpr daemon (PID: {})...", info.pid);
send_sigterm(info.pid);
wait_for_exit(info.pid, Duration::from_secs(10));
remove_pid_file();
eprintln!("Stopped.");
}
DaemonStatus::Stale(_) => {
remove_pid_file();
}
DaemonStatus::NotRunning => {}
}
}
#[cfg(unix)]
fn send_sigterm(pid: u32) {
let _ = signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM);
}
#[cfg(not(unix))]
fn send_sigterm(_pid: u32) {}
fn wait_for_exit(pid: u32, timeout: Duration) {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if !is_process_alive(pid) {
return;
}
std::thread::sleep(Duration::from_millis(100));
}
eprintln!(
"warning: daemon (PID: {pid}) did not exit within {}s",
timeout.as_secs()
);
}
pub fn ensure_not_running() -> bool {
match check_status() {
DaemonStatus::Running(info) => {
eprintln!("Daemon already running (PID: {}).", info.pid);
true
}
DaemonStatus::Stale(_) => {
remove_pid_file();
false
}
DaemonStatus::NotRunning => false,
}
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn pid_file__roundtrip() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("test.pid");
let pid = std::process::id();
let ts = chrono::Utc::now().timestamp();
let content = format!("{pid}\n{ts}\n");
std::fs::write(&pid_path, &content).unwrap();
let read = std::fs::read_to_string(&pid_path).unwrap();
let mut lines = read.lines();
let read_pid: u32 = lines.next().unwrap().parse().unwrap();
let read_ts: i64 = lines.next().unwrap().parse().unwrap();
assert_eq!(read_pid, pid);
assert_eq!(read_ts, ts);
}
#[test]
fn pid_file__malformed_parse_fails() {
let content = "not-a-number\ngarbage\n";
let mut lines = content.lines();
let result = lines.next().and_then(|s| s.parse::<u32>().ok());
assert!(result.is_none());
}
#[cfg(unix)]
#[test]
fn is_process_alive__self() {
assert!(is_process_alive(std::process::id()));
}
#[cfg(unix)]
#[test]
fn is_process_alive__nonexistent() {
assert!(!is_process_alive(99_999_999));
}
}