use std::time::Duration; use anyhow::{Result, bail}; use bollard::Docker; use tokio::sync::mpsc; use tokio::time::Instant; use crate::config::Config; use crate::rcon; use crate::state::{ServerState, SharedServerState}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum ContainerStatus { Running, Stopped, NotFound, } pub struct DockerManager { docker: Docker, container_name: String, } impl DockerManager { pub async fn new(container_name: String) -> Result { let docker = Docker::connect_with_socket_defaults()?; Ok(Self { docker, container_name, }) } pub async fn get_container_status(&self) -> Result { match self .docker .inspect_container(&self.container_name, None) .await { Ok(details) => { let Some(state) = details.state else { bail!("No state in container details"); }; let running = state.running.unwrap_or(false); if running { Ok(ContainerStatus::Running) } else { Ok(ContainerStatus::Stopped) } } Err(bollard::errors::Error::DockerResponseServerError { status_code: 404, .. }) => Ok(ContainerStatus::NotFound), Err(e) => Err(e.into()), } } pub async fn start_container(&self) -> Result<()> { println!("Starting Docker container: {}", self.container_name); self.docker .start_container(&self.container_name, None) .await?; Ok(()) } } pub async fn run_docker_lifecycle_manager( config: Config, state: SharedServerState, mut player_connect_rx: mpsc::Receiver<()>, ) -> Result<()> { let docker = DockerManager::new(config.container_name.clone()).await?; match docker.get_container_status().await { Ok(ContainerStatus::Running) => { println!( "Container '{}' is already running, checking for RCON availability...", config.container_name ); state.transition_to_starting().await; } Ok(ContainerStatus::Stopped) => { println!( "Container '{}' is stopped, waiting for player connection...", config.container_name ); state.transition_to_stopped().await; } Ok(ContainerStatus::NotFound) => { eprintln!("ERROR: Container '{}' not found!", config.container_name); bail!("Container not found"); } Err(e) => { eprintln!("ERROR: Failed to connect to Docker: {}", e); return Err(e); } } let startup_timeout = Duration::from_secs(config.startup_timeout_secs); let stop_timeout = Duration::from_secs(30); loop { tokio::select! { Some(_) = player_connect_rx.recv() => { let current_state = state.get().await; if matches!(current_state, ServerState::Stopped | ServerState::Unknown) { println!("Player connection detected, starting container..."); if let Err(e) = docker.start_container().await { eprintln!("Failed to start container: {}", e); } else { state.transition_to_starting().await; } } else { println!("Player connection detected, but server is already in state {:?}", current_state); } } _ = tokio::time::sleep(Duration::from_millis(500)) => { let current_state = state.get().await; match current_state { ServerState::Starting { started_at } => { match docker.get_container_status().await { Ok(ContainerStatus::Stopped) => { eprintln!("Container stopped unexpectedly during startup (crashed/exited)"); state.transition_to_stopped().await; continue; } Ok(ContainerStatus::NotFound) => { eprintln!("Container disappeared during startup"); state.transition_to_stopped().await; continue; } Err(e) => { eprintln!("Failed to check container status: {}", e); } Ok(ContainerStatus::Running) => { } } let rcon_available = rcon::connect_rcon(&config.rcon_addr, &config.rcon_password).await.is_ok(); if rcon_available { let startup_duration = (Instant::now() - started_at).as_secs(); println!("RCON connection established, server is ready!"); state.record_startup_duration(startup_duration).await; state.transition_to_running().await; } else if Instant::now() - started_at > startup_timeout { eprintln!( "Server start timeout ({}s), transitioning back to Stopped", startup_timeout.as_secs() ); state.transition_to_stopped().await; } } ServerState::Stopping { stop_requested_at } => { match docker.get_container_status().await { Ok(ContainerStatus::Stopped) => { println!("Container stopped successfully"); state.transition_to_stopped().await; } _ => { if Instant::now() - stop_requested_at > stop_timeout { eprintln!("Container stop timeout, forcing transition to Stopped"); state.transition_to_stopped().await; } } } } _ => {} } } } } }