diff --git a/Cargo.lock b/Cargo.lock index 88d1080..c2eb05b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,8 +158,8 @@ dependencies = [ "bitflags 2.10.0", "cc", "cesu8", - "jni", - "jni-sys", + "jni 0.21.1", + "jni-sys 0.3.0", "libc", "log", "ndk", @@ -2284,19 +2284,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2612,7 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ "bitflags 2.10.0", - "jni-sys", + "jni-sys 0.3.0", "log", "ndk-sys", "num_enum", @@ -2632,7 +2681,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.0", ] [[package]] @@ -3816,6 +3865,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4134,6 +4192,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -4455,6 +4529,7 @@ dependencies = [ "tracing", "tracing-subscriber", "walkdir", + "webbrowser", "xz2", "zip", ] @@ -5377,12 +5452,12 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni", + "jni 0.22.4", "log", "ndk-context", "objc2 0.6.3", diff --git a/Cargo.toml b/Cargo.toml index 37f3d78..ffdc3b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ steam-cdn = "1.0.0" libc = "0.2" regex = "1" rhai = { version = "1", features = ["sync"] } +webbrowser = "1.2.1" [patch.crates-io] steam-cdn = { path = "vendor/steam-cdn" } diff --git a/examples/debug_parameters.json b/examples/debug_parameters.json new file mode 100644 index 0000000..39da115 --- /dev/null +++ b/examples/debug_parameters.json @@ -0,0 +1,3 @@ +{ + "winedebug": "+process,+seh,+msi,+setupapi,+heap,+file" +} diff --git a/src/launch/diagnostics.rs b/src/launch/diagnostics.rs new file mode 100644 index 0000000..b8e832e --- /dev/null +++ b/src/launch/diagnostics.rs @@ -0,0 +1,155 @@ +//! Diagnostic capture utilities for Steam Runtime installation and repair. +//! +//! This module provides a way to capture Wine traces and process output +//! during the Master Steam installation flow. It is triggered by setting +//! the `STEAMFLOW_DIAGNOSE_INSTALL=1` environment variable. +//! +//! Configuration is loaded from `~/.config/SteamFlow/debug_parameters.json`. + +use std::process::{Command, Stdio}; +use std::fs::File; +use std::path::PathBuf; +use std::time::UNIX_EPOCH; +use anyhow::{Result, Context}; +use crate::config::config_dir; + +#[derive(Debug, Clone, serde::Deserialize, PartialEq)] +struct DebugParameters { + #[serde(default = "default_winedebug")] + winedebug: String, +} + +fn default_winedebug() -> String { + "err+all,warn+module,warn+loaddll,+setupapi".to_string() +} + +impl Default for DebugParameters { + fn default() -> Self { + Self { + winedebug: default_winedebug(), + } + } +} + +fn load_debug_parameters() -> DebugParameters { + let config_path = match config_dir() { + Ok(dir) => dir.join("debug_parameters.json"), + Err(_) => return DebugParameters::default(), + }; + + if !config_path.exists() { + return DebugParameters::default(); + } + + match std::fs::read_to_string(&config_path) { + Ok(content) => match serde_json::from_str(&content) { + Ok(params) => params, + Err(e) => { + tracing::warn!("Failed to parse debug_parameters.json: {}. Using defaults.", e); + DebugParameters::default() + } + }, + Err(e) => { + tracing::warn!("Failed to read debug_parameters.json: {}. Using defaults.", e); + DebugParameters::default() + } + } +} + +/// Applies diagnostic capture to the given command if opt-in flag is set. +/// +/// Behavior: +/// - Checks `STEAMFLOW_DIAGNOSE_INSTALL`. If not "1", returns `Ok(None)`. +/// - Replaces `WINEDEBUG` with values from `debug_parameters.json`. +/// - Redirects stdout and stderr to `logs/install_diagnose_.log`. +/// +/// NOTE: Callers must call this function LAST, after setting their own env vars, +/// as this function replaces existing WINEDEBUG settings. +pub fn apply_install_diagnostics(cmd: &mut Command) -> Result> { + if std::env::var("STEAMFLOW_DIAGNOSE_INSTALL").unwrap_or_default() != "1" { + return Ok(None); + } + + let params = load_debug_parameters(); + let timestamp = std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let log_dir = config_dir()?.join("logs"); + std::fs::create_dir_all(&log_dir).context("failed to create logs directory")?; + + let log_path = log_dir.join(format!("install_diagnose_{}.log", timestamp)); + tracing::info!("Diagnostic capture active. Logging to: {}", log_path.display()); + + let log_file = File::create(&log_path) + .context(format!("failed to create log file at {}", log_path.display()))?; + + cmd.env("WINEDEBUG", ¶ms.winedebug); + + let log_file_clone = log_file.try_clone()?; + cmd.stdout(Stdio::from(log_file)); + cmd.stderr(Stdio::from(log_file_clone)); + + Ok(Some(log_path)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + use std::path::Path; + use tempfile::tempdir; + + #[test] + fn test_default_parameters() { + let params = DebugParameters::default(); + assert_eq!(params.winedebug, "err+all,warn+module,warn+loaddll,+setupapi"); + } + + #[test] + fn test_parsing_valid_json() { + let json = r#"{"winedebug": "+process,+setupapi"}"#; + let params: DebugParameters = serde_json::from_str(json).unwrap(); + assert_eq!(params.winedebug, "+process,+setupapi"); + } + + #[test] + fn test_fallback_on_malformed_json() { + let tmp = tempdir().unwrap(); + let config_path = tmp.path().join("debug_parameters.json"); + + // Non-existent file + let params = load_debug_parameters_from_path(&config_path); + assert_eq!(params, DebugParameters::default()); + + // Malformed JSON + std::fs::write(&config_path, "not json").unwrap(); + let params = load_debug_parameters_from_path(&config_path); + assert_eq!(params, DebugParameters::default()); + } + + fn load_debug_parameters_from_path(path: &Path) -> DebugParameters { + if !path.exists() { + return DebugParameters::default(); + } + match std::fs::read_to_string(path) { + Ok(content) => serde_json::from_str(&content).unwrap_or_else(|_| DebugParameters::default()), + Err(_) => DebugParameters::default(), + } + } + + #[test] + fn test_apply_diagnostics_gating() { + let mut cmd = Command::new("ls"); + cmd.env("WINEDEBUG", "original"); + + // Flag NOT set + env::remove_var("STEAMFLOW_DIAGNOSE_INSTALL"); + let res = apply_install_diagnostics(&mut cmd).unwrap(); + assert!(res.is_none()); + + let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect(); + assert_eq!(envs.get(std::ffi::OsStr::new("WINEDEBUG")).unwrap().unwrap(), "original"); + } +} diff --git a/src/launch/mod.rs b/src/launch/mod.rs index 888bb3a..458f1b0 100644 --- a/src/launch/mod.rs +++ b/src/launch/mod.rs @@ -3,6 +3,7 @@ pub mod stages; pub mod validators; pub mod dll_provider_resolver; pub mod fixups; +pub mod diagnostics; use std::path::{Path, PathBuf}; use anyhow::{Result, Context, anyhow}; @@ -66,6 +67,8 @@ pub async fn install_master_steam(config: &LauncherConfig) -> Result<()> { tracing::info!("Launching Master Steam: {:?}", cmd); + let _ = diagnostics::apply_install_diagnostics(&mut cmd)?; + let _child = cmd.spawn().context("Failed to spawn master steam process")?; // We don't wait here because it's a background process or interactive installer diff --git a/src/ui.rs b/src/ui.rs index b5c9aa4..9af22a1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2585,7 +2585,16 @@ impl eframe::App for SteamLauncher { .min_width(320.0) .show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { - ui.heading("Library"); + ui.horizontal(|ui| { + ui.heading("Library"); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("📂 Open Config Folder").on_hover_text("Open application configuration directory").clicked() { + if let Ok(dir) = crate::config::config_dir() { + let _ = webbrowser::open(&dir.to_string_lossy()); + } + } + }); + }); ui.label("Steam Library Path"); ui.text_edit_singleline(&mut self.launcher_config.steam_library_path);