fix: harden dev stack process detection

This commit is contained in:
afei A
2026-03-27 23:57:47 +08:00
parent 98c6b741d6
commit 40bf9de8d7
2 changed files with 126 additions and 2 deletions

View File

@@ -181,6 +181,61 @@ function isPidRunning(pid) {
}
}
function readProcessCommand(pid) {
if (!pid) {
return null;
}
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
encoding: "utf8",
});
if (result.status !== 0) {
return null;
}
const command = result.stdout.trim();
return command.length > 0 ? command : null;
}
export function getServiceCommandMatchers(service) {
switch (service.key) {
case "temporal":
return ["temporal", "server", "start-dev"];
case "backend-api":
return ["uvicorn", "app.main:app"];
case "backend-worker":
return ["app.workers.runner"];
case "frontend":
return ["next", "dev", "127.0.0.1", "3000"];
default:
return service.command.map((part) => path.basename(part));
}
}
export function isServiceProcessMatch(service, processCommand) {
if (!processCommand) {
return false;
}
const normalizedCommand = processCommand.toLowerCase();
return getServiceCommandMatchers(service).every((matcher) =>
normalizedCommand.includes(matcher.toLowerCase()),
);
}
export function isServiceHealthy(service, { pid, processCommand, portOpen }) {
if (!pid || !isServiceProcessMatch(service, processCommand)) {
return false;
}
if (service.port) {
return portOpen;
}
return true;
}
function removePidFile(pidFilePath) {
if (fs.existsSync(pidFilePath)) {
fs.unlinkSync(pidFilePath);
@@ -309,6 +364,12 @@ async function stopService(config, service) {
return false;
}
const processCommand = readProcessCommand(pid);
if (!isServiceProcessMatch(service, processCommand)) {
removePidFile(pidFilePath);
return false;
}
process.kill(-pid, "SIGTERM");
for (let index = 0; index < 20; index += 1) {
if (!isPidRunning(pid)) {
@@ -430,10 +491,21 @@ async function printStatus() {
const pidFilePath = getServicePidFile(config, service.key);
const logFilePath = getServiceLogFile(config, service.key);
const pid = readPid(pidFilePath);
const running = pid ? isPidRunning(pid) : false;
const processCommand =
pid && isPidRunning(pid) ? readProcessCommand(pid) : null;
const portOpen = service.port ? await isLocalPortBusy(service.port) : false;
const running = isServiceHealthy(service, {
pid,
processCommand,
portOpen,
});
if (!running && pid) {
removePidFile(pidFilePath);
}
const displayPid = running ? pid : null;
const portText = service.port ? ` port=${service.port}` : "";
const logText = fs.existsSync(logFilePath) ? ` log=${logFilePath}` : "";
console.log(`${service.key}: ${running ? "running" : "stopped"} pid=${pid ?? "-"}${portText}${logText}`);
console.log(`${service.key}: ${running ? "running" : "stopped"} pid=${displayPid ?? "-"}${portText}${logText}`);
}
}

View File

@@ -4,10 +4,15 @@ import {
createStackConfig,
formatServiceLogs,
getDefaultTemporalCandidates,
getServiceCommandMatchers,
isServiceHealthy,
isServiceProcessMatch,
resolveTemporalCli,
selectServicesForLogs,
} from "../../scripts/dev-stack/stack.mjs";
type StackService = ReturnType<typeof createStackConfig>["services"][number];
describe("createStackConfig", () => {
it("derives frontend and sibling backend paths from the workspace root", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
@@ -181,3 +186,50 @@ describe("formatServiceLogs", () => {
);
});
});
describe("service process matching", () => {
it("does not treat an unrelated process with a reused pid as the frontend service", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const frontend = config.services.find((service: StackService) => service.key === "frontend");
expect(frontend).toBeDefined();
expect(
isServiceProcessMatch(frontend!, "node /tmp/unrelated-script.js --watch"),
).toBe(false);
});
it("builds stable command matchers for backend api and worker processes", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const backendApi = config.services.find((service: StackService) => service.key === "backend-api");
const backendWorker = config.services.find((service: StackService) => service.key === "backend-worker");
expect(getServiceCommandMatchers(backendApi!)).toEqual(["uvicorn", "app.main:app"]);
expect(getServiceCommandMatchers(backendWorker!)).toEqual(["app.workers.runner"]);
});
it("requires the expected port to be open for port-bound services", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const backendApi = config.services.find((service: StackService) => service.key === "backend-api");
expect(
isServiceHealthy(backendApi!, {
pid: 123,
processCommand: "/path/to/python -m uvicorn app.main:app --host 127.0.0.1 --port 8000",
portOpen: false,
}),
).toBe(false);
});
it("allows worker services without ports when the process command matches", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const backendWorker = config.services.find((service: StackService) => service.key === "backend-worker");
expect(
isServiceHealthy(backendWorker!, {
pid: 123,
processCommand: "/path/to/python -m app.workers.runner",
portOpen: false,
}),
).toBe(true);
});
});