Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions scripts/tests/ignore.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import test from "node:test";
import assert from "node:assert/strict";
import { shouldIgnoreProcess } from "../../src/process/index.js";

const ignores = {
linux: [
"/proc/123/exe",
// Important: don't confuse KDE file manager with Dolphin emulator (dolphin-emu)
"/usr/bin/dolphin",
],
win32: [
"C:\\Windows\\system32\\winedevice.exe",
"C:\\Windows\\System32\\wdhoersvc.exe",
],
};

const keeps = {
// Linux-native binaries (Steam/others)
linux: [
"/home/user/.local/share/Steam/steamapps/common/7 Days To Die/7DaysToDie.x86_64",
"/home/user/.local/share/Steam/steamapps/common/Hollow Knight/hollow_knight.x86_64",
"/home/user/.local/share/Steam/steamapps/common/Valheim/valheim.x86_64",
"/home/user/.local/share/Steam/steamapps/common/X4 Foundations/testandlaunch",
"/home/user/.local/share/Steam/steamapps/common/Factorio/bin/x64/factorio",
// Wrapper binary (no path)
"hytale-launcher-wrapper",
"/usr/bin/dolphin-emu",
"/usr/bin/obs",
"obs", // flatpak
],
// Windows executables launched via Wine/Proton/Lutris (paths live on Linux FS, but "platform" is win32)
win32: [
"/home/user/.local/share/Steam/steamapps/common/7 Days To Die/7dLauncher.exe",
"/home/user/.local/share/Steam/steamapps/common/Hollow Knight/hollow_knight.exe",
"/home/user/.local/share/Steam/steamapps/common/Valheim/valheim.exe",
"/home/user/.local/share/Steam/steamapps/common/X4 Foundations/X4.exe",
"/home/user/.local/share/Steam/steamapps/common/Factorio/bin/x64/factorio.exe",
"/home/user/.local/share/Steam/steamapps/common/FINAL FANTASY XIV Online/game/ffxiv_dx11.exe",
"/home/user/.local/share/Steam/steamapps/common/Wuthering Waves/Wuthering Waves.exe",
"/home/user/Games/Lutris/arknights-endfield/drive_c/Program Files/GRYPHLINK/games/EndField Game/Endfield.exe",
"ZenlessZoneZero.exe",
"obs.exe",
],
};

Object.entries(ignores).forEach(([os, paths]) => {
paths.forEach((path) => {
test(`ignores (${os}): ${path}`, () => {
assert.equal(shouldIgnoreProcess(path, os), true);
});
});
});

Object.entries(keeps).forEach(([os, paths]) => {
paths.forEach((path) => {
test(`keeps (${os}): ${path}`, () => {
assert.equal(shouldIgnoreProcess(path, os), false);
});
});
});
25 changes: 25 additions & 0 deletions scripts/tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env node

