2026-06-15 02:26:16 +07:00
|
|
|
import * as fs from 'fs'
|
|
|
|
|
import * as net from 'net'
|
|
|
|
|
import * as path from 'path'
|
2026-06-15 18:52:23 +07:00
|
|
|
import { execFileSync } from 'child_process'
|
2026-07-12 05:35:04 +07:00
|
|
|
import { MAX_SOCKETS, type ClaimedSocket, type ProcessInfo, type RelayPlatform } from './types'
|
2026-06-15 02:26:16 +07:00
|
|
|
|
|
|
|
|
const REAL_PREFIX = 'discord-ipc-real-'
|
|
|
|
|
const FAKE_INDEX = 0
|
2026-07-12 05:35:04 +07:00
|
|
|
const ALIVE_CHECK_TIMEOUT_MS = 1000
|
|
|
|
|
const STALE_SOCKET_MIN_AGE_MS = 10_000
|
2026-06-15 02:26:16 +07:00
|
|
|
|
|
|
|
|
function runtimeDir(): string {
|
|
|
|
|
if (process.env.XDG_RUNTIME_DIR) return process.env.XDG_RUNTIME_DIR
|
|
|
|
|
if (process.platform === 'darwin') return 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<void> {
|
|
|
|
|
for (let i = 0; i < MAX_SOCKETS; i++) {
|
2026-07-12 05:35:04 +07:00
|
|
|
try {
|
|
|
|
|
const leftover = claimedPath(i)
|
|
|
|
|
const original = ipcPath(i)
|
|
|
|
|
if (!fs.existsSync(leftover)) continue
|
2026-06-15 02:26:16 +07:00
|
|
|
|
2026-07-12 05:35:04 +07:00
|
|
|
if (!fs.existsSync(original)) {
|
|
|
|
|
fs.renameSync(leftover, original)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-06-15 02:26:16 +07:00
|
|
|
|
2026-07-12 05:35:04 +07:00
|
|
|
if (!(await isSocketAlive(original))) {
|
|
|
|
|
fs.unlinkSync(original)
|
|
|
|
|
fs.renameSync(leftover, original)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Another process may be touching the same files; keep recovering the rest.
|
2026-06-15 02:26:16 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 05:35:04 +07:00
|
|
|
async discoverAndClaim(): Promise<ClaimedSocket[]> {
|
2026-06-15 02:26:16 +07:00
|
|
|
const found: ClaimedSocket[] = []
|
|
|
|
|
for (let i = 0; i < MAX_SOCKETS; i++) {
|
2026-07-12 05:35:04 +07:00
|
|
|
const claimed = await this.discoverNewSocket(i)
|
2026-06-15 02:26:16 +07:00
|
|
|
if (claimed) found.push(claimed)
|
|
|
|
|
}
|
|
|
|
|
return found
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 05:35:04 +07:00
|
|
|
async discoverNewSocket(index: number): Promise<ClaimedSocket | null> {
|
2026-06-15 02:26:16 +07:00
|
|
|
const src = ipcPath(index)
|
|
|
|
|
if (!fs.existsSync(src)) return null
|
|
|
|
|
|
2026-07-12 05:35:04 +07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 02:26:16 +07:00
|
|
|
const dst = claimedPath(index)
|
2026-07-12 05:35:04 +07:00
|
|
|
try {
|
|
|
|
|
fs.renameSync(src, dst)
|
|
|
|
|
} catch {
|
|
|
|
|
return null // socket replaced between the check and the rename
|
|
|
|
|
}
|
2026-06-15 02:26:16 +07:00
|
|
|
return { index, path: dst }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restoreSocket(claimed: ClaimedSocket): void {
|
2026-07-12 05:35:04 +07:00
|
|
|
try {
|
|
|
|
|
const original = ipcPath(claimed.index)
|
|
|
|
|
if (fs.existsSync(claimed.path) && !fs.existsSync(original)) {
|
|
|
|
|
fs.renameSync(claimed.path, original)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Best effort.
|
2026-06-15 02:26:16 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 05:35:04 +07:00
|
|
|
discardClaimedSocket(claimed: ClaimedSocket): void {
|
|
|
|
|
try {
|
|
|
|
|
fs.unlinkSync(claimed.path)
|
|
|
|
|
} catch {
|
|
|
|
|
// Already gone.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSocketAlive(socketPath: string): Promise<boolean> {
|
|
|
|
|
return isSocketAlive(socketPath)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 02:26:16 +07:00
|
|
|
getInstanceProcess(index: number): ProcessInfo | null {
|
2026-06-15 18:52:23 +07:00
|
|
|
// /proc/net/unix reports the original bind path even after the file is renamed on disk
|
2026-06-15 02:26:16 +07:00
|
|
|
return findSocketOwner(ipcPath(index))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPeerProcess(fd: number): ProcessInfo | null {
|
|
|
|
|
return findPeerProcess(fd)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSocketAlive(socketPath: string): Promise<boolean> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const sock = net.createConnection(socketPath)
|
2026-07-12 05:35:04 +07:00
|
|
|
let settled = false
|
|
|
|
|
const done = (alive: boolean): void => {
|
|
|
|
|
if (settled) return
|
|
|
|
|
settled = true
|
2026-06-15 02:26:16 +07:00
|
|
|
sock.destroy()
|
2026-07-12 05:35:04 +07:00
|
|
|
resolve(alive)
|
|
|
|
|
}
|
|
|
|
|
sock.setTimeout(ALIVE_CHECK_TIMEOUT_MS, () => done(false))
|
|
|
|
|
sock.once('connect', () => done(true))
|
|
|
|
|
sock.once('error', () => done(false))
|
2026-06-15 02:26:16 +07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SOCKET_OWNER_CACHE_MS = 2000
|
|
|
|
|
const socketOwnerCache = new Map<string, { value: ProcessInfo | null; at: number }>()
|
|
|
|
|
|
|
|
|
|
function findSocketOwner(socketPath: string): ProcessInfo | null {
|
2026-06-15 18:52:23 +07:00
|
|
|
if (process.platform !== 'linux' && process.platform !== 'darwin') return null
|
2026-06-15 02:26:16 +07:00
|
|
|
|
|
|
|
|
const cached = socketOwnerCache.get(socketPath)
|
|
|
|
|
if (cached && Date.now() - cached.at < SOCKET_OWNER_CACHE_MS) {
|
|
|
|
|
return cached.value
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 18:52:23 +07:00
|
|
|
const result =
|
|
|
|
|
process.platform === 'darwin'
|
|
|
|
|
? locateSocketOwnerMacOS(socketPath)
|
|
|
|
|
: locateSocketOwner(socketPath)
|
2026-06-15 02:26:16 +07:00
|
|
|
socketOwnerCache.set(socketPath, { value: result, at: Date.now() })
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 18:52:23 +07:00
|
|
|
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 { pid: currentPid, name: currentName }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 02:26:16 +07:00
|
|
|
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<string>): 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])) {
|
|
|
|
|
return { pid: parseInt(pidStr, 10), name: processName(pidStr) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} 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
|
|
|
|
|
|
2026-06-15 18:52:23 +07:00
|
|
|
if (process.platform !== 'linux' && process.platform !== 'darwin') {
|
2026-06-15 02:26:16 +07:00
|
|
|
getsockopt = null
|
|
|
|
|
return getsockopt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
|
|
|
const koffi = require('koffi')
|
2026-06-15 18:52:23 +07:00
|
|
|
const libName = process.platform === 'darwin' ? 'libSystem.B.dylib' : 'libc.so.6'
|
|
|
|
|
const libc = koffi.load(libName)
|
2026-06-15 02:26:16 +07:00
|
|
|
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
|
2026-06-15 18:52:23 +07:00
|
|
|
const UCRED_SIZE = 12
|
|
|
|
|
const SOL_LOCAL = 0
|
|
|
|
|
const LOCAL_PEERPID = 2
|
2026-06-15 02:26:16 +07:00
|
|
|
|
|
|
|
|
function findPeerProcess(localFd: number): ProcessInfo | null {
|
|
|
|
|
const fn = loadGetSockOpt()
|
|
|
|
|
if (!fn) return null
|
|
|
|
|
|
2026-06-15 18:52:23 +07:00
|
|
|
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) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 02:26:16 +07:00
|
|
|
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)) }
|
|
|
|
|
}
|
2026-06-15 18:52:23 +07:00
|
|
|
|
|
|
|
|
function macProcessName(pid: number): string {
|
|
|
|
|
try {
|
|
|
|
|
return execFileSync('ps', ['-p', String(pid), '-o', 'comm='], {
|
|
|
|
|
encoding: 'utf8',
|
|
|
|
|
timeout: 1000
|
|
|
|
|
}).trim()
|
|
|
|
|
} catch {
|
|
|
|
|
return String(pid)
|
|
|
|
|
}
|
|
|
|
|
}
|