UNPKG

@firfi/quint-connect

Version:

Model-based testing framework connecting Quint specifications to TypeScript implementations

151 lines 6.33 kB
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