Skip to content

Commit 2b02b1a

Browse files
committed
feat: 使用 pm2 管理进程
1 parent edb1d2c commit 2b02b1a

File tree

8 files changed

+904
-52
lines changed

8 files changed

+904
-52
lines changed

doc/dev/realize.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ mockm 是一个基于 Node.js 和 Express 的 API 模拟和调试工具,集成
7676

7777
### 进程管理架构
7878

79-
mockm 采用多进程架构,通过 `@wll8/process-manager` 管理进程生命周期:
79+
mockm 采用多进程架构,通过 `pm2` 管理进程生命周期:
8080

8181
1. **主进程 (run.js)**: 负责配置解析、进程管理、文件监听
8282
2. **服务进程 (server.js)**: 负责启动各种服务器和业务逻辑

server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"@wll8/better-mock": "^0.3.3-alpha",
1818
"@wll8/express-ws": "^1.0.6",
1919
"@wll8/json-server": "^0.17.5-alpha.1",
20-
"@wll8/process-manager": "^1.0.4",
2120
"address": "^1.2.2",
2221
"axios": "^0.30.0",
2322
"body-parser": "^1.20.3",
@@ -36,6 +35,7 @@
3635
"morgan": "^1.10.0",
3736
"node-http-proxy-json": "^0.1.9",
3837
"path-to-regexp": "0.1.12",
38+
"pm2": "^6.0.8",
3939
"postinstall-postinstall": "^2.1.0",
4040
"vm2": "^3.9.19",
4141
"whatnpm": "^1.0.0"

server/pnpm-lock.yaml

Lines changed: 747 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/run.js

Lines changed: 109 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,27 @@ process.argv.includes(`--log-line`) && logHelper()
1313

1414
const fs = require(`fs`)
1515
const path = require(`path`)
16+
const util = require('util')
1617
const { tool, business } = require(`${__dirname}/util/index.js`)
1718
const lib = require(`${__dirname}/util/lib.js`)
1819
const packageJson = require(`${__dirname}/package.json`)
1920
const cli = tool.cli
2021
const cliArg = cli.parseArgv()
2122
const serverPath = path.normalize(`${__dirname}/server.js`) // 转换为跨平台的路径
22-
const { ProcessManager } = require(`@wll8/process-manager`)
23+
const pm2 = require('pm2')
24+
25+
// 使用 util.promisify 封装 PM2 的回调方法
26+
const pm2Async = {
27+
connect: util.promisify(pm2.connect.bind(pm2)),
28+
start: util.promisify(pm2.start.bind(pm2)),
29+
restart: util.promisify(pm2.restart.bind(pm2)),
30+
stop: util.promisify(pm2.stop.bind(pm2)),
31+
delete: util.promisify(pm2.delete.bind(pm2)),
32+
list: util.promisify(pm2.list.bind(pm2)),
33+
sendDataToProcessId: util.promisify(pm2.sendDataToProcessId.bind(pm2)),
34+
launchBus: util.promisify(pm2.launchBus.bind(pm2)),
35+
disconnect: () => pm2.disconnect() // 这个方法不需要 promisify
36+
}
2337

