@firfi/quint-connect
Version:
Model-based testing framework connecting Quint specifications to TypeScript implementations
151 lines • 6.33 kB
JavaScript
import { Effect, Option, Schema } from "effect";
import { execFile, spawnSync } from "node:child_process";
const defaultRunCommand = (command, args, callback) => execFile(command, [...args], { encoding: "utf8", windowsHide: true }, callback);
const defaultRunCommandSync = (command, args) => {
const result = spawnSync(command, [...args], { stdio: "ignore", windowsHide: true });
return result.error === undefined && result.status === 0;
};
const interruptExitCode = 130;
const terminationExitCode = 143;
const signalSelf = (signal) => {
try {
process.kill(process.pid, signal);
}
catch {
process.exit(signal === "SIGINT" ? interruptExitCode : terminationExitCode);
}
};
const defaultDeps = {
platform: process.platform,
runCommand: defaultRunCommand,
runCommandSync: defaultRunCommandSync,
killProcess: (pid, signal) => process.kill(pid, signal),
addExitListener: (listener) => process.on("exit", listener),
removeExitListener: (listener) => process.removeListener("exit", listener),
addSignalListener: (signal, listener) => process.once(signal, listener),
removeSignalListener: (signal, listener) => process.removeListener(signal, listener),
signalSelf
};
const ProcessCount = Schema.NumberFromString;
const WindowsTasklistRow = Schema.String.pipe(Schema.pattern(/^"quint_evaluator\.exe","[0-9]+","(?:[^"]|"")*","[0-9]+","(?:[^"]|"")*"$/iu));
const WindowsTasklistRows = Schema.Array(WindowsTasklistRow);
const parsePosixProcessCount = (stdout) => {
const decoded = Schema.decodeUnknownOption(ProcessCount)(stdout.trim());
return Option.isSome(decoded) ? decoded.value : 0;
};
const parseWindowsProcessCount = (stdout) => {
const rows = stdout
.split(/\r?\n/u)
.filter((line) => line.length > 0);
const decoded = Schema.decodeUnknownOption(WindowsTasklistRows)(rows);
return Option.isSome(decoded) ? decoded.value.length : 0;
};
export const makePlatformProcessBoundary = (deps = defaultDeps) => {
const isWindows = deps.platform === "win32";
const terminateImmediately = (processHandle) => {
if (processHandle.pid === undefined) {
return;
}
try {
deps.killProcess(isWindows ? processHandle.pid : -processHandle.pid, "SIGKILL");
}
catch {
// The process has already exited.
}
};
const terminateTree = (processHandle) => {
if (processHandle.pid === undefined) {
return Effect.void;
}
if (!isWindows) {
return Effect.sync(() => terminateImmediately(processHandle));
}
return Effect.async((resume) => {
const command = deps.runCommand("taskkill", ["/PID", String(processHandle.pid), "/T", "/F"], (error) => {
if (error !== null) {
terminateImmediately(processHandle);
}
resume(Effect.void);
});
return Effect.sync(() => {
command.kill();
terminateImmediately(processHandle);
});
});
};
const makeLifecycle = (getActiveProcess) => {
// Node/Effect-only lifecycle: upstream Rust waits synchronously for `output()` and
// therefore has no asynchronous child tree to supervise. Here the scoped finalizer,
// host-exit hooks, and signal hooks all converge on idempotent tree termination.
// https://github.com/informalsystems/quint-connect/blob/4f018f54fc7dd4cef341d10111427bab59d3b307/connect/src/trace/generator/mod.rs#L23-L33
const terminateOnExit = () => {
const activeProcess = getActiveProcess();
if (!isWindows || activeProcess.pid === undefined) {
terminateImmediately(activeProcess);
return;
}
try {
if (deps.runCommandSync("taskkill", ["/PID", String(activeProcess.pid), "/T", "/F"])) {
return;
}
}
catch {
// Fall through to direct-process cleanup when taskkill cannot start.
}
terminateImmediately(activeProcess);
};
let completed = false;
const propagateSignal = (signal) => {
complete();
terminateOnExit();
deps.signalSelf(signal);
};
const onSigint = () => propagateSignal("SIGINT");
const onSigterm = () => propagateSignal("SIGTERM");
const complete = () => {
if (!completed) {
completed = true;
deps.removeExitListener(terminateOnExit);
deps.removeSignalListener("SIGINT", onSigint);
deps.removeSignalListener("SIGTERM", onSigterm);
}
};
deps.addExitListener(terminateOnExit);
deps.addSignalListener("SIGINT", onSigint);
deps.addSignalListener("SIGTERM", onSigterm);
return {
complete,
interrupt: Effect.suspend(() => {
if (completed) {
return Effect.void;
}
complete();
return terminateTree(getActiveProcess());
})
};
};
const countEvaluatorProcesses = Effect.async((resume) => {
const command = deps.runCommand(isWindows ? "tasklist" : "pgrep", isWindows
? ["/FI", "IMAGENAME eq quint_evaluator.exe", "/FO", "CSV", "/NH"]
: ["-c", "quint_evaluator"], (error, stdout) => {
if (error !== null) {
resume(Effect.succeed(0));
return;
}
resume(Effect.succeed(isWindows ? parseWindowsProcessCount(stdout) : parsePosixProcessCount(stdout)));
});
return Effect.sync(() => command.kill());
});
return {
commandName: (name) => isWindows ? `${name}.cmd` : name,
executableName: (name) => isWindows ? `${name}.exe` : name,
detached: !isWindows,
makeLifecycle,
countEvaluatorProcesses,
evaluatorCleanupHint: isWindows
? "taskkill /F /T /IM quint_evaluator.exe"
: "killall -9 quint_evaluator"
};
};
export const platformProcess = makePlatformProcessBoundary();
//# sourceMappingURL=platform-process.js.map