import * as fs from 'fs' import * as net from 'net' import * as path from 'path' import { execFileSync, spawn } from 'child_process' import { MAX_SOCKETS, type ClaimedSocket, type ProcessInfo, type RelayPlatform } from './types' const REAL_PREFIX = 'discord-ipc-real-' const FAKE_INDEX = 0 const ALIVE_CHECK_TIMEOUT_MS = 1000 const STALE_SOCKET_MIN_AGE_MS = 10_000 let darwinTempDir: string | null | undefined // $TMPDIR can be overridden by a shell, so ask the OS where Discord really binds. function resolveDarwinTempDir(): string | null { if (darwinTempDir !== undefined) return darwinTempDir try { darwinTempDir = execFileSync('getconf', ['DARWIN_USER_TEMP_DIR'], { encoding: 'utf8', timeout: 1000 }).trim() } catch { darwinTempDir = null } return darwinTempDir } function runtimeDir(): string { if (process.env.XDG_RUNTIME_DIR) return process.env.XDG_RUNTIME_DIR if (process.platform === 'darwin') { return resolveDarwinTempDir() ?? process.env.TMPDIR ?? '/tmp' } return `/run/user/${process.getuid?.() ?? 0}` } function ipcPath(index: number): string { return path.join(runtimeDir(), `discord-ipc-${index}`) } function claimedPath(index: number): string { return path.join(runtimeDir(), `${REAL_PREFIX}${index}`) } export class PosixPlatform implements RelayPlatform { readonly isSupported = true fakeSocketPath(): string { return ipcPath(FAKE_INDEX) } ipcSocketPath(index: number): string { return ipcPath(index) } removeFakeSocket(fakePath: string): void { if (fs.existsSync(fakePath)) fs.unlinkSync(fakePath) } finalizeFakeSocket(fakePath: string): void { fs.chmodSync(fakePath, 0o777) } fakeSocketExists(fakePath: string): boolean { return fs.existsSync(fakePath) } async recoverLeftoverSockets(): Promise { for (let i = 0; i < MAX_SOCKETS; i++) { try { const leftover = claimedPath(i) const original = ipcPath(i) if (!fs.existsSync(leftover)) continue if (!fs.existsSync(original)) { fs.renameSync(leftover, original) continue } if (!(await isSocketAlive(original))) { fs.unlinkSync(original) fs.renameSync(leftover, original) } } catch { // Another process may be touching the same files; keep recovering the rest. } } } async discoverAndClaim(): Promise { const found: ClaimedSocket[] = [] for (let i = 0; i < MAX_SOCKETS; i++) { const claimed = await this.discoverNewSocket(i) if (claimed) found.push(claimed) } return found } async discoverNewSocket(index: number): Promise { const src = ipcPath(index) if (!fs.existsSync(src)) return null if (!(await isSocketAlive(src))) { try { if (Date.now() - fs.statSync(src).mtimeMs > STALE_SOCKET_MIN_AGE_MS) fs.unlinkSync(src) } catch { // Already gone. } return null } const dst = claimedPath(index) try { fs.renameSync(src, dst) } catch { return null // socket replaced between the check and the rename } return { index, path: dst } } restoreSocket(claimed: ClaimedSocket): void { try { const original = ipcPath(claimed.index) if (fs.existsSync(claimed.path) && !fs.existsSync(original)) { fs.renameSync(claimed.path, original) } } catch { // Best effort. } } discardClaimedSocket(claimed: ClaimedSocket): void { try { fs.unlinkSync(claimed.path) } catch { // Already gone. } } isSocketAlive(socketPath: string): Promise { return isSocketAlive(socketPath) } async removeStaleIpcSocket(index: number): Promise { const p = ipcPath(index) if (!fs.existsSync(p)) return if (await isSocketAlive(p)) return try { fs.unlinkSync(p) } catch { // Already gone or replaced. } } getInstanceProcess(index: number): ProcessInfo | null { // lsof/procfs still report the original bind path even after we rename to -real-N. return findSocketOwner(ipcPath(index)) } getPeerProcess(fd: number): ProcessInfo | null { return findPeerProcess(fd) } killProcess(pid: number): boolean { try { process.kill(pid, 'SIGTERM') return true } catch { return false } } isProcessAlive(pid: number): boolean { try { process.kill(pid, 0) return true } catch (err) { // EPERM means it's alive, we just aren't allowed to touch it. return (err as NodeJS.ErrnoException).code === 'EPERM' } } launchExecutable(executable: string): boolean { try { // Point Discord at the temp dir we watch, whatever TMPDIR we inherited. const env = { ...process.env } if (process.platform === 'darwin') { // Spawning the .app binary directly just dies, so let `open` launch it. const canonical = resolveDarwinTempDir() if (canonical) env.TMPDIR = canonical const appPath = executable.includes('.app/') ? executable.slice(0, executable.indexOf('.app/') + '.app'.length) : executable const child = spawn('open', ['-a', appPath], { detached: true, stdio: 'ignore', env }) child.unref() return true } const child = spawn(executable, [], { detached: true, stdio: 'ignore', env }) child.unref() return true } catch { return false } } } // The pid holding the socket is usually a helper; walk up to the real app process. function resolveMainAppProcess(pid: number): ProcessInfo | null { if (process.platform !== 'darwin') return null const exe = executablePath(pid) const bundle = bundleRoot(exe) if (!bundle) return null let mainPid = pid let mainExe = exe for (let depth = 0; depth < 20; depth++) { const ppid = parentPid(mainPid) if (ppid === null || ppid <= 1) break const parentExe = executablePath(ppid) if (bundleRoot(parentExe) !== bundle) break mainPid = ppid mainExe = parentExe } return { pid: mainPid, name: path.basename(mainExe ?? String(mainPid)), executable: mainExe } } function bundleRoot(exe: string | null): string | null { if (!exe) return null const idx = exe.indexOf('.app/Contents/') return idx >= 0 ? exe.slice(0, idx + '.app'.length) : null } function parentPid(pid: number): number | null { try { const out = execFileSync('ps', ['-p', String(pid), '-o', 'ppid='], { encoding: 'utf8', timeout: 1000 }).trim() const ppid = parseInt(out, 10) return Number.isNaN(ppid) ? null : ppid } catch { return null } } function executablePath(pid: number): string | null { try { if (process.platform === 'linux') { return fs.readlinkSync(`/proc/${pid}/exe`) } if (process.platform === 'darwin') { const out = execFileSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf8', timeout: 1000 }).trim() return out || null } } catch { return null } return null } function isSocketAlive(socketPath: string): Promise { return new Promise((resolve) => { const sock = net.createConnection(socketPath) let settled = false const done = (alive: boolean): void => { if (settled) return settled = true sock.destroy() resolve(alive) } sock.setTimeout(ALIVE_CHECK_TIMEOUT_MS, () => done(false)) sock.once('connect', () => done(true)) sock.once('error', () => done(false)) }) } const SOCKET_OWNER_CACHE_MS = 2000 const socketOwnerCache = new Map() function findSocketOwner(socketPath: string): ProcessInfo | null { if (process.platform !== 'linux' && process.platform !== 'darwin') return null const cached = socketOwnerCache.get(socketPath) if (cached && Date.now() - cached.at < SOCKET_OWNER_CACHE_MS) { return cached.value } const result = process.platform === 'darwin' ? locateSocketOwnerMacOS(socketPath) : locateSocketOwner(socketPath) socketOwnerCache.set(socketPath, { value: result, at: Date.now() }) return result } function locateSocketOwnerMacOS(socketPath: string): ProcessInfo | null { try { const output = execFileSync('lsof', ['-U', '-F', 'pcn'], { encoding: 'utf8', timeout: 3000 }) let currentPid = 0 let currentName = '' for (const line of output.split('\n')) { if (line.startsWith('p')) { currentPid = parseInt(line.slice(1), 10) currentName = '' } else if (line.startsWith('c')) { currentName = line.slice(1) } else if (line.startsWith('n') && line.slice(1) === socketPath) { if (currentPid > 0 && currentPid !== process.pid) { return ( resolveMainAppProcess(currentPid) ?? { pid: currentPid, name: currentName, executable: executablePath(currentPid) } ) } } } } catch { return null } return null } function locateSocketOwner(socketPath: string): ProcessInfo | null { for (const inode of findInodesForPath(socketPath)) { const owner = findProcessForInode(inode) if (owner && owner.pid !== process.pid) return owner } return null } function findInodesForPath(socketPath: string): string[] { const inodes: string[] = [] try { const unixTable = fs.readFileSync('/proc/net/unix', 'utf8') for (const line of unixTable.split('\n')) { if (line.endsWith(` ${socketPath}`)) { inodes.push(line.trim().split(/\s+/)[6]) } } } catch { return inodes } return inodes } function findProcessForInode(inode: string): ProcessInfo | null { return findProcessForInodes(new Set([inode])) } function findProcessForInodes(inodes: Set): ProcessInfo | null { try { for (const pidStr of fs.readdirSync('/proc')) { if (!/^\d+$/.test(pidStr)) continue if (parseInt(pidStr, 10) === process.pid) continue const fdDir = `/proc/${pidStr}/fd` let fds: string[] try { fds = fs.readdirSync(fdDir) } catch { continue } for (const fd of fds) { let link: string try { link = fs.readlinkSync(path.join(fdDir, fd)) } catch { continue } const match = /^socket:\[(\d+)\]$/.exec(link) if (match && inodes.has(match[1])) { const pid = parseInt(pidStr, 10) return { pid, name: processName(pidStr), executable: executablePath(pid) } } } } } catch { return null } return null } function processName(pidStr: string): string { try { return fs.readFileSync(`/proc/${pidStr}/comm`, 'utf8').trim() } catch { return pidStr } } interface GetSockOpt { (sockfd: number, level: number, optname: number, optval: Buffer, optlen: Buffer): number } let getsockopt: GetSockOpt | null | undefined function loadGetSockOpt(): GetSockOpt | null { if (getsockopt !== undefined) return getsockopt if (process.platform !== 'linux' && process.platform !== 'darwin') { getsockopt = null return getsockopt } try { // eslint-disable-next-line @typescript-eslint/no-require-imports const koffi = require('koffi') const libName = process.platform === 'darwin' ? 'libSystem.B.dylib' : 'libc.so.6' const libc = koffi.load(libName) getsockopt = libc.func( 'int getsockopt(int sockfd, int level, int optname, void *optval, int *optlen)' ) as GetSockOpt } catch { getsockopt = null } return getsockopt } const SOL_SOCKET = 1 const SO_PEERCRED = 17 const UCRED_SIZE = 12 const SOL_LOCAL = 0 const LOCAL_PEERPID = 2 function findPeerProcess(localFd: number): ProcessInfo | null { const fn = loadGetSockOpt() if (!fn) return null if (process.platform === 'darwin') { const optval = Buffer.alloc(4) const optlen = Buffer.alloc(4) optlen.writeInt32LE(4, 0) if (fn(localFd, SOL_LOCAL, LOCAL_PEERPID, optval, optlen) !== 0) return null const pid = optval.readInt32LE(0) if (pid <= 0) return null return { pid, name: macProcessName(pid), executable: executablePath(pid) } } const optval = Buffer.alloc(UCRED_SIZE) const optlen = Buffer.alloc(4) optlen.writeInt32LE(UCRED_SIZE, 0) if (fn(localFd, SOL_SOCKET, SO_PEERCRED, optval, optlen) !== 0) return null const pid = optval.readInt32LE(0) if (pid <= 0) return null return { pid, name: processName(String(pid)), executable: executablePath(pid) } } function macProcessName(pid: number): string { try { return execFileSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf8', timeout: 1000 }).trim() } catch { return String(pid) } }