import { Effect } from "effect"; import type { TerminalProcessInspectionError } from "./Services/TerminalProcessInspector.ts"; import { MAX_PORT_NUMBER, portFromAddress } from "./utils.ts"; interface InspectorCommandResult { readonly stdout: string; readonly stderr: string; readonly exitCode: number; } interface PosixRunCommand { ( input: Readonly<{ operation: string; terminalPid: number; command: string; args: ReadonlyArray; timeoutMs: number; maxOutputBytes: number; }>, ): Effect.Effect; } export const collectPosixProcessFamilyPids = Effect.fn("process.collectPosixProcessFamilyPids")( function* ( terminalPid: number, runCommand: PosixRunCommand, ): Effect.fn.Return { const psResult = yield* runCommand({ operation: "TerminalProcessInspector.collectPosixProcessFamilyPids", terminalPid, command: "ps", args: ["-eo", "pid=,ppid="], timeoutMs: 1_000, maxOutputBytes: 262_144, }); if (psResult.exitCode !== 0) { return []; } const childrenByParentPid = new Map(); for (const line of psResult.stdout.split(/\r?\n/g)) { const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); const pid = Number(pidRaw); const ppid = Number(ppidRaw); if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; const children = childrenByParentPid.get(ppid); if (children) { children.push(pid); } else { childrenByParentPid.set(ppid, [pid]); } } const processFamily = new Set([terminalPid]); const pendingParents = [terminalPid]; while (pendingParents.length > 0) { const parentPid = pendingParents.shift(); if (!parentPid) continue; const childPids = childrenByParentPid.get(parentPid); if (!childPids || childPids.length === 0) continue; for (const childPid of childPids) { if (processFamily.has(childPid)) continue; processFamily.add(childPid); pendingParents.push(childPid); } } return [...processFamily]; }, ); export const checkPosixListeningPorts = Effect.fn("process.checkPosixListeningPorts")(function* ( processIds: number[], input: { terminalPid: number; runCommand: PosixRunCommand; platform?: NodeJS.Platform; }, ): Effect.fn.Return { if (processIds.length === 0) return []; const ports = new Set(); const pidFilter = new Set(processIds); const platform = input.platform ?? process.platform; const lsofResult = yield* input .runCommand({ operation: "TerminalProcessInspector.checkPosixListeningPorts.lsof", terminalPid: input.terminalPid, command: "lsof", args: ["-nP", "-a", "-iTCP", "-sTCP:LISTEN", "-p", processIds.join(",")], timeoutMs: 1_500, maxOutputBytes: 262_144, }) .pipe(Effect.exit); if (lsofResult._tag === "Success") { if (lsofResult.value.exitCode === 0) { for (const line of lsofResult.value.stdout.split(/\r?\n/g)) { const match = line.match(/:(\d+)\s+\(LISTEN\)$/); if (!match?.[1]) continue; const port = Number(match[1]); if (Number.isInteger(port) && port > 0 && port <= MAX_PORT_NUMBER) { ports.add(port); } } return [...ports].toSorted((left, right) => left - right); } if (lsofResult.value.exitCode === 1 && platform === "darwin") { return []; } } const ssResult = yield* input.runCommand({ operation: "TerminalProcessInspector.checkPosixListeningPorts.ss", terminalPid: input.terminalPid, command: "ss", args: ["-ltnp"], timeoutMs: 1_500, maxOutputBytes: 524_288, }); if (ssResult.exitCode !== 0) { return []; } for (const line of ssResult.stdout.split(/\r?\n/g)) { if (!line.includes("pid=")) continue; const localAddress = line.trim().split(/\s+/g)[3]; if (!localAddress) continue; const port = portFromAddress(localAddress); if (port === null) continue; const pidMatches = [...line.matchAll(/pid=(\d+)/g)]; if (pidMatches.length === 0) continue; if ( pidMatches.some((match) => { const pid = Number(match[1]); return Number.isInteger(pid) && pidFilter.has(pid); }) ) { ports.add(port); } } return [...ports].toSorted((left, right) => left - right); });