Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1a31901
process: improve linux game detection
Justsnoopy30 May 18, 2024
d49e76a
process: small fix for false positives
Justsnoopy30 May 23, 2024
a8bf750
process: further improve game detection for Linux
XenHat Apr 20, 2025
e99dccd
process: add missing line break in performance logging
XenHat Apr 25, 2025
ea53247
process: update discord game db
XenHat Apr 25, 2025
357300d
process: tweak a debug logging line
XenHat Apr 25, 2025
217e9c7
process: fix game detection for many games
XenHat Apr 25, 2025
1753e97
process: work around false positive for Dolphin Emulator on Linux
XenHat Apr 27, 2025
96b48e3
process: stricter check for dolphin false positive
XenHat Apr 27, 2025
c3d4e46
Accept workaround for Java
XenHat Jun 30, 2025
aab4bb7
merge with local version used for testing
XenHat Jun 30, 2025
669145e
add more matching debug printing for now
XenHat Jun 30, 2025
48add08
update detectable.json
XenHat Aug 31, 2025
7a0eafa
add more debugging
XenHat Aug 31, 2025
28827cb
Comment out debugging in preparation for an eventual upstreaming
XenHat Aug 31, 2025
09c787e
code style cleanup
XenHat Sep 10, 2025
51e7911
process: add debug logging
XenHat Sep 10, 2025
07ddca6
process: comment out debug logging until default log level is changed
XenHat Sep 30, 2025
c906099
apply OBS Streamer Mode patch
XenHat Jan 3, 2026
9ef9a84
fix detectabledb import
XenHat Jan 3, 2026
f93d393
Add workaround for dolphin and dolphin-emu
XenHat Jan 3, 2026
88e1b34
move fixup for Zenless Zone Zero
XenHat Jan 3, 2026
70d0272
Remove Red Dead Online from the DB
XenHat Jan 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/process/detectable.json

Large diffs are not rendered by default.

149 changes: 130 additions & 19 deletions src/process/index.js
Copy link

@konovalov-nk konovalov-nk Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we refactor this file to something like this?

https://jsfiddle.net/jsq76fyc/ <-- much easier to reason about than quadruple nested if blocks 🙂

Then you can also keep the file minimal and export the methods outside of index.js to keep it sane for the mind.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turned out to break detection again, so I'll need more time to fiddle with it.

Copy link

@konovalov-nk konovalov-nk Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XenHat the approach I'd use for major refactors like this is:

  • Cover existing code with tests
  • Make sure when you change logic in the code it makes tests red (some tests can be green all the time, that's a useless test 🙂)
  • Now, refactor and keep tests green

Much easier in the long run to maintain it this way. Plus you can abstract the tests from actual OS/processes via fakes or mocks.

If I'd have some time I'll give you an example how to write tests for these. But it seems the maintainer didn't add any testing framework for this repo 🙂
So I guess we'd have to ask whether or not we should. E.g. jest/vitest @CanadaHonk

Original file line number Diff line number Diff line change
@@ -1,16 +1,74 @@
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}]`, ...args);
const debug = (...args) => console.debug(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process (DEBUG)')}]`, ...args);

import fs from 'node:fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
import DetectableDBTemp from './detectable.json' with { type: 'json' };
DetectableDBTemp.push(
{
aliases: ["Obs"],
executables: [
{ is_launcher: false, name: "obs", os: "linux" },
{ is_launcher: false, name: "obs.exe", os: "win32" },
{ is_launcher: false, name: "obs.app", os: "darwin" }
],
hook: true,
id: "STREAMERMODE",
name: "OBS"
}
);

// New entries and fixups
// Hacky workarounds for now
for(var entry of DetectableDBTemp) {
if(entry.id == "356943187589201930") {
entry.executables.push({ is_launcher: false, name: "dolphin-emu", os: "linux" })
}
if(entry.id == "1257819671114289184") {
entry.executables.push({ is_launcher: false, name: "zenlesszonezero", os: "win32" })
}
}
let FilteredDB = DetectableDBTemp.filter(entry => entry.name !== "Red Dead Online");
const DetectableDB = FilteredDB;

import * as Natives from './native/index.js';
const Native = Natives[process.platform];

// https://stackoverflow.com/a/56641259
/**
* Replaces all occurrences of words in a sentence with new words.
* @function
* @param {string} sentence - The sentence to modify.
* @param {Object} wordsToReplace - An object containing words to be replaced as the keys and their replacements as the values.
* @returns {string} - The modified sentence.
*/
function replaceAll(sentence, wordsToReplace) {
return Object.keys(wordsToReplace).reduce(
(f, s, i) =>
`${f}`.replace(new RegExp(s, 'ig'), wordsToReplace[s]),
sentence
)
}

const bitness_suffixes = {
'.x64': '',
'_64': '',
'x64': '',
'64': '',
}

String.prototype.replaceArray = function(find, replace) {
var replaceString = this;
var regex;
for (var i = 0; i < find.length; i++) {
regex = new RegExp(find[i], "g");
replaceString = replaceString.replace(regex, replace[i]);
}
return replaceString;
};

