670 lines
20 KiB
Rust
670 lines
20 KiB
Rust
use std::net::IpAddr;
|
|
use std::sync::atomic::{AtomicU8, Ordering};
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use futures::FutureExt;
|
|
use minecraft_protocol::data::server_status::ServerStatus;
|
|
use tokio::process::Command;
|
|
use tokio::sync::watch;
|
|
#[cfg(feature = "rcon")]
|
|
use tokio::sync::Semaphore;
|
|
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
|
use tokio::time;
|
|
|
|
use crate::config::{Config, Server as ConfigServer};
|
|
use crate::mc::ban::{BannedIp, BannedIps};
|
|
use crate::mc::whitelist::Whitelist;
|
|
use crate::os;
|
|
use crate::proto::packets::play::join_game::JoinGameData;
|
|
|
|
/// Server cooldown after the process quit.
|
|
/// Used to give it some more time to quit forgotten threads, such as for RCON.
|
|
const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);
|
|
|
|
/// RCON cooldown. Required period between RCON invocations.
|
|
///
|
|
/// The Minecraft RCON implementation is very broken and brittle, this is used in the hopes to
|
|
/// improve reliability.
|
|
#[cfg(feature = "rcon")]
|
|
const RCON_COOLDOWN: Duration = Duration::from_secs(15);
|
|
|
|
/// Exit code when SIGTERM is received on Unix.
|
|
#[cfg(unix)]
|
|
const UNIX_EXIT_SIGTERM: i32 = 130;
|
|
|
|
/// Shared server state.
|
|
#[derive(Debug)]
|
|
pub struct Server {
|
|
/// Server state.
|
|
///
|
|
/// Matches `State`, utilzes AtomicU8 for better performance.
|
|
state: AtomicU8,
|
|
|
|
/// State watch sender, broadcast state changes.
|
|
state_watch_sender: watch::Sender<State>,
|
|
|
|
/// State watch receiver, subscribe to state changes.
|
|
state_watch_receiver: watch::Receiver<State>,
|
|
|
|
/// Server process PID.
|
|
///
|
|
/// Set if a server process is running.
|
|
pid: Mutex<Option<u32>>,
|
|
|
|
/// Last known server status.
|
|
///
|
|
/// Will remain set once known, not cleared if server goes offline.
|
|
status: RwLock<Option<ServerStatus>>,
|
|
|
|
/// Last active time.
|
|
///
|
|
/// The last time there was activity on the server. Also set at the moment the server comes
|
|
/// online.
|
|
last_active: RwLock<Option<Instant>>,
|
|
|
|
/// Force server to stay online until.
|
|
keep_online_until: RwLock<Option<Instant>>,
|
|
|
|
/// Time to force kill the server process at.
|
|
///
|
|
/// Used as starting/stopping timeout.
|
|
kill_at: RwLock<Option<Instant>>,
|
|
|
|
/// List of banned IPs.
|
|
banned_ips: RwLock<BannedIps>,
|
|
|
|
/// Whitelist if enabled.
|
|
whitelist: RwLock<Option<Whitelist>>,
|
|
|
|
/// Lock for exclusive RCON operations.
|
|
#[cfg(feature = "rcon")]
|
|
rcon_lock: Semaphore,
|
|
|
|
/// Last time server was stopped over RCON.
|
|
#[cfg(feature = "rcon")]
|
|
rcon_last_stop: Mutex<Option<Instant>>,
|
|
|
|
/// Probed join game data.
|
|
pub probed_join_game: RwLock<Option<JoinGameData>>,
|
|
|
|
/// Forge payload.
|
|
///
|
|
/// Sent to clients when they connect to lobby. Recorded from server by probe.
|
|
pub forge_payload: RwLock<Vec<Vec<u8>>>,
|
|
}
|
|
|
|
impl Server {
|
|
/// Get current state.
|
|
pub fn state(&self) -> State {
|
|
State::from_u8(self.state.load(Ordering::Relaxed))
|
|
}
|
|
|
|
/// Get state receiver to subscribe on server state changes.
|
|
pub fn state_receiver(&self) -> watch::Receiver<State> {
|
|
self.state_watch_receiver.clone()
|
|
}
|
|
|
|
/// Set a new state.
|
|
///
|
|
/// This updates various other internal things depending on how the state changes.
|
|
///
|
|
/// Returns false if the state didn't change, in which case nothing happens.
|
|
async fn update_state(&self, state: State, config: &Config) -> bool {
|
|
self.update_state_from(None, state, config).await
|
|
}
|
|
|
|
/// Set new state, from a current state.
|
|
///
|
|
/// This updates various other internal things depending on how the state changes.
|
|
///
|
|
/// Returns false if current state didn't match `from` or if nothing changed.
|
|
async fn update_state_from(&self, from: Option<State>, new: State, config: &Config) -> bool {
|
|
// Atomically swap state to new, return if from doesn't match
|
|
let old = State::from_u8(match from {
|
|
Some(from) => match self.state.compare_exchange(
|
|
from.to_u8(),
|
|
new.to_u8(),
|
|
Ordering::Relaxed,
|
|
Ordering::Relaxed,
|
|
) {
|
|
Ok(old) => old,
|
|
Err(_) => return false,
|
|
},
|
|
None => self.state.swap(new.to_u8(), Ordering::Relaxed),
|
|
});
|
|
|
|
// State must be changed
|
|
if old == new {
|
|
return false;
|
|
}
|
|
|
|
trace!("Change server state from {:?} to {:?}", old, new);
|
|
|
|
// Broadcast change
|
|
let _ = self.state_watch_sender.send(new);
|
|
|
|
// Update kill at time for starting/stopping state
|
|
*self.kill_at.write().await = match new {
|
|
State::Starting if config.server.start_timeout > 0 => {
|
|
Some(Instant::now() + Duration::from_secs(config.server.start_timeout as u64))
|
|
}
|
|
State::Stopping if config.server.stop_timeout > 0 => {
|
|
Some(Instant::now() + Duration::from_secs(config.server.stop_timeout as u64))
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
// Online/offline messages
|
|
match new {
|
|
State::Started => info!(target: "lazymc::monitor", "Server is now online"),
|
|
State::Stopped => info!(target: "lazymc::monitor", "Server is now sleeping"),
|
|
_ => {}
|
|
}
|
|
|
|
// If Starting -> Started, update active time and keep it online for configured time
|
|
if old == State::Starting && new == State::Started {
|
|
self.update_last_active().await;
|
|
self.keep_online_for(Some(config.time.min_online_time))
|
|
.await;
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Update status as obtained from the server.
|
|
///
|
|
/// This updates various other internal things depending on the current state and the given
|
|
/// status.
|
|
pub async fn update_status(&self, config: &Config, status: Option<ServerStatus>) {
|
|
// Update state based on curren
|
|
match (self.state(), &status) {
|
|
(State::Stopped | State::Starting, Some(_)) => {
|
|
self.update_state(State::Started, config).await;
|
|
}
|
|
(State::Started, None) => {
|
|
self.update_state(State::Stopped, config).await;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// Update last status if known
|
|
if let Some(status) = status {
|
|
// Update last active time if there are online players
|
|
if status.players.online > 0 {
|
|
self.update_last_active().await;
|
|
}
|
|
|
|
self.status.write().await.replace(status);
|
|
}
|
|
}
|
|
|
|
/// Try to start the server.
|
|
///
|
|
/// Does nothing if currently not in stopped state.
|
|
pub async fn start(config: Arc<Config>, server: Arc<Server>, username: Option<String>) -> bool {
|
|
// Must set state from stopped to starting
|
|
if !server
|
|
.update_state_from(Some(State::Stopped), State::Starting, &config)
|
|
.await
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Log starting message
|
|
match username {
|
|
Some(username) => info!(target: "lazymc", "Starting server for '{}'...", username),
|
|
None => info!(target: "lazymc", "Starting server..."),
|
|
}
|
|
|
|
// Unfreeze server if it is frozen
|
|
#[cfg(unix)]
|
|
if config.server.freeze_process && unfreeze_server_signal(&config, &server).await {
|
|
return true;
|
|
}
|
|
|
|
// Spawn server in new task
|
|
Self::spawn_server_task(config, server);
|
|
true
|
|
}
|
|
|
|
/// Spawn the server task.
|
|
///
|
|
/// This should not be called directly.
|
|
fn spawn_server_task(config: Arc<Config>, server: Arc<Server>) {
|
|
tokio::spawn(invoke_server_cmd(config, server).map(|_| ()));
|
|
}
|
|
|
|
/// Stop running server.
|
|
///
|
|
/// This will attempt to stop the server with all available methods.
|
|
#[allow(unused_variables)]
|
|
pub async fn stop(&self, config: &Config) -> bool {
|
|
// Try to freeze through signal
|
|
#[cfg(unix)]
|
|
if config.server.freeze_process && freeze_server_signal(config, self).await {
|
|
return true;
|
|
}
|
|
|
|
// Try to stop through RCON if started
|
|
#[cfg(feature = "rcon")]
|
|
if self.state() == State::Started && stop_server_rcon(config, self).await {
|
|
return true;
|
|
}
|
|
|
|
// Try to stop through signal
|
|
#[cfg(unix)]
|
|
if stop_server_signal(config, self).await {
|
|
return true;
|
|
}
|
|
|
|
warn!(target: "lazymc", "Failed to stop server, no more suitable stopping method to use");
|
|
false
|
|
}
|
|
|
|
/// Force kill running server.
|
|
///
|
|
/// This requires the server PID to be known.
|
|
pub async fn force_kill(&self) -> bool {
|
|
if let Some(pid) = *self.pid.lock().await {
|
|
return os::force_kill(pid);
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Decide whether the server should sleep.
|
|
///
|
|
/// Always returns false if it is currently not online.
|
|
pub async fn should_sleep(&self, config: &Config) -> bool {
|
|
// Server must be online
|
|
if self.state() != State::Started {
|
|
return false;
|
|
}
|
|
|
|
// Never sleep if players are online
|
|
let players_online = self
|
|
.status
|
|
.read()
|
|
.await
|
|
.as_ref()
|
|
.map(|status| status.players.online > 0)
|
|
.unwrap_or(false);
|
|
if players_online {
|
|
trace!(target: "lazymc", "Not sleeping because players are online");
|
|
return false;
|
|
}
|
|
|
|
// Don't sleep when keep online until isn't expired
|
|
let keep_online = self
|
|
.keep_online_until
|
|
.read()
|
|
.await
|
|
.map(|i| i >= Instant::now())
|
|
.unwrap_or(false);
|
|
if keep_online {
|
|
trace!(target: "lazymc", "Not sleeping because of keep online");
|
|
return false;
|
|
}
|
|
|
|
// Last active time must have passed sleep threshold
|
|
if let Some(last_idle) = self.last_active.read().await.as_ref() {
|
|
return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64);
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Decide whether to force kill the server process.
|
|
pub async fn should_kill(&self) -> bool {
|
|
self.kill_at
|
|
.read()
|
|
.await
|
|
.map(|t| t <= Instant::now())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Read last known server status.
|
|
pub async fn status(&self) -> RwLockReadGuard<'_, Option<ServerStatus>> {
|
|
self.status.read().await
|
|
}
|
|
|
|
/// Update the last active time.
|
|
async fn update_last_active(&self) {
|
|
self.last_active.write().await.replace(Instant::now());
|
|
}
|
|
|
|
/// Force the server to be online for the given number of seconds.
|
|
async fn keep_online_for(&self, duration: Option<u32>) {
|
|
*self.keep_online_until.write().await = duration
|
|
.filter(|d| *d > 0)
|
|
.map(|d| Instant::now() + Duration::from_secs(d as u64));
|
|
}
|
|
|
|
/// Check whether the given IP is banned.
|
|
///
|
|
/// This uses the latest known `banned-ips.json` contents if known.
|
|
/// If this feature is disabled, this will always return false.
|
|
pub async fn is_banned_ip(&self, ip: &IpAddr) -> bool {
|
|
self.banned_ips.read().await.is_banned(ip)
|
|
}
|
|
|
|
/// Get user ban entry.
|
|
pub async fn ban_entry(&self, ip: &IpAddr) -> Option<BannedIp> {
|
|
self.banned_ips.read().await.get(ip)
|
|
}
|
|
|
|
/// Check whether the given IP is banned.
|
|
///
|
|
/// This uses the latest known `banned-ips.json` contents if known.
|
|
/// If this feature is disabled, this will always return false.
|
|
pub fn is_banned_ip_blocking(&self, ip: &IpAddr) -> bool {
|
|
futures::executor::block_on(async { self.is_banned_ip(ip).await })
|
|
}
|
|
|
|
/// Check whether the given username is whitelisted.
|
|
///
|
|
/// Returns `true` if no whitelist is currently used.
|
|
pub async fn is_whitelisted(&self, username: &str) -> bool {
|
|
self.whitelist
|
|
.read()
|
|
.await
|
|
.as_ref()
|
|
.map(|w| w.is_whitelisted(username))
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
/// Update the list of banned IPs.
|
|
pub async fn set_banned_ips(&self, ips: BannedIps) {
|
|
*self.banned_ips.write().await = ips;
|
|
}
|
|
|
|
/// Update the list of banned IPs.
|
|
pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
|
|
futures::executor::block_on(async { self.set_banned_ips(ips).await })
|
|
}
|
|
|
|
/// Update the whitelist.
|
|
pub async fn set_whitelist(&self, whitelist: Option<Whitelist>) {
|
|
*self.whitelist.write().await = whitelist;
|
|
}
|
|
|
|
/// Update the whitelist.
|
|
pub fn set_whitelist_blocking(&self, whitelist: Option<Whitelist>) {
|
|
futures::executor::block_on(async { self.set_whitelist(whitelist).await })
|
|
}
|
|
}
|
|
|
|
impl Default for Server {
|
|
fn default() -> Self {
|
|
let (state_watch_sender, state_watch_receiver) = watch::channel(State::Stopped);
|
|
|
|
Self {
|
|
state: AtomicU8::new(State::Stopped.to_u8()),
|
|
state_watch_sender,
|
|
state_watch_receiver,
|
|
pid: Default::default(),
|
|
status: Default::default(),
|
|
last_active: Default::default(),
|
|
keep_online_until: Default::default(),
|
|
kill_at: Default::default(),
|
|
banned_ips: Default::default(),
|
|
whitelist: Default::default(),
|
|
#[cfg(feature = "rcon")]
|
|
rcon_lock: Semaphore::new(1),
|
|
#[cfg(feature = "rcon")]
|
|
rcon_last_stop: Default::default(),
|
|
probed_join_game: Default::default(),
|
|
forge_payload: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Server state.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|
pub enum State {
|
|
/// Server is stopped.
|
|
Stopped,
|
|
|
|
/// Server is starting.
|
|
Starting,
|
|
|
|
/// Server is online and responding.
|
|
Started,
|
|
|
|
/// Server is stopping.
|
|
Stopping,
|
|
}
|
|
|
|
impl State {
|
|
/// From u8, panics if invalid.
|
|
pub fn from_u8(state: u8) -> Self {
|
|
match state {
|
|
0 => Self::Stopped,
|
|
1 => Self::Starting,
|
|
2 => Self::Started,
|
|
3 => Self::Stopping,
|
|
_ => panic!("invalid State u8"),
|
|
}
|
|
}
|
|
|
|
/// To u8.
|
|
pub fn to_u8(self) -> u8 {
|
|
match self {
|
|
Self::Stopped => 0,
|
|
Self::Starting => 1,
|
|
Self::Started => 2,
|
|
Self::Stopping => 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Invoke server command, store PID and wait for it to quit.
|
|
pub async fn invoke_server_cmd(
|
|
config: Arc<Config>,
|
|
state: Arc<Server>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Configure command
|
|
let args = shlex::split(&config.server.command).expect("invalid server command");
|
|
let mut cmd = Command::new(&args[0]);
|
|
cmd.args(args.iter().skip(1));
|
|
cmd.kill_on_drop(true);
|
|
|
|
// Set working directory
|
|
if let Some(ref dir) = ConfigServer::server_directory(&config) {
|
|
cmd.current_dir(dir);
|
|
}
|
|
|
|
// Spawn process
|
|
let mut child = match cmd.spawn() {
|
|
Ok(child) => child,
|
|
Err(err) => {
|
|
error!(target: "lazymc", "Failed to start server process through command");
|
|
return Err(err.into());
|
|
}
|
|
};
|
|
|
|
// Remember PID
|
|
state
|
|
.pid
|
|
.lock()
|
|
.await
|
|
.replace(child.id().expect("unknown server PID"));
|
|
|
|
// Wait for process to exit, handle status
|
|
let crashed = match child.wait().await {
|
|
Ok(status) if status.success() => {
|
|
debug!(target: "lazymc", "Server process stopped successfully ({})", status);
|
|
false
|
|
}
|
|
#[cfg(unix)]
|
|
Ok(status) if status.code() == Some(UNIX_EXIT_SIGTERM) => {
|
|
debug!(target: "lazymc", "Server process stopped successfully by SIGTERM ({})", status);
|
|
false
|
|
}
|
|
Ok(status) => {
|
|
warn!(target: "lazymc", "Server process stopped with error code ({})", status);
|
|
state.state() == State::Started
|
|
}
|
|
Err(err) => {
|
|
error!(target: "lazymc", "Failed to wait for server process to quit: {}", err);
|
|
error!(target: "lazymc", "Assuming server quit, cleaning up...");
|
|
false
|
|
}
|
|
};
|
|
|
|
// Forget server PID
|
|
state.pid.lock().await.take();
|
|
|
|
// Give server a little more time to quit forgotten threads
|
|
time::sleep(SERVER_QUIT_COOLDOWN).await;
|
|
|
|
// Set server state to stopped
|
|
state.update_state(State::Stopped, &config).await;
|
|
|
|
// Restart on crash
|
|
if crashed && config.server.wake_on_crash {
|
|
warn!(target: "lazymc", "Server crashed, restarting...");
|
|
Server::start(config, state, None).await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop server through RCON.
|
|
#[cfg(feature = "rcon")]
|
|
async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
|
|
use crate::mc::rcon::Rcon;
|
|
|
|
// RCON must be enabled
|
|
if !config.rcon.enabled {
|
|
trace!(target: "lazymc", "Not using RCON to stop server, disabled in config");
|
|
return false;
|
|
}
|
|
|
|
// Grab RCON lock
|
|
let rcon_lock = server.rcon_lock.acquire().await.unwrap();
|
|
|
|
// Ensure RCON has cooled down
|
|
let rcon_cooled_down = server
|
|
.rcon_last_stop
|
|
.lock()
|
|
.await
|
|
.map(|t| t.elapsed() >= RCON_COOLDOWN)
|
|
.unwrap_or(true);
|
|
if !rcon_cooled_down {
|
|
debug!(target: "lazymc", "Not using RCON to stop server, in cooldown, used too recently");
|
|
return false;
|
|
}
|
|
|
|
// Create RCON client
|
|
let mut rcon = match Rcon::connect_config(config).await {
|
|
Ok(rcon) => rcon,
|
|
Err(err) => {
|
|
error!(target: "lazymc", "Failed to RCON server to sleep: {}", err);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Invoke stop
|
|
if let Err(err) = rcon.cmd("stop").await {
|
|
error!(target: "lazymc", "Failed to invoke stop through RCON: {}", err);
|
|
return false;
|
|
}
|
|
|
|
// Set server to stopping state, update last RCON time
|
|
server.rcon_last_stop.lock().await.replace(Instant::now());
|
|
server.update_state(State::Stopping, config).await;
|
|
|
|
// Gracefully close connection
|
|
rcon.close().await;
|
|
|
|
drop(rcon_lock);
|
|
|
|
true
|
|
}
|
|
|
|
/// Stop server by sending SIGTERM signal.
|
|
///
|
|
/// Only available on Unix.
|
|
#[cfg(unix)]
|
|
async fn stop_server_signal(config: &Config, server: &Server) -> bool {
|
|
// Grab PID
|
|
let pid = match *server.pid.lock().await {
|
|
Some(pid) => pid,
|
|
None => {
|
|
debug!(target: "lazymc", "Could not send stop signal to server process, PID unknown");
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if !crate::os::kill_gracefully(pid) {
|
|
error!(target: "lazymc", "Failed to send stop signal to server process");
|
|
return false;
|
|
}
|
|
|
|
server
|
|
.update_state_from(Some(State::Starting), State::Stopping, config)
|
|
.await;
|
|
server
|
|
.update_state_from(Some(State::Started), State::Stopping, config)
|
|
.await;
|
|
|
|
true
|
|
}
|
|
|
|
/// Freeze server by sending SIGSTOP signal.
|
|
///
|
|
/// Only available on Unix.
|
|
#[cfg(unix)]
|
|
async fn freeze_server_signal(config: &Config, server: &Server) -> bool {
|
|
// Grab PID
|
|
let pid = match *server.pid.lock().await {
|
|
Some(pid) => pid,
|
|
None => {
|
|
debug!(target: "lazymc", "Could not send freeze signal to server process, PID unknown");
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if !os::freeze(pid) {
|
|
error!(target: "lazymc", "Failed to send freeze signal to server process.");
|
|
}
|
|
|
|
server
|
|
.update_state_from(Some(State::Starting), State::Stopped, config)
|
|
.await;
|
|
server
|
|
.update_state_from(Some(State::Started), State::Stopped, config)
|
|
.await;
|
|
|
|
true
|
|
}
|
|
|
|
/// Unfreeze server by sending SIGCONT signal.
|
|
///
|
|
/// Only available on Unix.
|
|
#[cfg(unix)]
|
|
async fn unfreeze_server_signal(config: &Config, server: &Server) -> bool {
|
|
// Grab PID
|
|
let pid = match *server.pid.lock().await {
|
|
Some(pid) => pid,
|
|
None => {
|
|
debug!(target: "lazymc", "Could not send unfreeze signal to server process, PID unknown");
|
|
return false;
|
|
}
|
|
};
|
|
|
|
if !os::unfreeze(pid) {
|
|
error!(target: "lazymc", "Failed to send unfreeze signal to server process.");
|
|
}
|
|
|
|
server
|
|
.update_state_from(Some(State::Stopping), State::Starting, config)
|
|
.await;
|
|
server
|
|
.update_state_from(Some(State::Stopped), State::Starting, config)
|
|
.await;
|
|
|
|
true
|
|
}
|