diff --git a/README.md b/README.md index b3f2b6b..40826cf 100644 --- a/README.md +++ b/README.md @@ -168,8 +168,11 @@ pnpm install pnpm build # Redis must be reachable at REDIS_URL (set REDIS_PASS if Redis requires AUTH) # Put secrets in .env (JFLOW_JWT_SECRET, JFLOW_SECRETS_KEY, REDIS_URL, …) -pm2 start ecosystem.config.cjs +# Use in-tree PM2 6.x (same module control.js requires). A global `pm2` 7.x +# against a 6.x daemon pegs CPU even when ls shows only 2 fork instances. +pnpm start:pm2 # UI: http://localhost:8500 +# If you already mixed versions: pnpm pm2 -- kill && pnpm start:pm2 ``` Or without the ecosystem file: diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 029f384..d42a8b3 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -3,6 +3,9 @@ * Starts always-on processes only; HTTP (:8700) and workers are owned by * control.js via PM2 (same as `pnpm dev:pm2`). * + * Use `pnpm start:pm2` (in-tree PM2 6.x). Do not use a global `pm2` 7.x — + * a CLI/daemon version mismatch pegs CPU even with instances: 1. + * * Prerequisites: `pnpm build` (packages/web/dist), Redis, .env secrets. */ const fs = require("fs"); @@ -41,7 +44,7 @@ module.exports = { name: "jflow-control", cwd: root, script: "packages/server/control.js", - interpreter: "node", + interpreter: process.execPath, instances: 1, exec_mode: "fork", autorestart: true, @@ -55,7 +58,7 @@ module.exports = { name: "jflow-web", cwd: root, script: "packages/server/web-server.js", - interpreter: "node", + interpreter: process.execPath, instances: 1, exec_mode: "fork", autorestart: true, diff --git a/package.json b/package.json index f41eead..84b8a5d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "start:worker": "pnpm --filter @jerapah-flow/server start:worker", "start:control": "pnpm --filter @jerapah-flow/server start:control", "start:web": "pnpm --filter @jerapah-flow/server start:web", + "pm2": "node scripts/pm2.mjs", + "start:pm2": "node scripts/pm2.mjs start ecosystem.config.cjs", "migrate": "pnpm --filter @jerapah-flow/server migrate", "test": "pnpm --filter @jerapah-flow/shared test && pnpm --filter @jerapah-flow/web test", "lint": "pnpm --filter @jerapah-flow/web lint" diff --git a/packages/server/package.json b/packages/server/package.json index 5f3c4ab..3cccc4b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -42,7 +42,7 @@ "nodemailer": "^9.0.5", "pino": "^10.3.1", "pino-roll": "^4.0.0", - "pm2": "^6.0.13", + "pm2": "6.0.14", "rss-parser": "^3.13.0", "ssh2-sftp-client": "^12.1.1", "webdav": "^5.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b69a5e..3132d66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,7 +77,7 @@ importers: specifier: ^4.0.0 version: 4.0.0 pm2: - specifier: ^6.0.13 + specifier: 6.0.14 version: 6.0.14(supports-color@7.2.0) rss-parser: specifier: ^3.13.0 diff --git a/scripts/pm2.mjs b/scripts/pm2.mjs new file mode 100644 index 0000000..8852c99 --- /dev/null +++ b/scripts/pm2.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * Run the same PM2 binary control.js `require("pm2")` uses. + * A global `pm2` 7.x talking to an in-memory 6.x daemon pegs CPU on start. + */ +import { spawn, spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const require = createRequire( + path.join(root, "packages/server/package.json"), +); +const pm2Root = path.dirname(require.resolve("pm2/package.json")); +const pm2Bin = path.join(pm2Root, "bin/pm2"); +const args = process.argv.slice(2); + +function run(pm2Args, opts = {}) { + return spawnSync(process.execPath, [pm2Bin, ...pm2Args], { + cwd: root, + encoding: "utf8", + ...opts, + }); +} + +const cmd = args[0]; +if (cmd === "start" || cmd === "restart" || cmd === "reload") { + const probe = run(["ls"], { stdio: ["ignore", "pipe", "pipe"] }); + const text = `${probe.stdout ?? ""}${probe.stderr ?? ""}`; + const mem = text.match(/In memory PM2 version:\s*(\S+)/); + const loc = text.match(/Local PM2 version:\s*(\S+)/); + if (mem && loc && mem[1] !== loc[1]) { + console.error( + `[jflow] PM2 daemon ${mem[1]} != CLI ${loc[1]}. Killing the daemon so control.js and the CLI share one version.`, + ); + run(["kill"], { stdio: "inherit" }); + } +} + +const child = spawn(process.execPath, [pm2Bin, ...args], { + cwd: root, + stdio: "inherit", +}); +child.on("exit", (code, signal) => { + if (signal) process.kill(process.pid, signal); + process.exit(code ?? 1); +});