mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
157 lines
4.8 KiB
JavaScript
157 lines
4.8 KiB
JavaScript
import { spawn, spawnSync } from "node:child_process";
|
|
import { platform } from "node:os";
|
|
import { isLocalPortListening } from "./storage-maintenance.mjs";
|
|
import { rootDir } from "./profile.mjs";
|
|
|
|
const DEV_PORT = 30141;
|
|
|
|
function stopWindowsDevelopmentProcesses() {
|
|
const projectRootLiteral = rootDir.replaceAll("'", "''");
|
|
const script = `
|
|
$projectRoot = '${projectRootLiteral}'
|
|
$currentProcessId = ${process.pid}
|
|
$excludedProcessIds = [Collections.Generic.HashSet[int]]::new()
|
|
$ancestorProcessId = $currentProcessId
|
|
|
|
while ($ancestorProcessId -gt 0 -and $excludedProcessIds.Add($ancestorProcessId)) {
|
|
$ancestor = Get-CimInstance Win32_Process -Filter "ProcessId = $ancestorProcessId" -ErrorAction SilentlyContinue
|
|
if (-not $ancestor) {
|
|
break
|
|
}
|
|
$ancestorProcessId = [int]$ancestor.ParentProcessId
|
|
}
|
|
|
|
$targets = Get-CimInstance Win32_Process | Where-Object {
|
|
-not $excludedProcessIds.Contains([int]$_.ProcessId) -and
|
|
$_.CommandLine -and
|
|
$_.CommandLine.IndexOf($projectRoot, [StringComparison]::OrdinalIgnoreCase) -ge 0 -and
|
|
$_.Name -in @('node.exe', 'cmd.exe')
|
|
}
|
|
|
|
foreach ($target in $targets) {
|
|
Stop-Process -Id $target.ProcessId -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 500
|
|
$listener = Get-NetTCPConnection -LocalPort ${DEV_PORT} -State Listen -ErrorAction SilentlyContinue
|
|
if ($listener) {
|
|
throw "Port ${DEV_PORT} is still occupied by PID $($listener[0].OwningProcess)"
|
|
}
|
|
|
|
exit 0
|
|
`;
|
|
|
|
const result = spawnSync(
|
|
"powershell.exe",
|
|
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
{ cwd: rootDir, stdio: "inherit" },
|
|
);
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
}
|
|
|
|
function readMacListenerPids() {
|
|
const result = spawnSync(
|
|
"lsof",
|
|
["-nP", "-t", `-iTCP:${DEV_PORT}`, "-sTCP:LISTEN"],
|
|
{ cwd: rootDir, encoding: "utf8" },
|
|
);
|
|
if (result.error) throw result.error;
|
|
const output = result.stdout ?? "";
|
|
if (result.status !== 0 && !output.trim()) return [];
|
|
|
|
return [...new Set(
|
|
output
|
|
.trim()
|
|
.split(/\s+/)
|
|
.map((value) => Number.parseInt(value, 10))
|
|
.filter((pid) => Number.isInteger(pid) && pid > 0),
|
|
)];
|
|
}
|
|
|
|
function readMacProcessCommand(pid) {
|
|
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.error) throw result.error;
|
|
return result.status === 0 ? (result.stdout ?? "").trim() : "";
|
|
}
|
|
|
|
function signalProcess(pid, signal) {
|
|
try {
|
|
process.kill(pid, signal);
|
|
} catch (error) {
|
|
if (error?.code !== "ESRCH") throw error;
|
|
}
|
|
}
|
|
|
|
async function waitForPortClosed(timeoutMs) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (await isLocalPortListening(DEV_PORT)) {
|
|
if (Date.now() >= deadline) return false;
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function stopMacDevelopmentProcesses() {
|
|
const listenerPids = readMacListenerPids();
|
|
if (listenerPids.length === 0) return;
|
|
|
|
const projectPids = listenerPids.filter((pid) => readMacProcessCommand(pid).includes(rootDir));
|
|
const foreignPids = listenerPids.filter((pid) => !projectPids.includes(pid));
|
|
if (foreignPids.length > 0 || projectPids.length === 0) {
|
|
const pids = foreignPids.length > 0 ? foreignPids : listenerPids;
|
|
throw new Error(`Port ${DEV_PORT} is occupied by a process that does not belong to this project (PID ${pids.join(", ")}).`);
|
|
}
|
|
|
|
for (const pid of projectPids) signalProcess(pid, "SIGTERM");
|
|
if (await waitForPortClosed(3000)) return;
|
|
|
|
for (const pid of projectPids) signalProcess(pid, "SIGKILL");
|
|
if (!(await waitForPortClosed(1000))) {
|
|
throw new Error(`Port ${DEV_PORT} is still occupied by this project after termination (PID ${projectPids.join(", ")}).`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (platform() === "win32") {
|
|
stopWindowsDevelopmentProcesses();
|
|
} else if (platform() === "darwin") {
|
|
await stopMacDevelopmentProcesses();
|
|
} else {
|
|
throw new Error("npm run restart currently supports Windows and macOS only. Stop the existing dev process and run npm run dev.");
|
|
}
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
}
|
|
|
|
const npmCliPath = process.env.npm_execpath;
|
|
if (!npmCliPath) {
|
|
console.error("Run this command through npm: npm run restart");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Restarting Pi Agent Integrated at http://127.0.0.1:${DEV_PORT}`);
|
|
const child = spawn(process.execPath, [npmCliPath, "run", "dev"], {
|
|
cwd: rootDir,
|
|
env: process.env,
|
|
stdio: "inherit",
|
|
});
|
|
|
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
process.on(signal, () => child.kill(signal));
|
|
}
|
|
|
|
child.on("error", (error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
|
|
child.on("exit", (code, signal) => {
|
|
if (signal) process.kill(process.pid, signal);
|
|
process.exit(code ?? 1);
|
|
});
|