fix(game): detect live Steam appid via /proc SteamAppId, not stale registry.vdf
Field test found Steam games were never detected on Linux. Root cause: Steam rewrites ~/.steam/registry.vdf only on SHUTDOWN, so its RunningAppID is stale (often absent) while a game is actually running — polling it can never see the live game. Fix: on Linux, read the live appid from the running game's environment (SteamAppId in /proc/<pid>/environ, the var Steam exports to every game process — the same signal MangoHud uses; readable for our own processes). registry.vdf stays as a best-effort fallback. Windows still reads the real registry's RunningAppID, which IS updated live there. Other Unix keeps the registry.vdf fallback. Pure parse_steam_app_id_from_environ() is unit-tested (nonzero filter, absent, substring-not-fooled, garbage). Also fixes a latent bug in the first draft where a single non-UTF8 SteamAppId value would abort the whole scan via ? instead of skipping. 396 lib tests, clippy --all-targets clean.
This commit is contained in:
+89
-11
@@ -138,23 +138,27 @@ impl SteamProbe {
|
||||
})
|
||||
}
|
||||
|
||||
/// The live RunningAppID (nonzero), or `None`. Linux/macOS read the client's
|
||||
/// `registry.vdf`; Windows reads the real registry.
|
||||
/// The live RunningAppID (nonzero), or `None`.
|
||||
///
|
||||
/// Platform notes: on **Windows** the real registry's `RunningAppID` is updated
|
||||
/// live, so we read it. On **Linux** the client's `registry.vdf` is only
|
||||
/// rewritten on Steam *shutdown* — it's stale while a game runs — so the live
|
||||
/// signal is the running game process's `SteamAppId` environment variable
|
||||
/// (`/proc/<pid>/environ`, readable for our own processes; the same approach
|
||||
/// MangoHud uses); `registry.vdf` stays as a best-effort fallback. Other Unix
|
||||
/// (macOS) only has the `registry.vdf` fallback for now.
|
||||
fn running_app_id(&self) -> Option<u32> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
win::running_app_id()
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
for path in registry_vdf_candidates() {
|
||||
if let Some(contents) = read_capped(&path)
|
||||
&& let Some(id) = parse_running_app_id(&contents)
|
||||
{
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
None
|
||||
running_app_id_from_environ().or_else(registry_running_app_id)
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
registry_running_app_id()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +270,64 @@ fn registry_vdf_candidates() -> Vec<PathBuf> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Best-effort `RunningAppID` from the on-disk `registry.vdf`. ⚠️ Stale while a
|
||||
/// game runs (Steam rewrites the file only on shutdown), so this is a *fallback*
|
||||
/// behind the live `/proc` `SteamAppId` scan on Linux — not the primary signal.
|
||||
#[cfg(not(windows))]
|
||||
fn registry_running_app_id() -> Option<u32> {
|
||||
for path in registry_vdf_candidates() {
|
||||
if let Some(contents) = read_capped(&path)
|
||||
&& let Some(id) = parse_running_app_id(&contents)
|
||||
{
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse a Steam appid out of a process's raw `environ` blob (NUL-separated
|
||||
/// `KEY=VALUE` pairs), reading the `SteamAppId` variable Steam exports to every
|
||||
/// game process. Returns the appid only when present and nonzero. Pure +
|
||||
/// unit-tested; the `/proc` iteration is the thin edge in
|
||||
/// [`running_app_id_from_environ`].
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn parse_steam_app_id_from_environ(environ: &[u8]) -> Option<u32> {
|
||||
for kv in environ.split(|&b| b == 0) {
|
||||
if let Some(val) = kv.strip_prefix(b"SteamAppId=")
|
||||
&& let Ok(s) = std::str::from_utf8(val)
|
||||
&& let Ok(id) = s.trim().parse::<u32>()
|
||||
&& id != 0
|
||||
{
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The live Steam appid of a running game, found by scanning `/proc/<pid>/environ`
|
||||
/// for the `SteamAppId` Steam exports to the game's process tree. `environ` is
|
||||
/// readable only for our own processes — exactly the ones a Steam game we launched
|
||||
/// runs as — and we skip the rest. The live signal that replaces the stale
|
||||
/// on-disk `registry.vdf` on Linux.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn running_app_id_from_environ() -> Option<u32> {
|
||||
let entries = std::fs::read_dir("/proc").ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let Some(name) = name.to_str() else { continue };
|
||||
if !name.bytes().all(|b| b.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
// Cap the read: an environ is small; this bounds a pathological case.
|
||||
if let Some(environ) = read_capped(&entry.path().join("environ"))
|
||||
&& let Some(id) = parse_steam_app_id_from_environ(environ.as_bytes())
|
||||
{
|
||||
return Some(id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod win {
|
||||
//! Windows registry reads via direct Win32 FFI (windows-sys), no `winreg`
|
||||
@@ -417,6 +479,22 @@ mod tests {
|
||||
assert!(parse_library_paths("totally broken {{{").is_empty());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn steam_app_id_parsed_from_environ_blob() {
|
||||
// A realistic NUL-separated environ with SteamAppId among other vars.
|
||||
let environ = b"PATH=/usr/bin\0SteamAppId=440\0HOME=/home/x\0SteamGameId=440\0";
|
||||
assert_eq!(parse_steam_app_id_from_environ(environ), Some(440));
|
||||
// Nonzero requirement: SteamAppId=0 (the launcher itself) is ignored.
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"SteamAppId=0\0FOO=bar\0"), None);
|
||||
// Absent → None (a non-Steam process).
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"PATH=/usr/bin\0HOME=/home/x\0"), None);
|
||||
// Not fooled by a different var that merely contains the substring.
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"MY_SteamAppId=999\0"), None);
|
||||
// Garbage value → None, no panic.
|
||||
assert_eq!(parse_steam_app_id_from_environ(b"SteamAppId=notanumber\0"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_name_extracts_and_filters_empty() {
|
||||
let acf = r#""AppState" { "appid" "440" "name" "Team Fortress 2" }"#;
|
||||
|
||||
Reference in New Issue
Block a user