import { glob } from "node:fs";
import { promisify } from "node:util";

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(255, 165, 0, "tests")}]`,
...args,
);

const globAsync = promisify(glob);

log("Running test suite...\n");

const testFiles = await globAsync(
new URL("./**.test.js", import.meta.url).pathname,
);

for (const testFile of testFiles) {
await import(testFile);
}

log("\nTest suite completed");
23 changes: 23 additions & 0 deletions scripts/tests/process.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createGetProcesses } from "../../src/process/native/linux.js";

test("parses cmdline and cwd correctly", async () => {
const fakeFS = {
async readdir() {
return ["123"];
},
async readFile() {
return "game\0--foo\0";
},
async readlink() {
return "/home/user/Games";
},
};

const getProcesses = createGetProcesses(fakeFS);
const rows = await getProcesses();

assert.equal(rows[0].pid, 123);
assert.equal(rows[0].exe, "game");
});
209 changes: 168 additions & 41 deletions src/process/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,145 @@ 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",
});

const extraExecutablesById = new Map([
[
"356943187589201930",
[{ is_launcher: false, name: "dolphin-emu", os: "linux" }],
],
[
"1257819671114289184",
[{ is_launcher: false, name: "ZenlessZoneZero.exe", os: "win32" }],
],
[
"451540911172747284",
[{ is_launcher: false, name: "bms_linux", os: "linux" }],
],
[
"1158877933042143272",
[{ is_launcher: false, name: "linuxsteamrt64/cs2", os: "linux" }],
]
]);

for (const entry of DetectableDBTemp) {
const extras = extraExecutablesById.get(entry.id);
if (extras) entry.executables.push(...extras);
}
const DetectableDB = DetectableDBTemp;

import * as Natives from './native/index.js';
import * as Natives from "./native/index.js";
// eslint-disable-next-line no-undef
const Native = Natives[process.platform];

const IGNORE_RULES = {
linux: [
/^\/proc\//,
/k(worker|softirq)/, // Linux kernel worker threads
/^\/(usr|)\/(bin|sbin|lib)\/(?!(dolphin-emu|obs))/,
/\/crashpad_handler$/,
/webhelper/,
/^\/tmp\//,
/(\/bin\/|)dolphin$/, // KDE, not emulator
],
win32: [
/^C:\\Windows/i,
(path) => path.includes("\\") && !path.includes("/"), // pure win path
],
};

/** Check our rules to determine if a process should be ignored **/
export function shouldIgnoreProcess(path, os) {
const rules = IGNORE_RULES[os] || [];
return rules.some((rule) =>
typeof rule === "function" ? rule(path) : rule.test(path),
);
}

// ------------------------ Refactor helpers -------------------------------
/** Normalize a filesystem path for comparisons. */
const normPath = (p = "") => String(p).toLowerCase().replaceAll("\\", "/");

/** Strip common 64-bit suffixes from executable names. */
const stripBitness = (s = "") => {
const bitness_suffixes = [".x86_64", ".x64", "_64", "64"];
for (const suf of bitness_suffixes) {
if (s.endsWith(suf)) return s.slice(0, -suf.length);
}
return s;
};

/**
* Build a compact set of candidates we will try to match against the DB.
* Examples returned: ['eldenring.exe', 'eldenring', 'steamapps/common/eldenring.exe']
*/
const buildCandidates = (rawPath, cwdPath) => {
const out = new Set();
const p = normPath(rawPath);

// Drop CLI args if present (e.g., "C:/Games/foo/bar.exe --flag ...")
const noArgs = p.includes(" --") ? p.split(" --")[0] : p;
const base = noArgs.slice(noArgs.lastIndexOf("/") + 1);

out.add(stripBitness(base));

// For Windows-style exe paths, include the last 2 segments to catch DB entries
// We also need to match Linux Native executables, since we swap the suffix later for comparison
if (noArgs.includes(".exe") || noArgs.includes(".x86_64")) {
const last2 = noArgs.split("/").slice(-2).join("/");
out.add(stripBitness(last2));
}

// Also include a cwd-anchored variant to help path.includes matches
if (cwdPath) out.add(`${normPath(cwdPath)}/${stripBitness(base)}`);

// Add exe-less variant if present
if (base.endsWith(".exe")) out.add(base.replace(/\.exe$/, ""));

return Array.from(out);
};

/**
* Decide whether a known executable entry matches the running process.
* Mirrors legacy behavior but is easier to read & extend.
*/
const matchesKnownExe = (known, candidates, cwdPath, argsStr) => {
if (!known || known.is_launcher) return false;
const kname = known.name || "";
const needsArgs = Boolean(known.arguments);
const hasReqArgs =
!needsArgs || (argsStr && argsStr.includes(known.arguments));

// Special '>' syntax: require exact match to the first candidate
if (kname[0] === ">") {
return candidates[0] === kname.slice(1) && hasReqArgs;
}

// Try direct name and common variants across all candidates
for (const cand of candidates) {
const running = cand;
if (kname === running) return hasReqArgs;
if (kname === `${running}.exe`) return hasReqArgs;
if (kname === running.replace(/\.exe$/, "")) return hasReqArgs;
if (String(running).includes(`/${kname}`)) return hasReqArgs; // handles cwd + filename
if (kname === running.replace(/\.x86_64$/, ".exe")) return hasReqArgs;
}

// Last resort: allow arg-only matches (previous behavior)
return needsArgs && hasReqArgs;
};
// -------------------------------------------------------------------------

const timestamps = {}, names = {}, pids = {};
export default class ProcessServer {
Expand All @@ -34,28 +167,22 @@ export default class ProcessServer {

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

for (const [ pid, _path, args ] of processes) {
const path = _path.toLowerCase().replaceAll('\\', '/');
const toCompare = [];
const splitPath = path.split('/');
for (let i = 1; i < splitPath.length; i++) {
toCompare.push(splitPath.slice(-i).join('/'));
}

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', ''));
}
// TODO: Make sure this works on windows (see in src/process/win32.js)
for (const { pid, exe: _path, args, cwd: _cwdPath = "" } of processes) {
if (shouldIgnoreProcess(_path, process.platform)) continue;
const argsStr = Array.isArray(args) ? args.join(" ") : "";
const path = _path.toLowerCase().replaceAll("\\", "/");
const cwdPath = _cwdPath.toLowerCase().replaceAll("\\", "/");
const toCompare = buildCandidates(path, cwdPath);

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 || !Array.isArray(executables)) continue;

const matched = executables.some((k) =>
matchesKnownExe(k, toCompare, cwdPath, argsStr),
);
if (!matched) continue;
{
names[id] = name;
pids[id] = pid;

Expand All @@ -68,19 +195,19 @@ export default class ProcessServer {
// Resending this on evry scan is intentional, so that in the case that arRPC scans processes before Discord, existing activities will be sent
this.handlers.message({
socketId: id
}, {
cmd: 'SET_ACTIVITY',
args: {
activity: {
application_id: id,
name,
timestamps: {
}, {
cmd: 'SET_ACTIVITY',
args: {
activity: {
application_id: id,
name,
timestamps: {
start: timestamps[id]
}
},
},
pid
}
});
}
});
}
}
}
Expand All @@ -91,14 +218,14 @@ export default class ProcessServer {
delete timestamps[id];

this.handlers.message({
socketId: id
}, {
cmd: 'SET_ACTIVITY',
args: {
activity: null,
pid: pids[id]
}
});
socketId: id
}, {
cmd: 'SET_ACTIVITY',
args: {
activity: null,
pid: pids[id]
}
});
}
}

Expand Down
Loading