Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added stdin command handling for lazymc #81

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ tokio = { version = "1", default-features = false, features = [
"signal",
"sync",
"fs",
"io-std",
] }
toml = "0.8"
version-compare = "0.2"
Expand Down
30 changes: 30 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -54,6 +56,11 @@ pub struct Server {
/// Set if a server process is running.
pid: Mutex<Option<u32>>,

/// Server process stdin.
///
/// Handle to write to the server process.
stdin: Mutex<Option<tokio::process::ChildStdin>>,

/// Last known server status.
///
/// Will remain set once known, not cleared if server goes offline.
Expand Down Expand Up @@ -394,6 +401,20 @@ impl Server {
pub fn set_whitelist_blocking(&self, whitelist: Option<Whitelist>) {
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<dyn std::error::Error>> {
// 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 {
Expand All @@ -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(),
Expand Down Expand Up @@ -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) {
Expand All @@ -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() => {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -673,3 +702,4 @@ async fn unfreeze_server_signal(config: &Config, server: &Server) -> bool {

true
}

1 change: 1 addition & 0 deletions src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod monitor;
pub mod probe;
pub mod server;
pub mod signal;
pub mod stdin;
4 changes: 4 additions & 0 deletions src/service/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,15 @@ pub async fn service(config: Arc<Config>) -> 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(())
}

Expand Down
33 changes: 20 additions & 13 deletions src/service/signal.rs
Original file line number Diff line number Diff line change
@@ -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<Config>, server: Arc<Server>) {
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<Config>, server: &Arc<Server>) {
// 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();
Expand Down
48 changes: 48 additions & 0 deletions src/service/stdin.rs
Original file line number Diff line number Diff line change
@@ -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<Config>, server: Arc<Server>) {
// 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);
}
}
}
}
}