114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { platform } from "node:os";
|
|
|
|
export interface NodePathFromPidResult {
|
|
nodePath: string | null;
|
|
error?: string;
|
|
}
|
|
|
|
function normalizeError(error: unknown): string {
|
|
if (error instanceof Error) {
|
|
return error.message;
|
|
}
|
|
return String(error);
|
|
}
|
|
|
|
function resolveNodePathFromPidUnix(pid: number): NodePathFromPidResult {
|
|
const result = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
if (result.error) {
|
|
return { nodePath: null, error: `ps failed: ${normalizeError(result.error)}` };
|
|
}
|
|
|
|
if ((result.status ?? 1) !== 0) {
|
|
const details = result.stderr?.trim();
|
|
return {
|
|
nodePath: null,
|
|
error: details ? `ps failed: ${details}` : `ps exited with code ${result.status ?? 1}`,
|
|
};
|
|
}
|
|
|
|
const resolved = result.stdout.trim();
|
|
return resolved ? { nodePath: resolved } : { nodePath: null, error: "ps returned an empty command path" };
|
|
}
|
|
|
|
function runProcessProbe(command: string, args: string[]): {
|
|
resolved: string | null;
|
|
error?: string;
|
|
} {
|
|
const result = spawnSync(command, args, {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
if (result.error) {
|
|
return { resolved: null, error: `${command} failed: ${normalizeError(result.error)}` };
|
|
}
|
|
|
|
if ((result.status ?? 1) !== 0) {
|
|
const details = result.stderr?.trim();
|
|
return {
|
|
resolved: null,
|
|
error: details ? `${command} failed: ${details}` : `${command} exited with code ${result.status ?? 1}`,
|
|
};
|
|
}
|
|
|
|
const resolved = result.stdout.trim();
|
|
return resolved ? { resolved } : { resolved: null, error: `${command} returned no executable path` };
|
|
}
|
|
|
|
function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
|
|
const probes: Array<{ label: string; command: string; args: string[]; parseValue?: (stdout: string) => string | null }> = [
|
|
{
|
|
label: "powershell-cim",
|
|
command: "powershell",
|
|
args: [
|
|
"-NoProfile",
|
|
"-Command",
|
|
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").ExecutablePath`,
|
|
],
|
|
},
|
|
{
|
|
label: "powershell-process",
|
|
command: "powershell",
|
|
args: ["-NoProfile", "-Command", `(Get-Process -Id ${pid}).Path`],
|
|
},
|
|
{
|
|
label: "wmic",
|
|
command: "wmic",
|
|
args: ["process", "where", `ProcessId=${pid}`, "get", "ExecutablePath", "/VALUE"],
|
|
parseValue: (stdout) => {
|
|
const match = stdout.match(/ExecutablePath=(.+)/);
|
|
return match?.[1]?.trim() ?? null;
|
|
},
|
|
},
|
|
];
|
|
|
|
const errors: string[] = [];
|
|
for (const probe of probes) {
|
|
const result = runProcessProbe(probe.command, probe.args);
|
|
if (result.resolved) {
|
|
const resolved = probe.parseValue ? probe.parseValue(result.resolved) : result.resolved;
|
|
if (resolved) {
|
|
return { nodePath: resolved };
|
|
}
|
|
errors.push(`${probe.label} returned no executable path`);
|
|
continue;
|
|
}
|
|
if (result.error) {
|
|
errors.push(`${probe.label}: ${result.error}`);
|
|
}
|
|
}
|
|
|
|
return { nodePath: null, error: errors.join("; ") || "could not resolve executable path from PID" };
|
|
}
|
|
|
|
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
|
|
return platform() === "win32"
|
|
? resolveNodePathFromPidWindows(pid)
|
|
: resolveNodePathFromPidUnix(pid);
|
|
}
|