fix: harden dev stack process detection
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user