const timestamps = {}, names = {}, pids = {};
export default class ProcessServer {
Expand All @@ -32,30 +90,83 @@ export default class ProcessServer {
const processes = await Native.getProcesses();
const ids = [];

// log(`got processed in ${(performance.now() - startTime).toFixed(2)}ms`);
// log(`got processes list in ${(performance.now() - startTime).toFixed(2)}ms`);

for (const [ pid, _path, args ] of processes) {
for (const [pid, _path, args, _cwdPath = ''] of processes) {
if (pid === 1) continue // init system
if (_path.length < 1) continue; // process has no name, i.e. kernel thread
if (_path.startsWith('/proc')) continue; // internal *nix stuff
if (_path.startsWith('/usr/lib/')) continue; // internal *nix stuff
if (_path.includes('systemd')) continue;
const cwdPath = _cwdPath.toLowerCase().replaceAll('\\', '/');
const path = _path.toLowerCase().replaceAll('\\', '/');
if (path.startsWith('c:/windows')) continue // system processes (wine)
if (_path.includes('webhelper')) continue; // CEF Processes
if (_path.endsWith('dolphin')) continue; // KDE file manager, not Dolphin Emulator
const toCompare = [];
const splitPath = path.split('/');
for (let i = 1; i < splitPath.length; i++) {
toCompare.push(splitPath.slice(-i).join('/'));
let newPath
if (path.includes(' --')) {
newPath = path.split(' --')[0];
}
else {
newPath = path;
}
newPath = newPath.substr(newPath.lastIndexOf('/') + 1);

for (const p of toCompare.slice()) { // add more possible tweaked paths for less false negatives
toCompare.push(p.replace('64', '')); // remove 64bit identifiers-ish
toCompare.push(p.replace('.x64', ''));
toCompare.push(p.replace('x64', ''));
toCompare.push(p.replace('_64', ''));
// log(`performance checkpoint: ${(performance.now() - startTime).toFixed(2)}ms`);

toCompare.push(newPath);
if (path.includes('.exe')) {
const part2 = path.split('/').slice(-2).join('/');
toCompare.push(part2);
replaceAll(toCompare, bitness_suffixes);
}

// TODO: Convert into an inline function similar to findInObjArray
// TODO: Don't try to match the running executable more than once
for (const { executables, id, name } of DetectableDB) {
if (executables?.some(x => {
if (x.is_launcher) return false;
if (x.name[0] === '>' ? x.name.substring(1) !== toCompare[0] : !toCompare.some(y => x.name === y)) return false;
if (args && x.arguments) return args.join(" ").indexOf(x.arguments) > -1;
return true;
})) {
if (
executables?.some((known_exe) => {
if (known_exe.is_launcher) return false;
if (known_exe.name[0] === '>') {
if (known_exe.name.substring(1) === toCompare[0]) {
// TODO: Deduplicate with that version at the end of the following 'else' statement
if (args && known_exe.arguments) {
// debug(`Match Level 1: "${name}" via ${known_exe.name} <==> ${running}`);
return args.join(" ").indexOf(known_exe.arguments) > -1;
}
}
} else {
if (
toCompare.some((running) => {
// explicit match first
if (known_exe.name === running) {
// debug(`Match Level 2: "${name}" via ${known_exe.name} <==> ${running}`)
return true;
}
// Try comparing against an exe.suffixed version (Linux native games and such)
if (known_exe.name === running + '.exe') {
// debug(`Match Level 3: "${name}" via ${known_exe.name} <==> ${running}`)
return true
}
// Try comparing against an exe-less version (mistake in database)
if (known_exe.name === running.replace('.exe', '')) {
// debug(`Match Level 4: "${name}" via ${known_exe.name} <==> ${running}`)
return true
}
if (`${cwdPath}/${running}`.includes(`/${known_exe.name}`)
) {
// debug(`Match Level 5: "${name}" via [${running}] with 'if ([${cwdPath}}/{${running}].includes(/[${known_exe.name}])'`)
return true;
}
})
) {
return true;
}
}
if (args && known_exe.arguments) return args.join(" ").indexOf(known_exe.arguments) > -1;
})
) {
names[id] = name;
pids[id] = pid;

Expand Down Expand Up @@ -103,6 +214,6 @@ export default class ProcessServer {
}

// log(`finished scan in ${(performance.now() - startTime).toFixed(2)}ms`);
// process.stdout.write(`\r${' '.repeat(100)}\r[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}] scanned (took ${(performance.now() - startTime).toFixed(2)}ms)`);
// process.stdout.write(`\r${' '.repeat(100)}\r[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}] scanned (took ${(performance.now() - startTime).toFixed(2)}ms)\n`);
}
}
10 changes: 8 additions & 2 deletions src/process/native/linux.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { readdir, readFile } from "fs/promises";
import { readdir, readFile, readlink } from "fs/promises";

export const getProcesses = async () => (await Promise.all(
(await readdir("/proc")).map(pid =>
(+pid > 0) && readFile(`/proc/${pid}/cmdline`, 'utf8')
.then(path => [+pid, path.split("\0")[0], path.split("\0").slice(1)], () => 0)
.then(async path => {
let cwdPath;
try {
cwdPath = await readlink(`/proc/${pid}/cwd`);
} catch (err) {};
return [+pid, path.split("\0")[0], path.split("\0").slice(1), cwdPath]
}, () => 0)
)
)).filter(x => x);