Files
mc-proxy-controller/src/docker.rs

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;
}
}
}
}
_ => {}
}
}
}
}
}