diff --git a/Cargo.lock b/Cargo.lock index 4f54161..d19496c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,7 +491,7 @@ dependencies = [ [[package]] name = "coa-core" -version = "0.6.11" +version = "0.6.12" dependencies = [ "base64 0.22.1", "chrono", @@ -520,7 +520,7 @@ dependencies = [ [[package]] name = "coa-release" -version = "0.6.11" +version = "0.6.12" dependencies = [ "chrono", "coa-core", @@ -529,7 +529,7 @@ dependencies = [ [[package]] name = "coa-server-manager" -version = "0.6.11" +version = "0.6.12" dependencies = [ "coa-core", "serde", diff --git a/Cargo.toml b/Cargo.toml index f1e6e23..36f32b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/coa-core", "crates/coa-release", "src-tauri"] [workspace.package] -version = "0.6.11" +version = "0.6.12" edition = "2021" license = "AGPL-3.0-only" diff --git a/changelog.d/20261010-headless-world-startup.md b/changelog.d/20261010-headless-world-startup.md new file mode 100644 index 0000000..4eb616c --- /dev/null +++ b/changelog.d/20261010-headless-world-startup.md @@ -0,0 +1,7 @@ +--- +area: manager +type: fixed +audience: admins +title: Background world servers no longer stop because the local console is closed +--- +The Manager automatically disables the local interactive console before starting the world server. Its remote console remains available, and no manual configuration changes are needed. diff --git a/crates/coa-core/src/allsettings.rs b/crates/coa-core/src/allsettings.rs index 4b5ea65..7b20068 100644 --- a/crates/coa-core/src/allsettings.rs +++ b/crates/coa-core/src/allsettings.rs @@ -31,6 +31,7 @@ fn hidden(key: &str) -> bool { k.contains("password") || k.contains("databaseinfo") || k.ends_with("port") + || k == "console.enable" || k.starts_with("ra.") || k.starts_with("logger.") || k.starts_with("appender.") @@ -229,7 +230,7 @@ mod tests { #[test] fn what_belongs_to_the_manager_is_hidden() { - for k in ["LoginDatabaseInfo", "WorldServerPort", "BindIP", "Ra.Enable", "Ra.Password", "Logger.root", "Appender.Console", "DataDir", "Updates.EnableDatabases", "LoginDatabase.WorkerThreads", "InstanceServerPort"] { + for k in ["LoginDatabaseInfo", "WorldServerPort", "BindIP", "Ra.Enable", "Ra.Password", "Console.Enable", "console.enable", "Logger.root", "Appender.Console", "DataDir", "Updates.EnableDatabases", "LoginDatabase.WorkerThreads", "InstanceServerPort"] { assert!(hidden(k), "{k}"); } for k in ["Rate.XP.Kill", "PlayerLimit", "SupportEnabled", "Instance.ResetTimeHour", "Warden.Enabled", "CharacterCreating.Disabled"] { diff --git a/crates/coa-core/src/driver.rs b/crates/coa-core/src/driver.rs index 930ed7b..954f4ef 100644 --- a/crates/coa-core/src/driver.rs +++ b/crates/coa-core/src/driver.rs @@ -107,6 +107,7 @@ fn run_inner(root: &Path, verb: Verb, validating_update: bool) -> Result Result Result Result<()> { + if !matches!(verb, Verb::StartAll | Verb::StartWorld) { return Ok(()); } + for relative in ["Settings/worldserver.conf.template", "Core/configs/worldserver.conf"] { + let path = root.join(relative); + let bytes = match std::fs::read(&path) { + Ok(bytes) => bytes, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, + Err(e) => return Err(e.into()), + }; + let mut config = crate::config::parser::ConfFile::parse_bytes(&bytes)?; + config.set("Console.Enable", "0", &["Local console is disabled for background startup; the Manager uses remote access."]); + let text = config.to_text(); + if text.as_bytes() != bytes { + fsx::atomic_write(&path, text.as_bytes())?; + } + } + Ok(()) +} + /// The launcher keeps one JSON record per service in `.state`. A crash or power loss while it writes one can leave /// a file of the right length that holds only zero bytes (NTFS keeps the length but not the data), and the launcher /// then dies on every start with an unreadable record. Such a file carries no information, so remove it: the @@ -264,6 +287,55 @@ pub(crate) fn startup_failure(root: &Path, started: &DriverOutcome) -> String { mod tests { use super::*; + #[test] + fn background_start_disables_cli_in_template_and_rendered_config() { + let folder = tempfile::tempdir().unwrap(); + let root = folder.path(); + std::fs::create_dir_all(root.join("Settings")).unwrap(); + std::fs::create_dir_all(root.join("Core/configs")).unwrap(); + let template = root.join("Settings/worldserver.conf.template"); + let rendered = root.join("Core/configs/worldserver.conf"); + let original = "\u{feff}# Custom settings\r\nConsole.Enable = 1\r\nRa.Enable = 1\r\nPlayerLimit = 42\r\n"; + std::fs::write(&template, original).unwrap(); + std::fs::write(&rendered, original).unwrap(); + let auth = root.join("Core/configs/authserver.conf"); + std::fs::write(&auth, "custom auth config").unwrap(); + for verb in [Verb::StartMysql, Verb::StopAll] { + prepare_headless_world(root, verb).unwrap(); + assert_eq!(std::fs::read_to_string(&template).unwrap(), original); + assert_eq!(std::fs::read_to_string(&rendered).unwrap(), original); + } + for verb in [Verb::StartAll, Verb::StartWorld] { + std::fs::write(&template, original).unwrap(); + std::fs::write(&rendered, original).unwrap(); + prepare_headless_world(root, verb).unwrap(); + let expected = original.replace("Console.Enable = 1", "Console.Enable = 0"); + assert_eq!(std::fs::read_to_string(&template).unwrap(), expected); + assert_eq!(std::fs::read_to_string(&rendered).unwrap(), expected); + prepare_headless_world(root, verb).unwrap(); + assert_eq!(std::fs::read_to_string(&template).unwrap(), expected); + } + assert_eq!(std::fs::read_to_string(auth).unwrap(), "custom auth config"); + } + + #[test] + fn background_start_overrides_an_absent_or_duplicate_console_setting() { + let folder = tempfile::tempdir().unwrap(); + let root = folder.path(); + prepare_headless_world(root, Verb::StartAll).unwrap(); + std::fs::create_dir_all(root.join("Core/configs")).unwrap(); + let path = root.join("Core/configs/worldserver.conf"); + for original in ["Ra.Enable = 1\n", "Console.Enable = 0\nConsole.Enable = 1\n"] { + std::fs::write(&path, original).unwrap(); + prepare_headless_world(root, Verb::StartAll).unwrap(); + let bytes = std::fs::read(&path).unwrap(); + let config = crate::config::parser::ConfFile::parse_bytes(&bytes).unwrap(); + assert_eq!(config.get("Console.Enable"), Some("0")); + prepare_headless_world(root, Verb::StartAll).unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), bytes); + } + } + #[test] fn large_squid_provisioning_has_time_to_finish_without_extending_shutdown() { let folder = tempfile::tempdir().unwrap(); diff --git a/package-lock.json b/package-lock.json index 2984d6b..0956d7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coa-server-manager", - "version": "0.6.11", + "version": "0.6.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coa-server-manager", - "version": "0.6.11", + "version": "0.6.12", "dependencies": { "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-dialog": "^2.8.0", diff --git a/package.json b/package.json index 4d3ff06..6a3d5fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coa-server-manager", "private": true, - "version": "0.6.11", + "version": "0.6.12", "license": "AGPL-3.0-only", "type": "module", "scripts": { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5f0fd20..fb36d12 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CoA Server Manager", - "version": "0.6.11", + "version": "0.6.12", "identifier": "com.coa.servermanager", "build": { "beforeDevCommand": "npm run dev",