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 watch receiver, subscribe to state changes. state_watch_receiver: watch::Receiver, /// Server process PID. /// /// Set if a server process is running. pid: Mutex>, /// Last known server status. /// /// Will remain set once known, not cleared if server goes offline. status: RwLock>, /// Last active time. /// /// The last time there was activity on the server. Also set at the moment the server comes /// online. last_active: RwLock>, /// Force server to stay online until. keep_online_until: RwLock>, /// Time to force kill the server process at. /// /// Used as starting/stopping timeout. kill_at: RwLock>, /// List of banned IPs. banned_ips: RwLock, /// Whitelist if enabled. whitelist: RwLock>, /// 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>, /// Probed join game data. pub probed_join_game: RwLock>, /// Forge payload. /// /// Sent to clients when they connect to lobby. Recorded from server by probe. pub forge_payload: RwLock>>, } 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 { 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, 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) { // 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, server: Arc, username: Option) -> 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, server: Arc) { 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> { 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) { *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 { 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) { *self.whitelist.write().await = whitelist; } /// Update the whitelist. pub fn set_whitelist_blocking(&self, whitelist: Option) { 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, state: Arc, ) -> Result<(), Box> { // 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 }