mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
fix(runtime): verify Windows port ownership
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a88c241b55
commit
b8637fb3f4
3 files changed
+84
-18
No files matched your search
@@ -49,6 +49,7 @@ import {
|
||||
import {
|
||||
findAdoptableLocalService,
|
||||
isLocalServiceRegistryCwdCompatible,
|
||||
isLocalServiceProcessOwnedBy,
|
||||
isLocalServiceProcessInWorkspace,
|
||||
readLocalServicePortOwner,
|
||||
writeLocalServiceRegistryRecord,
|
||||
@@ -4718,6 +4719,35 @@ describe("readLocalServicePortOwner", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("attributes a Windows listener to a descendant of the launched process", async () => {
|
||||
const fakeBin = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-windows-tools-"));
|
||||
const previousPath = process.env.PATH;
|
||||
const port = 43_123;
|
||||
const listenerPid = 43_210;
|
||||
const launchedPid = 32_000;
|
||||
await fs.writeFile(
|
||||
path.join(fakeBin, "netstat"),
|
||||
`#!/bin/sh\nprintf ' TCP 127.0.0.1:${port} 0.0.0.0:0 LISTENING ${listenerPid}\\n'\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(fakeBin, "powershell.exe"),
|
||||
`#!/bin/sh\nprintf '${launchedPid}\\n'\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
process.env.PATH = `${fakeBin}${path.delimiter}${previousPath ?? ""}`;
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
|
||||
try {
|
||||
await expect(readLocalServicePortOwner(port)).resolves.toBe(listenerPid);
|
||||
await expect(isLocalServiceProcessOwnedBy(listenerPid, launchedPid)).resolves.toBe(true);
|
||||
} finally {
|
||||
if (previousPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = previousPath;
|
||||
await fs.rm(fakeBin, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts service cwd nested within the requested workspace", async () => {
|
||||
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-workspace-"));
|
||||
const serviceCwd = path.join(workspace, "server");
|
||||
|
||||
@@ -296,6 +296,39 @@ export async function readLocalServiceProcessGroupId(pid: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function isLocalServiceProcessOwnedBy(pid: number, ownerProcessId: number) {
|
||||
if (pid === ownerProcessId) return true;
|
||||
if (process.platform !== "win32") {
|
||||
return (await readLocalServiceProcessGroupId(pid)) === ownerProcessId;
|
||||
}
|
||||
|
||||
try {
|
||||
const script = [
|
||||
`$currentProcessId = ${pid}`,
|
||||
"while ($currentProcessId -gt 0) {",
|
||||
" $process = Get-CimInstance Win32_Process -Filter \"ProcessId = $currentProcessId\" -ErrorAction SilentlyContinue",
|
||||
" if ($null -eq $process) { break }",
|
||||
" $parentProcessId = [int]$process.ParentProcessId",
|
||||
" Write-Output $parentProcessId",
|
||||
" if ($parentProcessId -eq $currentProcessId) { break }",
|
||||
" $currentProcessId = $parentProcessId",
|
||||
"}",
|
||||
].join("\n");
|
||||
const { stdout } = await execFileAsync("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
script,
|
||||
]);
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => Number.parseInt(line.trim(), 10))
|
||||
.some((ancestorPid) => ancestorPid === ownerProcessId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function adoptLocalServiceFromPortOwner(input: {
|
||||
serviceKey: string;
|
||||
profileKind?: string | null;
|
||||
@@ -405,8 +438,24 @@ export async function terminateLocalService(
|
||||
}
|
||||
|
||||
export async function readLocalServicePortOwner(port: number) {
|
||||
if (!Number.isInteger(port) || port <= 0 || process.platform === "win32") return null;
|
||||
if (!Number.isInteger(port) || port <= 0) return null;
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"]);
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const columns = line.trim().split(/\s+/);
|
||||
if (columns.length < 5 || columns[0]?.toUpperCase() !== "TCP") continue;
|
||||
const localAddress = columns[1] ?? "";
|
||||
const separatorIndex = localAddress.lastIndexOf(":");
|
||||
const localPort = Number.parseInt(localAddress.slice(separatorIndex + 1), 10);
|
||||
const state = columns.at(-2)?.toUpperCase();
|
||||
const pid = Number.parseInt(columns.at(-1) ?? "", 10);
|
||||
if (localPort === port && state === "LISTENING" && Number.isInteger(pid) && pid > 0) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const { stdout } = await execFileAsync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
|
||||
const firstPid = stdout
|
||||
.split("\n")
|
||||
|
||||
@@ -28,9 +28,9 @@ import {
|
||||
createLocalServiceKey,
|
||||
findLocalServiceRegistryRecordByRuntimeServiceId,
|
||||
findAdoptableLocalService,
|
||||
isLocalServiceProcessOwnedBy,
|
||||
isLocalServiceProcessInWorkspace,
|
||||
readLocalServiceProcessCwd,
|
||||
readLocalServiceProcessGroupId,
|
||||
readLocalServicePortOwner,
|
||||
removeLocalServiceRegistryRecord,
|
||||
terminateLocalService,
|
||||
@@ -4008,10 +4008,7 @@ async function waitForAllocatedPortBind(input: {
|
||||
const ownerPid = await readLocalServicePortOwner(input.port);
|
||||
if (ownerPid) {
|
||||
const childPid = input.child.pid ?? null;
|
||||
const ownerProcessGroupId = ownerPid === childPid
|
||||
? childPid
|
||||
: await readLocalServiceProcessGroupId(ownerPid);
|
||||
if (!childPid || ownerProcessGroupId !== childPid) {
|
||||
if (!childPid || !(await isLocalServiceProcessOwnedBy(ownerPid, childPid))) {
|
||||
throw new RuntimeServicePortBindCollision(input.port);
|
||||
}
|
||||
// Require the same launched process group to retain ownership across a stability delay.
|
||||
@@ -4021,12 +4018,7 @@ async function waitForAllocatedPortBind(input: {
|
||||
throw new Error("service process exited after losing its allocated port");
|
||||
}
|
||||
const stableOwnerPid = await readLocalServicePortOwner(input.port);
|
||||
const stableOwnerProcessGroupId = stableOwnerPid === childPid
|
||||
? childPid
|
||||
: stableOwnerPid
|
||||
? await readLocalServiceProcessGroupId(stableOwnerPid)
|
||||
: null;
|
||||
if (stableOwnerProcessGroupId !== childPid) {
|
||||
if (!stableOwnerPid || !(await isLocalServiceProcessOwnedBy(stableOwnerPid, childPid))) {
|
||||
throw new RuntimeServicePortBindCollision(input.port);
|
||||
}
|
||||
return;
|
||||
@@ -4041,12 +4033,7 @@ async function waitForAllocatedPortBind(input: {
|
||||
}
|
||||
const stableOwnerPid = await readLocalServicePortOwner(input.port);
|
||||
const childPid = input.child.pid ?? null;
|
||||
const stableOwnerProcessGroupId = stableOwnerPid === childPid
|
||||
? childPid
|
||||
: stableOwnerPid
|
||||
? await readLocalServiceProcessGroupId(stableOwnerPid)
|
||||
: null;
|
||||
if (childPid && stableOwnerProcessGroupId === childPid) return;
|
||||
if (stableOwnerPid && childPid && await isLocalServiceProcessOwnedBy(stableOwnerPid, childPid)) return;
|
||||
throw new RuntimeServicePortBindCollision(input.port);
|
||||
}
|
||||
await delay(50);
|
||||
|
||||
Reference in new issue
Block a user