use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnsureOutcome {
AlreadyRunning,
Spawned,
}
const LIVENESS_TCP_TIMEOUT_MS: u64 = 200;
const SPAWN_SETTLE_MS: u64 = 300;
pub fn ensure_running<F>(bind: &str, pid_path: &Path, spawn: F) -> anyhow::Result<EnsureOutcome>
where
F: FnOnce(&str) -> anyhow::Result<u32>,
{
if read_pidfile(pid_path)?.is_some() && probe_tcp(bind, LIVENESS_TCP_TIMEOUT_MS) {
return Ok(EnsureOutcome::AlreadyRunning);
}
if let Some(parent) = pid_path.parent() {
std::fs::create_dir_all(parent)?;
}
let lock_path = lockfile_path(pid_path);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(_) => {
let result = (|| -> anyhow::Result<EnsureOutcome> {
if read_pidfile(pid_path)?.is_some() && probe_tcp(bind, LIVENESS_TCP_TIMEOUT_MS) {
return Ok(EnsureOutcome::AlreadyRunning);
}
let pid = spawn(bind)?;
write_pidfile_atomic(pid_path, pid)?;
std::thread::sleep(std::time::Duration::from_millis(SPAWN_SETTLE_MS));
if !probe_tcp(bind, LIVENESS_TCP_TIMEOUT_MS) {
let _ = std::fs::remove_file(pid_path);
anyhow::bail!(
"mcp-proxy spawned (pid {pid}) but did not bind to {bind} \
within {}ms; check that the port is free and mcp-proxy is \
correctly installed",
SPAWN_SETTLE_MS
);
}
Ok(EnsureOutcome::Spawned)
})();
let _ = std::fs::remove_file(&lock_path);
result
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if read_pidfile(pid_path)?.is_some() && probe_tcp(bind, LIVENESS_TCP_TIMEOUT_MS) {
Ok(EnsureOutcome::AlreadyRunning)
} else {
Err(anyhow::anyhow!(
"another process holds {} but has not published a live pid",
lock_path.display()
))
}
}
Err(e) => Err(e.into()),
}
}
fn lockfile_path(pid_path: &Path) -> PathBuf {
let mut s = pid_path.as_os_str().to_owned();
s.push(".lock");
PathBuf::from(s)
}
fn write_pidfile_atomic(pid_path: &Path, pid: u32) -> anyhow::Result<()> {
let tmp = pid_path.with_extension(format!("pid.{}.tmp", std::process::id()));
std::fs::write(&tmp, pid.to_string())?;
std::fs::rename(&tmp, pid_path)?;
Ok(())
}
pub fn default_pid_path() -> anyhow::Result<PathBuf> {
if let Ok(xdg) = std::env::var("XDG_STATE_HOME")
&& !xdg.is_empty()
{
return Ok(PathBuf::from(xdg).join("llmenv").join("mcp-proxy.pid"));
}
if let Ok(home) = std::env::var("HOME")
&& !home.is_empty()
{
return Ok(PathBuf::from(home)
.join(".local/state/llmenv")
.join("mcp-proxy.pid"));
}
Err(anyhow::anyhow!(
"cannot determine pidfile path: neither XDG_STATE_HOME nor HOME is set"
))
}
fn mcp_proxy_command() -> anyhow::Result<(&'static str, Vec<&'static str>)> {
if on_path("mcp-proxy") {
Ok(("mcp-proxy", vec![]))
} else if on_path("uvx") {
Ok(("uvx", vec!["mcp-proxy"]))
} else {
Err(anyhow::anyhow!(
"neither `mcp-proxy` nor `uvx` found on PATH; install one to run the \
memory server, or disable the `memory` config block"
))
}
}
fn on_path(program: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| {
let candidate = dir.join(program);
is_executable(&candidate)
})
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
path.is_file()
}
pub fn spawn_mcp_proxy(bind: &str) -> anyhow::Result<u32> {
let (host, port) = bind
.rsplit_once(':')
.ok_or_else(|| anyhow::anyhow!("bind missing :port suffix: {bind}"))?;
let port: u16 = port
.parse()
.map_err(|e| anyhow::anyhow!("bind port {port:?} is not a valid u16: {e}"))?;
host.parse::<std::net::IpAddr>()
.map_err(|e| anyhow::anyhow!("bind host {host:?} is not a valid IP address: {e}"))?;
let (program, leading) = mcp_proxy_command()?;
let mut cmd = Command::new(program);
cmd.args(leading)
.arg("--host")
.arg(host)
.arg("--port")
.arg(port.to_string())
.arg("--")
.arg("icm")
.arg("serve");
configure_detached(&mut cmd);
let child = cmd.spawn()?;
Ok(child.id())
}
fn configure_detached(cmd: &mut Command) {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt as _;
cmd.process_group(0);
}
#[cfg(not(unix))]
{
}
}
fn read_pidfile(pid_path: &Path) -> anyhow::Result<Option<u32>> {
if !pid_path.exists() {
return Ok(None);
}
let s = std::fs::read_to_string(pid_path)?;
let trimmed = s.trim();
if trimmed.is_empty() {
return Ok(None);
}
let pid: u32 = trimmed
.parse()
.map_err(|e| anyhow::anyhow!("invalid pid {trimmed:?} in {}: {e}", pid_path.display()))?;
Ok(Some(pid))
}
#[must_use]
pub fn probe_tcp(bind: &str, timeout_ms: u64) -> bool {
use std::net::TcpStream;
let Ok(addr) = bind.parse::<std::net::SocketAddr>() else {
return false;
};
TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(timeout_ms)).is_ok()
}
#[must_use]
pub fn is_alive(pid: u32) -> bool {
#[cfg(unix)]
{
let pid_i32 = i32::try_from(pid).unwrap_or(i32::MAX);
let status = Command::new("kill")
.arg("-0")
.arg(pid_i32.to_string())
.stderr(std::process::Stdio::null())
.status();
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
#[cfg(not(unix))]
{
#[expect(
unused_variables,
reason = "pid is only used on Unix for the kill(2) signal-0 liveness check"
)]
let _ = pid;
false
}
}
#[cfg(all(test, unix))]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::is_executable;
use std::os::unix::fs::PermissionsExt;
#[test]
fn is_executable_true_only_for_executable_files() {
let dir = tempfile::tempdir().expect("tempdir");
let exe = dir.path().join("tool");
std::fs::write(&exe, b"#!/bin/sh\n").expect("write");
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).expect("chmod");
assert!(is_executable(&exe), "0o755 file should be executable");
let plain = dir.path().join("data");
std::fs::write(&plain, b"x").expect("write");
std::fs::set_permissions(&plain, std::fs::Permissions::from_mode(0o644)).expect("chmod");
assert!(
!is_executable(&plain),
"0o644 file should not be executable"
);
assert!(
!is_executable(&dir.path().join("missing")),
"missing path should not be executable"
);
assert!(
!is_executable(dir.path()),
"a directory should not count as an executable file"
);
}
#[test]
fn configure_detached_spawns_child_in_new_process_group() {
use super::configure_detached;
use std::process::Command;
let mut cmd = Command::new("sleep");
cmd.arg("30");
configure_detached(&mut cmd);
let mut child = cmd.spawn().expect("spawn sleep");
let child_pid = child.id();
let pgid = |pid: u32| -> String {
let out = Command::new("ps")
.args(["-o", "pgid=", "-p", &pid.to_string()])
.output()
.expect("ps");
String::from_utf8_lossy(&out.stdout).trim().to_string()
};
let child_pgid = pgid(child_pid);
let _ = child.kill();
let _ = child.wait();
assert_eq!(
child_pgid,
child_pid.to_string(),
"configure_detached must make the child its own process-group leader"
);
}
mod props {
use super::super::{read_pidfile, write_pidfile_atomic};
use proptest::prelude::*;
proptest! {
#[test]
fn pidfile_write_read_roundtrips(pid in any::<u32>()) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("mcp-proxy.pid");
write_pidfile_atomic(&path, pid).expect("write");
let read = read_pidfile(&path).expect("read");
prop_assert_eq!(read, Some(pid));
}
#[test]
fn pidfile_parse_never_invents_a_pid(s in "[^0-9]{1,12}") {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("mcp-proxy.pid");
std::fs::write(&path, &s).expect("write");
match read_pidfile(&path) {
Ok(None) | Err(_) => {}
Ok(Some(pid)) => prop_assert!(false, "parsed bogus pid {pid} from {s:?}"),
}
}
}
}
#[test]
fn probe_tcp_returns_false_for_invalid_address() {
use super::probe_tcp;
assert!(!probe_tcp("not-a-valid-address", 200));
assert!(!probe_tcp("127.0.0.1:0", 200));
}
#[test]
fn probe_tcp_returns_true_for_open_port() {
use super::probe_tcp;
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let addr = listener.local_addr().expect("local_addr");
let bind = addr.to_string();
assert!(
probe_tcp(&bind, 200),
"probe_tcp must return true when a listener is bound on {bind}"
);
}
#[test]
fn ensure_running_spawns_when_pidfile_exists_but_port_is_not_bound() {
use super::{EnsureOutcome, ensure_running, probe_tcp, write_pidfile_atomic};
use std::net::TcpListener;
use std::sync::{Arc, Mutex};
let port = {
let l = TcpListener::bind("127.0.0.1:0").expect("bind");
l.local_addr().expect("addr").port()
};
let bind = format!("127.0.0.1:{port}");
let dir = tempfile::tempdir().expect("tempdir");
let pid_path = dir.path().join("mcp-proxy.pid");
write_pidfile_atomic(&pid_path, 99_999).expect("write pidfile");
assert!(!probe_tcp(&bind, 50), "port must be closed before test");
let held_listener: Arc<Mutex<Option<TcpListener>>> = Arc::new(Mutex::new(None));
let held2 = Arc::clone(&held_listener);
let bind_clone = bind.clone();
let result = ensure_running(&bind, &pid_path, move |_b| {
let l = TcpListener::bind(&bind_clone as &str).expect("bind for spawn simulation");
*held2.lock().expect("lock") = Some(l);
Ok(42_u32)
});
assert!(result.is_ok(), "ensure_running failed: {:?}", result);
assert_eq!(
result.unwrap(),
EnsureOutcome::Spawned,
"must spawn when pidfile exists but port is not bound (PID-reuse scenario)"
);
drop(held_listener);
}
#[test]
fn ensure_running_returns_already_running_when_port_is_bound() {
use super::{EnsureOutcome, ensure_running, write_pidfile_atomic};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().expect("addr").port();
let bind = format!("127.0.0.1:{port}");
let dir = tempfile::tempdir().expect("tempdir");
let pid_path = dir.path().join("mcp-proxy.pid");
write_pidfile_atomic(&pid_path, 12_345).expect("write pidfile");
let result = ensure_running(&bind, &pid_path, |_| {
panic!("spawn must not be called when proxy is already running")
});
assert_eq!(
result.expect("ensure_running"),
EnsureOutcome::AlreadyRunning,
"must return AlreadyRunning when port is bound"
);
}
#[test]
fn ensure_running_errors_when_spawn_succeeds_but_port_never_binds() {
use super::ensure_running;
let port = {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
l.local_addr().expect("addr").port()
};
let bind = format!("127.0.0.1:{port}");
let dir = tempfile::tempdir().expect("tempdir");
let pid_path = dir.path().join("mcp-proxy.pid");
let result = ensure_running(&bind, &pid_path, |_| Ok(99_999));
assert!(
result.is_err(),
"ensure_running must error when spawn succeeds but port never binds"
);
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("did not bind"),
"error message should mention bind failure, got: {msg}"
);
}
#[test]
fn spawn_mcp_proxy_rejects_missing_port() {
use super::spawn_mcp_proxy;
let result = spawn_mcp_proxy("127.0.0.1");
assert!(result.is_err(), "must fail without a port");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("missing :port suffix"),
"error should mention missing port, got: {msg}"
);
}
#[test]
fn spawn_mcp_proxy_rejects_non_numeric_port() {
use super::spawn_mcp_proxy;
let result = spawn_mcp_proxy("127.0.0.1:notaport");
assert!(result.is_err(), "must fail with a non-numeric port");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("not a valid u16"),
"error should mention invalid port, got: {msg}"
);
}
#[test]
fn spawn_mcp_proxy_rejects_hostname_host() {
use super::spawn_mcp_proxy;
let result = spawn_mcp_proxy("localhost:7878");
assert!(result.is_err(), "must reject a hostname");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("not a valid IP address"),
"error should mention invalid host, got: {msg}"
);
}
mod bind_string_props {
use proptest::prelude::*;
proptest! {
#[test]
fn valid_ip_port_does_not_fail_at_parse(
a in 0u8..=255,
b in 0u8..=255,
c in 0u8..=255,
d in 0u8..=255,
port in 1u16..=65535,
) {
use super::super::spawn_mcp_proxy;
let bind = format!("{a}.{b}.{c}.{d}:{port}");
match spawn_mcp_proxy(&bind) {
Ok(_) => {} Err(e) => {
let msg = e.to_string();
prop_assert!(
!msg.contains("missing :port") &&
!msg.contains("not a valid u16") &&
!msg.contains("not a valid IP address"),
"parse-stage error for valid bind {bind}: {msg}"
);
}
}
}
#[test]
fn no_colon_always_errors(s in "[a-zA-Z0-9]{1,20}") {
use super::super::spawn_mcp_proxy;
let result = spawn_mcp_proxy(&s);
prop_assert!(result.is_err(), "must reject bind without colon: {s}");
prop_assert!(
result.unwrap_err().to_string().contains("missing :port"),
"error should mention missing port"
);
}
}
}
}