integrate docker and implement state tracking for server lifecycle management
This commit is contained in:
174
src/docker.rs
Normal file
174
src/docker.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user