diff --git a/packages/bridge/src/injector.ts b/packages/bridge/src/injector.ts index 70f01e64..df491faa 100644 --- a/packages/bridge/src/injector.ts +++ b/packages/bridge/src/injector.ts @@ -2,6 +2,8 @@ import { existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { listSnowlumaPipePidsSync } from './qq-hook-client'; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); export interface ManualMapHandle { @@ -98,6 +100,17 @@ export function getNativeHookLoadError(): string | null { } export function listHookProcesses(): HookProcessBaseInfo[] { + // macOS: no native enumerate-QQ addon. The DYLD_INSERT injection model + // means QQ ran with our dylib already mapped, so the dylib's listener + // socket IS the discovery signal. Treat every `mojo..control.sock` + // in the runtime dir as a known process (the watcher's subsequent + // connectable-probe gates the pipe-up emit, so a stale dangling socket + // here doesn't drive a false connect). + if (process.platform === 'darwin') { + return [...listSnowlumaPipePidsSync()] + .sort((a, b) => a - b) + .map(pid => ({ pid, name: defaultProcessName(), path: '' })); + } const addon = getNativeHookAddon(); if (!addon) return []; return [...new Set(addon.getAllMainProcess())] diff --git a/packages/bridge/src/qq-hook-client.ts b/packages/bridge/src/qq-hook-client.ts index 0651bbc8..5b08a8be 100644 --- a/packages/bridge/src/qq-hook-client.ts +++ b/packages/bridge/src/qq-hook-client.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { promises as fs } from 'fs'; +import { readdirSync, promises as fs } from 'fs'; import net from 'net'; import os from 'os'; import path from 'path'; @@ -85,12 +85,59 @@ interface PendingAck { wantReply: boolean; } +/** + * Sync directory scan for `mojo..control.sock` socket files in the + * runtime dir, returning the pid set. Used by the darwin path of + * `listHookProcesses()` — on macOS the DYLD_INSERT injection model means + * there is no native enumerate-QQ-processes addon; the existence of our + * dylib's listener socket IS the proof that a QQ process is hooked. The + * sync variant is needed because the watcher's `listProcesses` dep is sync. + * + * Mirrors `listLiveLinuxPipePids` (the async + connectable-probe variant) + * but skips the connectable probe — the existence of the socket file is + * sufficient signal for "process exists with our dylib mapped". The async + * variant still gates the subsequent `pipe-up` emit via a real connect. + */ +export function listSnowlumaPipePidsSync( + runtimeDir = linuxRuntimeDir(), +): Set { + const result = new Set(); + let names: string[]; + try { + names = readdirSync(runtimeDir); + } catch { + return result; + } + for (const name of names) { + const m = /^mojo\.(\d+)\.control\.sock$/i.exec(name); + if (!m) continue; + const pid = Number(m[1]); + if (Number.isInteger(pid) && pid > 0) result.add(pid); + } + return result; +} + function linuxRuntimeDir(): string { + // Name kept for historical reasons (the function predates the macOS port); + // applies to every non-win32 platform. The fallback order mirrors each + // platform hook's runtime_dir(): explicit override first, then the OS- + // native ephemeral-dir convention. const explicit = process.env.SNOWLUMA_HOOK_RUNTIME_DIR; if (explicit && explicit.length > 0) return explicit; const xdg = process.env.XDG_RUNTIME_DIR; if (xdg && xdg.length > 0) return xdg; - // Mirrors hook_stub.cpp runtime_dir() fallback. + // macOS: dylib defaults to "$TMPDIR/snowluma-hook" (apps/qq/macos/ + // qq_hook_dylib.cpp's runtime_dir()). $TMPDIR is per-user on macOS + // (under /var/folders/.../T) so this is the right place — Linux's + // /tmp/snowluma- doesn't exist on a stock Mac. + if (process.platform === 'darwin') { + const tmpdir = process.env.TMPDIR; + if (tmpdir && tmpdir.length > 0) { + const trimmed = tmpdir.endsWith('/') ? tmpdir.slice(0, -1) : tmpdir; + return path.join(trimmed, 'snowluma-hook'); + } + } + // Linux fallback: mirrors hook_stub.cpp runtime_dir() last-resort. // process.geteuid is POSIX-only; cast to allow non-Linux type checks. const uid = typeof process.geteuid === 'function' ? process.geteuid() : os.userInfo().uid; return `/tmp/snowluma-${uid}`;