mirror of
https://github.com/Corfirean/coa-server-manager.git
synced 2026-11-11 03:51:01 +00:00
Channel source selection lives in coa-core; e2e checks for the published channel and SQUID servers
preview_channel_e2e installs from the stable channel, takes the preview from the published preview channel the way the Manager does, and checks the server is current afterwards. preview_migration_e2e gains a squid mode that keeps SQUID's SQL journals and expects SQUID's Playerbots instead of Companions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4facefc5dc
commit
048d746d29
4 files changed
+171
-29
No files matched your search
@@ -0,0 +1,97 @@
|
||||
//! The published Modules Preview channel, end to end on a DISPOSABLE server, through the same source selection
|
||||
//! the Manager uses when the owner does not choose a package: install from the stable channel's installation
|
||||
//! image, check for updates (the preview channel), apply the preview update (modules are built on this PC and
|
||||
//! the plugin host starts with them), then check again: the server must now be current.
|
||||
//!
|
||||
//! usage: preview_channel_e2e <new folder ending in -fixture>
|
||||
//! The server gets its own free ports before anything starts; it never uses another server's database.
|
||||
use std::path::Path;
|
||||
|
||||
use coa_core::install::{install_base, Params, Source};
|
||||
use coa_core::registry::{MetaDir, Registry};
|
||||
use coa_core::update::{self, DatabaseAccess, Env, RepackEnv};
|
||||
|
||||
const KEY: &str = coa_core::signing::EMBEDDED_PUBLIC_KEY;
|
||||
|
||||
fn free_port() -> u16 {
|
||||
std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
|
||||
}
|
||||
|
||||
fn sql(root: &Path, q: &str) -> String {
|
||||
coa_core::backup::with_database(root, |db| db.query(q)).unwrap_or_else(|e| panic!("{q}: {e}"))
|
||||
}
|
||||
|
||||
/// The Manager's "up to date" rule (src/lib/serverUpdateStatus.ts).
|
||||
fn is_current(p: &update::Preview) -> bool {
|
||||
p.from_version.as_deref() == Some(p.to_version.as_str())
|
||||
&& p.items.iter().all(|i| format!("{:?}", i.action).eq_ignore_ascii_case("skip"))
|
||||
&& (p.database_check_error.is_some() || !p.database_checked || p.pending_migrations == 0)
|
||||
}
|
||||
|
||||
fn check(root: &Path, meta_dir: &Path) -> Result<(Source, update::Preview), Box<dyn std::error::Error>> {
|
||||
let (_, meta) = MetaDir::open(meta_dir)?;
|
||||
let selection = coa_core::update_route::select(&meta, coa_core::channels::preview_update_source()?, KEY)?;
|
||||
let preview = update::preview_with(root, &meta, &selection.source, KEY, &Default::default(), DatabaseAccess::Start)?;
|
||||
Ok((selection.source, preview))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let a: Vec<String> = std::env::args().skip(1).collect();
|
||||
assert_eq!(a.len(), 1, "usage: preview_channel_e2e <new-folder-fixture>");
|
||||
let root = Path::new(&a[0]);
|
||||
assert!(a[0].ends_with("-fixture") && !root.exists(), "a new folder ending in -fixture is required");
|
||||
let reg_dir = std::env::temp_dir().join("coa-preview-channel-e2e-registry");
|
||||
let _ = std::fs::remove_dir_all(®_dir);
|
||||
let reg = Registry::at(reg_dir.join("installs.json"));
|
||||
|
||||
// 1. New server: the Manager installs the stable channel's image (the preview publishes updates only).
|
||||
let image = coa_core::channels::stable_source()?;
|
||||
println!("installation image: {image:?}");
|
||||
let t = std::time::Instant::now();
|
||||
let installed = install_base(
|
||||
&Params { source: Source::Installation(Box::new(image)), dest: root.into(), trusted_key: KEY, registry: ®, cancel: Default::default() },
|
||||
&|s| eprintln!(" {:>3}% {}", s.percent, s.step),
|
||||
)?;
|
||||
println!("installed {} in {:?}", installed.version, t.elapsed());
|
||||
|
||||
let repack = root.join("Settings/repack.json");
|
||||
let mut cfg: serde_json::Value = serde_json::from_slice(&std::fs::read(&repack)?)?;
|
||||
for k in ["mysqlPort", "authPort", "worldPort", "raPort"] {
|
||||
cfg[k] = free_port().into();
|
||||
}
|
||||
std::fs::write(&repack, serde_json::to_vec_pretty(&cfg)?)?;
|
||||
sql(root, "INSERT INTO acore_auth.account (username, salt, verifier, email, reg_mail) \
|
||||
VALUES ('E2ECHANNEL', UNHEX(REPEAT('11',32)), UNHEX(REPEAT('22',32)), '', '')");
|
||||
let accounts = sql(root, "SELECT COUNT(*) FROM acore_auth.account").trim().to_string();
|
||||
|
||||
// 2. Check for updates: the preview channel offers the preview.
|
||||
let meta_dir = coa_core::registry::metadata_dir_for(root)?;
|
||||
let (source, preview) = check(root, &meta_dir)?;
|
||||
println!("check: {:?} -> {} from {source:?}, {} items, {} migrations", preview.from_version, preview.to_version, preview.items.len(), preview.migrations);
|
||||
assert!(!is_current(&preview), "a fresh stable server must be offered the preview");
|
||||
|
||||
// 3. Apply it, exactly as the Update button does.
|
||||
let env = RepackEnv { root, meta_dir: &meta_dir };
|
||||
let t = std::time::Instant::now();
|
||||
let outcome = update::apply(
|
||||
&update::Params { root, meta_dir: &meta_dir, source, trusted_key: KEY, cancel: Default::default(), resolutions: Default::default(), env: &env, fail_after_ops: None },
|
||||
&|phase, percent| println!("{percent}% {phase}"),
|
||||
);
|
||||
let _ = coa_core::driver::run(root, coa_core::driver::Verb::StopAll);
|
||||
let outcome = outcome?;
|
||||
println!("update {:?} to {:?} in {:?}; modules {:?}", outcome.txn.state, outcome.txn.to_version, t.elapsed(), outcome.modules);
|
||||
if let Ok(t) = std::fs::read_to_string(root.join("Core/plugins/plugin-boot-report.json")) {
|
||||
println!("--- boot report\n{t}");
|
||||
}
|
||||
assert_eq!(outcome.txn.state, update::State::Committed);
|
||||
|
||||
// 4. Check again: nothing newer, nothing to change.
|
||||
let (_, again) = check(root, &meta_dir)?;
|
||||
let changing: Vec<_> = again.items.iter().filter(|i| !format!("{:?}", i.action).eq_ignore_ascii_case("skip")).map(|i| format!("{i:?}")).collect();
|
||||
println!("second check: {:?} -> {}, pending migrations {}, changing items {changing:?}", again.from_version, again.to_version, again.pending_migrations);
|
||||
assert_eq!(sql(root, "SELECT COUNT(*) FROM acore_auth.account").trim(), accounts, "accounts changed");
|
||||
env.ensure_stopped()?;
|
||||
assert!(is_current(&again), "the updated server is not reported as current");
|
||||
println!("PASS");
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
//! builds the recommended modules and starts the plugin-host core with them), and check that the data and the
|
||||
//! setting survived.
|
||||
//!
|
||||
//! usage: preview_migration_e2e <new folder ending in -fixture> <stable release URL> <preview package dir>
|
||||
//! usage: preview_migration_e2e <new folder ending in -fixture> <stable release URL> <preview package dir or URL> [squid]
|
||||
//! With `squid` the stable server runs SQUID's Playerbots (started once so its SQL is imported) before the update;
|
||||
//! SQUID's own SQL journals must come through unchanged.
|
||||
//! The server gets its own free ports before anything starts; it never uses another server's database.
|
||||
use std::path::Path;
|
||||
|
||||
@@ -26,9 +28,30 @@ fn fingerprint(root: &Path) -> String {
|
||||
FROM acore_auth.account a JOIN acore_characters.characters c ON c.account = a.id WHERE a.username = 'E2EPREVIEW'")
|
||||
}
|
||||
|
||||
/// SQUID's journals and its database, read the same way before and after (absent tables read as "-").
|
||||
fn squid_journals(root: &Path) -> String {
|
||||
let mut out = Vec::new();
|
||||
for db in ["acore_characters", "acore_world", "acore_playerbots"] {
|
||||
for t in ["coa_squid_migrations", "coa_squid_pending", "coa_bots_installed"] {
|
||||
let exists = sql(root, &format!("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='{db}' AND TABLE_NAME='{t}'"));
|
||||
if exists.trim() == "0" {
|
||||
out.push(format!("{db}.{t}=-"));
|
||||
continue;
|
||||
}
|
||||
let cols = sql(root, &format!("SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='{db}' AND TABLE_NAME='{t}'"));
|
||||
out.push(format!("{db}.{t}={}", sql(root, &format!(
|
||||
"SELECT CONCAT(COUNT(*), ':', COALESCE(SUM(CRC32(CONCAT_WS('|', {}))), 0)) FROM {db}.{t}", cols.trim()))
|
||||
.trim()));
|
||||
}
|
||||
}
|
||||
out.push(format!("playerbots tables={}", sql(root, "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='acore_playerbots'").trim()));
|
||||
out.join(" ")
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let a: Vec<String> = std::env::args().skip(1).collect();
|
||||
assert_eq!(a.len(), 3, "usage: preview_migration_e2e <new-folder-fixture> <stable release URL> <preview package dir>");
|
||||
assert!(a.len() == 3 || (a.len() == 4 && a[3] == "squid"), "usage: preview_migration_e2e <new-folder-fixture> <stable release URL> <preview package dir or URL> [squid]");
|
||||
let squid = a.len() == 4;
|
||||
let root = Path::new(&a[0]);
|
||||
assert!(a[0].ends_with("-fixture") && !root.exists(), "a new folder ending in -fixture is required");
|
||||
let reg_dir = std::env::temp_dir().join("coa-preview-e2e-registry");
|
||||
@@ -58,6 +81,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
std::fs::write(&repack, serde_json::to_vec_pretty(&cfg)?)?;
|
||||
println!("ports {}", serde_json::json!({"mysql": cfg["mysqlPort"], "auth": cfg["authPort"], "world": cfg["worldPort"], "ra": cfg["raPort"]}));
|
||||
|
||||
let meta = coa_core::registry::metadata_dir_for(root)?;
|
||||
if squid {
|
||||
// The owner switches the bots to SQUID (Companions off) and starts the server once: the launcher imports
|
||||
// SQUID's SQL into its own journals.
|
||||
coa_core::modules::set_enabled(root, &meta, "playerbots", true)?;
|
||||
let started = coa_core::driver::run(root, coa_core::driver::Verb::StartAll)?;
|
||||
println!("stable with SQUID started: {}", started.ok);
|
||||
let stopped = coa_core::driver::run(root, coa_core::driver::Verb::StopAll)?;
|
||||
assert!(started.ok && stopped.ok, "the stable server with SQUID did not start and stop");
|
||||
}
|
||||
|
||||
// 3. Test data: an account with a character and a spell; a changed module setting.
|
||||
let cols = sql(root, "SELECT GROUP_CONCAT(CONCAT(COLUMN_NAME, ':', DATA_TYPE)) FROM information_schema.COLUMNS \
|
||||
WHERE TABLE_SCHEMA='acore_characters' AND TABLE_NAME='characters' AND IS_NULLABLE='NO' AND COLUMN_DEFAULT IS NULL");
|
||||
@@ -82,11 +116,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let changed = format!("{} = 7", line.split(" = ").next().unwrap().trim());
|
||||
std::fs::write(&conf, text.replacen(&line, &changed, 1))?;
|
||||
let before = fingerprint(root);
|
||||
let journals_before = squid_journals(root);
|
||||
println!("squid journals before: {journals_before}");
|
||||
println!("before: {before}\nsetting: {changed}");
|
||||
assert!(!before.trim().is_empty());
|
||||
|
||||
// 4. The preview update, through the production updater.
|
||||
let meta = coa_core::registry::metadata_dir_for(root)?;
|
||||
let (_, info) = MetaDir::open(&meta)?;
|
||||
assert_eq!(coa_core::fsx::canonicalize_lenient(Path::new(&info.server_path))?, coa_core::fsx::canonicalize_lenient(root)?);
|
||||
let env = RepackEnv { root, meta_dir: &meta };
|
||||
@@ -95,7 +130,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
&update::Params {
|
||||
root,
|
||||
meta_dir: &meta,
|
||||
source: Source::Dir(a[2].clone().into()),
|
||||
source: if a[2].starts_with("http") { Source::Url(a[2].clone()) } else { Source::Dir(a[2].clone().into()) },
|
||||
trusted_key: coa_core::signing::EMBEDDED_PUBLIC_KEY,
|
||||
cancel: Default::default(),
|
||||
resolutions: Default::default(),
|
||||
@@ -111,6 +146,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// 5. Same data, same setting, plugin host with its modules.
|
||||
let after = fingerprint(root);
|
||||
let journals_after = squid_journals(root);
|
||||
println!("squid journals after: {journals_after}");
|
||||
let text = std::fs::read_to_string(&conf)?;
|
||||
println!("after: {after}");
|
||||
println!("setting kept: {}", text.lines().any(|l| l.trim() == changed));
|
||||
@@ -123,6 +160,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
assert_eq!(outcome.txn.state, update::State::Committed);
|
||||
assert_eq!(before, after, "seeded character data changed");
|
||||
assert!(text.lines().any(|l| l.trim() == changed), "module setting lost");
|
||||
assert_eq!(journals_before, journals_after, "SQUID's journals changed");
|
||||
let ids: Vec<String> = outcome.modules.iter().map(|m| format!("{m:?}")).collect();
|
||||
let has = |id: &str| ids.iter().any(|m| m.contains(&format!("\"{id}\"")));
|
||||
if squid {
|
||||
assert!(has("playerbots") && !has("companions"), "SQUID server must get SQUID's Playerbots, not Companions: {ids:?}");
|
||||
} else {
|
||||
assert!(has("companions") && !has("playerbots"), "Companions server must get Companions, not SQUID: {ids:?}");
|
||||
}
|
||||
println!("PASS");
|
||||
Ok(())
|
||||
}
|
||||
@@ -78,6 +78,29 @@ pub fn resolve(channel: &str) -> Result<Source> {
|
||||
)))
|
||||
}
|
||||
|
||||
/// The stable release used when the stable pointer has not been published yet.
|
||||
pub const STABLE_FALLBACK_URL: &str =
|
||||
"https://github.com/Corfirean/coa-server-build/releases/download/stable";
|
||||
|
||||
/// Stable updates and installation images: the signed stable pointer, else the fixed stable release.
|
||||
pub fn stable_source() -> Result<Source> {
|
||||
match resolve("stable") {
|
||||
Ok(source) => Ok(source),
|
||||
Err(Error::PackageNotPublished(_)) => Ok(Source::Url(STABLE_FALLBACK_URL.into())),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the Modules Preview Manager looks for server updates when none is chosen: its own signed channel,
|
||||
/// or the stable channel until a preview exists (a stable server is where every preview server starts).
|
||||
pub fn preview_update_source() -> Result<Source> {
|
||||
match resolve("preview") {
|
||||
Ok(source) => Ok(source),
|
||||
Err(Error::PackageNotPublished(_)) => stable_source(),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(pointer: &Pointer, seed: &str) -> Result<Vec<u8>> {
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
+2
-25
@@ -263,11 +263,6 @@ async fn portable_launch(
|
||||
}
|
||||
}
|
||||
|
||||
/// Where official server packages are published (created by the release pipeline, Phase 6).
|
||||
/// Where signed update packages are published; override with COA_UPDATE_SOURCE (URL or local package folder).
|
||||
const DEFAULT_UPDATE_URL: &str =
|
||||
"https://github.com/Corfirean/coa-server-build/releases/download/stable";
|
||||
|
||||
fn update_source(custom: Option<String>) -> Result<Source> {
|
||||
let pick = custom
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
@@ -275,17 +270,7 @@ fn update_source(custom: Option<String>) -> Result<Source> {
|
||||
match pick {
|
||||
Some(p) if p.to_ascii_lowercase().starts_with("http") => Ok(Source::Url(p)),
|
||||
Some(p) => Ok(Source::Dir(PathBuf::from(p))),
|
||||
// The Modules Preview follows its own signed channel; until the first preview release exists it
|
||||
// falls back to the stable channel (a stable server is where every preview server starts).
|
||||
None => match coa_core::channels::resolve("preview") {
|
||||
Ok(source) => Ok(source),
|
||||
Err(Error::PackageNotPublished(_)) => match coa_core::channels::resolve("stable") {
|
||||
Ok(source) => Ok(source),
|
||||
Err(Error::PackageNotPublished(_)) => Ok(Source::Url(DEFAULT_UPDATE_URL.into())),
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
None => coa_core::channels::preview_update_source(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,15 +296,7 @@ fn installation_source(custom: Option<String>) -> Result<Source> {
|
||||
});
|
||||
// A new server is installed from the qualified stable image; the preview channel publishes updates only,
|
||||
// so the preview core arrives as the first update (the same path every existing server takes).
|
||||
select_installation_source(custom, stable_update_source)
|
||||
}
|
||||
|
||||
fn stable_update_source() -> Result<Source> {
|
||||
match coa_core::channels::resolve("stable") {
|
||||
Ok(source) => Ok(source),
|
||||
Err(Error::PackageNotPublished(_)) => Ok(Source::Url(DEFAULT_UPDATE_URL.into())),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
select_installation_source(custom, coa_core::channels::stable_source)
|
||||
}
|
||||
|
||||
fn select_installation_source(
|
||||
|
||||
Reference in new issue
Block a user