use color_eyre::eyre::Result;
use console::style;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use tokio::time::timeout;
use tracing::{info, warn, error};
use serde_json;
pub async fn handle_stop() -> Result<()> {
println!("{}", style("đ Stopping Fortress Server").bold().cyan());
println!();
let server_info = match get_server_info().await {
Ok(info) => info,
Err(e) => {
println!("{} No running Fortress server found.", style("âšī¸").blue());
println!("Error: {}", e);
return Ok(());
}
};
println!("Server Information:");
println!(" PID: {}", style(server_info.pid).bold());
println!(" Address: {}", style(server_info.address).bold());
println!(" Uptime: {}", style(format_duration(server_info.uptime)).bold());
println!(" Version: {}", style(server_info.version).bold());
println!();
println!("Attempting graceful shutdown...");
match graceful_shutdown(&server_info).await {
Ok(()) => {
println!("â Fortress Server stopped gracefully");
info!("Fortress server stopped gracefully (PID: {})", server_info.pid);
return Ok(());
}
Err(e) => {
warn!("Graceful shutdown failed: {}", e);
println!("â Graceful shutdown failed: {}", e);
}
}
println!("Attempting force shutdown...");
match force_shutdown(&server_info).await {
Ok(()) => {
println!("â Fortress Server stopped forcefully");
info!("Fortress server stopped forcefully (PID: {})", server_info.pid);
}
Err(e) => {
error!("Force shutdown failed: {}", e);
println!("â Force shutdown failed: {}", e);
println!("\nYou may need to manually kill the process:");
println!(" kill -9 {}", server_info.pid);
return Err(e);
}
}
Ok(())
}
#[derive(Debug, Clone)]
struct ServerInfo {
pid: u32,
address: String,
uptime: Duration,
version: String,
}
async fn get_server_info() -> Result<ServerInfo> {
if let Ok(info) = get_server_info_from_api().await {
return Ok(info);
}
get_server_info_from_process().await
}
async fn get_server_info_from_api() -> Result<ServerInfo> {
let client = reqwest::Client::new();
let ports = vec![8080, 8081, 3000, 8082];
for port in ports {
let url = format!("http://127.0.0.1:{}/status", port);
match timeout(Duration::from_secs(2), client.get(&url).send()).await {
Ok(Ok(response)) if response.status().is_success() => {
let status: serde_json::Value = response.json().await
.map_err(|e| color_eyre::eyre::eyre!("Failed to parse status: {}", e))?;
let server_data = status.get("server").ok_or_else(|| {
color_eyre::eyre::eyre!("Missing server information in status response")
})?;
let uptime_seconds = server_data.get("uptime_seconds")
.and_then(|v| v.as_u64())
.ok_or_else(|| color_eyre::eyre::eyre!("Missing uptime information"))?;
let version = server_data.get("version")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
return Ok(ServerInfo {
pid: get_pid_from_port(port).await.unwrap_or(0),
address: format!("127.0.0.1:{}", port),
uptime: Duration::from_secs(uptime_seconds),
version: version.to_string(),
});
}
_ => continue,
}
}
Err(color_eyre::eyre::eyre!("No running Fortress server found via API"))
}
async fn get_server_info_from_process() -> Result<ServerInfo> {
let output = Command::new("ps")
.args(&["aux"])
.output()
.map_err(|e| color_eyre::eyre::eyre!("Failed to run ps command: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("fortress") && line.contains("server") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(pid) = parts[1].parse::<u32>() {
let port = extract_port_from_line(line).unwrap_or(8080);
return Ok(ServerInfo {
pid,
address: format!("127.0.0.1:{}", port),
uptime: Duration::from_secs(0), version: "unknown".to_string(),
});
}
}
}
}
Err(color_eyre::eyre::eyre!("No running Fortress process found"))
}
async fn get_pid_from_port(port: u16) -> Result<u32> {
let output = Command::new("lsof")
.args(&["-i", &format!(":{}", port)])
.output()
.map_err(|e| color_eyre::eyre::eyre!("Failed to run lsof: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().skip(1) { let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(pid) = parts[1].parse::<u32>() {
return Ok(pid);
}
}
}
Err(color_eyre::eyre::eyre!("No process found using port {}", port))
}
fn extract_port_from_line(line: &str) -> Option<u16> {
let re = regex::Regex::new(r"(?:--port|-p)\s+(\d+)").ok()?;
if let Some(captures) = re.captures(line) {
if let Some(port_match) = captures.get(1) {
return port_match.as_str().parse().ok();
}
}
None
}
async fn graceful_shutdown(server_info: &ServerInfo) -> Result<()> {
let client = reqwest::Client::new();
let shutdown_url = format!("http://{}/shutdown", server_info.address);
match timeout(Duration::from_secs(10), client.post(&shutdown_url).send()).await {
Ok(Ok(response)) if response.status().is_success() => {
for _ in 0..30 { tokio::time::sleep(Duration::from_secs(1)).await;
if !is_server_running(server_info).await {
return Ok(());
}
}
Err(color_eyre::eyre::eyre!("Server did not stop within timeout"))
}
Ok(Ok(response)) => {
Err(color_eyre::eyre::eyre!("Shutdown request failed with status: {}", response.status()))
}
Ok(Err(e)) => {
Err(color_eyre::eyre::eyre!("Failed to send shutdown request: {}", e))
}
Err(_) => {
Err(color_eyre::eyre::eyre!("Shutdown request timeout"))
}
}
}
async fn force_shutdown(server_info: &ServerInfo) -> Result<()> {
let output = Command::new("kill")
.args(&["-TERM", &server_info.pid.to_string()])
.output()
.map_err(|e| color_eyre::eyre::eyre!("Failed to send SIGTERM: {}", e))?;
if !output.status.success() {
return Err(color_eyre::eyre::eyre!("Failed to send SIGTERM signal"));
}
for _ in 0..10 { tokio::time::sleep(Duration::from_secs(1)).await;
if !is_server_running(server_info).await {
return Ok(());
}
}
warn!("Server did not respond to SIGTERM, sending SIGKILL");
let output = Command::new("kill")
.args(&["-KILL", &server_info.pid.to_string()])
.output()
.map_err(|e| color_eyre::eyre::eyre!("Failed to send SIGKILL: {}", e))?;
if !output.status.success() {
return Err(color_eyre::eyre::eyre!("Failed to send SIGKILL signal"));
}
for _ in 0..5 {
tokio::time::sleep(Duration::from_secs(1)).await;
if !is_server_running(server_info).await {
return Ok(());
}
}
Err(color_eyre::eyre::eyre!("Failed to stop server with SIGKILL"))
}
async fn is_server_running(server_info: &ServerInfo) -> bool {
if let Ok(output) = Command::new("kill").args(&["-0", &server_info.pid.to_string()]).output() {
if output.status.success() {
return true;
}
}
if let Ok(output) = Command::new("lsof")
.args(&["-i", &format!(":{}", server_info.address.split(':').last().unwrap_or("8080"))])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
return stdout.contains(&server_info.pid.to_string());
}
false
}
fn format_duration(duration: Duration) -> String {
let total_seconds = duration.as_secs();
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
if hours > 0 {
format!("{}h {}m {}s", hours, minutes, seconds)
} else if minutes > 0 {
format!("{}m {}s", minutes, seconds)
} else {
format!("{}s", seconds)
}
}