From f9b674f09724f046ddc2c58e4064b0bd7260ed0a Mon Sep 17 00:00:00 2001 From: weter11 Date: Sat, 15 Aug 2026 15:02:36 -0200 Subject: [PATCH 1/2] feat(config): reject OnlineContainerized with a non-Proton runner at save time Phase 5 validation guard: OnlineContainerized runs the game through /proton run inside the Steam Linux Runtime, which a plain Wine runner cannot satisfy. validate_online_containerized_runner (src/config.rs) mirrors resolve_effective_proton_name precedence (per-game forced -> global proton_version) and requires RunnerKind::Proton; the per-game settings save path in ui.rs refuses the update with a clear status message instead of failing at launch. Unit tests cover Proton accepted (forced + global), bare Wine rejected (forced + global), OfflineEmulated/Auto never blocked, and empty-forced fallback. docs/architecture/phase5-onlinecontainerized-validation.md: guard design + PROTON_LOG path behavior (proton-script vs bare-wine), debug.json precedence, OnlineContainerized x runner matrix, P2 white-screen context. --- .../phase5-onlinecontainerized-validation.md | 83 ++++++++++++ src/config.rs | 125 ++++++++++++++++++ src/ui.rs | 26 +++- 3 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 docs/architecture/phase5-onlinecontainerized-validation.md diff --git a/docs/architecture/phase5-onlinecontainerized-validation.md b/docs/architecture/phase5-onlinecontainerized-validation.md new file mode 100644 index 00000000..ba1e8a93 --- /dev/null +++ b/docs/architecture/phase5-onlinecontainerized-validation.md @@ -0,0 +1,83 @@ +# Phase 5 — OnlineContainerized Runner Validation Guard + Launch-Path Logging Facts + +> Status: **IMPLEMENTED (2026-08-15)**, branch `phase5/container-mode-validation`. +> Phase 5 part 1: the `OnlineContainerized` config-save guard; part 2 documents the +> launch-path logging facts discovered during the Portal 2 (620) pure-PE rendering +> diagnostic. + +## Part 1 — Validation guard + +### Problem + +`OnlineContainerized` runs the game through `/proton run` inside the Steam +Linux Runtime container (`src/container/launch.rs`). A plain Wine runner has no +`proton` entry script, so a misconfigured game fails **at launch** with +`OnlineContainerized requires a Proton compatibility tool` — a confusing error that +only appears when the user presses Play. + +Worse, the configuration can be internally inconsistent without any warning: the +per-game `forced_proton_version` pin and the `steam_mode` are stored separately +(`config.json` vs `user_apps.json`), so a containerized test can silently run a +different runner than the file states (observed 2026-08-15: the running app held an +older in-memory config, launching purepe while the file pinned wine11-wow64). + +### Rule + +> If `steam_mode` is `OnlineContainerized`, the effective runner (per-game +> `forced_proton_version` → global `proton_version`, mirroring +> `resolve_effective_proton_name`) must classify as a Proton compatibility tool +> (`classify_runner` → `RunnerKind::Proton`, i.e. a `proton` entry script exists). + +Rejected updates are refused at config-save time with: + +> `OnlineContainerized mode requires a Proton compatibility tool runner (e.g., steamflow-proton-11.0-purepe). Bare Wine runners are not supported in container mode.` + +### Implementation + +- `src/config.rs` — `validate_online_containerized_runner(steam_mode, forced_proton_version, global_proton_version, library_root) -> Result<(), String>`. + Pure function: non-`OnlineContainerized` modes always pass; container mode resolves + the effective runner (`resolve_runner`) and requires `RunnerKind::Proton`. +- `src/ui.rs` — per-game settings save block: the guard runs before the + `user_configs.insert` + `save_user_configs`; on rejection the update is NOT + persisted and the status bar shows `Configuration rejected: `. +- Unit tests in `src/config.rs` (`online_containerized_rejects_bare_wine_and_accepts_proton`): + Proton forced ✓, Proton global ✓, bare-Wine forced ✗ (exact message), bare-Wine + global ✗, `OfflineEmulated`/`Auto` never blocked, empty forced → global fallback ✓. + +## Part 2 — Launch-path logging facts (from the Phase 5 diagnostic) + +### PROTON_LOG only works on the proton-script path + +| Launch path | Proton script? | `PROTON_LOG=1` → `~/steam-.log` | Wine debug destination | +|---|---|---|---| +| `OnlineContainerized` (SLR → `/proton run`) | yes | **yes** (19.6 KB capture, proton-11.0-1b) | container stderr → `WINE_LOG_OUTPUT` | +| DirectWine, PlainWine runner (`steamflow-runner-wine11-wow64`) | no | **silent no-op** | `~/.config/SteamFlow/logs/wine_.log` (truncated per launch) | + +The proton script also **rewrites `WINEDEBUG`** (its own +`+d3d,+winevulkan,+win32u,+mfplat,+wg_transform,+gstreamer,err+all`), so CLI +`WINEDEBUG="+loaddll,+vulkan,+d3d"` does not survive the container path. + +### debug.json precedence + +`~/.config/SteamFlow/debug.json` → `DebugConfig { env }` is applied **last** in +`build_env` (after per-game env vars and built-in debug toggles), so its keys win +over everything — including the CLI and the proton default. Caveat: setting +`WINEDLLOVERRIDES` there **replaces** the whole computed override string; include the +full base set when overriding it. Verify via `effective_env.json` in the session dir. + +### OnlineContainerized × runner matrix + +| steam_mode | effective runner kind | result | +|---|---|---| +| `OnlineContainerized` | Proton (e.g. `steamflow-proton-11.0-purepe`) | OK | +| `OnlineContainerized` | PlainWine / Unknown (e.g. `steamflow-runner-wine11-wow64`) | **rejected at save** (guard) / launch error (without guard) | +| `OfflineEmulated` / `Auto` | anything | never blocked | + +## White-screen context (Portal 2 / RTX Remix, 2026-08-15) + +Runner-independent remix render-path stall (0 GBuffer passes, all draws skipped, +dxvk-cache frozen, GPU 100%) + a Steam-session gate (game stuck on the loading +screen without a reachable Steam client). Stock D3D9 (mod off + `d3d9=n,b`) renders +under purepe — the runner/DXVK/Vulkan-ICD stack is healthy. Evidence: +`/home/wer/devis/tmp/p2-phase5-20260815/`; full detail in the `rtx-remix-modding` +skill. diff --git a/src/config.rs b/src/config.rs index 4a04e0ff..55429831 100644 --- a/src/config.rs +++ b/src/config.rs @@ -333,3 +333,128 @@ pub async fn save_user_configs(configs: &UserConfigStore) -> Result<()> { .with_context(|| format!("failed writing {}", path.display()))?; Ok(()) } + +/// Validate a per-game Steam-mode configuration against the effective runner +/// (Phase 5 — `OnlineContainerized` guard). +/// +/// The containerized launch path (`OnlineContainerized`) runs the game through +/// `/proton run` inside the Steam Linux Runtime (see +/// [`crate::container::launch`]); a plain Wine runner has no `proton` entry +/// script and would fail at launch time with "OnlineContainerized requires a +/// Proton compatibility tool". Validating at config-save time surfaces the +/// misconfiguration in the UI/CLI before the user hits that launch error. +/// +/// The effective runner mirrors [`crate::utils::resolve_effective_proton_name`] +/// precedence: per-game `forced_proton_version` → global `proton_version`. +pub fn validate_online_containerized_runner( + steam_mode: crate::models::SteamMode, + forced_proton_version: Option<&str>, + global_proton_version: &str, + library_root: &std::path::Path, +) -> Result<(), String> { + if steam_mode != crate::models::SteamMode::OnlineContainerized { + return Ok(()); + } + let runner_name = forced_proton_version + .filter(|s| !s.trim().is_empty()) + .unwrap_or(global_proton_version); + let runner_path = crate::utils::resolve_runner(runner_name, library_root); + if matches!( + crate::utils::classify_runner(&runner_path), + crate::utils::RunnerKind::Proton { .. } + ) { + Ok(()) + } else { + Err("OnlineContainerized mode requires a Proton compatibility tool runner (e.g., steamflow-proton-11.0-purepe). Bare Wine runners are not supported in container mode.".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::SteamMode; + use std::path::Path; + + /// Create a fake runner root (with or without a `proton` entry script). + fn write_runner(dir: &Path, name: &str, with_proton_script: bool) -> std::path::PathBuf { + let root = dir.join(name); + std::fs::create_dir_all(&root).expect("create runner dir"); + if with_proton_script { + std::fs::write(root.join("proton"), "#!/bin/sh\n").expect("write proton script"); + } + root + } + + #[test] + fn online_containerized_rejects_bare_wine_and_accepts_proton() { + let tmp = tempfile::tempdir().expect("tempdir"); + let purepe = write_runner(tmp.path(), "steamflow-proton-11.0-purepe", true); + let wine11 = write_runner(tmp.path(), "steamflow-runner-wine11-wow64", false); + + // Proton runner as the per-game forced override → accepted. + assert!(validate_online_containerized_runner( + SteamMode::OnlineContainerized, + Some(purepe.to_str().unwrap()), + "global-default", + tmp.path(), + ) + .is_ok()); + + // Proton runner via the global default (no forced override) → accepted. + assert!(validate_online_containerized_runner( + SteamMode::OnlineContainerized, + None, + purepe.to_str().unwrap(), + tmp.path(), + ) + .is_ok()); + + // Bare Wine runner as the per-game forced override → rejected with the + // exact user-facing message. + let err = validate_online_containerized_runner( + SteamMode::OnlineContainerized, + Some(wine11.to_str().unwrap()), + purepe.to_str().unwrap(), + tmp.path(), + ) + .unwrap_err(); + assert!( + err.contains("OnlineContainerized mode requires a Proton compatibility tool runner") + ); + assert!(err.contains("Bare Wine runners are not supported in container mode")); + + // Bare Wine runner via the global default → rejected too. + assert!(validate_online_containerized_runner( + SteamMode::OnlineContainerized, + None, + wine11.to_str().unwrap(), + tmp.path(), + ) + .is_err()); + + // Non-containerized modes are never blocked, even with a bare runner. + assert!(validate_online_containerized_runner( + SteamMode::OfflineEmulated, + Some(wine11.to_str().unwrap()), + wine11.to_str().unwrap(), + tmp.path(), + ) + .is_ok()); + assert!(validate_online_containerized_runner( + SteamMode::Auto, + Some(wine11.to_str().unwrap()), + wine11.to_str().unwrap(), + tmp.path(), + ) + .is_ok()); + + // An empty forced override falls back to the global runner. + assert!(validate_online_containerized_runner( + SteamMode::OnlineContainerized, + Some(""), + purepe.to_str().unwrap(), + tmp.path(), + ) + .is_ok()); + } +} diff --git a/src/ui.rs b/src/ui.rs index eaba03b4..d624bd50 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2166,11 +2166,27 @@ impl SteamLauncher { // Proton/Wine version". if changed { - self.user_configs.insert(game.app_id, config); - let store = self.user_configs.clone(); - self.runtime.spawn(async move { - let _ = crate::config::save_user_configs(&store).await; - }); + // Phase 5: `OnlineContainerized` runs the game through + // `/proton run` inside the Steam Linux Runtime — a bare + // Wine runner cannot satisfy that. Reject the update at save time + // with a clear message instead of failing at launch. + if let Err(msg) = crate::config::validate_online_containerized_runner( + config.steam_mode, + self.launcher_config + .game_configs + .get(&game.app_id) + .and_then(|c| c.forced_proton_version.as_deref()), + &self.launcher_config.proton_version, + std::path::Path::new(&self.launcher_config.steam_library_path), + ) { + self.status = format!("Configuration rejected: {msg}"); + } else { + self.user_configs.insert(game.app_id, config); + let store = self.user_configs.clone(); + self.runtime.spawn(async move { + let _ = crate::config::save_user_configs(&store).await; + }); + } } } From 4351725a36ae2eb9cb134fa6d433f499c2bbeec9 Mon Sep 17 00:00:00 2001 From: weter11 Date: Sat, 15 Aug 2026 15:02:40 -0200 Subject: [PATCH 2/2] fix(clippy): use octal literal for set_mode(0) (non_octal_unix_permissions) Pre-existing deny-by-default lint on rust 1.92: two chmod-000 sites used the decimal literal 0. 0o0 is identical in effect; fixes cargo clippy --all-targets. --- src/steam_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/steam_client.rs b/src/steam_client.rs index bed2d8de..474de5cb 100644 --- a/src/steam_client.rs +++ b/src/steam_client.rs @@ -3414,7 +3414,7 @@ impl SteamClient { if actual.starts_with(expected) { if let Ok(metadata) = std::fs::metadata(&actual) { let mut permissions = metadata.permissions(); - permissions.set_mode(0); + permissions.set_mode(0o0); let _ = std::fs::set_permissions(&actual, permissions); } } @@ -5036,7 +5036,7 @@ mod steamwebhelper_management_tests { std::fs::write(&helper, b"MZ fake webhelper").unwrap(); // Simulate the per-game "Disable CEF" enforcement lock (chmod 000). let mut perms = std::fs::metadata(&helper).unwrap().permissions(); - perms.set_mode(0); + perms.set_mode(0o0); std::fs::set_permissions(&helper, perms).unwrap(); assert_eq!(std::fs::metadata(&helper).unwrap().permissions().mode() & 0o111, 0);