//! Running-process enumeration for the non-Steam detection fallback (D6/D7): //! native adapters only — `/proc` on Linux, Toolhelp on Windows — so there is no //! `sysinfo` dependency and the audit surface stays small. //! //! This module is *just the OS edge*: it returns the list of running executable //! paths/names. The trustworthy part — turning that list into a game via the //! user's explicit mappings and the launcher denylist — is the pure //! [`match_processes`](super::match_processes), unit-tested in the parent module. /// Enumerate the executables of currently-running processes as paths/basenames. /// Best-effort: processes we can't introspect (other users') are skipped rather /// than erroring. The result is fed to [`match_processes`](super::match_processes), /// which normalizes each entry to a basename before matching. pub fn running_executables() -> Vec { #[cfg(target_os = "linux")] { linux_proc_executables() } #[cfg(windows)] { windows_toolhelp_executables() } #[cfg(not(any(target_os = "linux", windows)))] { Vec::new() } } #[cfg(target_os = "linux")] fn linux_proc_executables() -> Vec { let mut out = Vec::new(); let Ok(entries) = std::fs::read_dir("/proc") else { return out; }; for entry in entries.flatten() { let name = entry.file_name(); let Some(name) = name.to_str() else { continue }; // Only numeric entries are processes. if !name.bytes().all(|b| b.is_ascii_digit()) { continue; } let proc_dir = entry.path(); // Prefer the real exe path (full, untruncated); fall back to `comm`, which // is readable for all processes but truncated to 15 bytes. if let Ok(exe) = std::fs::read_link(proc_dir.join("exe")) && let Some(s) = exe.to_str() { out.push(s.to_string()); continue; } if let Ok(comm) = std::fs::read_to_string(proc_dir.join("comm")) { let trimmed = comm.trim(); if !trimmed.is_empty() { out.push(trimmed.to_string()); } } } out } #[cfg(windows)] fn windows_toolhelp_executables() -> Vec { use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE}; use windows_sys::Win32::System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS, }; let mut out = Vec::new(); // SAFETY: standard Toolhelp snapshot of all processes; handle checked below. let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; if snapshot == INVALID_HANDLE_VALUE { return out; } let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() }; entry.dwSize = std::mem::size_of::() as u32; // SAFETY: entry is zeroed with dwSize set, as Process32FirstW requires. let mut ok = unsafe { Process32FirstW(snapshot, &mut entry) }; while ok != 0 { // szExeFile is a NUL-terminated UTF-16 array (the basename, e.g. game.exe). let end = entry .szExeFile .iter() .position(|&c| c == 0) .unwrap_or(entry.szExeFile.len()); let name = String::from_utf16_lossy(&entry.szExeFile[..end]); if !name.is_empty() { out.push(name); } // SAFETY: same valid snapshot + entry struct. ok = unsafe { Process32NextW(snapshot, &mut entry) }; } // SAFETY: snapshot handle came from CreateToolhelp32Snapshot above. unsafe { CloseHandle(snapshot) }; out } #[cfg(test)] mod tests { use super::*; #[cfg(target_os = "linux")] #[test] fn enumerates_at_least_this_process() { // The test runner itself is a process, so /proc enumeration must be // non-empty and include something that normalizes to our own exe basename. let exes = running_executables(); assert!( !exes.is_empty(), "expected to see running processes via /proc" ); // Our own /proc/self/exe basename should appear among them. let me = std::fs::read_link("/proc/self/exe") .ok() .and_then(|p| p.file_name().map(|f| f.to_string_lossy().into_owned())); if let Some(me) = me { let me_norm = super::super::normalize_exe(&me); assert!( exes.iter() .any(|e| super::super::normalize_exe(e) == me_norm), "running list should include our own executable {me_norm:?}" ); } } }