integrate docker and implement state tracking for server lifecycle management

This commit is contained in:
2026-02-02 17:00:32 +01:00
parent 84b56a163a
commit 753d9e1e64
10 changed files with 756 additions and 93 deletions

239
Cargo.lock generated
View File

@@ -119,6 +119,12 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -146,6 +152,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
@@ -410,6 +422,49 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bollard"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "227aa051deec8d16bd9c34605e7aaf153f240e35483dd42f6f78903847934738"
dependencies = [
"base64 0.22.1",
"bollard-stubs",
"bytes",
"futures-core",
"futures-util",
"hex",
"http 1.4.0",
"http-body-util",
"hyper 1.8.1",
"hyper-named-pipe",
"hyper-util",
"hyperlocal",
"log",
"pin-project-lite",
"serde",
"serde_derive",
"serde_json",
"serde_urlencoded",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"tower-service",
"url",
"winapi",
]
[[package]]
name = "bollard-stubs"
version = "1.52.1-rc.29.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066"
dependencies = [
"serde",
"serde_json",
"serde_repr",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
@@ -767,6 +822,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -786,6 +852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
@@ -860,7 +927,7 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"http",
"http 0.2.12",
"indexmap 2.13.0",
"slab",
"tokio",
@@ -906,6 +973,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
@@ -926,6 +999,16 @@ dependencies = [
"itoa",
]
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
@@ -933,7 +1016,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
"http 1.4.0",
]
[[package]]
name = "http-body-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"pin-project-lite",
]
@@ -960,8 +1066,8 @@ dependencies = [
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
@@ -973,6 +1079,43 @@ dependencies = [
"want",
]
[[package]]
name = "hyper"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-core",
"http 1.4.0",
"http-body 1.0.1",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-named-pipe"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
dependencies = [
"hex",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
"winapi",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
@@ -980,13 +1123,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http",
"hyper",
"http 0.2.12",
"hyper 0.14.32",
"rustls",
"tokio",
"tokio-rustls",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.4.0",
"http-body 1.0.1",
"hyper 1.8.1",
"libc",
"pin-project-lite",
"socket2 0.6.2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "hyperlocal"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
"hex",
"http-body-util",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -1217,6 +1395,8 @@ dependencies = [
name = "mc-proxy-controller"
version = "0.1.0"
dependencies = [
"anyhow",
"bollard",
"rcon",
"serde",
"tokio",
@@ -1610,15 +1790,15 @@ version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64",
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-rustls",
"ipnet",
"js-sys",
@@ -1719,7 +1899,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64",
"base64 0.21.7",
]
[[package]]
@@ -1803,6 +1983,17 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
@@ -2597,7 +2788,7 @@ checksum = "1b31cde2e3178f2d3167a8fb0bd74120804a7f43655f308a1eac56f81ff22dee"
dependencies = [
"anyhow",
"async-trait",
"base64",
"base64 0.21.7",
"bevy_app",
"bevy_ecs",
"bytes",
@@ -2641,7 +2832,7 @@ checksum = "db702fdbaf978d864f7a0dc965ed67311878a047125c6613dfcb0faf090f5c5e"
dependencies = [
"aes",
"anyhow",
"base64",
"base64 0.21.7",
"bevy_ecs",
"bitfield-struct",
"byteorder",
@@ -2900,6 +3091,28 @@ version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"

View File

@@ -9,3 +9,5 @@ tokio = { version = "1.49.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
valence = "0.2.0-alpha.1+mc.1.20.1"
bollard = "0.20.1"
anyhow = "1.0.100"

View File

@@ -1,5 +1,5 @@
use std::error::Error;
use std::fs;
use anyhow::Result;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
@@ -11,10 +11,26 @@ pub struct Config {
pub rcon_password: String,
pub idle_timeout_secs: u64,
pub polling_interval_millis: u64,
pub container_name: String,
#[serde(default = "default_startup_timeout_secs")]
pub startup_timeout_secs: u64,
#[serde(default = "default_rcon_retry_interval_secs")]
pub rcon_retry_interval_secs: u64,
}
fn default_startup_timeout_secs() -> u64 {
600
}
fn default_rcon_retry_interval_secs() -> u64 {
1
}
impl Config {
pub fn load(path: &str) -> Result<Self, Box<dyn Error>> {
pub fn load(path: &str) -> Result<Self> {
let content = fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)

174
src/docker.rs Normal file
View 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;
}
}
}
}
_ => {}
}
}
}
}
}

