diff --git a/res/start-server b/res/start-server new file mode 100644 index 0000000..884a018 --- /dev/null +++ b/res/start-server @@ -0,0 +1,20 @@ +#!/bin/bash + +# Server file +FILE=server.jar + +# Switch to script directory +DIR="$(dirname "$(realpath "$0")")" +cd $DIR + +# Catch SIGTERM to gracefully stop server +trap 'kill -TERM $PID' TERM INT + +# Start server +java -Xms1G -Xmx1G -jar $FILE --nogui & + +# Clean up stopped server +PID=$! +wait $PID +trap - TERM INT +wait $PID diff --git a/src/config.rs b/src/config.rs index 1071670..0b48e4f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,18 +1,21 @@ /// Command to start server. -pub(crate) const SERVER_CMD: &str = "/home/timvisee/git/lazymc/mcserver/start"; +pub const SERVER_CMD: &str = "/home/timvisee/git/lazymc/mcserver/start"; /// Public address for users to connect to. -pub(crate) const ADDRESS_PUBLIC: &str = "127.0.0.1:9090"; +pub const ADDRESS_PUBLIC: &str = "127.0.0.1:9090"; /// Minecraft server address to proxy to. -pub(crate) const ADDRESS_PROXY: &str = "127.0.0.1:9091"; +pub const ADDRESS_PROXY: &str = "127.0.0.1:9091"; /// Server description shown when server is starting. -pub(crate) const LABEL_SERVER_SLEEPING: &str = "☠ Server is sleeping\n§2☻ Join to start it up"; +pub const LABEL_SERVER_SLEEPING: &str = "☠ Server is sleeping\n§2☻ Join to start it up"; /// Server description shown when server is starting. -pub(crate) const LABEL_SERVER_STARTING: &str = "§2☻ Server is starting...\n§7⌛ Please wait..."; +pub const LABEL_SERVER_STARTING: &str = "§2☻ Server is starting...\n§7⌛ Please wait..."; /// Kick message shown when user tries to connect to starting server. -pub(crate) const LABEL_SERVER_STARTING_MESSAGE: &str = +pub const LABEL_SERVER_STARTING_MESSAGE: &str = "Server is starting... §c♥§r\n\nThis may take some time.\n\nPlease try to reconnect in a minute."; + +/// Idle server sleeping delay in seconds. +pub const SLEEP_DELAY: u64 = 10; diff --git a/src/main.rs b/src/main.rs index b79910a..d35d27c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,8 +133,10 @@ async fn serve_status( writer.write_all(&response).await.map_err(|_| ())?; // Start server if not starting yet + // TODO: move this into server state? if !server.starting() { server.set_starting(true); + server.update_last_active_time(); tokio::spawn(server::start(server).map(|_| ())); } diff --git a/src/monitor.rs b/src/monitor.rs index b061556..355be40 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -25,13 +25,15 @@ const STATUS_TIMEOUT: u64 = 8; /// Monitor server. pub async fn monitor_server(addr: SocketAddr, state: Arc) { loop { + // Poll server state and update internal status trace!("Fetching status for {} ... ", addr); let status = poll_server(addr).await; + state.update_status(status); - // Update server state - state.set_online(status.is_some()); - if let Some(status) = status { - state.set_status(status); + // Sleep server when it's bedtime + if state.should_sleep() { + info!("Server has been idle, initiating sleep..."); + state.kill_server(); } // TODO: use interval instead, for a more reliable polling interval? diff --git a/src/server.rs b/src/server.rs index edd7c91..e59af7e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,10 +1,11 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; use minecraft_protocol::data::server_status::ServerStatus; use tokio::process::Command; -use crate::config::SERVER_CMD; +use crate::config::{SERVER_CMD, SLEEP_DELAY}; /// Shared server state. #[derive(Default, Debug)] @@ -13,8 +14,12 @@ pub struct ServerState { online: AtomicBool, /// Whether the server is starting. + // TODO: use enum for starting/started/stopping states starting: AtomicBool, + /// Whether the server is stopping. + stopping: AtomicBool, + /// Server PID. pid: Mutex>, @@ -23,6 +28,11 @@ pub struct ServerState { /// Once set, this will remain set, and isn't cleared when the server goes offline. // TODO: make this private? pub status: Mutex>, + + /// Last active time. + /// + /// The last known time when the server was active with online players. + last_active: Mutex>, } impl ServerState { @@ -49,11 +59,18 @@ impl ServerState { /// Kill any running server. pub fn kill_server(&self) -> bool { if let Some(pid) = *self.pid.lock().unwrap() { - warn!("Sending kill signal to server"); + debug!("Sending kill signal to server"); kill_gracefully(pid); + + // TODO: should we set this? + self.set_online(false); + return true; } + // TODO: set stopping state elsewhere + self.stopping.store(true, Ordering::Relaxed); + false } @@ -71,6 +88,62 @@ impl ServerState { pub fn set_status(&self, status: ServerStatus) { self.status.lock().unwrap().replace(status); } + + /// Update the server status, online state and last active time. + // TODO: clean this up + pub fn update_status(&self, status: Option) { + let stopping = self.stopping.load(Ordering::Relaxed); + let was_online = self.online(); + let online = status.is_some() && !stopping; + self.set_online(online); + + // If server just came online, update last active time + if !was_online && online { + // TODO: move this somewhere else + info!("Server is now online"); + self.update_last_active_time(); + } + + // // If server just went offline, reset stopping state + // // TODO: do this elsewhere + // if stopping && was_online && !online { + // self.stopping.store(false, Ordering::Relaxed); + // } + + if let Some(status) = status { + // Update last active time if there are online players + if status.players.online > 0 { + self.update_last_active_time(); + } + + // Update last known players + self.set_status(status); + } + } + + /// Update the last active time. + pub fn update_last_active_time(&self) { + self.last_active.lock().unwrap().replace(Instant::now()); + } + + /// Check whether the server should now sleep. + pub fn should_sleep(&self) -> bool { + // TODO: when initating server start, set last active time! + // TODO: do not initiate sleep when starting? + // TODO: do not initiate sleep when already initiated (with timeout) + + // Server must be online, and must not be starting + if !self.online() || !self.starting() { + return false; + } + + // Last active time must have passed sleep threshold + if let Some(last_idle) = self.last_active.lock().unwrap().as_ref() { + return last_idle.elapsed() >= Duration::from_secs(SLEEP_DELAY); + } + + false + } } /// Start Minecraft server. @@ -90,6 +163,7 @@ pub async fn start(state: Arc) -> Result<(), Box) -> Result<(), Box