use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use std::process::{Child, Command};
use std::time::Duration;
use tokio::net::TcpListener;
#[derive(Debug, serde::Deserialize)]
struct CallwireToml {
services: Option<HashMap<String, ServiceConfig>>,
}
#[derive(Debug, serde::Deserialize)]
struct ServiceConfig {
dev_cmd: Option<String>,
prod_cmd: Option<String>,
}
fn load_toml() -> anyhow::Result<CallwireToml> {
let path = Path::new("callwire.toml");
if !path.exists() {
return Ok(CallwireToml { services: None });
}
let content = std::fs::read_to_string(path)?;
let config: CallwireToml = toml::from_str(&content)?;
Ok(config)
}
fn kill_stale_pids() {
let pid_file = Path::new(".callwire/pids");
if !pid_file.exists() {
return;
}
if let Ok(content) = std::fs::read_to_string(pid_file) {
for line in content.lines() {
if let Ok(pid) = line.trim().parse::<u32>() {
#[cfg(unix)]
unsafe {
libc::kill(pid as libc::pid_t, libc::SIGTERM);
}
}
}
}
let _ = std::fs::remove_file(pid_file);
}
fn write_pid_file(children: &[Child]) {
let _ = std::fs::create_dir_all(".callwire");
if let Ok(mut f) = std::fs::File::create(".callwire/pids") {
for child in children {
let _ = writeln!(f, "{}", child.id());
}
}
}
async fn free_port() -> std::io::Result<u16> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
Ok(listener.local_addr()?.port())
}
pub struct OrchestratorGuard {
children: Vec<Child>,
}
impl OrchestratorGuard {
pub fn shutdown(mut self) {
self.terminate_children();
}
fn terminate_children(&mut self) {
for child in &mut self.children {
let _ = child.kill();
}
for child in &mut self.children {
let _ = child.wait();
}
let _ = std::fs::remove_file(".callwire/pids");
}
}
impl Drop for OrchestratorGuard {
fn drop(&mut self) {
self.terminate_children();
}
}
pub async fn init() -> anyhow::Result<Option<OrchestratorGuard>> {
if std::env::var("CALLWIRE_SPAWNED").as_deref() == Ok("1") {
init_as_worker().await?;
return Ok(None);
}
let guard = init_as_orchestrator().await?;
Ok(Some(guard))
}
async fn init_as_worker() -> anyhow::Result<()> {
let registry_addr = std::env::var("CALLWIRE_REGISTRY")
.map_err(|_| anyhow::anyhow!("CALLWIRE_REGISTRY not set in worker mode"))?;
let listener = TcpListener::bind("127.0.0.1:0").await?;
let worker_addr = listener.local_addr()?.to_string();
eprintln!("[callwire] Worker serving on {}", worker_addr);
let (_tx, rx) = tokio::sync::watch::channel(false);
tokio::spawn(crate::server::run_accept_loop(listener, rx));
tokio::time::sleep(Duration::from_millis(150)).await;
let func_names: Vec<String> = {
let reg = crate::server::REGISTRY.lock().unwrap();
reg.keys()
.filter(|k| !k.starts_with("callwire."))
.cloned()
.collect()
};
let client = crate::client::Client::connect(®istry_addr).await?;
for name in &func_names {
if let Err(e) = client
.import::<(), _>("callwire.register", &(name.as_str(), worker_addr.as_str()))
.await
{
eprintln!("[callwire] Warning: could not register '{}': {}", name, e);
}
}
eprintln!("[callwire] Worker registered {:?} → {}", func_names, worker_addr);
let parent_pid = std::os::unix::process::parent_id();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(2)).await;
#[cfg(unix)]
{
let current = std::os::unix::process::parent_id();
if current == 1 && current != parent_pid {
eprintln!("[callwire] Parent process gone — worker exiting");
std::process::exit(0);
}
}
}
});
Ok(())
}
async fn init_as_orchestrator() -> anyhow::Result<OrchestratorGuard> {
let config = load_toml()?;
let services = config.services.unwrap_or_default();
if services.is_empty() {
return Ok(OrchestratorGuard { children: vec![] });
}
kill_stale_pids();
let reg_port = free_port().await?;
let registry_addr = format!("127.0.0.1:{}", reg_port);
crate::registry::start_embedded_registry().await;
crate::server::serve_on(®istry_addr).await?;
eprintln!("[callwire] Registry listening on {}", registry_addr);
unsafe {
std::env::set_var("CALLWIRE_REGISTRY", ®istry_addr);
}
let is_prod = std::env::var("CALLWIRE_ENV")
.map(|v| v.to_lowercase() == "prod")
.unwrap_or(false);
let mut children = Vec::new();
for (name, svc) in &services {
let cmd = if is_prod {
svc.prod_cmd.as_deref().or(svc.dev_cmd.as_deref())
} else {
svc.dev_cmd.as_deref().or(svc.prod_cmd.as_deref())
};
let Some(cmd) = cmd else {
eprintln!("[callwire] Warning: service '{}' has no command — skipping", name);
continue;
};
let child = Command::new("sh")
.arg("-c")
.arg(cmd)
.env("CALLWIRE_SPAWNED", "1")
.env("CALLWIRE_REGISTRY", ®istry_addr)
.spawn();
match child {
Ok(c) => {
eprintln!("[callwire] Spawned '{}' (PID {}): {}", name, c.id(), cmd);
children.push(c);
}
Err(e) => {
eprintln!("[callwire] Failed to spawn '{}': {}", name, e);
}
}
}
write_pid_file(&children);
let wait_ms = std::cmp::min(1500 * children.len().max(1) as u64, 5000);
tokio::time::sleep(Duration::from_millis(wait_ms)).await;
eprintln!("[callwire] Orchestrator ready — registry at {}", registry_addr);
Ok(OrchestratorGuard { children })
}