use std::net::SocketAddr;
use std::process::ExitCode;
use std::time::Duration;
use clap::Args;
use serde::Serialize;
use super::pid;
#[derive(Debug, Args)]
pub struct StatusArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Serialize)]
struct StatusOutput {
running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pid: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
listen: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
serving: Option<bool>,
}
fn is_serving(addr: &str) -> bool {
let Ok(sock_addr) = addr.parse::<SocketAddr>() else {
return false;
};
std::net::TcpStream::connect_timeout(&sock_addr, Duration::from_secs(1)).is_ok()
}
fn is_alive(pid: u32) -> bool {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
ret == 0
}
#[cfg(not(unix))]
{
let _ = pid;
false
}
}
pub fn dispatch(args: StatusArgs) -> ExitCode {
let Some((proxy_pid, addr)) = pid::read_pid() else {
if args.json {
let out = serde_json::to_string(&StatusOutput {
running: false,
pid: None,
listen: None,
serving: None,
})
.unwrap_or_default();
println!("{out}");
} else {
println!("not running");
}
return ExitCode::SUCCESS;
};
if !is_alive(proxy_pid) {
let _ = pid::remove_pid();
if args.json {
let out = serde_json::to_string(&StatusOutput {
running: false,
pid: None,
listen: None,
serving: None,
})
.unwrap_or_default();
println!("{out}");
} else {
println!("not running (stale PID file cleaned up)");
}
return ExitCode::SUCCESS;
}
let serving = is_serving(&addr);
if args.json {
let out = serde_json::to_string(&StatusOutput {
running: true,
pid: Some(proxy_pid),
listen: Some(addr.clone()),
serving: Some(serving),
})
.unwrap_or_default();
println!("{out}");
} else {
println!("running (PID {proxy_pid}, listening on {addr})");
if !serving {
eprintln!("warning: TCP connect to {addr} failed — proxy may still be starting");
}
}
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct Wrapper {
#[command(flatten)]
inner: StatusArgs,
}
#[test]
fn status_args_json_defaults_false() {
let w = Wrapper::parse_from(["test"]);
assert!(!w.inner.json);
}
#[test]
fn status_args_json_flag() {
let w = Wrapper::parse_from(["test", "--json"]);
assert!(w.inner.json);
}
#[test]
fn status_output_serialises_not_running() {
let out = StatusOutput {
running: false,
pid: None,
listen: None,
serving: None,
};
let json = serde_json::to_string(&out).unwrap();
assert_eq!(json, r#"{"running":false}"#);
}
#[test]
fn status_output_serialises_running() {
let out = StatusOutput {
running: true,
pid: Some(12345),
listen: Some("127.0.0.1:8899".into()),
serving: Some(true),
};
let json = serde_json::to_string(&out).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["running"], true);
assert_eq!(parsed["pid"], 12345);
assert_eq!(parsed["listen"], "127.0.0.1:8899");
assert_eq!(parsed["serving"], true);
}
#[test]
fn is_serving_returns_false_for_unbound_port() {
assert!(!is_serving("127.0.0.1:1"));
}
#[test]
fn is_serving_returns_false_for_invalid_addr() {
assert!(!is_serving("not-an-addr"));
}
}