2438
{ // 尽早的, 无依赖的修改 cwd, 避免其他读取到旧值
2539
const cwd = tool.cli.handlePathArg(
@@ -90,42 +104,109 @@ new Promise( async () => { // 检查更新
90104

91105
new Promise(async () => { // 启动 server.js
92106
let log = ``
107+
let isShuttingDown = false
93108
const nodeArg = typeof(cliArg[`--node-options`]) === `string` ? cliArg[`--node-options`] : ``
94-
const arr = [nodeArg, serverPath, ...process.argv.slice(2), `_base64=${base64config}`, `_share=${sharePath}`].filter(item => item.trim() !== ``)
95-
const cp = new ProcessManager(arr)
96-
cp.on(`stdout`, (data) => {
97-
log = String(data)
98-
})
99-
cp.on(`stderr`, (data) => {
100-
log = String(data)
101-
})
102-
cp.on(`message`, ({action, data} = {}) => {
103-
if(action === `reboot`) {
104-
cp.reboot(0)
105-
}
106-
if(action === `config`) {
107-
cp.autoReStart = data.guard
108-
}
109-
})
110-
cp.on(`close`, () => {
111-
if(log.match(/killProcess:/)) { // 保存错误日志
112-
saveLog({
113-
code: ``,
114-
logStr: log,
115-
logPath: shareConfig._errLog,
116-
})
117-
}
118-
})
109+
const processName = `mockm-${process.pid}`
110+
111+
// PM2 进程配置
112+
const pm2Config = {
113+
name: processName,
114+
script: serverPath,
115+
args: [...process.argv.slice(2), `_base64=${base64config}`, `_share=${sharePath}`],
116+
nodeArgs: nodeArg ? nodeArg.split(' ').filter(arg => arg.trim()) : [],
117+
cwd: process.cwd(),
118+
autorestart: false, // 手动控制重启
119+
watch: false, // 关闭 PM2 自带的文件监听,使用自定义监听
120+
max_memory_restart: '500M',
121+
merge_logs: true,
122+
kill_timeout: 5000,
123+
namespace: process.env.PM2_NAMESPACE,
124+
// 不指定日志文件,让 PM2 使用默认位置,然后我们通过流来转发
125+
silent: false
126+
}
119127

128+
// 监听进程消息和日志
129+
async function listenToProcessMessages() {
130+
const pm2_bus = await pm2Async.launchBus()
131+
132+
// 监听日志输出
133+
pm2_bus.on('log:out', (packet) => {
134+
if (packet.process.name === processName) {
135+
process.stdout.write(packet.data)
136+
log = String(packet.data)
137+
}
138+
})
139+
140+
pm2_bus.on('log:err', (packet) => {
141+
if (packet.process.name === processName) {
142+
process.stderr.write(packet.data)
143+
log = String(packet.data)
144+
}
145+
})
146+
147+
pm2_bus.on('process:msg', (packet) => {
148+
if (packet.process.name === processName) {
149+
const { action, data = {} } = packet.data || {}
150+
if (action === 'err-exit') {
151+
if(pm2Config.autorestart) {
152+
console.log(`[${processName}] Auto restarting process...`)
153+
} else {
154+
killProcess()
155+
}
156+
}
157+
if (action === 'reboot') {
158+
pm2Async.restart(processName)
159+
}
160+
161+
if (action === 'config') {
162+
pm2Config.autorestart = data.guard
163+
}
164+
}
165+
})
166+
}
167+
168+
// 优雅关闭函数
120169
function killProcess() {
121-
cp.kill()
122-
process.exit()
170+
isShuttingDown = true
171+
console.log(`[${processName}] Shutting down...`)
172+
pm2Async.delete(processName)
173+
.then(() => {
174+
pm2Async.disconnect()
175+
process.exit(0)
176+
})
177+
.catch(err => {
178+
console.error('Error during shutdown:', err)
179+
pm2Async.disconnect()
180+
process.exit(1)
181+
})
123182
}
183+
184+
// 绑定进程信号
124185
process.on(`SIGTERM`, killProcess)
125186
process.on(`SIGINT`, killProcess)
126187
process.on(`uncaughtException`, killProcess)
127188
process.on(`unhandledRejection`, killProcess)
128189

190+
try {
191+
192+
// 启动 PM2 管理的进程
193+
await pm2Async.connect()
194+
195+
// 先清理可能存在的同名进程
196+
await pm2Async.delete(processName).catch(() => {}) // 忽略错误,因为进程可能不存在
197+
198+
// 启动新进程
199+
await pm2Async.start(pm2Config)
200+
201+
// 监听进程消息
202+
listenToProcessMessages()
203+
204+
} catch (err) {
205+
console.error('Failed to start process:', err)
206+
pm2Async.disconnect()
207+
process.exit(1)
208+
}
209+
129210
const {
130211
showLocalInfo,
131212
remoteServer,

server/server.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ const util = require(`./util/index.js`)
99

1010
new Promise(async () => {
1111
global.config = await require(`./config.js`)
12-
process.send({action: `config`, data: global.config})
13-
util.tool.file.fileChange([global.config._configFile, ...global.config.watch], (files) => process.send({action: `reboot`, data: files}))
12+
process.send({
13+
type: 'process:msg',
14+
data: { action: 'config', data: global.config }
15+
})
16+
util.tool.file.fileChange([global.config._configFile, ...global.config.watch], (files) => process.send({
17+
type: 'process:msg',
18+
data: { action: 'reboot', data: files }
19+
}))
1420
const {
1521
tool,
1622
business,

server/util/tool.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,8 +1176,15 @@ function tool() { // 与业务没有相关性, 可以脱离业务使用的工具
11761176
*/
11771177
function clearProcess({hostname} = {}) {
11781178
function killProcess(...arg) {
1179-
arg[0] !== `SIGINT` && console.log(`killProcess:`, ...arg) // ctrl+c 不需要显示 err
1180-
hostname ? sysHost(`remove`, {hostname}).finally(process.exit) : process.exit()
1179+
if(arg[0] !== `SIGINT`) {
1180+
process.send({
1181+
type: 'process:msg',
1182+
data: { action: 'err-exit', data: [] }
1183+
})
1184+
} else {
1185+
process.exit()
1186+
}
1187+
hostname ? sysHost(`remove`, {hostname}).finally(() => {}) : () => {}
11811188
}
11821189
process.on(`SIGTERM`, killProcess)
11831190
process.on(`SIGINT`, killProcess)

test/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@
2424
"mochawesome": "^7.1.3",
2525
"shelljs": "^0.8.4",
2626
"ws": "^7.5.3"
27+
},
28+
"dependencies": {
29+
"pm2": "^6.0.8"
2730
}
2831
}

test/util.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,24 +383,39 @@ function testCliText({cmd = str, timeout = 30 * 1e3, fn = (str) => str, } = {})
383383
return new Promise((resolve, reject) => {
384384
const { spawn } = require('child_process');
385385
const [bin, ...arg] = cmd.split(/\s+/)
386-
const cmdRef = spawn(bin, arg);
386+
const cmdRef = spawn(bin, arg, {
387+
env: {
388+
...process.env,
389+
PM2_NAMESPACE: `MOCKM_TEST`,
390+
}
391+
});
392+
const pm2Kill = async () => {
393+
// hack: 由于 kill() 没办法停止 pm2 进程, 所以使用 pm2 进行删除
394+
return new Promise(async (ok) => {
395+
cmdRef.kill()
396+
require(`child_process`).exec(`npx pm2 del mockm-${cmdRef.pid}`, {
397+
cwd: `${__dirname}/../server/`,
398+
stdio: `inherit`,
399+
}, ok)
400+
})
401+
}
387402
cmdRef.stdout.on('data', async (data) => {
388403
const str = String(data)
389404
if(await fn(str)) {
405+
await pm2Kill()
390406
resolve(str)
391-
cmdRef.kill()
392407
}
393408
});
394409
cmdRef.stderr.on('data', async (data) => {
395410
const str = String(data)
396411
if(await fn(str)) {
412+
await pm2Kill()
397413
resolve(str)
398-
cmdRef.kill()
399414
}
400415
});
401-
setTimeout(() => {
416+
setTimeout(async () => {
417+
await pm2Kill()
402418
reject(false)
403-
cmdRef.kill()
404419
}, timeout);
405420
})
406421
}
@@ -441,7 +456,12 @@ function allTestBefore() {
441456
console.log('备份用户配置')
442457
}
443458

444-
function allTestAfter() {
459+
async function allTestAfter() {
460+
require(`child_process`).exec(`npx pm2 del MOCKM_TEST`, {
461+
cwd: `${__dirname}/../server/`,
462+
stdio: `inherit`,
463+
}, () => {})
464+
await sleep(5000)
445465
console.log('恢复用户配置')
446466
}
447467

0 commit comments

Comments
 (0)