@moonwall/cli
Version:
Testing framework for the Moon family of projects
183 lines (180 loc) • 5.97 kB
JavaScript
// 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
};