Files
peerspeak/src/game/scan.rs
T
molluskandClaude Opus 4.8 d0a16cb8b9 style: apply cargo fmt across the crate (A20)
The repo never enforced rustfmt, so formatting had drifted broadly. This is a
single mechanical `cargo fmt` pass over the whole crate (no behavioral change;
lib suite green, 493 passed). Going forward fmt should be enforced (planned CI
fmt --check step). Part of the 0.6.1 hygiene pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:11:44 -04:00

126 lines
4.6 KiB
Rust

//! 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<String> {
#[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<String> {
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<String> {
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::<PROCESSENTRY32W>() 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:?}"
);
}
}
}