From 2026b6c846de743e9420625b23f9a94320336f80 Mon Sep 17 00:00:00 2001 From: timvisee Date: Wed, 10 Nov 2021 23:07:19 +0100 Subject: [PATCH] Add ping as fall back method for server status monitor --- Cargo.toml | 4 +- src/monitor.rs | 122 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d29dec..aa01618 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ edition = "2021" [features] default = ["rcon"] -rcon = ["rust_rcon", "rand"] +rcon = ["rust_rcon"] [dependencies] anyhow = "1.0" @@ -34,6 +34,7 @@ libc = "0.2" log = "0.4" minecraft-protocol = { git = "https://github.com/timvisee/minecraft-protocol", rev = "31041b8" } pretty_env_logger = "0.4" +rand = "0.8" serde = "1.0" shlex = "1.1" thiserror = "1.0" @@ -41,5 +42,4 @@ tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-t toml = "0.5" # Feature: rcon -rand = { version = "0.8", optional = true } rust_rcon = { package = "rcon", version = "0.5", optional = true } diff --git a/src/monitor.rs b/src/monitor.rs index b076e67..9b96aa4 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -9,49 +9,82 @@ use minecraft_protocol::data::server_status::ServerStatus; use minecraft_protocol::decoder::Decoder; use minecraft_protocol::encoder::Encoder; use minecraft_protocol::version::v1_14_4::handshake::Handshake; -use minecraft_protocol::version::v1_14_4::status::StatusResponse; +use minecraft_protocol::version::v1_14_4::status::{PingRequest, PingResponse, StatusResponse}; +use rand::Rng; use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; +use tokio::time; use crate::config::Config; use crate::proto::{self, ClientState, RawPacket}; -use crate::server::Server; +use crate::server::{Server, State}; /// Monitor ping inverval in seconds. -const MONITOR_PING_INTERVAL: u64 = 2; +const MONITOR_POLL_INTERVAL: Duration = Duration::from_secs(2); /// Status request timeout in seconds. const STATUS_TIMEOUT: u64 = 8; +/// Ping request timeout in seconds. +const PING_TIMEOUT: u64 = 10; + /// Monitor server. -pub async fn monitor_server(config: Arc, state: Arc) { +pub async fn monitor_server(config: Arc, server: Arc) { // Server address let addr = config.server.address; + let mut poll_interval = time::interval(MONITOR_POLL_INTERVAL); + loop { // Poll server state and update internal status trace!(target: "lazymc::monitor", "Fetching status for {} ... ", addr); - let status = poll_server(&config, addr).await; - state.update_status(&config, status); + let status = poll_server(&config, &server, addr).await; + + match status { + // Got status, update + Ok(Some(status)) => server.update_status(&config, Some(status)), + + // Error, reset status + Err(_) => server.update_status(&config, None), + + // Didn't get status, but ping fallback worked, leave as-is, show warning + Ok(None) => { + warn!(target: "lazymc::monitor", "Failed to poll server status, ping fallback succeeded"); + } + } // Sleep server when it's bedtime - if state.should_sleep(&config) { + if server.should_sleep(&config) { info!(target: "lazymc::montior", "Server has been idle, sleeping..."); - if !state.stop(&config).await { + if !server.stop(&config).await { warn!(target: "lazymc", "Failed to stop server"); } } - // TODO: use interval instead, for a more reliable polling interval? - tokio::time::sleep(Duration::from_secs(MONITOR_PING_INTERVAL)).await; + poll_interval.tick().await; } } /// Poll server state. /// -/// Returns server status if connection succeeded. -pub async fn poll_server(config: &Config, addr: SocketAddr) -> Option { - fetch_status(config, addr).await.ok() +/// Returns `Ok` if status/ping succeeded, includes server status most of the time. +/// Returns `Err` if no connection could be established or if an error occurred. +pub async fn poll_server( + config: &Config, + server: &Server, + addr: SocketAddr, +) -> Result, ()> { + // Fetch status + if let Ok(status) = fetch_status(config, addr).await { + return Ok(Some(status)); + } + + // Try ping fallback if server is currently started + if server.state() == State::Started { + do_ping(config, addr).await?; + } + + Err(()) } /// Attemp to fetch status from server. @@ -63,6 +96,15 @@ async fn fetch_status(config: &Config, addr: SocketAddr) -> Result Result<(), ()> { + let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?; + + send_handshake(&mut stream, &config, addr).await?; + let token = send_ping(&mut stream).await?; + wait_for_ping_timeout(&mut stream, token).await +} + /// Send handshake. async fn send_handshake( stream: &mut TcpStream, @@ -96,6 +138,21 @@ async fn request_status(stream: &mut TcpStream) -> Result<(), ()> { Ok(()) } +/// Send status request. +async fn send_ping(stream: &mut TcpStream) -> Result { + let token = rand::thread_rng().gen(); + let ping = PingRequest { time: token }; + + let mut packet = Vec::new(); + ping.encode(&mut packet).map_err(|_| ())?; + + let raw = RawPacket::new(proto::STATUS_PACKET_ID_PING, packet) + .encode() + .map_err(|_| ())?; + stream.write_all(&raw).await.map_err(|_| ())?; + Ok(token) +} + /// Wait for a status response. async fn wait_for_status(stream: &mut TcpStream) -> Result { // Get stream reader, set up buffer @@ -128,3 +185,42 @@ async fn wait_for_status_timeout(stream: &mut TcpStream) -> Result Result<(), ()> { + // Get stream reader, set up buffer + let (mut reader, mut _writer) = stream.split(); + let mut buf = BytesMut::new(); + + loop { + // Read packet from stream + let (packet, _raw) = match proto::read_packet(&mut buf, &mut reader).await { + Ok(Some(packet)) => packet, + Ok(None) => break, + Err(_) => continue, + }; + + // Catch ping response + if packet.id == proto::STATUS_PACKET_ID_PING { + let ping = PingResponse::decode(&mut packet.data.as_slice()).map_err(|_| ())?; + + // Ping token must match + if ping.time == token { + return Ok(()); + } else { + debug!(target: "lazymc", "Got unmatched ping response when polling server status by ping"); + } + } + } + + // Some error occurred + Err(()) +} + +/// Wait for a status response. +async fn wait_for_ping_timeout(stream: &mut TcpStream, token: u64) -> Result<(), ()> { + let status = wait_for_ping(stream, token); + tokio::time::timeout(Duration::from_secs(PING_TIMEOUT), status) + .await + .map_err(|_| ())? +}