Fix headless world startup for issue #46 and release Manager 0.6.12

This commit is contained in:
Дмитрий committed 2026-10-10 07:59:03 +05:00
1 parent 376f1dbb46
commit 94d4ade1c7
8 files changed
+89 -9

No files matched your search

Generated
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -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"
@@ -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.
+2 -1
View File
@@ -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"] {
+72
View File
@@ -107,6 +107,7 @@ fn run_inner(root: &Path, verb: Verb, validating_update: bool) -> Result<DriverO
}
if matches!(verb, Verb::StartAll | Verb::StartWorld) { crate::modules::ensure_bot_exclusivity(&root)?; }
if crate::docker::is_docker(&root) {
prepare_headless_world(&root, verb)?;
return crate::docker::run(&root, verb);
}
if verb == Verb::StopAll { crate::multiworld::stop(&root)?; }
@@ -123,6 +124,7 @@ fn run_inner(root: &Path, verb: Verb, validating_update: bool) -> Result<DriverO
}
if matches!(verb, Verb::StartAll | Verb::StartWorld) { crate::modules::ensure_bot_exclusivity(&root)?; }
let (python, script) = launcher(&root)?;
prepare_headless_world(&root, verb)?;
let original = std::fs::read_to_string(&script)?;
let patched = patch_launcher_imports(&original)?;
if original != patched {
@@ -198,6 +200,27 @@ fn run_inner(root: &Path, verb: Verb, validating_update: bool) -> Result<DriverO
Ok(DriverOutcome { ok, exit_code, code, human: code.map(ErrorCode::human), output })
}
/// Background servers receive no interactive stdin. Keep CLI EOF from stopping the world server;
/// the Manager's console uses remote access instead. The template survives launcher regeneration.
fn prepare_headless_world(root: &Path, verb: Verb) -> 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();
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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",