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