From f4ffc0208a674aa43e2ed71cf957daebbcbbfc3d Mon Sep 17 00:00:00 2001 From: ceddicedced Date: Sat, 26 Oct 2024 00:37:59 +0200 Subject: [PATCH 1/4] Update dependencies and add stdin service --- Cargo.toml | 1 + src/service/mod.rs | 1 + src/service/server.rs | 3 +++ src/service/stdin.rs | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 src/service/stdin.rs diff --git a/Cargo.toml b/Cargo.toml index 4a80820..98ce8b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ tokio = { version = "1", default-features = false, features = [ "signal", "sync", "fs", + "io-std", ] } toml = "0.8" version-compare = "0.2" diff --git a/src/service/mod.rs b/src/service/mod.rs index 3bc2c0f..151ea97 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -3,3 +3,4 @@ pub mod monitor; pub mod probe; pub mod server; pub mod signal; +pub mod stdin; \ No newline at end of file diff --git a/src/service/server.rs b/src/service/server.rs index 8192c6e..99f8eb0 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -67,6 +67,9 @@ pub async fn service(config: Arc) -> Result<(), ()> { route(inbound, config.clone(), server.clone()); } + // Spawn service to redirect stdin to server + tokio::spawn(service::stdin::service(config.clone(), server.clone())); + Ok(()) } diff --git a/src/service/stdin.rs b/src/service/stdin.rs new file mode 100644 index 0000000..e5653ef --- /dev/null +++ b/src/service/stdin.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; +use tokio::io::{self, AsyncBufReadExt, BufReader}; +use crate::config::Config; +use crate::server::Server; + +#[cfg(feature = "rcon")] +use crate::mc::rcon::Rcon; + +/// Service to read terminal input and send it to the server via RCON or signal handling. +pub async fn service(config: Arc, _server: Arc) { + // Use `tokio::io::stdin` for asynchronous standard input handling + let stdin = io::stdin(); + let mut reader = BufReader::new(stdin).lines(); + + while let Ok(Some(line)) = reader.next_line().await { + if line.trim().is_empty() { + continue; + } + + // Attempt to send via RCON if enabled + #[cfg(feature = "rcon")] + if config.rcon.enabled { + if let Err(err) = send_rcon_command(&line, &config).await { + eprintln!("Failed to send command via RCON: {}", err); + } + } else { + // Handle other cases if RCON is not enabled + eprintln!("RCON not enabled; alternative handling for command: {}", line); + } + } +} + +#[cfg(feature = "rcon")] +async fn send_rcon_command(command: &str, config: &Config) -> Result<(), Box> { + let mut rcon = Rcon::connect_config(config).await?; + rcon.cmd(command).await?; + rcon.close().await; + Ok(()) +} From fd4877956e180dcc2c8ed7d5b30c9177750983a1 Mon Sep 17 00:00:00 2001 From: ceddicedced Date: Sat, 26 Oct 2024 02:10:18 +0200 Subject: [PATCH 2/4] Refactor server code to improve signal handling and stdin service --- src/service/server.rs | 5 +++-- src/service/signal.rs | 33 +++++++++++++++++++------------ src/service/stdin.rs | 46 +++++++++++++++++++++++-------------------- 3 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/service/server.rs b/src/service/server.rs index 99f8eb0..605029b 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -62,13 +62,14 @@ pub async fn service(config: Arc) -> Result<(), ()> { || service::file_watcher::service(config, server) }); + // Spawn service to redirect stdin to server + tokio::spawn(service::stdin::service(config.clone(), server.clone())); + // Route all incomming connections while let Ok((inbound, _)) = listener.accept().await { route(inbound, config.clone(), server.clone()); } - // Spawn service to redirect stdin to server - tokio::spawn(service::stdin::service(config.clone(), server.clone())); Ok(()) } diff --git a/src/service/signal.rs b/src/service/signal.rs index 6512da0..d3eb41b 100644 --- a/src/service/signal.rs +++ b/src/service/signal.rs @@ -1,31 +1,38 @@ use std::sync::Arc; +use tokio::signal; use crate::config::Config; use crate::server::{self, Server}; use crate::util::error; -/// Signal handler task. +/// Main signal handler task. pub async fn service(config: Arc, server: Arc) { loop { // Wait for SIGTERM/SIGINT signal - tokio::signal::ctrl_c().await.unwrap(); + signal::ctrl_c().await.unwrap(); + + // Call the shutdown function + shutdown(&config, &server).await; + } +} - // Quit if stopped - if server.state() == server::State::Stopped { - quit(); - } +/// Shutdown the server gracefully, can be called from other modules. +pub async fn shutdown(config: &Arc, server: &Arc) { + // Quit immediately if the server is already stopped + if server.state() == server::State::Stopped { + quit(); + } - // Try to stop server - let stopping = server.stop(&config).await; + // Try to stop the server gracefully + let stopping = server.stop(config).await; - // If not stopping, maybe due to failure, just quit - if !stopping { - quit(); - } + // If stopping fails, quit immediately + if !stopping { + quit(); } } -/// Gracefully quit. +/// Gracefully quit the application. fn quit() -> ! { // TODO: gracefully quit self error::quit(); diff --git a/src/service/stdin.rs b/src/service/stdin.rs index e5653ef..c9a3957 100644 --- a/src/service/stdin.rs +++ b/src/service/stdin.rs @@ -2,38 +2,42 @@ use std::sync::Arc; use tokio::io::{self, AsyncBufReadExt, BufReader}; use crate::config::Config; use crate::server::Server; - -#[cfg(feature = "rcon")] -use crate::mc::rcon::Rcon; +use crate::service::signal::shutdown; /// Service to read terminal input and send it to the server via RCON or signal handling. -pub async fn service(config: Arc, _server: Arc) { +pub async fn service(config: Arc, server: Arc) { // Use `tokio::io::stdin` for asynchronous standard input handling let stdin = io::stdin(); let mut reader = BufReader::new(stdin).lines(); while let Ok(Some(line)) = reader.next_line().await { - if line.trim().is_empty() { + let trimmed_line = line.trim(); + if trimmed_line.is_empty() { + continue; + } + + // Check for quit command + if trimmed_line.eq_ignore_ascii_case("!quit") || trimmed_line.eq_ignore_ascii_case("!exit") { + info!("Received quit command"); + // Gracefully shutdown + shutdown(&config, &server).await; + break; + } + + // If !start command, start the server + if trimmed_line.eq_ignore_ascii_case("!start") { + info!("Received start command"); + Server::start(config.clone(), server.clone(), None).await; continue; } - // Attempt to send via RCON if enabled - #[cfg(feature = "rcon")] - if config.rcon.enabled { - if let Err(err) = send_rcon_command(&line, &config).await { - eprintln!("Failed to send command via RCON: {}", err); - } - } else { - // Handle other cases if RCON is not enabled - eprintln!("RCON not enabled; alternative handling for command: {}", line); + // If !stop command, stop the server + if trimmed_line.eq_ignore_ascii_case("!stop") { + info!("Received stop command"); + server.stop(&config).await; + continue; } + } } -#[cfg(feature = "rcon")] -async fn send_rcon_command(command: &str, config: &Config) -> Result<(), Box> { - let mut rcon = Rcon::connect_config(config).await?; - rcon.cmd(command).await?; - rcon.close().await; - Ok(()) -} From 78ccb366fd18d55eebcf33dd9a1a5efad7d017bd Mon Sep 17 00:00:00 2001 From: ceddicedced Date: Sat, 26 Oct 2024 17:04:29 +0200 Subject: [PATCH 3/4] Refactor server code to improve signal handling and stdin service --- src/server.rs | 30 ++++++++++++++++++++++++++++ src/service/stdin.rs | 47 ++++++++++++++++++++++++-------------------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/server.rs b/src/server.rs index 7748fda..ace6a17 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,10 +1,12 @@ use std::net::IpAddr; +use std::process::Stdio; use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use futures::FutureExt; use minecraft_protocol::version::v1_20_3::status::ServerStatus; +use tokio::io::AsyncWriteExt; use tokio::process::Command; use tokio::sync::watch; #[cfg(feature = "rcon")] @@ -54,6 +56,11 @@ pub struct Server { /// Set if a server process is running. pid: Mutex>, + /// Server process stdin. + /// + /// Handle to write to the server process. + stdin: Mutex>, + /// Last known server status. /// /// Will remain set once known, not cleared if server goes offline. @@ -394,6 +401,20 @@ impl Server { pub fn set_whitelist_blocking(&self, whitelist: Option) { futures::executor::block_on(async { self.set_whitelist(whitelist).await }) } + + /// Sends a command to the Minecraft server's stdin. + pub async fn send_command(&self, command: &str) -> Result<(), Box> { + // Lock the stdin handle + let mut stdin = self.stdin.lock().await; + // Check if stdin is available and send the command + if let Some(ref mut stdin_handle) = *stdin { + stdin_handle.write_all(command.as_bytes()).await?; + Ok(()) + } else { + Err("Server stdin handle is not available".into()) + } + + } } impl Default for Server { @@ -405,6 +426,7 @@ impl Default for Server { state_watch_sender, state_watch_receiver, pid: Default::default(), + stdin: Default::default(), status: Default::default(), last_active: Default::default(), keep_online_until: Default::default(), @@ -470,6 +492,7 @@ pub async fn invoke_server_cmd( let mut cmd = Command::new(&args[0]); cmd.args(args.iter().skip(1)); cmd.kill_on_drop(true); + cmd.stdin(Stdio::piped()); // Set working directory if let Some(ref dir) = ConfigServer::server_directory(&config) { @@ -492,6 +515,9 @@ pub async fn invoke_server_cmd( .await .replace(child.id().expect("unknown server PID")); + // Remember stdin + state.stdin.lock().await.replace(child.stdin.take().expect("unknown server stdin")); + // Wait for process to exit, handle status let crashed = match child.wait().await { Ok(status) if status.success() => { @@ -521,6 +547,9 @@ pub async fn invoke_server_cmd( // Forget server PID state.pid.lock().await.take(); + // Forget stdin + state.stdin.lock().await.take(); + // Give server a little more time to quit forgotten threads time::sleep(SERVER_QUIT_COOLDOWN).await; @@ -673,3 +702,4 @@ async fn unfreeze_server_signal(config: &Config, server: &Server) -> bool { true } + diff --git a/src/service/stdin.rs b/src/service/stdin.rs index c9a3957..f66d5dc 100644 --- a/src/service/stdin.rs +++ b/src/service/stdin.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::server::Server; use crate::service::signal::shutdown; -/// Service to read terminal input and send it to the server via RCON or signal handling. +/// Service to read terminal input and send it to the server via piped stdin or RCON handling. pub async fn service(config: Arc, server: Arc) { // Use `tokio::io::stdin` for asynchronous standard input handling let stdin = io::stdin(); @@ -16,28 +16,33 @@ pub async fn service(config: Arc, server: Arc) { continue; } - // Check for quit command - if trimmed_line.eq_ignore_ascii_case("!quit") || trimmed_line.eq_ignore_ascii_case("!exit") { - info!("Received quit command"); - // Gracefully shutdown - shutdown(&config, &server).await; - break; - } + match trimmed_line.to_ascii_lowercase().as_str() { + // Quit command + "!quit" | "!exit" => { + info!("Received quit command"); + shutdown(&config, &server).await; + break; + } - // If !start command, start the server - if trimmed_line.eq_ignore_ascii_case("!start") { - info!("Received start command"); - Server::start(config.clone(), server.clone(), None).await; - continue; - } + // Start the server + "!start" => { + info!("Received start command"); + Server::start(config.clone(), server.clone(), None).await; + } - // If !stop command, stop the server - if trimmed_line.eq_ignore_ascii_case("!stop") { - info!("Received stop command"); - server.stop(&config).await; - continue; + // Stop the server + "!stop" => { + info!("Received stop command"); + server.stop(&config).await; + } + + // Any other command is sent to the Minecraft server's stdin + command => { + info!("Sending command to Minecraft server: {}", command); + if let Err(e) = server.send_command(command).await { + eprintln!("Failed to send command to server: {}", e); + } + } } - } } - From 0bac28b6134fb70c5fb0d107ce6c2ae04bd1550b Mon Sep 17 00:00:00 2001 From: ceddicedced Date: Sat, 26 Oct 2024 17:32:24 +0200 Subject: [PATCH 4/4] Refactor server code to append newline character to command before writing to stdin --- src/server.rs | 2 +- src/service/stdin.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server.rs b/src/server.rs index ace6a17..ae1bfbe 100644 --- a/src/server.rs +++ b/src/server.rs @@ -408,7 +408,7 @@ impl Server { let mut stdin = self.stdin.lock().await; // Check if stdin is available and send the command if let Some(ref mut stdin_handle) = *stdin { - stdin_handle.write_all(command.as_bytes()).await?; + stdin_handle.write_all(format!("{}\n", command).as_bytes()).await?; Ok(()) } else { Err("Server stdin handle is not available".into()) diff --git a/src/service/stdin.rs b/src/service/stdin.rs index f66d5dc..a3401a6 100644 --- a/src/service/stdin.rs +++ b/src/service/stdin.rs @@ -3,6 +3,7 @@ use tokio::io::{self, AsyncBufReadExt, BufReader}; use crate::config::Config; use crate::server::Server; use crate::service::signal::shutdown; +use crate::util::error; /// Service to read terminal input and send it to the server via piped stdin or RCON handling. pub async fn service(config: Arc, server: Arc) { @@ -21,7 +22,7 @@ pub async fn service(config: Arc, server: Arc) { "!quit" | "!exit" => { info!("Received quit command"); shutdown(&config, &server).await; - break; + error::quit(); } // Start the server @@ -38,11 +39,10 @@ pub async fn service(config: Arc, server: Arc) { // Any other command is sent to the Minecraft server's stdin command => { - info!("Sending command to Minecraft server: {}", command); if let Err(e) = server.send_command(command).await { eprintln!("Failed to send command to server: {}", e); } } } } -} +} \ No newline at end of file