{"version":3,"file":"process-identity.d.ts","sourceRoot":"","sources":["../../../src/core/jobs/process-identity.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,MAAM,WAAW,mBAAmB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,OAAO,CAAC;CACf;AA6FD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAOnF;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAC9B,IAAI,EAAE,mBAAmB,EACzB,QAAQ,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAkBrC;AAOD,eAAO,MAAM,YAAY,uCAAwD,CAAC","sourcesContent":["import { readFile } from \"node:fs/promises\";\n\n/**\n * Process-identity primitives for authoritative job ownership and PID-reuse\n * protection. Ownership must NEVER rely on PID alone.\n */\n\nexport interface ProcessIdentityInfo {\n\tpid: number;\n\t/** Command line (cmdline joined with spaces). */\n\tcommandLine: string;\n\t/** Start identity: epoch seconds (POSIX) or creation timestamp (Windows). */\n\tstartIdentity?: number;\n\talive: boolean;\n}\n\nfunction linuxCmdline(pid: number): string | null {\n\ttry {\n\t\t// Node's fs.realpathSync may choke; use readlink on /proc.\n\t\treturn require(\"node:fs\").readFileSync(`/proc/${pid}/cmdline`, \"utf-8\").replace(/\\0/g, \" \").trim() || null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction linuxStartTicks(pid: number): number | null {\n\ttry {\n\t\tconst stat = require(\"node:fs\").readFileSync(`/proc/${pid}/stat`, \"utf-8\");\n\t\t// field 22 (after comm in parentheses) is starttime in clock ticks.\n\t\tconst idx = stat.lastIndexOf(\")\");\n\t\tif (idx < 0) return null;\n\t\tconst fields = stat.slice(idx + 2).split(\" \");\n\t\tconst starttime = Number(fields[19]); // 0-indexed: field 22 -> index 21, minus comm shift\n\t\treturn Number.isFinite(starttime) ? starttime : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nconst psCmdline: { pid: number; cmdline: string }[] = [];\nlet psLoaded = false;\n\nfunction loadPsSnapshot(): void {\n\tif (psLoaded) return;\n\tpsLoaded = true;\n\ttry {\n\t\tconst out = require(\"node:child_process\").execFileSync(\"ps\", [\"-eo\", \"pid=,args=\"], {\n\t\t\tencoding: \"utf-8\",\n\t\t\ttimeout: 3000,\n\t\t});\n\t\tpsCmdline.length = 0;\n\t\tfor (const line of out.split(\"\\n\")) {\n\t\t\tconst m = line.match(/^\\s*(\\d+)\\s+(.*)$/);\n\t\t\tif (m) psCmdline.push({ pid: Number(m[1]), cmdline: m[2] });\n\t\t}\n\t} catch {\n\t\t// ps unavailable\n\t}\n}\n\nfunction posixIdentity(pid: number): ProcessIdentityInfo {\n\tconst alive = require(\"node:fs\").existsSync(`/proc/${pid}`) || isAlivePosixFallback(pid);\n\tconst cmdline =\n\t\tlinuxCmdline(pid) ??\n\t\t(() => {\n\t\t\tloadPsSnapshot();\n\t\t\treturn psCmdline.find((p) => p.pid === pid)?.cmdline ?? null;\n\t\t})();\n\treturn { pid, commandLine: cmdline ?? \"\", startIdentity: linuxStartTicks(pid) ?? undefined, alive };\n}\n\nfunction isAlivePosixFallback(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (e) {\n\t\treturn (e as NodeJS.ErrnoException).code === \"EPERM\";\n\t}\n}\n\nfunction windowsIdentity(pid: number): ProcessIdentityInfo {\n\tlet alive = false;\n\tlet commandLine = \"\";\n\tlet startIdentity: number | undefined;\n\ttry {\n\t\tconst out = require(\"node:child_process\").execFileSync(\n\t\t\t\"powershell\",\n\t\t\t[\n\t\t\t\t\"-NoProfile\",\n\t\t\t\t\"-Command\",\n\t\t\t\t`$p=Get-CimInstance Win32_Process -Filter \"ProcessId = ${pid}\" -ErrorAction SilentlyContinue | Select-Object -First 1; if ($p) { \"ALIVE|\" + $p.CommandLine + \"|\" + $p.CreationDate }`,\n\t\t\t],\n\t\t\t{ encoding: \"utf-8\", timeout: 4000, windowsHide: true },\n\t\t);\n\t\tconst parts = out.trim().split(\"|\");\n\t\tif (parts[0] === \"ALIVE\") {\n\t\t\talive = true;\n\t\t\tcommandLine = parts[1] ?? \"\";\n\t\t\tconst created = Date.parse(parts[2] ?? \"\");\n\t\t\tif (!Number.isNaN(created)) startIdentity = created;\n\t\t}\n\t} catch {\n\t\t// fall through: alive=false\n\t}\n\treturn { pid, commandLine, startIdentity, alive };\n}\n\n/**\n * Read authoritative identity for a PID on the current platform. Never trusts\n * PID alone.\n */\nexport async function readProcessIdentity(pid: number): Promise<ProcessIdentityInfo> {\n\ttry {\n\t\tif (isWindowsPlatform()) return windowsIdentity(pid);\n\t\treturn posixIdentity(pid);\n\t} catch {\n\t\treturn { pid, commandLine: \"\", alive: false };\n\t}\n}\n\nexport function isWindowsPlatform(): boolean {\n\treturn process.platform === \"win32\";\n}\n\n/** Verify a live PID matches a recorded identity (executable + start time). */\nexport function identityMatches(\n\tlive: ProcessIdentityInfo,\n\texpected: { commandIdentity?: string; processStartIdentity?: number },\n): { match: boolean; reason?: string } {\n\tif (!live.alive) return { match: false, reason: \"process_not_alive\" };\n\tif (expected.processStartIdentity !== undefined && live.startIdentity !== undefined) {\n\t\tif (live.startIdentity !== expected.processStartIdentity) {\n\t\t\treturn { match: false, reason: \"pid_reuse_start_identity_mismatch\" };\n\t\t}\n\t}\n\tif (expected.commandIdentity) {\n\t\t// commandIdentity is the (sanitized) executable+args; verify the live\n\t\t// cmdline contains the executable as its first token.\n\t\tconst firstToken = expected.commandIdentity.split(/\\s+/)[0];\n\t\tif (firstToken && live.commandLine) {\n\t\t\tif (!live.commandLine.includes(firstToken) && !live.commandLine.includes(basename(firstToken))) {\n\t\t\t\treturn { match: false, reason: \"command_identity_mismatch\" };\n\t\t\t}\n\t\t}\n\t}\n\treturn { match: true };\n}\n\nfunction basename(p: string): string {\n\tconst parts = p.split(/[\\\\/]/);\n\treturn parts[parts.length - 1] ?? p;\n}\n\nexport const readFileUtf8 = (p: string) => readFile(p, \"utf-8\").catch(() => null);\n"]}