UNPKG

@bluecadet/launchpad-cli

Version:
82 lines 3.94 kB
import path from "node:path"; import { deletePidFile, isProcessRunning } from "@bluecadet/launchpad-controller/pid-utils"; import { ensureError } from "@bluecadet/launchpad-utils/errors"; import { err, ok, ResultAsync } from "neverthrow"; import { cliLogger } from "../utils/cli-logger.js"; import { handleFatalError, loadConfigAndEnv } from "../utils/command-utils.js"; import { DaemonNotRunningError, IPCConnectionError, withDaemon, } from "../utils/controller-execution.js"; /** * Stop command - Gracefully stop the persistent controller via IPC, * with SIGTERM and SIGKILL fallbacks if IPC fails. */ export function stop(argv) { return loadConfigAndEnv(argv) .andThen(({ dir, config }) => { const pidFile = path.resolve(dir, config.controller.pidFile); return withDaemon(dir, config.controller, true, (client, pid) => { cliLogger.info("Stopping Launchpad gracefully..."); return client .shutdown() .andThen(() => wait(2000)) .andThen(() => { // Verify it stopped if (!isProcessRunning(pid)) { deletePidFile(pidFile); cliLogger.info("Launchpad stopped"); return ok(); } // IPC shutdown didn't work - fall back to SIGTERM cliLogger.info("Process still running, sending SIGTERM..."); return safeKill(pid, "SIGTERM") .mapErr((e) => new IPCConnectionError("Failed to stop process via signal", { cause: e })) .asyncAndThen(() => wait(2000)) .andThen(() => { if (!isProcessRunning(pid)) { deletePidFile(pidFile); cliLogger.info("Launchpad stopped"); return ok(); } // Still running - force kill cliLogger.info("Process did not stop gracefully, sending SIGKILL..."); return safeKill(pid, "SIGKILL") .mapErr((e) => new IPCConnectionError("Failed to force kill process", { cause: e })) .map(() => { deletePidFile(pidFile); cliLogger.warn("Launchpad force stopped"); }); }); }); }).orElse((e) => { if (e instanceof DaemonNotRunningError && config.plugins?.some((p) => p.name === "monitor")) { // try to just stop the monitor process if possible // This is for compatibility with older versions of launchpad, where the 'stop' command only managed the pm2 process // TODO: in a future major version, we can probably remove this fallback or move it to a separate command cliLogger.info("Launchpad is not running."); cliLogger.info("Found monitor configuration, attempting to kill monitor process..."); return ResultAsync.fromPromise(import("@bluecadet/launchpad-monitor/launchpad-monitor"), () => new Error('Could not import "@bluecadet/launchpad-monitor"')).andThen((module) => { const killPM2 = module.killPM2; return killPM2(cliLogger); }); } return err(e); }); }) .orElse((error) => handleFatalError(error)); } // wait helper wrapped in ResultAsync function wait(ms) { return ResultAsync.fromSafePromise(new Promise((resolve) => setTimeout(resolve, ms))); } // kill fn wrapped in Result function safeKill(pid, signal) { try { process.kill(pid, signal); return ok(); } catch (e) { const cause = ensureError(e); return err(new Error(`Failed to send ${signal} to process ${pid}`, { cause })); } } //# sourceMappingURL=stop.js.map