All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m27s
The compose job carries the latest RunicNPC release that answers the API the bridge's manifest declares (runicnpc_api), verified against its SHA256SUMS like the other two; none when the bridge needs none, RunicNPC has not released, or its API is too old, each said in the summary. Walked against a mock Gitea in all three cases. The installer reads the optional npc component (older bundles still parse), fetches and checks RunicNPC's tarball against its manifest, places RunicNPC.cs before the bridge in each instance's plugins directory, records it, puts it back on update when edited or deleted, reports it in doctor, and removes it on uninstall. Its data directory is never touched. Kits joins the required plugins it reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
218 lines
7.8 KiB
Rust
218 lines
7.8 KiB
Rust
//! `uninstall --game rust [--server-id <id>] [--purge]`.
|
|
//!
|
|
//! What it removes, per instance (docs/modules/rust/PLAN.md §34.2.3, §34.4):
|
|
//!
|
|
//! - **the service**, and **our plugin file** — `RunicGateway.cs` is the installer's;
|
|
//! - **never the plugin's config** — `RunicGateway.json` is the website's, and it holds the
|
|
//! server's id; a reinstall must find it;
|
|
//! - with `--purge`, the instance's sidecar config (its token) and database.
|
|
//!
|
|
//! With the last instance gone: the shared binary, the template unit, the record, and — only if
|
|
//! this installer created it and no ServUO deployment uses it — the service account.
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::Result;
|
|
|
|
use super::record::RustRecord;
|
|
use crate::cli::Cli;
|
|
use crate::{paths, service, ui};
|
|
|
|
pub fn run(cli: &Cli) -> Result<i32> {
|
|
let layout = paths::layout();
|
|
let record_path = layout.rust_record();
|
|
println!(
|
|
"\nRunic Gateway installer {} — uninstall (Rust)",
|
|
env!("CARGO_PKG_VERSION")
|
|
);
|
|
|
|
let Some(mut record) = RustRecord::load(&record_path)? else {
|
|
println!();
|
|
ui::warn(&format!(
|
|
"Nothing to uninstall — no Rust deployment is recorded on this host (looked for {}).",
|
|
record_path.display()
|
|
));
|
|
return Ok(0);
|
|
};
|
|
|
|
let ids: Vec<String> = match &cli.server_id {
|
|
Some(id) if record.instances.contains_key(id) => vec![id.clone()],
|
|
Some(id) => anyhow::bail!(
|
|
"{id:?} is not a recorded Rust instance (recorded: {})",
|
|
record
|
|
.instances
|
|
.keys()
|
|
.cloned()
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
),
|
|
None => record.instances.keys().cloned().collect(),
|
|
};
|
|
let last = ids.len() == record.instances.len();
|
|
|
|
// ── Say what will happen, then ask ───────────────────────────────────────
|
|
println!();
|
|
for id in &ids {
|
|
let i = &record.instances[id];
|
|
ui::heading(&format!("Instance {id}"));
|
|
if let Some(svc) = &i.service {
|
|
println!(" · remove the service {}", svc.name);
|
|
}
|
|
println!(" · remove {}", i.plugin_path);
|
|
for helper in i.helpers.values() {
|
|
println!(" · remove {}", helper.path);
|
|
}
|
|
if let Some(npc) = &i.npc {
|
|
println!(
|
|
" · remove {} (RunicNPC; its placements in data/RunicNPC/ are kept)",
|
|
npc.path
|
|
);
|
|
}
|
|
println!(
|
|
" · keep {} (the website's; it names this server)",
|
|
i.plugin_config
|
|
);
|
|
if cli.purge {
|
|
println!(
|
|
" · remove {} and the database {}",
|
|
i.config_path, i.db_path
|
|
);
|
|
} else {
|
|
println!(
|
|
" · keep {} and {} (--purge removes them)",
|
|
i.config_path, i.db_path
|
|
);
|
|
}
|
|
}
|
|
if last {
|
|
println!(
|
|
"\n and, with no instance left: {} and {}",
|
|
record.binary.path,
|
|
record_path.display()
|
|
);
|
|
}
|
|
println!();
|
|
let proceed = if cli.assume_yes {
|
|
println!("Remove the components listed above? [y/N] (--yes)");
|
|
true
|
|
} else {
|
|
ui::confirm("Remove the components listed above?", false, false)?
|
|
};
|
|
if !proceed {
|
|
println!("\nNothing was removed.");
|
|
return Ok(0);
|
|
}
|
|
|
|
// ── Remove ───────────────────────────────────────────────────────────────
|
|
let mut done = Vec::new();
|
|
let mut problems = Vec::new();
|
|
for id in &ids {
|
|
let Some(instance) = record.instances.remove(id) else {
|
|
continue;
|
|
};
|
|
if let Some(svc) = &instance.service {
|
|
let removal = service::remove(svc);
|
|
done.extend(removal.done);
|
|
problems.extend(removal.problems);
|
|
}
|
|
// The helpers first: without the bridge they have nothing to do, and a helper left behind
|
|
// would keep patching ZoneManager for a bridge that is gone.
|
|
for helper in instance.helpers.values() {
|
|
remove_file(Path::new(&helper.path), &mut done, &mut problems);
|
|
}
|
|
// RunicNPC is the installer's file (D224); its data directory is the admins', and stays.
|
|
if let Some(npc) = &instance.npc {
|
|
remove_file(Path::new(&npc.path), &mut done, &mut problems);
|
|
}
|
|
remove_file(Path::new(&instance.plugin_path), &mut done, &mut problems);
|
|
if cli.purge {
|
|
remove_file(Path::new(&instance.config_path), &mut done, &mut problems);
|
|
let data_dir = layout.rust_data_dir(id);
|
|
if data_dir.exists() {
|
|
match std::fs::remove_dir_all(&data_dir) {
|
|
Ok(()) => done.push(format!("removed {}", data_dir.display())),
|
|
Err(e) => problems.push(format!("cannot remove {}: {e}", data_dir.display())),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if record.instances.is_empty() {
|
|
remove_file(Path::new(&record.binary.path), &mut done, &mut problems);
|
|
done.extend(service::remove_rust_template(&layout));
|
|
remove_shared_user(&record, &layout, &mut done, &mut problems);
|
|
remove_file(&record_path, &mut done, &mut problems);
|
|
} else {
|
|
match record.save(&record_path) {
|
|
Ok(()) => done.push(format!(
|
|
"{} keeps {} instance(s): {}",
|
|
record_path.display(),
|
|
record.instances.len(),
|
|
record
|
|
.instances
|
|
.keys()
|
|
.cloned()
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
)),
|
|
Err(e) => problems.push(format!("cannot update {}: {e}", record_path.display())),
|
|
}
|
|
}
|
|
|
|
for line in &done {
|
|
println!(" · {line}");
|
|
}
|
|
for problem in &problems {
|
|
println!();
|
|
ui::warn(problem);
|
|
}
|
|
println!(
|
|
"\nRemove each server from Admin -> Rust -> Servers on the website too; the installer never \
|
|
contacts it."
|
|
);
|
|
Ok(if problems.is_empty() { 0 } else { 1 })
|
|
}
|
|
|
|
/// The `runicgateway` account is shared: every Rust instance runs as it, and so does ServUO's
|
|
/// sidecar. It goes only when this installer created it for Rust and no ServUO deployment remains.
|
|
fn remove_shared_user(
|
|
record: &RustRecord,
|
|
layout: &paths::Layout,
|
|
done: &mut Vec<String>,
|
|
problems: &mut Vec<String>,
|
|
) {
|
|
if !record.service_user_created || cfg!(windows) {
|
|
return;
|
|
}
|
|
if layout.install_record().exists() {
|
|
done.push(format!(
|
|
"left the {} account — the ServUO deployment on this host runs as it",
|
|
service::SERVICE_USER
|
|
));
|
|
return;
|
|
}
|
|
let removed = crate::util::run_ok("userdel", &[service::SERVICE_USER])
|
|
.or_else(|_| crate::util::run_ok("deluser", &[service::SERVICE_USER]));
|
|
match removed {
|
|
Ok(_) => done.push(format!(
|
|
"removed the {} service user",
|
|
service::SERVICE_USER
|
|
)),
|
|
Err(e) => problems.push(format!(
|
|
"cannot remove the {} service user ({}); remove it by hand if you want it gone",
|
|
service::SERVICE_USER,
|
|
e.to_string().replace('\n', " ")
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn remove_file(path: &Path, done: &mut Vec<String>, problems: &mut Vec<String>) {
|
|
match std::fs::remove_file(path) {
|
|
Ok(()) => done.push(format!("removed {}", path.display())),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
|
done.push(format!("{} was already gone", path.display()))
|
|
}
|
|
Err(e) => problems.push(format!("cannot remove {}: {e}", path.display())),
|
|
}
|
|
}
|