mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
Fix headless world startup for issue #46 and release Manager 0.6.12
This commit is contained in:
1 parent
376f1dbb46
commit
94d4ade1c7
8 files changed
+89
-9
No files matched your search
Generated
+3
-3
@@ -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
@@ -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.
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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,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",
|
||||
|
||||
Reference in new issue
Block a user