use crate::config::ServerConfig;
use crate::error::{Error, Result};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use tracing::{debug, info, warn};
pub struct ManagedProcess {
child: Child,
http_port: u16,
pid: u32,
}
impl ManagedProcess {
pub fn spawn(config: &ServerConfig) -> Result<Self> {
let binary_path = find_mockforge_binary(config)?;
debug!("Using MockForge binary at: {:?}", binary_path);
let mut cmd = Command::new(&binary_path);
cmd.arg("serve");
cmd.arg("--http-port").arg(config.http_port.to_string());
if let Some(ws_port) = config.ws_port {
cmd.arg("--ws-port").arg(ws_port.to_string());
}
if let Some(grpc_port) = config.grpc_port {
cmd.arg("--grpc-port").arg(grpc_port.to_string());
}
if let Some(admin_port) = config.admin_port {
cmd.arg("--admin-port").arg(admin_port.to_string());
}
if let Some(metrics_port) = config.metrics_port {
cmd.arg("--metrics-port").arg(metrics_port.to_string());
}
if config.enable_admin {
cmd.arg("--admin");
}
if config.enable_metrics {
cmd.arg("--metrics");
}
if let Some(spec_file) = &config.spec_file {
cmd.arg("--spec").arg(spec_file);
}
if let Some(workspace_dir) = &config.workspace_dir {
cmd.arg("--workspace-dir").arg(workspace_dir);
}
if let Some(profile) = &config.profile {
cmd.arg("--profile").arg(profile);
}
for arg in &config.extra_args {
cmd.arg(arg);
}
if let Some(working_dir) = &config.working_dir {
cmd.current_dir(working_dir);
}
for (key, value) in &config.env_vars {
cmd.env(key, value);
}
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
debug!("Spawning MockForge process: {:?}", cmd);
let child = cmd
.spawn()
.map_err(|e| Error::ServerStartFailed(format!("Failed to spawn process: {}", e)))?;
let pid = child.id();
info!("Spawned MockForge process with PID: {}", pid);
Ok(Self {
child,
http_port: config.http_port,
pid,
})
}
pub fn http_port(&self) -> u16 {
self.http_port
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_running(&mut self) -> bool {
matches!(self.child.try_wait(), Ok(None))
}
pub fn kill(&mut self) -> Result<()> {
if self.is_running() {
debug!("Killing MockForge process (PID: {})", self.pid);
self.child
.kill()
.map_err(|e| Error::ProcessError(format!("Failed to kill process: {}", e)))?;
let _ = self.child.wait();
info!("MockForge process (PID: {}) terminated", self.pid);
} else {
debug!("Process (PID: {}) already exited", self.pid);
}
Ok(())
}
}
impl Drop for ManagedProcess {
fn drop(&mut self) {
if let Err(e) = self.kill() {
warn!("Failed to kill process on drop: {}", e);
}
}
}
fn find_mockforge_binary(config: &ServerConfig) -> Result<PathBuf> {
if let Some(binary_path) = &config.binary_path {
if binary_path.exists() {
return Ok(binary_path.clone());
}
return Err(Error::BinaryNotFound);
}
if let Ok(env_path) = std::env::var("MOCKFORGE_TEST_BINARY") {
let p = PathBuf::from(env_path);
if p.exists() {
return Ok(p);
}
}
if let Some(p) = workspace_target_binary() {
return Ok(p);
}
which::which("mockforge")
.map_err(|_| Error::BinaryNotFound)
.map(|p| p.to_path_buf())
}
fn workspace_target_binary() -> Option<PathBuf> {
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.or_else(target_dir_from_manifest)
.or_else(target_dir_from_current_exe)?;
let debug = target_dir.join("debug").join("mockforge");
if debug.exists() {
return Some(debug);
}
let release = target_dir.join("release").join("mockforge");
if release.exists() {
return Some(release);
}
None
}
fn target_dir_from_manifest() -> Option<PathBuf> {
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from)?;
let mut dir: &std::path::Path = &manifest_dir;
loop {
let candidate = dir.join("target");
if candidate.is_dir() {
return Some(candidate);
}
dir = dir.parent()?;
}
}
fn target_dir_from_current_exe() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let mut dir = exe.parent()?;
loop {
if dir.file_name() == Some(std::ffi::OsStr::new("target")) {
return Some(dir.to_path_buf());
}
dir = dir.parent()?;
}
}
pub fn is_port_available(port: u16) -> bool {
use std::net::TcpListener;
TcpListener::bind(("127.0.0.1", port)).is_ok()
}
pub fn find_available_port(start_port: u16) -> Result<u16> {
for port in start_port..start_port.saturating_add(100) {
if is_port_available(port) {
return Ok(port);
}
}
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| {
Error::ConfigError(format!(
"No available ports in {}-{} and OS-assigned fallback failed: {}",
start_port,
start_port.saturating_add(100),
e
))
})?;
let port = listener
.local_addr()
.map_err(|e| Error::ConfigError(format!("Failed to read OS-assigned port: {}", e)))?
.port();
drop(listener);
Ok(port)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_port_available() {
assert!(is_port_available(0));
}
#[test]
fn test_find_available_port() {
let port = find_available_port(30000).expect("Failed to find available port");
assert!(port >= 30000);
assert!(port < 30100);
}
#[test]
fn test_find_available_port_from_different_start() {
let port = find_available_port(40000).expect("Failed to find available port");
assert!(port >= 40000);
assert!(port < 40100);
}
#[test]
fn test_find_available_port_high_range() {
let port = find_available_port(60000).expect("Failed to find available port");
assert!(port >= 60000);
assert!(port < 60100);
}
#[test]
fn test_is_port_available_high_port() {
let available = is_port_available(59999);
let _ = available;
}
#[test]
fn test_multiple_port_allocations() {
let port1 = find_available_port(31000).expect("Failed to find port 1");
let port2 = find_available_port(32000).expect("Failed to find port 2");
let port3 = find_available_port(33000).expect("Failed to find port 3");
assert!((31000..31100).contains(&port1));
assert!((32000..32100).contains(&port2));
assert!((33000..33100).contains(&port3));
}
}