memory-eternal 0.9.23 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -16
- package/.codex-plugin/plugin.json +2 -16
- package/.cursor-plugin/plugin.json +2 -16
- package/.mcp.json +9 -9
- package/LICENSE +21 -21
- package/README.en.md +27 -7
- package/README.md +40 -7
- package/bin/dsh-memory.mjs +318 -188
- package/examples/mcp-memory/memory-eternal.cordis.yml +59 -0
- package/hooks/capture.mjs +77 -77
- package/hooks/claude-session.js +56 -56
- package/hooks/hooks.claude.json +12 -12
- package/hooks/hooks.codex.json +15 -15
- package/hooks/hooks.cursor.json +11 -11
- package/index.js +187 -32
- package/lib/api.js +592 -536
- package/lib/capture-log.js +65 -59
- package/lib/capture-run.js +134 -126
- package/lib/capture.js +278 -14
- package/lib/client.js +20 -17
- package/lib/config-sync.js +161 -89
- package/lib/db.js +359 -212
- package/lib/feedback.js +214 -205
- package/lib/llm-openai.js +84 -84
- package/lib/mcp.js +197 -166
- package/lib/node-bin.js +99 -99
- package/lib/setup.js +470 -470
- package/lib/sse.js +51 -51
- package/lib/stall.js +30 -30
- package/lib/sweep.js +56 -56
- package/lib/vault-resolve.js +84 -84
- package/lib/vault.js +347 -106
- package/lib/watchdog.js +527 -392
- package/lib/web-assets.js +41 -0
- package/lib/web.js +316 -176
- package/package.json +12 -7
- package/scripts/release.mjs +204 -0
- package/skills/memory-recall/SKILL.md +14 -14
- package/web/app.js +20 -17
- package/web/index.html +46 -46
package/lib/watchdog.js
CHANGED
|
@@ -1,393 +1,528 @@
|
|
|
1
|
-
// 记忆核心 · 看门狗(独立进程保活 lib/web.js)。
|
|
2
|
-
//
|
|
3
|
-
// 用法(经 CLI):dsh-memory watchdog [--port 7999] [--interval 5] [--max-restart 10]
|
|
4
|
-
//
|
|
5
|
-
// 行为:
|
|
6
|
-
// - 启动时探测端口:活则只监督不接管;死则 spawn 新 web server
|
|
7
|
-
// - 周期性探活:HTTP GET /memory-eternal/api/overview(不是我们服务视为死)
|
|
8
|
-
// - 探到死:清理残留进程 → spawn 新 web server → restart 计数 +1
|
|
9
|
-
// - restart 超过 max-restart(默认 10)→ fatal 退出,不再保活
|
|
10
|
-
// - SIGINT/SIGTERM:优雅退出(停掉 watchdog 自己拉起的 web,但不动用户手动起的)
|
|
11
|
-
// - 所有状态输出到 stderr(看门狗 daemon 的标准约定)
|
|
12
|
-
//
|
|
13
|
-
// 独立性:watchdog 完全独立于 DSH 宿主——即便旧版插件(无 autoWeb)跑的
|
|
14
|
-
// DSH 环境,watchdog 也能拉起并保活 web server,让浏览器始终可达。
|
|
15
|
-
|
|
16
|
-
import http from 'node:http'
|
|
17
|
-
import net from 'node:net'
|
|
18
|
-
import fs from 'node:fs'
|
|
19
|
-
import os from 'node:os'
|
|
20
|
-
import { spawn } from 'node:child_process'
|
|
21
|
-
import path from 'node:path'
|
|
22
|
-
import { fileURLToPath } from 'node:url'
|
|
23
|
-
import process from 'node:process'
|
|
24
|
-
import { nodeBinary, childEnv } from './node-bin.js'
|
|
25
|
-
|
|
26
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
-
const WEB_JS = path.join(__dirname, 'web.js')
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1
|
+
// 记忆核心 · 看门狗(独立进程保活 lib/web.js)。
|
|
2
|
+
//
|
|
3
|
+
// 用法(经 CLI):dsh-memory watchdog [--port 7999] [--interval 5] [--max-restart 10]
|
|
4
|
+
//
|
|
5
|
+
// 行为:
|
|
6
|
+
// - 启动时探测端口:活则只监督不接管;死则 spawn 新 web server
|
|
7
|
+
// - 周期性探活:HTTP GET /memory-eternal/api/overview(不是我们服务视为死)
|
|
8
|
+
// - 探到死:清理残留进程 → spawn 新 web server → restart 计数 +1
|
|
9
|
+
// - restart 超过 max-restart(默认 10)→ fatal 退出,不再保活
|
|
10
|
+
// - SIGINT/SIGTERM:优雅退出(停掉 watchdog 自己拉起的 web,但不动用户手动起的)
|
|
11
|
+
// - 所有状态输出到 stderr(看门狗 daemon 的标准约定)
|
|
12
|
+
//
|
|
13
|
+
// 独立性:watchdog 完全独立于 DSH 宿主——即便旧版插件(无 autoWeb)跑的
|
|
14
|
+
// DSH 环境,watchdog 也能拉起并保活 web server,让浏览器始终可达。
|
|
15
|
+
|
|
16
|
+
import http from 'node:http'
|
|
17
|
+
import net from 'node:net'
|
|
18
|
+
import fs from 'node:fs'
|
|
19
|
+
import os from 'node:os'
|
|
20
|
+
import { spawn } from 'node:child_process'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
import process from 'node:process'
|
|
24
|
+
import { nodeBinary, childEnv } from './node-bin.js'
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const WEB_JS = path.join(__dirname, 'web.js')
|
|
28
|
+
const PKG_JSON = path.join(__dirname, '..', 'package.json')
|
|
29
|
+
const API_PREFIX = '/memory-eternal/api'
|
|
30
|
+
// -- 单例锁 -------------------------------------------------------------------
|
|
31
|
+
// 每个 DSH 启动都会 spawn 一个 detached watchdog,而旧代码没有任何存活检查 ——
|
|
32
|
+
// 多次重启就堆积多个 watchdog 常驻轮询同一端口(dsh-memory-eternal#6)。
|
|
33
|
+
// 这里用一个 pid 锁文件:同端口已有活着的 watchdog 就让它直接退出。
|
|
34
|
+
const LOCK_NAME = 'memory-eternal-watchdog.json'
|
|
35
|
+
|
|
36
|
+
/** 锁文件路径($DSH_HOME 下,与共享配置同处)。 */
|
|
37
|
+
export function watchdogLockPath(env = process.env) {
|
|
38
|
+
return path.join(env.DSH_HOME || path.join(os.homedir(), '.dsh'), LOCK_NAME)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 当前包的版本号(写进锁文件,用于发现「常驻实例是旧版」这一漂移,issue #19)。
|
|
43
|
+
* 读不到就返回空串(不影响保活)。
|
|
44
|
+
*/
|
|
45
|
+
export function currentPkgVersion() {
|
|
46
|
+
try { return String(JSON.parse(fs.readFileSync(PKG_JSON, 'utf8')).version || '') } catch { return '' }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 进程是否存活(Windows 下 process.kill(pid,0) 同样可用;EPERM 说明进程在但无权限)。 */
|
|
50
|
+
export function isPidAlive(pid) {
|
|
51
|
+
const n = Number(pid)
|
|
52
|
+
if (!n || n <= 0) return false
|
|
53
|
+
if (n === process.pid) return true
|
|
54
|
+
try { process.kill(n, 0); return true } catch (error) { return error?.code === 'EPERM' }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 读取锁文件,归一化成 { watchdogs: [{pid, port, vault, startedAt}] }。
|
|
59
|
+
* 兼容早期单槽格式(顶层就是 {pid, port}):多端口/多 vault 并存时需要多槽。
|
|
60
|
+
*/
|
|
61
|
+
export function readWatchdogLock(env = process.env) {
|
|
62
|
+
let raw = null
|
|
63
|
+
try { raw = JSON.parse(fs.readFileSync(watchdogLockPath(env), 'utf8')) } catch { return { watchdogs: [] } }
|
|
64
|
+
if (!raw || typeof raw !== 'object') return { watchdogs: [] }
|
|
65
|
+
if (Array.isArray(raw.watchdogs)) return { watchdogs: raw.watchdogs.filter((w) => w && w.pid) }
|
|
66
|
+
if (raw.pid) return { watchdogs: [raw] } // 旧格式
|
|
67
|
+
return { watchdogs: [] }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function writeWatchdogLock(env, list) {
|
|
71
|
+
const file = watchdogLockPath(env)
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
74
|
+
fs.writeFileSync(file, JSON.stringify({ watchdogs: list }))
|
|
75
|
+
} catch { /* 写不了锁不阻塞保活 */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 尝试取得看门狗锁(按端口分槽)。
|
|
80
|
+
* @returns {{acquired:boolean, file:string, previous:object|null, reaped:number[]}} acquired=false 表示同端口已有活着的看门狗,调用方应直接退出。
|
|
81
|
+
*/
|
|
82
|
+
export function acquireWatchdogLock({ env = process.env, port, vault = '', pid = process.pid, pkgVersion = currentPkgVersion() } = {}) {
|
|
83
|
+
const file = watchdogLockPath(env)
|
|
84
|
+
const cur = readWatchdogLock(env)
|
|
85
|
+
const samePort = cur.watchdogs.filter((w) => Number(w.port) === Number(port) && w.pid !== pid)
|
|
86
|
+
const alive = samePort.find((w) => isPidAlive(w.pid))
|
|
87
|
+
if (alive) return { acquired: false, file, previous: alive, reaped: [] }
|
|
88
|
+
// 清掉死掉的槽(含其它端口的陈旧记录),保留仍然存活的其它端口
|
|
89
|
+
const reaped = []
|
|
90
|
+
const kept = []
|
|
91
|
+
for (const w of cur.watchdogs) {
|
|
92
|
+
if (Number(w.port) === Number(port)) { reaped.push(w.pid); continue }
|
|
93
|
+
if (isPidAlive(w.pid)) kept.push(w)
|
|
94
|
+
else reaped.push(w.pid)
|
|
95
|
+
}
|
|
96
|
+
const previous = samePort[0] || (reaped.length ? { pid: reaped[0] } : null)
|
|
97
|
+
kept.push({ pid, port: Number(port), vault, startedAt: new Date().toISOString(), ...(pkgVersion ? { pkgVersion } : {}) })
|
|
98
|
+
writeWatchdogLock(env, kept)
|
|
99
|
+
return { acquired: true, file, previous, reaped }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 释放锁(只清自己那一槽;没有其它槽就删文件)。 */
|
|
103
|
+
export function releaseWatchdogLock({ env = process.env, pid = process.pid } = {}) {
|
|
104
|
+
const cur = readWatchdogLock(env)
|
|
105
|
+
const kept = cur.watchdogs.filter((w) => w.pid !== pid)
|
|
106
|
+
if (kept.length === cur.watchdogs.length) return false
|
|
107
|
+
if (!kept.length) { try { fs.unlinkSync(watchdogLockPath(env)) } catch { /* 已不存在 */ } return true }
|
|
108
|
+
writeWatchdogLock(env, kept)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 就地更新某一槽的字段(不存在则忽略)。
|
|
114
|
+
*
|
|
115
|
+
* 用途:watchdog spawn / 回收 web 子进程时,把 webPid / webPort 记进锁里 ——
|
|
116
|
+
* Windows 上 `process.kill(pid,'SIGTERM')` 是**无条件终止**,watchdog 的 stop() 处理器
|
|
117
|
+
* 根本不会执行,所以「停看门狗」时必须靠这份记录才能连带收掉它拉起的 web(issue #19)。
|
|
118
|
+
*
|
|
119
|
+
* @returns {object|null} 更新后的槽;槽不存在时返回 null
|
|
120
|
+
*/
|
|
121
|
+
export function updateWatchdogSlot({ env = process.env, pid = process.pid, patch = {} } = {}) {
|
|
122
|
+
const cur = readWatchdogLock(env)
|
|
123
|
+
let hit = null
|
|
124
|
+
const next = cur.watchdogs.map((w) => {
|
|
125
|
+
if (Number(w.pid) !== Number(pid)) return w
|
|
126
|
+
hit = { ...w, ...patch }
|
|
127
|
+
return hit
|
|
128
|
+
})
|
|
129
|
+
if (!hit) return null
|
|
130
|
+
writeWatchdogLock(env, next)
|
|
131
|
+
return hit
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 看门狗运行状态(issue #19 的 `dsh-memory status`):锁文件里登记了什么、谁还活着、
|
|
136
|
+
* 版本是否与本机磁盘上的包一致。
|
|
137
|
+
* @returns {{lockPath:string, pkgVersion:string, watchdogs:Array<object>}}
|
|
138
|
+
*/
|
|
139
|
+
export function watchdogStatus({ env = process.env, port } = {}) {
|
|
140
|
+
const pkgVersion = currentPkgVersion()
|
|
141
|
+
const want = port === undefined || port === null || port === '' ? null : Number(port)
|
|
142
|
+
const list = readWatchdogLock(env).watchdogs
|
|
143
|
+
.filter((w) => want === null || Number(w.port) === want)
|
|
144
|
+
.map((w) => ({
|
|
145
|
+
pid: Number(w.pid),
|
|
146
|
+
port: Number(w.port),
|
|
147
|
+
vault: String(w.vault || ''),
|
|
148
|
+
startedAt: String(w.startedAt || ''),
|
|
149
|
+
pkgVersion: String(w.pkgVersion || ''),
|
|
150
|
+
alive: isPidAlive(w.pid),
|
|
151
|
+
versionMismatch: !!(w.pkgVersion && pkgVersion && String(w.pkgVersion) !== pkgVersion),
|
|
152
|
+
webPid: Number(w.webPid) || 0,
|
|
153
|
+
webPort: Number(w.webPort) || 0,
|
|
154
|
+
webAlive: !!(Number(w.webPid) > 0 && isPidAlive(w.webPid)),
|
|
155
|
+
}))
|
|
156
|
+
return { lockPath: watchdogLockPath(env), pkgVersion, watchdogs: list }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 显式停止常驻看门狗(issue #19:此前只能手工 kill)。
|
|
161
|
+
*
|
|
162
|
+
* 只动**锁文件里登记、且真的活着**的实例;SIGTERM → 等它退出 → 清锁槽。
|
|
163
|
+
* 多会话共用同一个 watchdog,所以这是显式命令,绝不在配置变更时自动调用。
|
|
164
|
+
*
|
|
165
|
+
* @param {object} opts
|
|
166
|
+
* @param {number|string} [opts.port] 只停这个端口;缺省 = 锁里所有端口
|
|
167
|
+
* @param {number} [opts.timeoutMs] 等待退出的上限
|
|
168
|
+
* @param {(pid:number)=>boolean} [opts.kill] 注入用(单测)
|
|
169
|
+
* @param {(pid:number)=>boolean} [opts.isAlive] 注入用(单测)
|
|
170
|
+
* @param {()=>Promise<Array<{pid:number,port:number,command:string}>>} [opts.listProcesses] 注入用(单测)
|
|
171
|
+
* @returns {Promise<{stopped:number[], webStopped:number[], failed:number[], skipped:number[]}>}
|
|
172
|
+
*/
|
|
173
|
+
export async function stopWatchdogs({ env = process.env, port, timeoutMs = 5000, kill, isAlive, sleep, listProcesses } = {}) {
|
|
174
|
+
const want = port === undefined || port === null || port === '' ? null : Number(port)
|
|
175
|
+
const alive = isAlive || isPidAlive
|
|
176
|
+
const killFn = kill || ((pid) => { try { process.kill(pid, 'SIGTERM'); return true } catch { return false } })
|
|
177
|
+
const wait = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
178
|
+
const slots = readWatchdogLock(env).watchdogs.filter((w) => want === null || Number(w.port) === want)
|
|
179
|
+
|
|
180
|
+
// 先确认「锁里登记的 web 子进程」现在确实是 web.js(防 pid 复用误杀)。
|
|
181
|
+
// 枚举不到(极端环境)就只报 skipped,交由用户手工处理 —— 宁可少杀,不可错杀。
|
|
182
|
+
const wantedWeb = slots.map((w) => ({ pid: Number(w.webPid) || 0, port: Number(w.webPort) || Number(w.port) })).filter((x) => x.pid > 0)
|
|
183
|
+
let confirmedWeb = null
|
|
184
|
+
if (wantedWeb.length) {
|
|
185
|
+
try {
|
|
186
|
+
const procs = listProcesses ? await listProcesses() : await listWatchdogProcesses(defaultExec(), process.platform, 'web.js')
|
|
187
|
+
confirmedWeb = new Set(procs.filter((p) => Number(p.pid) > 0).map((p) => Number(p.pid)))
|
|
188
|
+
} catch { confirmedWeb = null }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stopped = []
|
|
192
|
+
const failed = []
|
|
193
|
+
const skipped = []
|
|
194
|
+
for (const w of slots) {
|
|
195
|
+
if (!alive(w.pid)) { skipped.push(Number(w.pid)); releaseWatchdogLock({ env, pid: w.pid }); continue }
|
|
196
|
+
if (!killFn(w.pid)) { failed.push(Number(w.pid)); continue }
|
|
197
|
+
stopped.push(Number(w.pid))
|
|
198
|
+
}
|
|
199
|
+
// 等它们真的退出(最多 timeoutMs),再清锁槽 —— 否则新实例会以为槽里还是活的
|
|
200
|
+
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0)
|
|
201
|
+
while (Date.now() < deadline && stopped.some((pid) => alive(pid))) await wait(100)
|
|
202
|
+
|
|
203
|
+
// 收掉 watchdog 拉起的 web:Windows 上 process.kill(pid,'SIGTERM') 是无条件终止,
|
|
204
|
+
// watchdog 的 stop() 处理器不会执行 —— 不在这里收,就会留下一个占着端口的孤儿 web,
|
|
205
|
+
// 而「stop 了却还占端口」正是 issue #19 抱怨的那类不可控状态。
|
|
206
|
+
const webStopped = []
|
|
207
|
+
for (const target of wantedWeb) {
|
|
208
|
+
if (!alive(target.pid)) continue
|
|
209
|
+
if (confirmedWeb && !confirmedWeb.has(target.pid)) { skipped.push(target.pid); continue }
|
|
210
|
+
if (!killFn(target.pid)) { failed.push(target.pid); continue }
|
|
211
|
+
webStopped.push(target.pid)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const pid of [...stopped, ...failed, ...webStopped]) {
|
|
215
|
+
if (alive(pid)) { if (pid !== 0) failed.push(pid); continue }
|
|
216
|
+
releaseWatchdogLock({ env, pid })
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
stopped: [...new Set(stopped.filter((p) => !failed.includes(p)))],
|
|
220
|
+
webStopped: [...new Set(webStopped.filter((p) => !failed.includes(p)))],
|
|
221
|
+
failed: [...new Set(failed)],
|
|
222
|
+
skipped: [...new Set(skipped)],
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 解析进程列表(纯函数,便于单测)。
|
|
228
|
+
* 支持 `ps -eo pid,args` 与 Windows 的 `ConvertTo-Csv` 两种输入。
|
|
229
|
+
* @param {string} text 进程列表原文
|
|
230
|
+
* @param {string} [platform]
|
|
231
|
+
* @param {string} [marker] 只保留命令行里带该标记的进程(默认 watchdog.js;确认 web 子进程时传 web.js)
|
|
232
|
+
* @returns {Array<{pid:number, port:number, command:string}>}
|
|
233
|
+
*/
|
|
234
|
+
export function parseWatchdogProcesses(text, platform = process.platform, marker = 'watchdog.js') {
|
|
235
|
+
const out = []
|
|
236
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
237
|
+
const line = raw.trim()
|
|
238
|
+
if (!line || !line.includes(marker)) continue
|
|
239
|
+
let pid = 0
|
|
240
|
+
let command = line
|
|
241
|
+
const csv = /^"?(\d+)"?,(.*)$/.exec(line) // Windows: "1234","node ... watchdog.js --port 7999"
|
|
242
|
+
const ps = /^(\d+)\s+(.*)$/.exec(line) // POSIX: 1234 node ... watchdog.js --port 7999
|
|
243
|
+
if (csv) { pid = Number(csv[1]); command = csv[2].replace(/^"|"$/g, '') }
|
|
244
|
+
else if (ps) { pid = Number(ps[1]); command = ps[2] }
|
|
245
|
+
if (!pid) continue
|
|
246
|
+
const m = /--port\s+(\d+)/.exec(command)
|
|
247
|
+
out.push({ pid, port: m ? Number(m[1]) : 7999, command })
|
|
248
|
+
}
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 列出机器上带 marker 的 node 进程(平台相关,失败返回空数组)。 */
|
|
253
|
+
async function listWatchdogProcesses(exec, platform = process.platform, marker = 'watchdog.js') {
|
|
254
|
+
if (platform === 'win32') {
|
|
255
|
+
const ps = "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation"
|
|
256
|
+
const out = await exec('powershell', ['-NoProfile', '-Command', ps])
|
|
257
|
+
return parseWatchdogProcesses(out, 'win32', marker)
|
|
258
|
+
}
|
|
259
|
+
const out = await exec('ps', ['-eo', 'pid,args'])
|
|
260
|
+
return parseWatchdogProcesses(out, 'linux', marker)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 默认的进程执行器(spawn + 收 stdout;失败返回空串)。 */
|
|
264
|
+
function defaultExec() {
|
|
265
|
+
return async (cmd, args) => {
|
|
266
|
+
const { spawn } = await import('node:child_process')
|
|
267
|
+
return await new Promise((resolve) => {
|
|
268
|
+
const c = spawn(cmd, args, { windowsHide: true })
|
|
269
|
+
let buf = ''
|
|
270
|
+
c.stdout.on('data', (d) => { buf += d })
|
|
271
|
+
c.on('error', () => resolve(''))
|
|
272
|
+
c.on('close', () => resolve(buf))
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 回收历史遗留的孤儿看门狗(新锁只能防新增,管不了升级前已经在跑的)。
|
|
279
|
+
* 只杀「命令行是 watchdog.js + 端口相同 + pid 不是自己 + 不在锁里存活」的进程。
|
|
280
|
+
* @returns {Promise<{scanned:number, killed:number[], skipped:number[]}>}
|
|
281
|
+
*/
|
|
282
|
+
export async function reapStaleWatchdogs({ port = 7999, keepPid = process.pid, env = process.env, exec, list, kill } = {}) {
|
|
283
|
+
const runExec = exec || defaultExec()
|
|
284
|
+
const procs = list ? await list() : await listWatchdogProcesses(runExec)
|
|
285
|
+
const lock = readWatchdogLock(env)
|
|
286
|
+
const lockedAlive = new Set(lock.watchdogs.filter((w) => isPidAlive(w.pid)).map((w) => Number(w.pid)))
|
|
287
|
+
// 绝不能杀自己(也不杀父进程):直接跑 lib/watchdog.js --reap 时,调用者自己的命令行
|
|
288
|
+
// 就含 watchdog.js + 同端口、且不在锁里 —— 早期实现在这里把自己 SIGTERM 了,
|
|
289
|
+
// 表现为「没有任何输出 + 退出码 1」。并发 reap 的另一个进程同理:命令行带 --reap 就放过。
|
|
290
|
+
const protectedPids = new Set([Number(keepPid), process.pid, process.ppid].filter((n) => n > 0))
|
|
291
|
+
const killFn = kill || ((pid) => { try { process.kill(pid, 'SIGTERM'); return true } catch { return false } })
|
|
292
|
+
const killed = []
|
|
293
|
+
const skipped = []
|
|
294
|
+
for (const p of procs) {
|
|
295
|
+
if (Number(p.port) !== Number(port)) { skipped.push(p.pid); continue }
|
|
296
|
+
if (protectedPids.has(Number(p.pid))) { skipped.push(p.pid); continue }
|
|
297
|
+
if (String(p.command || '').includes('--reap')) { skipped.push(p.pid); continue } // 另一个正在 reap 的进程
|
|
298
|
+
if (lockedAlive.has(Number(p.pid))) { skipped.push(p.pid); continue } // 锁里活着的是当前合法实例
|
|
299
|
+
if (!isPidAlive(p.pid)) { skipped.push(p.pid); continue }
|
|
300
|
+
if (killFn(p.pid)) killed.push(p.pid); else skipped.push(p.pid)
|
|
301
|
+
}
|
|
302
|
+
return { scanned: procs.length, killed, skipped }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const ts = () => new Date().toISOString().slice(11, 19) // HH:MM:SS
|
|
306
|
+
const log = (...args) => process.stderr.write(`[${ts()}] [watchdog] ${args.join(' ')}\n`)
|
|
307
|
+
|
|
308
|
+
/** 探测某端口是否在响应 + 是我们的服务(响应含 vaultDir 标记)。 */
|
|
309
|
+
export async function probe(port, timeoutMs = 1000) {
|
|
310
|
+
return new Promise((resolve) => {
|
|
311
|
+
const req = http.request({ host: '127.0.0.1', port, path: `${API_PREFIX}/overview`, method: 'GET', timeout: timeoutMs }, (res) => {
|
|
312
|
+
if (res.statusCode !== 200) return resolve(null)
|
|
313
|
+
const chunks = []
|
|
314
|
+
res.on('data', (c) => chunks.push(c))
|
|
315
|
+
res.on('end', () => {
|
|
316
|
+
try {
|
|
317
|
+
const data = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
318
|
+
if (data && data.ok === true && typeof data.vaultDir === 'string') return resolve(data)
|
|
319
|
+
resolve(null)
|
|
320
|
+
} catch { resolve(null) }
|
|
321
|
+
})
|
|
322
|
+
res.on('error', () => resolve(null))
|
|
323
|
+
})
|
|
324
|
+
req.on('error', () => resolve(null))
|
|
325
|
+
req.on('timeout', () => { req.destroy(); resolve(null) })
|
|
326
|
+
req.end()
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** 探测端口是否有任何 TCP 监听(即便不是我们的服务也占用)。 */
|
|
331
|
+
export async function probeTcp(port, timeoutMs = 600) {
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
const s = new net.Socket()
|
|
334
|
+
s.setTimeout(timeoutMs)
|
|
335
|
+
s.once('connect', () => { s.destroy(); resolve(true) })
|
|
336
|
+
s.once('timeout', () => { s.destroy(); resolve(false) })
|
|
337
|
+
s.once('error', () => resolve(false))
|
|
338
|
+
s.connect(port, '127.0.0.1')
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 启动看门狗。返回对象含 .stop() 优雅退出。
|
|
344
|
+
* @param {object} opts
|
|
345
|
+
* @param {number} [opts.port=7999]
|
|
346
|
+
* @param {string} [opts.vaultRoot] // 默认 ~/.dsh/memory-vault
|
|
347
|
+
* @param {number} [opts.interval=5000]
|
|
348
|
+
* @param {number} [opts.maxRestart=10]
|
|
349
|
+
* @param {boolean} [opts.autoStart=true] // 首次探测到死时是否自动拉起
|
|
350
|
+
*/
|
|
351
|
+
export function startWatchdog({ port = 7999, vaultRoot, interval = 5000, maxRestart = 10, autoStart = true } = {}) {
|
|
352
|
+
let child = null // 我们 spawn 的 web 进程(最近一次)
|
|
353
|
+
const spawned = new Map() // child -> 它监听的端口(用于回收多余实例)
|
|
354
|
+
let ownedByUs = false
|
|
355
|
+
let restartCount = 0
|
|
356
|
+
let stopped = false
|
|
357
|
+
let timer = null
|
|
358
|
+
|
|
359
|
+
// 找空闲端口:port, port+1, ... 最多 +10
|
|
360
|
+
const findFreePort = async (start) => {
|
|
361
|
+
for (let p = start; p < start + 10; p++) {
|
|
362
|
+
if (await probeTcp(p) === false) return p
|
|
363
|
+
}
|
|
364
|
+
return start
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const spawnWeb = async () => {
|
|
368
|
+
if (child && !child.killed) {
|
|
369
|
+
try { child.kill('SIGTERM') } catch {}
|
|
370
|
+
}
|
|
371
|
+
const targetPort = await findFreePort(port)
|
|
372
|
+
const args = [WEB_JS, '--port', String(targetPort)]
|
|
373
|
+
if (vaultRoot) args.push('--vault', vaultRoot)
|
|
374
|
+
log(`spawn web: ${nodeBinary()} ${args.join(' ')}`)
|
|
375
|
+
const c = spawn(nodeBinary(), args, {
|
|
376
|
+
detached: true,
|
|
377
|
+
stdio: 'ignore',
|
|
378
|
+
env: childEnv({ MEMORY_VAULT_DIR: vaultRoot || process.env.MEMORY_VAULT_DIR || '' }),
|
|
379
|
+
windowsHide: true,
|
|
380
|
+
})
|
|
381
|
+
// spawn 失败默认静默,显式记录(Electron 宿主下曾因此完全无迹可查)
|
|
382
|
+
c.on('error', (error) => {
|
|
383
|
+
log(`web spawn failed (${nodeBinary()}): ${error?.message || error}`)
|
|
384
|
+
})
|
|
385
|
+
c.on('exit', (code, sig) => {
|
|
386
|
+
spawned.delete(c)
|
|
387
|
+
if (child === c) child = null
|
|
388
|
+
if (child === null) updateWatchdogSlot({ patch: { webPid: 0, webPort: 0 } })
|
|
389
|
+
if (!stopped && code !== 0) log(`web exited code=${code} sig=${sig}`)
|
|
390
|
+
})
|
|
391
|
+
c.unref()
|
|
392
|
+
spawned.set(c, targetPort)
|
|
393
|
+
child = c
|
|
394
|
+
ownedByUs = true
|
|
395
|
+
// 记进锁:stop 时即便 watchdog 被硬终止(Windows 无真信号),也能找到并收掉这个 web
|
|
396
|
+
updateWatchdogSlot({ patch: { webPid: c.pid, webPort: targetPort } })
|
|
397
|
+
return targetPort
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 回收「跑到备用端口上的多余实例」。
|
|
402
|
+
*
|
|
403
|
+
* findFreePort 会在目标端口被占时退到 port+1…(曾经因此留下一个 8000 的重复 web
|
|
404
|
+
* 进程常驻 66MB,而 tick 只探目标端口,永远发现不了)。目标端口确认可用后,
|
|
405
|
+
* 把不在目标端口上的自家子进程收掉即可(只杀我们自己 spawn 的,不动用户手起的)。
|
|
406
|
+
*/
|
|
407
|
+
const reapExtraChildren = () => {
|
|
408
|
+
for (const [c, p] of spawned) {
|
|
409
|
+
if (p === port) continue
|
|
410
|
+
if (c.exitCode !== null || c.killed) { spawned.delete(c); continue }
|
|
411
|
+
try { c.kill('SIGTERM') } catch {}
|
|
412
|
+
spawned.delete(c)
|
|
413
|
+
log(`目标端口 ${port} 已可用 → 回收多余实例(端口 ${p},pid ${c.pid})`)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const tick = async () => {
|
|
418
|
+
if (stopped) return
|
|
419
|
+
const alive = await probe(port)
|
|
420
|
+
if (alive) {
|
|
421
|
+
// 是我们的服务
|
|
422
|
+
if (!ownedByUs) {
|
|
423
|
+
log(`port ${port} 已存活(外部实例)→ 切换为保活模式(不接管)`)
|
|
424
|
+
ownedByUs = true // 至少逻辑上"知道"它活了
|
|
425
|
+
}
|
|
426
|
+
reapExtraChildren()
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
// 端口死
|
|
430
|
+
if (restartCount >= maxRestart) {
|
|
431
|
+
log(`restart 次数达上限 ${maxRestart} → 退出看门狗(请人工排查)`)
|
|
432
|
+
stop()
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
if (!autoStart) {
|
|
436
|
+
log(`port ${port} 已死,但 --no-restart 设置,跳过`)
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
restartCount += 1
|
|
440
|
+
log(`port ${port} 离线 → 第 ${restartCount} 次拉起 web server`)
|
|
441
|
+
try {
|
|
442
|
+
const newPort = await spawnWeb()
|
|
443
|
+
// 等待 ready
|
|
444
|
+
for (let i = 0; i < 30; i++) {
|
|
445
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
446
|
+
const a = await probe(newPort)
|
|
447
|
+
if (a) {
|
|
448
|
+
log(`web 在 ${newPort} 就绪(vault: ${a.vaultDir})`)
|
|
449
|
+
if (newPort !== port) log(`提示:目标端口 ${port} 被占,watchdog 在 ${newPort} 上保活`)
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
log(`web 未能在 15s 内就绪`)
|
|
454
|
+
} catch (error) {
|
|
455
|
+
log(`spawn 失败:${error?.message || error}`)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const stop = () => {
|
|
460
|
+
if (stopped) return
|
|
461
|
+
stopped = true
|
|
462
|
+
if (timer) { clearInterval(timer); timer = null }
|
|
463
|
+
for (const c of spawned.keys()) {
|
|
464
|
+
if (c.exitCode === null && !c.killed) {
|
|
465
|
+
try { c.kill('SIGTERM') } catch {}
|
|
466
|
+
setTimeout(() => { try { c.kill('SIGKILL') } catch {} }, 1000).unref()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
spawned.clear()
|
|
470
|
+
log('看门狗退出')
|
|
471
|
+
process.exit(0)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 首次探测
|
|
475
|
+
;(async () => {
|
|
476
|
+
const alive = await probe(port)
|
|
477
|
+
if (alive) {
|
|
478
|
+
log(`port ${port} 已活(外部实例),启动保活监督(间隔 ${interval}ms)`)
|
|
479
|
+
} else if (autoStart) {
|
|
480
|
+
log(`port ${port} 离线 → 首次拉起 web`)
|
|
481
|
+
restartCount += 1
|
|
482
|
+
try {
|
|
483
|
+
const np = await spawnWeb()
|
|
484
|
+
for (let i = 0; i < 30; i++) {
|
|
485
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
486
|
+
if (await probe(np)) { log(`web 在 ${np} 就绪`); break }
|
|
487
|
+
}
|
|
488
|
+
} catch (e) { log(`首次拉起失败:${e?.message || e}`) }
|
|
489
|
+
}
|
|
490
|
+
timer = setInterval(tick, interval)
|
|
491
|
+
})()
|
|
492
|
+
|
|
493
|
+
process.on('SIGINT', stop)
|
|
494
|
+
process.on('SIGTERM', stop)
|
|
495
|
+
return { stop, get port() { return port }, get restartCount() { return restartCount } }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 脚本入口:node lib/watchdog.js [--port N] [--interval MS] [--max-restart N] [--vault DIR]
|
|
499
|
+
if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
|
|
500
|
+
const argv = process.argv.slice(2)
|
|
501
|
+
const argOf = (name) => {
|
|
502
|
+
const i = argv.indexOf(name)
|
|
503
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null
|
|
504
|
+
}
|
|
505
|
+
const port = Number(argOf('--port')) || 7999
|
|
506
|
+
const interval = Number(argOf('--interval')) || 5000
|
|
507
|
+
const maxRestart = Number(argOf('--max-restart')) || 10
|
|
508
|
+
const vaultRoot = argOf('--vault') || undefined
|
|
509
|
+
const autoStart = !argv.includes('--no-restart')
|
|
510
|
+
if (argv.includes('--reap')) {
|
|
511
|
+
const out = await reapStaleWatchdogs({ port, keepPid: 0 })
|
|
512
|
+
const msg = `回收孤儿看门狗:扫描 ${out.scanned} 个,清理 ${out.killed.length} 个${out.killed.length ? '(pid ' + out.killed.join(', ') + ')' : ''},保留 ${out.skipped.length} 个`
|
|
513
|
+
// 必须同步写:process.exit() 会截断 stderr 的异步写入(重定向/管道时表现为「完全没有输出」)
|
|
514
|
+
try { fs.writeSync(2, `[watchdog] ${msg}\n`) } catch { log(msg) }
|
|
515
|
+
process.exitCode = 0
|
|
516
|
+
} else {
|
|
517
|
+
const lock = acquireWatchdogLock({ port, vault: vaultRoot || '' })
|
|
518
|
+
if (!lock.acquired) {
|
|
519
|
+
log('同端口已有看门狗在跑(pid ' + (lock.previous && lock.previous.pid) + ',启动于 ' + (lock.previous && lock.previous.startedAt) + ')→ 本次启动退出,避免进程堆积')
|
|
520
|
+
process.exitCode = 0
|
|
521
|
+
} else {
|
|
522
|
+
if (lock.previous) log('清理陈旧看门狗记录(pid ' + lock.previous.pid + ' 已不存在)')
|
|
523
|
+
process.on('exit', () => releaseWatchdogLock())
|
|
524
|
+
startWatchdog({ port, interval, maxRestart, vaultRoot, autoStart })
|
|
525
|
+
log('看门狗已启动(pid ' + process.pid + ',端口 ' + port + ')')
|
|
526
|
+
}
|
|
527
|
+
}
|
|
393
528
|
}
|