use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use anyhow::{Context, Result, bail};
use crate::cli::GatewayCommand;
use crate::model::Gateway;
use crate::{gateway, store};
pub fn run(command: GatewayCommand) -> Result<()> {
match command {
GatewayCommand::Start => start(),
GatewayCommand::Run { front_port } => {
if let Some(running) = &store::load_state()?.gateway
&& probe(running)
{
bail!("the gateway is already running (pid {}) - stop it with `turnout gateway stop`", running.pid);
}
gateway::run(front_port)
}
GatewayCommand::Stop => stop(),
}
}
const START_TIMEOUT: Duration = Duration::from_secs(5);
fn start() -> Result<()> {
let apps = store::load_apps()?;
let ports = gateway::listening_ports(&apps)?;
let mut state = store::load_state()?;
if let Some(running) = &state.gateway
&& probe(running)
{
bail!("the gateway is already running (pid {})", running.pid);
}
for (port, app) in &ports {
if port_answers(*port) {
bail!("port {port} is already in use by another process - free it, or give '{app}' another port with `turnout app edit {app} --port PORT`");
}
}
let front_port = crate::front::pick_port();
let exe = std::env::current_exe().context("cannot locate the turnout binary")?;
let mut command = Command::new(exe);
command.args(["gateway", "run"]);
if let Some(port) = front_port {
command.args(["--front-port", &port.to_string()]);
}
command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
command.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
}
crate::utils::stop_inheriting_stdio();
let mut child = command.spawn().context("cannot start the gateway process")?;
let gateway = Gateway {
pid: child.id(),
ports: ports.clone(),
front_port,
};
let started = Instant::now();
loop {
if let Some(status) = child.try_wait().context("cannot check on the gateway process")? {
bail!("the gateway exited right after starting ({status}) - run `turnout gateway run` in the foreground to see why");
}
if probe(&gateway) {
break;
}
if started.elapsed() > START_TIMEOUT {
let _ = kill(child.id());
bail!(
"the gateway did not answer on port {} within {}s - run `turnout gateway run` in the foreground to see why",
gateway.ports.keys().next().copied().unwrap_or_default(),
START_TIMEOUT.as_secs()
);
}
std::thread::sleep(Duration::from_millis(50));
}
state.gateway = Some(gateway);
store::save_state(&state)?;
crate::journal::record("gateway.start", None, None, Some(&format!("{} apps", ports.len())));
println!("Gateway started (pid {}).", child.id());
for (port, app) in ports {
println!(" {app}: http://localhost:{port}");
}
match front_port {
Some(front) => {
println!("Front door: {}", crate::front::door(front));
for app in &apps {
println!(" {}: {}", app.name, crate::front::address(&app.name, front));
}
}
None => println!(
"Front door: closed - ports {} and {} are taken; set {} to open it elsewhere",
crate::front::PORT,
crate::front::FALLBACK_PORT,
crate::front::ENV_PORT
),
}
for app in apps.iter().filter(|app| app.gateway_port.is_some()) {
match crate::envfile::write(app) {
Ok(crate::envfile::Outcome::Written(assignment)) => println!(" {}: wrote {} ({assignment})", app.name, app.env_file_name()),
Ok(_) => {}
Err(error) => eprintln!("warning: {}: {error:#}", app.name),
}
}
Ok(())
}
fn stop() -> Result<()> {
let mut state = store::load_state()?;
let Some(running) = state.gateway.take() else {
println!("The gateway is not running.");
return Ok(());
};
if let Err(error) = kill(running.pid) {
if probe(&running) {
return Err(error);
}
store::save_state(&state)?;
println!("The gateway (pid {}) was no longer running - cleared the stale record.", running.pid);
return Ok(());
}
store::save_state(&state)?;
crate::journal::record("gateway.stop", None, None, None);
println!("Gateway stopped (pid {}).", running.pid);
Ok(())
}
pub fn probe(gateway: &Gateway) -> bool {
gateway.ports.keys().next().is_some_and(|port| port_answers(*port))
}
fn port_answers(port: u16) -> bool {
let address = std::net::SocketAddr::from(([127, 0, 0, 1], port));
std::net::TcpStream::connect_timeout(&address, Duration::from_millis(300)).is_ok()
}
fn kill(pid: u32) -> Result<()> {
if pid <= 1 || pid > i32::MAX as u32 {
bail!("refusing to signal pid {pid}: not a process id");
}
kill_process(pid)
}
#[cfg(windows)]
fn kill_process(pid: u32) -> Result<()> {
let output = Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/T", "/F"])
.output()
.context("cannot run taskkill")?;
if !output.status.success() {
bail!("taskkill failed: {}", String::from_utf8_lossy(&output.stderr).trim());
}
Ok(())
}
#[cfg(not(windows))]
fn kill_process(pid: u32) -> Result<()> {
let output = Command::new("kill").arg(pid.to_string()).output().context("cannot run kill")?;
if !output.status.success() {
bail!("kill failed: {}", String::from_utf8_lossy(&output.stderr).trim());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kill_refuses_a_number_that_is_not_a_pid() {
for pid in [0, 1, u32::MAX, i32::MAX as u32 + 1] {
let error = kill(pid).unwrap_err().to_string();
assert!(error.contains("not a process id"), "pid {pid}: {error}");
}
}
}