fix(session/gamelease): four things the first pass got wrong
Found reviewing the riskiest paths of the previous commit. - An install dir of `/games/x` matched a process running out of `/games/xyz`. The image-path check was already component-wise and fine; the command-line check compared raw bytes, so it needed the separator. Two library folders where one name prefixes the other is not a contrived case. - Ending a nested game force-releases kept displays, which is not per-display — so with another client streaming it could retire a display that was never ours. Skip it while anyone else has a live session: the failure mode becomes a game that keeps running, which is the default everywhere else anyway. - A lease nothing polls (no detect signals, or a platform without a matcher yet) sat at "launching" forever in the console. Report it as running: the host did just launch it, and with no watcher it is claiming nothing about the exit. - A game ended after its session was already gone reported no `game.exited` at all — its watcher had stopped with the session, so nothing was left to emit it. That is every grace expiry and every `POST /game/end`, i.e. exactly the ones an operator most wants to see. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -192,11 +192,16 @@ impl Scanner {
|
||||
}
|
||||
}
|
||||
if let Some(dir) = install_dir {
|
||||
// Require a path separator after the directory, so an install dir of `/games/x` is
|
||||
// not satisfied by an unrelated `/games/xyz/…` argument. (The image-path check above
|
||||
// needs no such care — `Path::starts_with` already compares whole components.)
|
||||
let needle = dir.as_os_str().as_encoded_bytes();
|
||||
for arg in cmdline.split(|&b| b == 0) {
|
||||
if arg.starts_with(needle) {
|
||||
return true;
|
||||
}
|
||||
let under_dir = |arg: &[u8]| {
|
||||
arg.strip_prefix(needle)
|
||||
.is_some_and(|rest| rest.first() == Some(&b'/'))
|
||||
};
|
||||
if cmdline.split(|&b| b == 0).any(under_dir) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(want) = exe {
|
||||
@@ -376,6 +381,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_dir_does_not_match_a_sibling_with_the_same_prefix() {
|
||||
// `/games/x` must not adopt a process running out of `/games/xyz` — a real hazard when one
|
||||
// library folder's name is a prefix of another's.
|
||||
let td = fake_proc_root(
|
||||
1000.0,
|
||||
&[
|
||||
FakeProc::new(90, 50_000).exe("/games/xyz/other"),
|
||||
FakeProc::new(91, 50_000).cmdline(&["wrapper", "/games/xyz/other"]),
|
||||
],
|
||||
);
|
||||
let s = scanner(td.path());
|
||||
assert!(s.find(&DetectSpec::dir("/games/x"), None).is_empty());
|
||||
// The genuine directory still matches, by image path and by argument.
|
||||
assert_eq!(
|
||||
pids(s.find(&DetectSpec::dir("/games/xyz"), None)),
|
||||
vec![90, 91]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_proton_style_game_only_in_the_cmdline() {
|
||||
// The image is the Proton runtime; the game appears only as an argument.
|
||||
|
||||
Reference in New Issue
Block a user