UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

183 lines (180 loc) 5.97 kB
// src/internal/effect/ProcessManagerService.ts import { Context, Effect, Layer, pipe } from "effect"; import { Path } from "@effect/platform"; import { NodePath } from "@effect/platform-node"; import { spawn } from "child_process"; import * as fs from "fs"; // src/internal/effect/errors.ts import { Data } from "effect"; var PortDiscoveryError = class extends Data.TaggedError("PortDiscoveryError") { }; var NodeLaunchError = class extends Data.TaggedError("NodeLaunchError") { }; var NodeReadinessError = class extends Data.TaggedError("NodeReadinessError") { }; var ProcessError = class extends Data.TaggedError("ProcessError") { }; var StartupCacheError = class extends Data.TaggedError("StartupCacheError") { }; var FileLockError = class extends Data.TaggedError("FileLockError") { }; // src/internal/effect/ProcessManagerService.ts import { createLogger } from "@moonwall/util"; var logger = createLogger({ name: "ProcessManagerService" }); var ProcessManagerService = class extends Context.Tag("ProcessManagerService")() { }; var getLogPath = (config, pid) => pipe( Effect.all([Path.Path, Effect.sync(() => process.cwd())]), Effect.map(([pathService, cwd]) => { const dirPath = config.logDirectory || pathService.join(cwd, "tmp", "node_logs"); const portArg = config.args.find((a) => a.includes("port")); const port = portArg?.split("=")[1] || "unknown"; const baseName = pathService.basename(config.command); return pathService.join(dirPath, `${baseName}_node_${port}_${pid}.log`).replace(/node_node_undefined/g, "chopsticks"); }), Effect.provide(NodePath.layer), Effect.mapError( (cause) => new ProcessError({ cause, operation: "spawn" }) ) ); var ensureLogDirectory = (dirPath) => Effect.tryPromise({ try: async () => { try { await fs.promises.access(dirPath); } catch { await fs.promises.mkdir(dirPath, { recursive: true }); } }, catch: (cause) => new ProcessError({ cause, operation: "spawn" }) }); var spawnProcess = (config) => Effect.try({ try: () => { const child = spawn(config.command, [...config.args]); child.on("error", (error) => { logger.error(`Process spawn error: ${error}`); }); return child; }, catch: (cause) => new NodeLaunchError({ cause, command: config.command, args: [...config.args] }) }); var createLogStream = (logPath) => Effect.try({ try: () => fs.createWriteStream(logPath, { flags: "a" }), catch: (cause) => new ProcessError({ cause, operation: "spawn" }) }); var setupLogHandlers = (process2, logStream) => Effect.sync(() => { const logHandler = (chunk) => { logStream.write(chunk); }; process2.stdout?.on("data", logHandler); process2.stderr?.on("data", logHandler); }); var constructExitMessage = (process2, code, signal) => { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); if (process2.isMoonwallTerminating) { return `${timestamp} [moonwall] process killed. reason: ${process2.moonwallTerminationReason || "unknown"} `; } if (code !== null) { return `${timestamp} [moonwall] process closed with status code ${code} `; } if (signal !== null) { return `${timestamp} [moonwall] process terminated by signal ${signal} `; } return `${timestamp} [moonwall] process closed unexpectedly `; }; var setupExitHandler = (process2, logStream) => Effect.sync(() => { process2.once("close", (code, signal) => { const message = constructExitMessage(process2, code, signal); logStream.end(message); }); }); var killProcess = (process2, logStream, reason) => Effect.sync(() => { process2.isMoonwallTerminating = true; process2.moonwallTerminationReason = reason; }).pipe( Effect.flatMap( () => process2.pid ? Effect.try({ try: () => { process2.kill("SIGTERM"); }, catch: (cause) => new ProcessError({ cause, pid: process2.pid, operation: "kill" }) }) : Effect.sync(() => logStream.end()) ) ); var launchProcess = (config) => pipe( Effect.all([Path.Path, Effect.sync(() => config.logDirectory || void 0)]), Effect.flatMap(([pathService, customLogDir]) => { const dirPath = customLogDir || pathService.join(process.cwd(), "tmp", "node_logs"); return pipe( ensureLogDirectory(dirPath), Effect.flatMap(() => spawnProcess(config)), Effect.flatMap((childProcess) => { if (childProcess.pid === void 0) { return Effect.fail( new ProcessError({ cause: new Error("Process PID is undefined after spawn"), operation: "spawn" }) ); } return pipe( getLogPath(config, childProcess.pid), Effect.flatMap( (logPath) => pipe( createLogStream(logPath), Effect.flatMap( (logStream) => pipe( setupLogHandlers(childProcess, logStream), Effect.flatMap(() => setupExitHandler(childProcess, logStream)), Effect.map(() => { const processInfo = { process: childProcess, logPath }; const cleanup = pipe( killProcess(childProcess, logStream, "Manual cleanup requested"), Effect.catchAll( (error) => Effect.sync(() => { logger.error(`Failed to cleanly kill process: ${error}`); }) ) ); return { result: processInfo, cleanup }; }) ) ) ) ) ); }) ); }), Effect.provide(NodePath.layer) ); var ProcessManagerServiceLive = Layer.succeed(ProcessManagerService, { launch: launchProcess }); export { ProcessManagerService, ProcessManagerServiceLive };