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/server.rs b/src/server.rs index 7748fda..ae1bfbe 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(format!("{}\n", 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/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..605029b 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -62,11 +62,15 @@ 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()); } + 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 new file mode 100644 index 0000000..a3401a6 --- /dev/null +++ b/src/service/stdin.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; +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) { + // 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 { + let trimmed_line = line.trim(); + if trimmed_line.is_empty() { + continue; + } + + match trimmed_line.to_ascii_lowercase().as_str() { + // Quit command + "!quit" | "!exit" => { + info!("Received quit command"); + shutdown(&config, &server).await; + error::quit(); + } + + // Start the server + "!start" => { + info!("Received start command"); + Server::start(config.clone(), server.clone(), None).await; + } + + // Stop the server + "!stop" => { + info!("Received stop command"); + server.stop(&config).await; + } + + // Any other command is sent to the Minecraft server's stdin + command => { + if let Err(e) = server.send_command(command).await { + eprintln!("Failed to send command to server: {}", e); + } + } + } + } +} \ No newline at end of file