use crate::backup::Manager as BackupManager; use crate::server::ServerProcess; use clap::ValueEnum; use serde::{Deserialize, Serialize}; use std::fmt; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] pub enum ServerType { Unknown, Paper, Forge, Vanilla, } impl fmt::Display for ServerType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { ServerType::Unknown => "Unknown", ServerType::Paper => "PaperMC", ServerType::Forge => "Forge", ServerType::Vanilla => "Vanilla", }; write!(f, "{}", s) } } #[derive(Clone, Serialize, Deserialize)] pub struct Metadata { pub server_type: ServerType, pub server_version: String, } pub struct ServerCommand { type_: ServerType, version: String, java: String, jar: PathBuf, config_dir: PathBuf, world_dir: PathBuf, backup_dir: PathBuf, xms: u64, xmx: u64, chain_len: u64, chains_to_keep: u64, } impl ServerCommand { pub fn new(type_: ServerType, version: &str) -> Self { ServerCommand { type_, version: String::from(version), java: String::from("java"), jar: PathBuf::from("server.jar"), config_dir: PathBuf::from("config"), world_dir: PathBuf::from("worlds"), backup_dir: PathBuf::from("backups"), xms: 1024, xmx: 2048, chain_len: 4, chains_to_keep: 7, } } pub fn java(mut self, java: &str) -> Self { self.java = String::from(java); self } pub fn jar>(mut self, path: T) -> Self { self.jar = PathBuf::from(path.as_ref()); self } pub fn config>(mut self, path: T) -> Self { self.config_dir = PathBuf::from(path.as_ref()); self } pub fn world>(mut self, path: T) -> Self { self.world_dir = PathBuf::from(path.as_ref()); self } pub fn backup>(mut self, path: T) -> Self { self.backup_dir = PathBuf::from(path.as_ref()); self } pub fn xms(mut self, v: u64) -> Self { self.xms = v; self } pub fn xmx(mut self, v: u64) -> Self { self.xmx = v; self } pub fn chain_len(mut self, v: u64) -> Self { self.chain_len = v; self } pub fn chains_to_keep(mut self, v: u64) -> Self { self.chains_to_keep = v; self } fn accept_eula(&self) -> std::io::Result<()> { let mut eula_path = self.config_dir.clone(); eula_path.push("eula.txt"); let mut eula_file = File::create(eula_path)?; eula_file.write_all(b"eula=true")?; Ok(()) } /// Canonicalize all paths to absolute paths. Without this command, all paths will be /// interpreted relatively from the config directory. pub fn canonicalize(&mut self) -> std::io::Result<()> { // To avoid any issues, we use absolute paths for everything when spawning the process self.jar = self.jar.canonicalize()?; self.config_dir = self.config_dir.canonicalize()?; self.world_dir = self.world_dir.canonicalize()?; self.backup_dir = self.backup_dir.canonicalize()?; Ok(()) } fn create_cmd(&self) -> std::process::Command { let mut cmd = Command::new(&self.java); // Apply JVM optimisation flags // https://aikar.co/2018/07/02/tuning-the-jvm-g1gc-garbage-collector-flags-for-minecraft/ cmd.arg(format!("-Xms{}M", self.xms)) .arg(format!("-Xmx{}M", self.xmx)) .args([ "-XX:+UseG1GC", "-XX:+ParallelRefProcEnabled", "-XX:MaxGCPauseMillis=200", "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch", ]); if self.xms > 12 * 1024 { cmd.args([ "-XX:G1NewSizePercent=40", "-XX:G1MaxNewSizePercent=50", "-XX:G1HeapRegionSize=16M", "-XX:G1ReservePercent=15", ]); } else { cmd.args([ "-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=40", "-XX:G1HeapRegionSize=8M", "-XX:G1ReservePercent=20", ]); } cmd.args(["-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=4"]); if self.xms > 12 * 1024 { cmd.args(["-XX:InitiatingHeapOccupancyPercent=20"]); } else { cmd.args(["-XX:InitiatingHeapOccupancyPercent=15"]); } cmd.args([ "-XX:G1MixedGCLiveThresholdPercent=90", "-XX:G1RSetUpdatingPauseTimePercent=5", "-XX:SurvivorRatio=32", "-XX:+PerfDisableSharedMem", "-XX:MaxTenuringThreshold=1", "-Dusing.aikars.flags=https://mcflags.emc.gs", "-Daikars.new.flags=true", ]); cmd.current_dir(&self.config_dir) .arg("-jar") .arg(&self.jar) .arg("--universe") .arg(&self.world_dir) .arg("--nogui") .stdin(Stdio::piped()); cmd } pub fn spawn(&mut self) -> std::io::Result { let metadata = Metadata { server_type: self.type_, server_version: self.version.clone(), }; let mut manager = BackupManager::new( self.backup_dir.clone(), self.config_dir.clone(), self.world_dir.clone(), metadata, self.chain_len, self.chains_to_keep, ); manager.load()?; let mut cmd = self.create_cmd(); self.accept_eula()?; let child = cmd.spawn()?; Ok(ServerProcess::new(manager, child)) } } impl fmt::Display for ServerCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let cmd = self.create_cmd(); writeln!(f, "Command: {}", self.java)?; writeln!(f, "Working dir: {}", self.config_dir.as_path().display())?; // Print command env vars writeln!(f, "Environment:")?; for (key, val) in cmd.get_envs().filter(|(_, v)| v.is_some()) { let val = val.unwrap(); writeln!(f, " {}={}", key.to_string_lossy(), val.to_string_lossy())?; } // Print command arguments writeln!(f, "Arguments:")?; for arg in cmd.get_args() { writeln!(f, " {}", arg.to_string_lossy())?; } Ok(()) } }