paperclip / scripts /dev-service.ts
cjovs's picture
Deploy Paperclip CN to Hugging Face Space
96e86e5
#!/usr/bin/env -S node --import tsx
import { execFile } from "node:child_process";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { promisify } from "node:util";
import {
forceKillLocalServiceProcessTree,
isPidAlive,
listLocalServiceRegistryRecords,
removeLocalServiceRegistryRecord,
terminateLocalService,
} from "../server/src/services/local-service-supervisor.ts";
import { getDevServiceControlFilePath, repoRoot } from "./dev-service-profile.ts";
const execFileAsync = promisify(execFile);
function toDisplayLines(records: Awaited<ReturnType<typeof listLocalServiceRegistryRecords>>) {
return records.map((record) => {
const childPid = typeof record.metadata?.childPid === "number" ? ` child=${record.metadata.childPid}` : "";
const url = typeof record.metadata?.url === "string" ? ` url=${record.metadata.url}` : "";
return `${record.serviceName} pid=${record.pid}${childPid} cwd=${record.cwd}${url}`;
});
}
const command = process.argv[2] ?? "list";
const records = await listLocalServiceRegistryRecords({
profileKind: "paperclip-dev",
metadata: { repoRoot },
});
function getRecordChildPid(record: (typeof records)[number]) {
return typeof record.metadata?.childPid === "number" && record.metadata.childPid > 0
? record.metadata.childPid
: null;
}
async function isDevServiceHealthy(port: number | null) {
if (!port || port <= 0) return false;
try {
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
signal: AbortSignal.timeout(1_500),
});
return response.ok;
} catch {
return false;
}
}
async function findListeningPid(port: number | null) {
if (!port || port <= 0) return null;
try {
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "tcp"], {
windowsHide: true,
});
const match = stdout
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => {
const parts = line.split(/\s+/);
return (
parts.length >= 5 &&
/^tcp$/i.test(parts[0]) &&
parts[1]?.endsWith(`:${port}`) &&
/^listening$/i.test(parts[3])
);
});
if (!match) return null;
const pid = Number.parseInt(match.split(/\s+/).at(-1) ?? "", 10);
return Number.isInteger(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
async function stopRecordGracefullyOnWindows(record: (typeof records)[number]) {
const controlFilePath = getDevServiceControlFilePath(record.serviceKey);
const childPid = getRecordChildPid(record);
mkdirSync(path.dirname(controlFilePath), { recursive: true });
writeFileSync(
controlFilePath,
`${JSON.stringify({ requestedAt: new Date().toISOString(), command: "stop" })}\n`,
"utf8",
);
const deadline = Date.now() + 20_000;
while (Date.now() < deadline) {
const wrapperAlive = isPidAlive(record.pid);
const childAlive = childPid ? isPidAlive(childPid) : false;
const serviceHealthy = await isDevServiceHealthy(record.port);
if (!wrapperAlive && !childAlive && !serviceHealthy) {
rmSync(controlFilePath, { force: true });
return;
}
await delay(200);
}
rmSync(controlFilePath, { force: true });
if (childPid && isPidAlive(childPid)) {
await forceKillLocalServiceProcessTree({ pid: childPid, processGroupId: null });
}
if (isPidAlive(record.pid)) {
await forceKillLocalServiceProcessTree(record);
}
const listeningPid = await findListeningPid(record.port);
if (listeningPid && isPidAlive(listeningPid)) {
await forceKillLocalServiceProcessTree({ pid: listeningPid, processGroupId: null });
}
}
if (command === "list") {
if (records.length === 0) {
console.log("No Paperclip dev services registered for this repo.");
process.exit(0);
}
for (const line of toDisplayLines(records)) {
console.log(line);
}
process.exit(0);
}
if (command === "stop") {
if (records.length === 0) {
console.log("No Paperclip dev services registered for this repo.");
process.exit(0);
}
for (const record of records) {
if (process.platform === "win32") {
await stopRecordGracefullyOnWindows(record);
} else {
await terminateLocalService(record);
}
await removeLocalServiceRegistryRecord(record.serviceKey);
console.log(`Stopped ${record.serviceName} (pid ${record.pid})`);
}
process.exit(0);
}
console.error(`Unknown dev-service command: ${command}`);
process.exit(1);