1/**
2 * This file is expecting a full rewrite to abstract the Node API away
3 * entirely, exposing web streams I/O.
4 *
5 * @module
6 */
7
8const execFileRaw: typeof child_process.execFile.__promisify__ = util.promisify(
9 child_process.execFile,
10);
11/*** @deprecated */
12export const exec: typeof execFileRaw = ((
13 ...args: Parameters<typeof execFileRaw>
14) =>
15 execFileRaw(...args).catch((e: any) => {
16 if (e?.message?.startsWith?.("Command failed")) {
17 if (e.code > 2 ** 31) e.code |= 0;
18 const code = e.signal ? `signal ${e.signal}` : `code ${e.code}`;
19 e.message = `${e.cmd.split(" ")[0]} failed with ${code}`;
20 }
21 throw e;
22 })) as any;
23
24// const activePids = new Set<number>();
25
26/*** @deprecated */
27export interface SpawnOptions extends child_process.SpawnOptions {
28 cmd: string[];
29}
30/*** @deprecated */
31export function spawn(
32 { cmd, ...rest }: SpawnOptions,
33): child_process.ChildProcess {
34 // rest.detached ??= true;
35 const process = child_process.spawn(cmd[0]!, cmd.slice(1), rest);
36 // const pid = rest.detached && process.pid ? -process.pid : process.pid;
37 // if (pid) {
38 // activePids.add(pid);
39 // process.on("exit", () => void activePids.delete(pid));
40 // }
41 return process;
42}
43
44/*** @deprecated */
45export interface ForkOptions extends child_process.ForkOptions {
46 cmd: string[];
47}
48
49/*** @deprecated */
50export function fork(
51 { cmd, ...rest }: ForkOptions,
52): child_process.ChildProcess {
53 // rest.detached ??= true;
54 const process = child_process.fork(cmd[0]!, cmd.slice(1), rest);
55 // const pid = rest.detached && process.pid ? -process.pid : process.pid;
56 // if (pid) {
57 // activePids.add(pid);
58 // process.on("exit", () => void activePids.delete(pid));
59 // }
60 return process;
61}
62
63/*** @deprecated */
64export async function spawnAndWait(
65 opts: SpawnOptions,
66): Promise<{ stdout: Buffer | null; stderr: Buffer | null }> {
67 opts.stdio ??= ["ignore", "inherit", "inherit"];
68 const process = spawn(opts);
69 const output = Promise.all([
70 process.stdout ? Array.fromAsync(process.stdout).then(Buffer.concat) : null,
71 process.stderr ? Array.fromAsync(process.stderr).then(Buffer.concat) : null,
72 ]);
73 let [code, signal] = (await events.EventEmitter.once(process, "exit")) as [
74 number | null,
75 string | null,
76 ];
77 if (code !== 0) {
78 if (code !== null && code > 2 ** 31) code |= 0;
79 const exit = signal ? `signal ${signal}` : `code ${code}`;
80 throw new Error(`${opts.cmd[0]} failed with ${exit}`);
81 }
82 const [stdout, stderr] = await output;
83 return { stdout, stderr };
84}
85
86// process.on("exit", () => {
87// for (const pid of activePids) {
88// console.info(`Kill ${Math.abs(pid)}`);
89// process.kill(pid, 9);
90// }
91// });
92
93async function findInodeForPort(port: number) {
94 const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
95 for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
96 try {
97 const lines = (await fs.readFile(f, "utf8")).split("\n").slice(1);
98 for (const line of lines) {
99 const parts = line.trim().split(/\s+/);
100 if (parts[1]?.split(":")[1] === hexPort) return parts[9];
101 }
102 } catch {}
103 }
104 return null;
105}
106
107async function findPidForInode(inode: string) {
108 const target = `socket:[${inode}]`;
109 for (const pid of await fs.readdir("/proc")) {
110 if (!/^\d+$/.test(pid)) continue;
111 try {
112 for (const fd of await fs.readdir(`/proc/${pid}/fd`)) {
113 try {
114 if (await fs.readlink(`/proc/${pid}/fd/${fd}`) === target) return pid;
115 } catch {}
116 }
117 } catch {}
118 }
119 return null;
120}
121
122async function commandFromPortLinux(port: number) {
123 const inode = await findInodeForPort(port);
124 if (!inode) throw new Error(`no process on port ${port}`);
125 const pid = await findPidForInode(inode);
126 if (!pid) throw new Error(`no pid for inode ${inode}`);
127 const raw = await fs.readFile(`/proc/${pid}/cmdline`, "utf8");
128 return { pid: Number(pid), command: raw.split("\0").filter(Boolean).join(" ") };
129}
130
131async function commandFromPortMac(port: number) {
132 const { stdout: lsofOut } = await exec("lsof", ["-ti", `tcp:${port}`]);
133 const pid = lsofOut.trim().split("\n")[0];
134 if (!pid) throw new Error(`no process on port ${port}`);
135 const { stdout: psOut } = await exec("ps", ["-p", pid, "-o", "args="]);
136 return { pid: Number(pid), command: psOut.trim() };
137}
138
139async function commandFromPortWindows(port: number) {
140 const { stdout } = await exec("netstat", ["-ano"]);
141 const line = stdout.split("\n").find(l => {
142 const m = l.match(/\s+[\d.:]+:(\d+)\s+/);
143 return m && parseInt(m[1]!) === port && l.includes("LISTENING");
144 });
145 if (!line) throw new Error(`no process on port ${port}`);
146 const pid = line.trim().split(/\s+/).at(-1);
147 const { stdout: psOut } = await exec("powershell", [
148 "-NoProfile",
149 "-Command",
150 `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CommandLine`,
151 ]);
152 const cmdline = psOut.trim();
153 if (!cmdline) throw new Error(`no cmdline for pid ${pid}`);
154 return { pid: Number(pid), command: cmdline };
155}
156
157/** Racily searches for what process is taking up a given port. */
158export async function lookupProcessOnPort(port: number): Promise<ProcessLookup> {
159 switch (os.platform()) {
160 case "linux":
161 return commandFromPortLinux(port);
162 case "darwin":
163 return commandFromPortMac(port);
164 case "win32":
165 return commandFromPortWindows(port);
166 default:
167 throw new Error(`unsupported platform: ${os.platform()}`);
168 }
169}
170
171interface ProcessLookup {
172 pid: number;
173 command: string;
174}
175
176import { Buffer } from "node:buffer";
177import * as child_process from "node:child_process";
178import * as events from "node:events";
179import * as fs from "node:fs/promises";
180import * as os from "node:os";
181import * as util from "node:util";