View File

@@ -1,29 +1,57 @@
mod config;
mod motd;
mod docker;
mod monitor;
mod motd;
mod proxy;
mod rcon;
mod state;
use std::error::Error;
use anyhow::Result;
use tokio::main;
use tokio::sync::mpsc;
use config::Config;
use state::SharedServerState;
#[main]
async fn main() -> Result<(), Box<dyn Error>> {
async fn main() -> Result<()> {
let config = Config::load("config.toml")?;
let shared_state = SharedServerState::new();
let (player_connect_tx, player_connect_rx) = mpsc::channel::<()>(10);
let motd_config = config.clone();
let motd_state = shared_state.clone();
let motd_tx = player_connect_tx.clone();
tokio::spawn(async move {
motd::create_motd_server(&motd_config).run();
motd::create_motd_server(&motd_config, motd_state, Some(motd_tx)).run();
});
let monitor_config = config.clone();
let docker_config = config.clone();
let docker_state = shared_state.clone();
tokio::spawn(async move {
if let Err(e) = monitor::run_idle_monitor(monitor_config).await {
eprintln!("Idle monitor error: {e}");
if let Err(e) =
docker::run_docker_lifecycle_manager(docker_config, docker_state, player_connect_rx)
.await
{
eprintln!("Docker lifecycle manager error: {}", e);
}
});
proxy::run_proxy(config.listen_addr, config.server_addr).await
let monitor_config = config.clone();
let monitor_state = shared_state.clone();
tokio::spawn(async move {
if let Err(e) = monitor::run_idle_monitor(monitor_config, monitor_state).await {
eprintln!("Idle monitor error: {}", e);
}
});
proxy::run_proxy(
config.listen_addr,
config.server_addr,
config.motd_server_addr,
shared_state,
)
.await
}

View File

@@ -1,43 +1,73 @@
use std::error::Error;
use std::time::Duration;
use anyhow::Result;
use tokio::time::Instant;
use crate::config::Config;
use crate::rcon;
use crate::state::{ServerState, SharedServerState};
fn parse_players_online(s: &str) -> Option<u32> {
s.split_whitespace()
.find_map(|tok| tok.parse::<u32>().ok())
}
pub async fn run_idle_monitor(config: Config) -> Result<(), Box<dyn Error>> {
let mut conn = rcon::connect_rcon(&config.rcon_addr, &config.rcon_password).await?;
let mut idle = false;
pub async fn run_idle_monitor(
config: Config,
state: SharedServerState,
) -> Result<()> {
let mut last_online = Instant::now();
let idle_timeout = Duration::from_secs(config.idle_timeout_secs);
let polling_interval = Duration::from_millis(config.polling_interval_millis);
let mut backoff = Duration::from_secs(config.rcon_retry_interval_secs);
let max_backoff = Duration::from_secs(30);
loop {
let players_cmd_output = conn.cmd("list").await?;
let players_number = parse_players_online(&players_cmd_output).unwrap_or(0);
if players_number > 0 {
last_online = Instant::now();
}
println!("{players_number} {idle}");
if Instant::now() - last_online > idle_timeout {
if !idle {
idle = true;
println!("Stopping the server");
conn.cmd("stop").await?;
}
} else {
idle = false;
}
tokio::time::sleep(polling_interval).await;
let current_state = state.get().await;
if !matches!(current_state, ServerState::Running { .. }) {
last_online = Instant::now();
backoff = Duration::from_secs(config.rcon_retry_interval_secs);
continue;
}
let conn_result = rcon::connect_rcon(&config.rcon_addr, &config.rcon_password).await;
let mut conn = if let Ok(c) = conn_result {
backoff = Duration::from_secs(config.rcon_retry_interval_secs);
c
} else {
eprintln!("RCON connection failed, retrying in {:?}", backoff);
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(max_backoff);
continue;
};
match conn.cmd("list").await {
Ok(output) => {
let players_number = parse_players_online(&output).unwrap_or(0);
if players_number > 0 {
last_online = Instant::now();
}
if Instant::now() - last_online > idle_timeout {
println!("Idle timeout reached, stopping server");
state.transition_to_stopping().await;
if let Err(e) = conn.cmd("stop").await {
eprintln!("Failed to send stop command: {}", e);
}
last_online = Instant::now();
}
}
Err(e) => {
eprintln!("RCON command failed: {}", e);
}
}
}
}

View File

@@ -1,4 +1,5 @@
use std::net::SocketAddr;
use tokio::sync::mpsc;
use valence::network::{
async_trait, BroadcastToLan, CleanupFn, ConnectionMode, HandshakeData, ServerListPing,
};
@@ -6,8 +7,12 @@ use valence::prelude::*;
use valence::MINECRAFT_VERSION;
use crate::config::Config;
use crate::state::{ServerState, SharedServerState};
struct MotdCallbacks;
struct MotdCallbacks {
state: SharedServerState,
player_connect_tx: Option<mpsc::Sender<()>>,
}
#[async_trait]
impl NetworkCallbacks for MotdCallbacks {
@@ -17,19 +22,65 @@ impl NetworkCallbacks for MotdCallbacks {
_remote_addr: SocketAddr,
handshake_data: &HandshakeData,
) -> ServerListPing {
let current_state = self.state.get().await;
let (description, eta_secs) = match current_state {
ServerState::Stopped | ServerState::Unknown => {
let avg_time = self.state.get_average_startup_time().await;
(
"Serwer sobie ".into_text()
+ "śpi".into_text().color(Color::rgb(250, 50, 50))
+ "!\n"
+ "Dołącz aby go obudzić! :3 "
.into_text()
.color(Color::rgb(255, 150, 230)),
avg_time,
)
}
ServerState::Starting { started_at } => {
let elapsed = (tokio::time::Instant::now() - started_at).as_secs();
let avg_time = self.state.get_average_startup_time().await;
let remaining = avg_time.map(|avg| avg.saturating_sub(elapsed));
(
"Serwer się ".into_text()
+ "budzi UwU".into_text().color(Color::rgb(255, 200, 50))
+ "...\n"
+ "Poczekaj chwileczkę!!! :3 "
.into_text()
.color(Color::rgb(150, 255, 150)),
remaining,
)
}
ServerState::Running { .. } => {
(
"Serwer jest ".into_text()
+ "obudzony".into_text().color(Color::rgb(50, 250, 50))
+ "!\n",
None,
)
}
ServerState::Stopping { .. } => (
"Serwer idzie sobie ".into_text()
+ "spać".into_text().color(Color::rgb(250, 150, 50))
+ "...\n",
Some(30),
),
};
let description = if let Some(secs) = eta_secs {
description
+ format!("{}s", secs)
.into_text()
.color(Color::rgb(80, 80, 80))
} else {
description
};
ServerListPing::Respond {
online_players: 0,
max_players: 0,
player_sample: vec![],
description: "Serwer jest ".into_text()
+ "wyłączony".into_text().color(Color::rgb(250, 50, 50))
+ "!\n"
+ "Dołącz aby uruchomić serwer! "
.into_text()
.color(Color::rgb(255, 150, 230))
+ format!("{}s", 20)
.into_text()
.color(Color::rgb(80, 80, 80)),
description,
favicon_png: include_bytes!("../assets/icon.png"),
version_name: MINECRAFT_VERSION.to_string(),
protocol: handshake_data.protocol_version,
@@ -37,7 +88,7 @@ impl NetworkCallbacks for MotdCallbacks {
}
async fn broadcast_to_lan(&self, _shared: &SharedNetworkState) -> BroadcastToLan {
BroadcastToLan::Enabled("Hello Valence!".into())
BroadcastToLan::Disabled
}
async fn login(
@@ -45,15 +96,55 @@ impl NetworkCallbacks for MotdCallbacks {
_shared: &SharedNetworkState,
_info: &NewClientInfo,
) -> Result<CleanupFn, Text> {
Err("You are not meant to join this example".color(Color::rgb(250, 30, 21)))
let current_state = self.state.get().await;
match current_state {
ServerState::Stopped | ServerState::Unknown => {
if let Some(tx) = &self.player_connect_tx {
let _ = tx.send(()).await;
}
if let Some(avg_secs) = self.state.get_average_startup_time().await {
Err(format!("Serwer się słodko budzi, poczekaj jeszcze ~{}s... :>", avg_secs)
.color(Color::rgb(255, 200, 50)))
} else {
Err("Serwer się słodko budzi, poczekaj chwilę... :>"
.color(Color::rgb(255, 200, 50)))
}
}
ServerState::Starting { started_at } => {
let elapsed = (tokio::time::Instant::now() - started_at).as_secs();
if let Some(avg) = self.state.get_average_startup_time().await {
let remaining = avg.saturating_sub(elapsed);
Err(format!("Serwer się słodko budzi, poczekaj jeszcze ~{}s... :>", remaining)
.color(Color::rgb(255, 150, 50)))
} else {
Err("Serwer się słodko budzi, poczekaj chwilę... :>"
.color(Color::rgb(255, 150, 50)))
}
}
ServerState::Stopping { .. } => Err("Serwer idzie spać, poczekaj... >:c"
.color(Color::rgb(250, 150, 50))),
ServerState::Running { .. } => {
Err("Połącz się z głównym serwerem".color(Color::rgb(50, 250, 50)))
}
}
}
}
pub fn create_motd_server(config: &Config) -> App {
pub fn create_motd_server(
config: &Config,
state: SharedServerState,
player_connect_tx: Option<mpsc::Sender<()>>,
) -> App {
let mut app = App::new();
app.insert_resource(NetworkSettings {
connection_mode: ConnectionMode::Offline,
callbacks: MotdCallbacks.into(),
callbacks: MotdCallbacks {
state,
player_connect_tx,
}
.into(),
address: config.motd_server_addr.parse().unwrap(),
..Default::default()
})

View File

@@ -1,25 +1,48 @@
use std::error::Error;
use anyhow::Result;
use tokio::io::copy_bidirectional;
use tokio::net::{TcpListener, TcpStream};
pub async fn run_proxy(listen_addr: String, server_addr: String) -> Result<(), Box<dyn Error>> {
use crate::state::{ServerState, SharedServerState};
pub async fn run_proxy(
listen_addr: String,
server_addr: String,
motd_server_addr: String,
state: SharedServerState,
) -> Result<()> {
println!("Listening on {}", listen_addr);
println!("Proxying to {}", server_addr);
println!("Server backend: {}", server_addr);
println!("MOTD backend: {}", motd_server_addr);
let listener = TcpListener::bind(&listen_addr).await?;
while let Ok((mut inbound, _)) = listener.accept().await {
while let Ok((mut inbound, client_addr)) = listener.accept().await {
let server_addr = server_addr.clone();
let motd_server_addr = motd_server_addr.clone();
let state = state.clone();
tokio::spawn(async move {
match TcpStream::connect(&server_addr).await {
let current_state = state.get().await;
let backend = match current_state {
ServerState::Running { .. } => {
println!("Client {} -> server (Running)", client_addr);
server_addr
}
_ => {
println!("Client {} -> MOTD (state: {:?})", client_addr, current_state);
motd_server_addr
}
};
match TcpStream::connect(&backend).await {
Ok(mut outbound) => {
if let Err(e) = copy_bidirectional(&mut inbound, &mut outbound).await {
println!("Failed to transfer; error={e}");
eprintln!("Failed to transfer; error={}", e);
}
}
Err(e) => {
println!("Failed to connect to server; error={e}");
eprintln!("Failed to connect to backend {}; error={}", backend, e);
}
}
});

View File

@@ -1,37 +1,14 @@
use std::error::Error;
use std::time::Duration;
use anyhow::Result;
use rcon::Connection;
use tokio::net::TcpStream;
pub async fn connect_rcon(
addr: &str,
password: &str,
) -> Result<Connection<TcpStream>, Box<dyn Error>> {
) -> Result<Connection<TcpStream>> {
let conn = <Connection<TcpStream>>::builder()
.enable_minecraft_quirks(true)
.connect(addr, password)
.await?;
Ok(conn)
}
pub async fn wait_for_rcon(
addr: &str,
password: &str,
timeout: Duration,
retry_interval: Duration,
) -> Result<Connection<TcpStream>, Box<dyn Error>> {
let start = tokio::time::Instant::now();
loop {
if tokio::time::Instant::now() - start > timeout {
return Err("RCON connection timeout".into());
}
match connect_rcon(addr, password).await {
Ok(conn) => return Ok(conn),
Err(_) => {
tokio::time::sleep(retry_interval).await;
}
}
}
}

109
src/state.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::time::Instant;
#[derive(Debug, Clone, Default)]
pub struct StartupMetrics {
pub total_startups: u32,
pub total_duration_secs: u64,
}
impl StartupMetrics {
pub fn record_startup(&mut self, duration_secs: u64) {
self.total_startups += 1;
self.total_duration_secs += duration_secs;
}
pub fn average_startup_time(&self) -> Option<u64> {
if self.total_startups == 0 {
None
} else {
Some(self.total_duration_secs / self.total_startups as u64)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ServerState {
Unknown,
Stopped,
Starting {
started_at: Instant,
},
Running {
rcon_connected_at: Instant,
},
Stopping {
stop_requested_at: Instant,
},
}
#[derive(Clone)]
pub struct SharedServerState {
state: Arc<RwLock<ServerState>>,
metrics: Arc<RwLock<StartupMetrics>>,
}
impl SharedServerState {
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(ServerState::Unknown)),
metrics: Arc::new(RwLock::new(StartupMetrics::default())),
}
}
pub async fn get(&self) -> ServerState {
self.state.read().await.clone()
}
pub async fn set(&self, new_state: ServerState) {
let mut state = self.state.write().await;
let old_state = state.clone();
*state = new_state.clone();
if old_state != new_state {
println!("State transition: {:?} -> {:?}", old_state, new_state);
}
}
pub async fn transition_to_starting(&self) {
self.set(ServerState::Starting {
started_at: Instant::now(),
}).await;
}
pub async fn transition_to_running(&self) {
self.set(ServerState::Running {
rcon_connected_at: Instant::now(),
}).await;
}
pub async fn transition_to_stopping(&self) {
self.set(ServerState::Stopping {
stop_requested_at: Instant::now(),
}).await;
}
pub async fn transition_to_stopped(&self) {
self.set(ServerState::Stopped).await;
}
pub async fn record_startup_duration(&self, duration_secs: u64) {
let mut metrics = self.metrics.write().await;
metrics.record_startup(duration_secs);
println!("Server startup took {}s (avg: {}s over {} startups)",
duration_secs,
metrics.average_startup_time().unwrap_or(0),
metrics.total_startups);
}
pub async fn get_average_startup_time(&self) -> Option<u64> {
self.metrics.read().await.average_startup_time()
}
}
impl Default for SharedServerState {
fn default() -> Self {
Self::new()
}
}