feat(rust): install the helpers a plugin release ships beside the bridge (D182)
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m37s
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m37s
Rust-Plugins now releases a ZoneManager helper, RunicGatewayZones.cs, beside RunicGateway.cs (docs/modules/rust/PLAN_FIXES.md D181, D182), and the org lead decided it is installed by default. The tarball reader took only the bridge, so the helper would have been downloaded and dropped. - plugin::read_tarball reads every other .cs the release's manifest lists in `files`, and refuses the release when one is missing, does not match its own sha256, or is not a plain `<Name>.cs` (the name is written into a plugins directory, so nothing in a tarball may reach outside it). A .cs the manifest does not list is ignored, so a release older than helpers has none. - install/update place each helper before the bridge, in place like the bridge (Oxide and Carbon reload on a change, not on a rename), put back one that was deleted or edited, and remove one a later release stops shipping. - The record gains per-instance `helpers` (path + sha256). It is absent from every record written before this and reads back as none. - doctor warns — never fails — on a missing or edited helper and says what the bridge loses without it; uninstall removes helpers before the bridge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -19,7 +19,7 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
use super::plugin::{self, ConfigView};
|
||||
use super::record::{ComponentRecord, Instance, RustRecord, SCHEMA};
|
||||
use super::record::{ComponentRecord, DeployedFile, Instance, RustRecord, SCHEMA};
|
||||
use super::server::{self, RustServer};
|
||||
use super::sidecar;
|
||||
use crate::bundle::{self, RustBundle};
|
||||
@@ -42,6 +42,10 @@ struct Planned {
|
||||
running: bool,
|
||||
plugin_config: Option<ConfigView>,
|
||||
plugin_action: BinaryAction,
|
||||
/// Each helper in the release and what happens to it, in the release's (name) order.
|
||||
helper_actions: Vec<BinaryAction>,
|
||||
/// Helpers this instance's record holds that the release no longer ships: removed.
|
||||
retired_helpers: Vec<String>,
|
||||
game_port: u16,
|
||||
web_port: u16,
|
||||
config_path: PathBuf,
|
||||
@@ -179,7 +183,7 @@ pub fn deploy(cli: &Cli, mode: Mode) -> Result<()> {
|
||||
)?);
|
||||
}
|
||||
let binary_action = crate::sidecar::decide(&asset, &layout.rust_sidecar_bin)?;
|
||||
print_plan(&layout, &planned, binary_action, &bundle);
|
||||
print_plan(&layout, &planned, binary_action, &bundle, &released);
|
||||
|
||||
if cli.verify {
|
||||
println!(
|
||||
@@ -326,7 +330,14 @@ pub fn deploy(cli: &Cli, mode: Mode) -> Result<()> {
|
||||
Some(b) if b.tag == bundle.bundle => {
|
||||
// The same bundle can still have written something: a plugin edited by hand is
|
||||
// put back, which is what `doctor` tells an operator to run `update` for.
|
||||
let restored = planned.iter().filter(|p| p.plugin_action.writes()).count();
|
||||
let restored = planned
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.plugin_action.writes()
|
||||
|| p.helper_actions.iter().any(|a| a.writes())
|
||||
|| !p.retired_helpers.is_empty()
|
||||
})
|
||||
.count();
|
||||
if restored == 0 && !binary_action.writes() {
|
||||
println!("\nAlready on bundle {} — nothing moved.", bundle.bundle)
|
||||
} else {
|
||||
@@ -431,6 +442,30 @@ fn plan_instance(
|
||||
}
|
||||
Ok(_) => BinaryAction::Replace,
|
||||
};
|
||||
// Helpers are decided the same way as the plugin: absent, identical or replaced. One the
|
||||
// operator deleted is put back — the installer owns it, and `doctor` says what its absence costs.
|
||||
let helper_actions = released
|
||||
.helpers
|
||||
.iter()
|
||||
.map(
|
||||
|helper| match std::fs::read(server.plugins_dir().join(&helper.name)) {
|
||||
Err(_) => BinaryAction::Install,
|
||||
Ok(bytes) if crate::util::sha256_bytes(&bytes) == helper.sha256 => {
|
||||
BinaryAction::Unchanged
|
||||
}
|
||||
Ok(_) => BinaryAction::Replace,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
let retired_helpers = recorded
|
||||
.map(|i| {
|
||||
i.helpers
|
||||
.keys()
|
||||
.filter(|name| !released.helpers.iter().any(|h| &h.name == *name))
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let config_path = layout.rust_config(id);
|
||||
Ok(Planned {
|
||||
id: id.to_string(),
|
||||
@@ -441,6 +476,8 @@ fn plan_instance(
|
||||
server,
|
||||
plugin_config,
|
||||
plugin_action,
|
||||
helper_actions,
|
||||
retired_helpers,
|
||||
game_port,
|
||||
web_port,
|
||||
})
|
||||
@@ -451,6 +488,7 @@ fn print_plan(
|
||||
planned: &[Planned],
|
||||
binary: BinaryAction,
|
||||
bundle: &RustBundle,
|
||||
released: &plugin::Released,
|
||||
) {
|
||||
ui::row(
|
||||
"binary",
|
||||
@@ -479,6 +517,25 @@ fn print_plan(
|
||||
p.plugin_action.label()
|
||||
),
|
||||
);
|
||||
for (helper, action) in released.helpers.iter().zip(&p.helper_actions) {
|
||||
ui::row(
|
||||
"helper",
|
||||
&format!(
|
||||
"{} {}",
|
||||
p.server.plugins_dir().join(&helper.name).display(),
|
||||
action.label()
|
||||
),
|
||||
);
|
||||
}
|
||||
for name in &p.retired_helpers {
|
||||
ui::row(
|
||||
"helper",
|
||||
&format!(
|
||||
"{} removed (no longer released)",
|
||||
p.server.plugins_dir().join(name).display()
|
||||
),
|
||||
);
|
||||
}
|
||||
ui::row(
|
||||
"plugin config",
|
||||
&match &p.plugin_config {
|
||||
@@ -662,6 +719,45 @@ fn deploy_instance(
|
||||
}
|
||||
};
|
||||
|
||||
// The helpers before the plugin (D182): each loads the moment it lands, and the bridge reads a
|
||||
// helper's state at hello — a helper already there is one the bridge's first hello reports.
|
||||
// Written in place for the plugin's reason below.
|
||||
let mut helpers = std::collections::BTreeMap::new();
|
||||
for (helper, action) in released.helpers.iter().zip(&plan.helper_actions) {
|
||||
let path = plan.server.plugins_dir().join(&helper.name);
|
||||
if action.writes() {
|
||||
std::fs::create_dir_all(plan.server.plugins_dir()).with_context(|| {
|
||||
format!("cannot create {}", plan.server.plugins_dir().display())
|
||||
})?;
|
||||
std::fs::write(&path, &helper.source)
|
||||
.with_context(|| format!("cannot write {}", path.display()))?;
|
||||
ui::ok(&format!(
|
||||
"helper {} {}",
|
||||
if *action == BinaryAction::Replace {
|
||||
"replaced"
|
||||
} else {
|
||||
"installed"
|
||||
},
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
helpers.insert(
|
||||
helper.name.clone(),
|
||||
DeployedFile {
|
||||
path: path.display().to_string(),
|
||||
sha256: helper.sha256.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
for name in &plan.retired_helpers {
|
||||
let path = plan.server.plugins_dir().join(name);
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => ui::ok(&format!("helper removed {}", path.display())),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => ui::warn(&format!("could not remove {}: {e}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
// The plugin last: it loads the moment it lands, and its sidecar is now there to dial.
|
||||
let plugin_path = plan.server.plugin_path();
|
||||
if plan.plugin_action.writes() {
|
||||
@@ -694,6 +790,7 @@ fn deploy_instance(
|
||||
framework: plan.server.framework.as_str().to_string(),
|
||||
plugin_path: plugin_path.display().to_string(),
|
||||
plugin_sha256: released.sha256.clone(),
|
||||
helpers,
|
||||
plugin_config: plugin_config_path.display().to_string(),
|
||||
plugin_config_written: wrote_plugin_config || plugin_config_written_before,
|
||||
game_port: plan.game_port,
|
||||
|
||||
Reference in New Issue
Block a user