175 lines
6.5 KiB
Rust
175 lines
6.5 KiB
Rust
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<Self> {
|
|
let docker = Docker::connect_with_socket_defaults()?;
|
|
Ok(Self {
|
|
docker,
|
|
container_name,
|
|
})
|
|
}
|
|
|
|
pub async fn get_container_status(&self) -> Result<ContainerStatus> {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|