//! `uninstall --game rust [--server-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 { 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 = 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::>() .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); 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); } 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::>() .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, problems: &mut Vec, ) { 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, problems: &mut Vec) { 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())), } }