//! Launching and supervising the Minecraft server process. use std::collections::HashMap; use std::io::{self, BufRead, Write}; use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio}; use std::sync::{Arc, Mutex}; use crate::ui; /// Print a process output stream line by line, tagged as `[server]`. fn pump(stream: impl io::Read + Send + 'static) { std::thread::spawn(move || { for line in io::BufReader::new(stream).lines().map_while(Result::ok) { ui::server(&line); } }); } #[derive(Debug, Clone)] pub struct LaunchOptions { pub java: String, pub jar: PathBuf, pub dir: PathBuf, pub memory: Option, pub extra_args: Vec, } pub struct MinecraftServer { child: Arc>, stdin: Arc>, } impl MinecraftServer { pub fn start(opts: &LaunchOptions) -> io::Result { let mut cmd = Command::new(&opts.java); if let Some(mem) = &opts.memory { cmd.arg(format!("-Xms{mem}")).arg(format!("-Xmx{mem}")); } cmd.arg("-jar") .arg(&opts.jar) .arg("nogui") .args(&opts.extra_args) .current_dir(&opts.dir) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let mut child = cmd.spawn()?; let stdin = child.stdin.take().expect("stdin was piped"); Self::pump_stdio(&mut child); Ok(Self { child: Arc::new(Mutex::new(child)), stdin: Arc::new(Mutex::new(stdin)), }) } /// Print everything the server writes, line by line. fn pump_stdio(child: &mut Child) { if let Some(stdout) = child.stdout.take() { pump(stdout); } if let Some(stderr) = child.stderr.take() { pump(stderr); } } /// Send a line to the Minecraft server console. pub fn send_line(&self, line: &str) { if let Ok(mut stdin) = self.stdin.lock() { let _ = stdin.write_all(format!("{line}\n").as_bytes()); let _ = stdin.flush(); } } pub fn try_wait(&self) -> io::Result> { self.child.lock().unwrap().try_wait() } /// Forcefully terminate the server (escalation path for a stuck shutdown). pub fn kill(&self) { if let Ok(mut child) = self.child.lock() { let _ = child.kill(); let _ = child.wait(); } } } /// Read `server.properties` from the server directory, if present. pub fn read_server_properties(dir: &Path) -> HashMap { let mut props = HashMap::new(); let Ok(text) = std::fs::read_to_string(dir.join("server.properties")) else { return props; }; for line in text.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } if let Some((key, value)) = line.split_once('=') { props.insert(key.trim().to_string(), value.trim().to_string()); } } props } /// Make sure eula.txt exists and (optionally) accepts the Minecraft EULA. pub fn ensure_eula(dir: &Path, accept: bool) { let path = dir.join("eula.txt"); let existing = std::fs::read_to_string(&path).unwrap_or_default(); if existing.lines().any(|l| l.trim() == "eula=true") { return; } if !accept { ui::warn(&format!( "Minecraft EULA not accepted ({}). The server will exit immediately; \ re-run with --eula to accept it.", path.display() )); return; } let patched: String = existing .lines() .map(|l| { if l.trim() == "eula=false" { "eula=true".to_string() } else { l.to_string() } }) .collect::>() .join("\n"); let content = if patched.contains("eula=true") { format!("{patched}\n") } else { format!("{patched}eula=true\n") }; match std::fs::write(&path, content) { Ok(()) => ui::ok(&format!( "Accepted the Minecraft EULA ({}). By running --eula you agree to \ https://aka.ms/MinecraftEULA", path.display() )), Err(e) => ui::warn(&format!("could not write {}: {e}", path.display())), } } /// Pick a jar to run when the user did not name one. pub fn auto_detect_jar(dir: &Path) -> Option { let mut jars: Vec<(std::time::SystemTime, PathBuf)> = std::fs::read_dir(dir) .ok()? .filter_map(|entry| { let entry = entry.ok()?; let path = entry.path(); if path.extension()?.to_str()? != "jar" { return None; } let modified = entry.metadata().ok()?.modified().ok()?; Some((modified, path)) }) .collect(); jars.sort_by_key(|(modified, _)| std::cmp::Reverse(*modified)); // Prefer jars that look like server jars, then any jar. jars.iter() .find(|(_, p)| name_looks_like_server_jar(p)) .or_else(|| jars.first()) .map(|(_, p)| p.clone()) } fn name_looks_like_server_jar(path: &Path) -> bool { let name = path .file_name() .and_then(|n| n.to_str()) .unwrap_or_default() .to_ascii_lowercase(); let server_side = [ "server", "paper", "spigot", "purpur", "velocity", "pufferfish", ]; let client_side = ["client", "sources", "javadoc", "dev bundle"]; server_side.iter().any(|k| name.contains(k)) && !client_side.iter().any(|k| name.contains(k)) }