// Frida host (Node ESM) for the pdb-capture agent. // - Attaches the agent (rbx_pdb_capture.js) to rekordbox processes. // - Streams ALL their output to THIS console (agent lines are [pid]-tagged). // - STAYS ALIVE via the Node event loop -- no frida-CLI REPL/EOF problem. // - Uses CHILD-GATING (Windows-supported; spawn-gating is not) so children of a // gated process are instrumented AT BIRTH. This is how we reach the // rekordboxAgent that writes export.pdb: it's CIG-protected and refuses // post-hoc injection, but as a freshly-spawned child it can be hooked. // - Clears the captures/ folder on start. // // Setup (Windows, repo root, once): npm i frida // Run: // node tools\capture.mjs --spawn (RECOMMENDED: launch rekordbox under the // gate so every child incl. the DB agent is // hooked at birth. Close rekordbox first.) // node tools\capture.mjs (attach to running procs + gate their // future children; restart rekordbox to // respawn the agent as a gated child.) import * as fridaNs from "frida"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const frida = fridaNs.default ?? fridaNs; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REKORDBOX = "C:\\Program Files\\rekordbox\\rekordbox 7.0.5\\rekordbox.exe"; const AGENT = fs.readFileSync(path.join(__dirname, "rbx_pdb_capture.js"), "utf8"); const CAP = path.join(__dirname, "..", "captures"); const SPAWN = process.argv.includes("--spawn"); function clearCaptures() { fs.mkdirSync(CAP, { recursive: true }); let n = 0; for (const f of fs.readdirSync(CAP)) { try { fs.rmSync(path.join(CAP, f), { force: true, recursive: true }); n++; } catch (e) {} } console.log(`[host] cleared ${n} file(s) from ${CAP}`); } function isRbx(name) { return /rekordbox/i.test(name || ""); } // Attach, load the agent, and gate this process's future children too. async function instrument(device, pid, label) { try { const session = await device.attach(pid); session.detached.connect(reason => console.log(`[host] detached ${label} (${pid}): ${reason}`)); const script = await session.createScript(AGENT); script.logHandler = (level, text) => console.log(text); script.message.connect(msg => { if (msg.type === "error") console.error(`[host] ${label} (${pid}) script error:`, msg.stack || msg.description || msg); }); await script.load(); try { await session.enableChildGating(); } catch (e) { /* not always available */ } console.log(`[host] instrumented ${label} (${pid})`); return session; } catch (e) { console.error(`[host] FAILED to instrument ${label} (${pid}): ${e.message}`); return null; } } async function main() { clearCaptures(); const device = await frida.getLocalDevice(); // Children of any gated process are suspended at birth -> instrument, resume. device.childAdded.connect(async child => { const name = child.identifier || child.path || String(child.pid); try { console.log(`[host] child spawned: ${name} (${child.pid})`); if (isRbx(name)) await instrument(device, child.pid, "child:" + name); } catch (e) { console.error(`[host] child handler error (${child.pid}): ${e.message}`); } finally { try { await device.resume(child.pid); } catch (_) {} } }); if (SPAWN) { console.log(`[host] spawning ${REKORDBOX}`); const pid = await device.spawn([REKORDBOX]); await instrument(device, pid, "main"); // enables child gating on main await device.resume(pid); console.log(`[host] resumed main rekordbox (${pid}); children caught at birth`); } else { for (const p of await device.enumerateProcesses()) { if (isRbx(p.name)) await instrument(device, p.pid, p.name); } console.log("[host] attached to running rekordbox processes (+child gating)."); console.log("[host] >>> the export.pdb writer (rekordboxAgent) refuses live injection."); console.log("[host] >>> RESTART rekordbox -- the new agent spawns as a GATED CHILD and"); console.log("[host] >>> gets hooked at birth. Then Export and watch for [SAVED] ...export.pdb"); console.log("[host] >>> (or re-run with --spawn to launch rekordbox under the gate.)"); } console.log("[host] ready. Ctrl-C to stop."); process.stdin.resume(); process.on("SIGINT", () => { console.log("\n[host] stopping."); process.exit(0); }); } main().catch(e => { console.error("[host] fatal:", e); process.exit(1); });