An unattended run keeps its state in a fresh folder of its own (#980)
An unattended run (`WOW_UNATTENDED`, `WOW_RIG`) resolved the same `benilla-config/` as a person. In a dev build, every linked worktree maps that to the primary checkout's folder. So a probe read the player's CVars, bindings and layout, and wrote its realm, settings and window positions back over them. `local_state::home` now gives an unattended run a fresh folder of its own (`$TMPDIR/benilla-run-<pid>/benilla-config`) and prints it at boot. `BENILLA_HOME` still pins one, and a capture still resolves none. CONTRIBUTING's "Running it unattended" says so. Test: `an_unattended_run_keeps_its_own_state_folder` checks the rig path, the override and the capture, and that the folder is never the resident one. Fixes #975 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a329f19e06
commit
8ccaa7b411
@@ -253,6 +253,15 @@ fn launch(build: BuildId, extend: Option<Extension<'_>>) -> AppExit {
|
||||
// A dev build's install, state folder and probe identity resolve from the launcher's folder,
|
||||
// so a crate on top of benilla keeps them in its own and not in cargo's checkout of benilla.
|
||||
benilla_formats::set_project_folder(build.project_dir);
|
||||
if run_mode::unattended() && std::env::var_os("BENILLA_HOME").is_none() {
|
||||
// Before the logger exists; whoever reads what the run wrote needs the folder.
|
||||
if let Some(home) = local_state::home() {
|
||||
println!(
|
||||
"state: an unattended run keeps its own state in {} (BENILLA_HOME pins one)",
|
||||
home.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// From here a panic leaves `benilla-config/Diagnostics/crash-<unix>.txt`; armed before the
|
||||
// `App` exists, so a panic while plugins build is reported too.
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
//!
|
||||
//! Resolution, in order, the same shape as [`benilla_formats::wow_data`]:
|
||||
//! 1. `$BENILLA_HOME`.
|
||||
//! 2. `<project folder>/benilla-config/`, dev builds only ([`benilla_formats::project_folder`], the
|
||||
//! 2. An unattended run ([`crate::run_mode::unattended`]): a fresh folder of its own for the run,
|
||||
//! under the system temp folder, so a probe neither reads the player's settings nor writes back
|
||||
//! over them.
|
||||
//! 3. `<project folder>/benilla-config/`, dev builds only ([`benilla_formats::project_folder`], the
|
||||
//! launcher's): a shipped binary must not carry the build machine's source tree.
|
||||
//! 3. `<exe dir>/benilla-config/`.
|
||||
//! 4. `<exe dir>/benilla-config/`.
|
||||
//!
|
||||
//! With `$WOW_CAPTURE` set every path resolves to `None`: a capture neither reads nor writes
|
||||
//! player state.
|
||||
@@ -22,25 +25,45 @@ const STATE_DIR: &str = "benilla-config";
|
||||
/// It may not exist yet: [`write_atomic`] creates it, and a reader treats a missing file as
|
||||
/// defaults.
|
||||
pub(crate) fn home() -> Option<PathBuf> {
|
||||
home_in(cfg!(test))
|
||||
}
|
||||
|
||||
/// [`home`], with the unit-test stop as an argument so a test can walk the steps behind it.
|
||||
fn home_in(unit_test: bool) -> Option<PathBuf> {
|
||||
if std::env::var_os("WOW_CAPTURE").is_some() {
|
||||
return None; // hermetic: captures neither read nor write player state
|
||||
}
|
||||
if let Some(over) = std::env::var_os("BENILLA_HOME") {
|
||||
return Some(PathBuf::from(over));
|
||||
}
|
||||
if cfg!(test) {
|
||||
if unit_test {
|
||||
return None; // a unit test reaches no real state folder unless it pins `$BENILLA_HOME`
|
||||
}
|
||||
if crate::run_mode::unattended() {
|
||||
return Some(run_home().to_path_buf());
|
||||
}
|
||||
resident_home()
|
||||
}
|
||||
|
||||
/// Steps 2 and 3 of [`home`]: the folder the build itself lives in.
|
||||
/// Step 2 of [`home`]: the unattended run's own folder, one per process, created on first write.
|
||||
/// Left behind for whoever reads what the run wrote; the system cleans its temp folder.
|
||||
pub(crate) fn run_home() -> &'static Path {
|
||||
static RUN_HOME: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||
RUN_HOME.get_or_init(|| run_home_in(&std::env::temp_dir(), std::process::id()))
|
||||
}
|
||||
|
||||
/// [`run_home`]'s path under `temp` for process `pid`.
|
||||
fn run_home_in(temp: &Path, pid: u32) -> PathBuf {
|
||||
temp.join(format!("benilla-run-{pid}")).join(STATE_DIR)
|
||||
}
|
||||
|
||||
/// Steps 3 and 4 of [`home`]: the folder the build itself lives in.
|
||||
fn resident_home() -> Option<PathBuf> {
|
||||
// 2 · the project folder, dev builds only.
|
||||
// 3 · the project folder, dev builds only.
|
||||
if let Some(root) = dev_project_root() {
|
||||
return Some(root.join(STATE_DIR));
|
||||
}
|
||||
// 3 · beside the binary; a dev build never reaches here.
|
||||
// 4 · beside the binary; a dev build never reaches here.
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|e| e.parent().map(Path::to_path_buf))
|
||||
@@ -384,6 +407,46 @@ mod tests {
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
/// An unattended run keeps its state in a folder of its own, never the player's, unless
|
||||
/// `$BENILLA_HOME` pins one; a capture still resolves none.
|
||||
#[test]
|
||||
fn an_unattended_run_keeps_its_own_state_folder() {
|
||||
let _l = ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _c = EnvGuard::unset("WOW_CAPTURE");
|
||||
let _r = EnvGuard::unset("WOW_RIG");
|
||||
let _h = EnvGuard::unset("BENILLA_HOME");
|
||||
let _u = EnvGuard::set("WOW_UNATTENDED", "1");
|
||||
let run = home_in(false).expect("an unattended run has a state folder");
|
||||
assert_eq!(run, run_home());
|
||||
assert_ne!(
|
||||
Some(&run),
|
||||
resident_home().as_ref(),
|
||||
"never the player's folder"
|
||||
);
|
||||
assert_eq!(
|
||||
home_in(false),
|
||||
Some(run.clone()),
|
||||
"one folder for the whole run"
|
||||
);
|
||||
assert_eq!(
|
||||
run_home_in(Path::new("/tmp"), 42),
|
||||
Path::new("/tmp/benilla-run-42/benilla-config")
|
||||
);
|
||||
|
||||
// A rig is unattended too.
|
||||
let _u2 = EnvGuard::unset("WOW_UNATTENDED");
|
||||
let _r2 = EnvGuard::set("WOW_RIG", "tauren druid 60");
|
||||
assert_eq!(home_in(false), Some(run));
|
||||
|
||||
// The override still wins, and a capture still resolves nothing.
|
||||
let _h2 = EnvGuard::set("BENILLA_HOME", "/tmp/benilla-pinned");
|
||||
assert_eq!(home_in(false), Some(PathBuf::from("/tmp/benilla-pinned")));
|
||||
let _c2 = EnvGuard::set("WOW_CAPTURE", "ui-options");
|
||||
assert_eq!(home_in(false), None);
|
||||
}
|
||||
|
||||
/// A unit test that does not pin `$BENILLA_HOME` resolves no state folder, so no test can write
|
||||
/// the real one.
|
||||
#[test]
|
||||
|
||||
@@ -100,6 +100,9 @@ out of scope is closed with the reason.
|
||||
- `WOW_UNATTENDED=1` reconnects instead of waiting at a dialog, and exits non-zero on a login
|
||||
it cannot pass. `WOW_NOSOUND=1` runs silent. A capture (`WOW_CAPTURE`) is both, and a rig
|
||||
(`WOW_RIG`) is unattended. `WOW_ALLOW_ACCOUNT=1` overrides the account guard.
|
||||
- An unattended run starts from defaults in a fresh state folder of its own, printed at boot
|
||||
(`state:`), so it never reads or overwrites your settings, layout or addons. `BENILLA_HOME`
|
||||
pins a folder, for a run that needs addons, seeded files or state kept across runs.
|
||||
- On vmangos, `WOW_RIG="tauren druid 60 gear:heal-preraid-bis spec:heal-preraid-bis
|
||||
at:ThunderBluff"` finds or creates that character on the account and applies the server's
|
||||
premade sets (`WOW_RIG="gear:?"` lists them). `WOW_PROBE_CHAT="<command>"` sends one GM
|
||||
|
||||
+2
-2
@@ -454,7 +454,7 @@
|
||||
- `WOW_RES_CENSUS` — benilla-app/src/perf/census.rs
|
||||
- `WOW_RESIZE` — benilla-app/src/capture/mod.rs
|
||||
- `WOW_REVEAL` — benilla-app/src/capture/probes/reveal.rs, benilla-app/src/dev.rs
|
||||
- `WOW_RIG` — benilla-app/src/capture/probe_rig.rs, benilla-app/src/dev.rs, benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/probe_shield.rs, benilla-app/src/realm_select/smoke.rs, benilla-app/src/run_mode.rs, benilla-world/src/bgwin.rs
|
||||
- `WOW_RIG` — benilla-app/src/capture/probe_rig.rs, benilla-app/src/dev.rs, benilla-app/src/local_state.rs, benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/probe_shield.rs, benilla-app/src/realm_select/smoke.rs, benilla-app/src/run_mode.rs, benilla-world/src/bgwin.rs
|
||||
- `WOW_RIG_CENSUS` — benilla-world/src/rig_palette.rs
|
||||
- `WOW_RIG_COST` — benilla-world/src/rig_palette.rs
|
||||
- `WOW_RIG_SKIN` — benilla-assets/src/materials.rs
|
||||
@@ -496,7 +496,7 @@
|
||||
- `WOW_UI_SCALE` — benilla-app/src/cvars/mod.rs, benilla-app/src/ui_script/mod.rs
|
||||
- `WOW_UI_SPLICE_VERIFY` — benilla-app/src/ui_script/extract/mod.rs
|
||||
- `WOW_UISCRIPT_DUMP` — benilla-app/tests/ui_script_wall.rs
|
||||
- `WOW_UNATTENDED` — benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs, benilla-world/src/bgwin.rs
|
||||
- `WOW_UNATTENDED` — benilla-app/src/local_state.rs, benilla-app/src/login/mod.rs, benilla-app/src/net.rs, benilla-app/src/run_mode.rs, benilla-world/src/bgwin.rs
|
||||
- `WOW_UNIT_VISUALS` — benilla-app/src/capture/probes/visual_census.rs, benilla-app/src/dev.rs
|
||||
- `WOW_UNIT_VISUALS_RADIUS` — benilla-app/src/capture/probes/visual_census.rs
|
||||
- `WOW_UPLOAD_BUDGET` — benilla-world/src/world_plugins.rs
|
||||
|
||||
Reference in New Issue
Block a user