Add basic server idle checking, sleep server if idle for specified time

This commit is contained in:
timvisee 2021-11-08 14:19:52 +01:00
parent 049fce78b7
commit fed541c893
No known key found for this signature in database
GPG Key ID: B8DB720BC383E172
5 changed files with 120 additions and 13 deletions

20
res/start-server Normal file
View File

@ -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

View File

@ -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;

View File

@ -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(|_| ()));
}

View File

@ -25,13 +25,15 @@ const STATUS_TIMEOUT: u64 = 8;
/// Monitor server.
pub async fn monitor_server(addr: SocketAddr, state: Arc<ServerState>) {
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?

View File

@ -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<Option<u32>>,
@ -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<Option<ServerStatus>>,
/// Last active time.
///
/// The last known time when the server was active with online players.
last_active: Mutex<Option<Instant>>,
}
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<ServerStatus>) {
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<ServerState>) -> Result<(), Box<dyn std::error::Er
state.set_pid(None);
state.set_online(false);
state.set_starting(false);
state.stopping.store(false, Ordering::Relaxed);
Ok(())
}
@ -98,9 +172,15 @@ pub async fn start(state: Arc<ServerState>) -> Result<(), Box<dyn std::error::Er
fn kill_gracefully(pid: u32) {
#[cfg(unix)]
unsafe {
libc::kill(pid as i32, libc::SIGINT);
debug!("Sending SIGTERM signal to {} to kill server", pid);
let result = libc::kill(pid as i32, libc::SIGTERM);
trace!("SIGTERM result: {}", result);
// TODO: send sigterm to childs as well?
// TODO: handle error if != 0
}
// TODO: implement for Windows
#[cfg(not(unix))]
{
// TODO: implement this for Windows