tt-help-cli-ycl 1.4.107 → 1.4.110
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/package.json +1 -1
- package/src/watchdog/upgrader.js +188 -55
package/package.json
CHANGED
package/src/watchdog/upgrader.js
CHANGED
|
@@ -255,33 +255,149 @@ async function terminateProcess(pid, waitMs = 5000) {
|
|
|
255
255
|
return false;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
/**
|
|
259
|
+
* Windows:向 targetPid 所在控制台发送 CTRL_C_EVENT(等效用户在 cmd 窗口按 Ctrl+C)。
|
|
260
|
+
* 旧 watchdog 收到后触发 SIGINT → gracefulShutdown → 优雅停业务进程后退出。
|
|
261
|
+
* 实现:PowerShell P/Invoke AttachConsole(attach 到目标控制台)+
|
|
262
|
+
* GenerateConsoleCtrlEvent(0, 0)(发给共享该控制台的所有进程)。
|
|
263
|
+
* 非 Windows / attach 失败(目标无控制台)返回 false,由调用方强杀兜底。
|
|
264
|
+
*/
|
|
265
|
+
async function sendConsoleCtrlC(targetPid) {
|
|
266
|
+
if (process.platform !== "win32" || !targetPid) return false;
|
|
267
|
+
const ps =
|
|
268
|
+
"Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;" +
|
|
269
|
+
'public class CC{[DllImport("kernel32.dll",SetLastError=true)]public static extern bool FreeConsole();' +
|
|
270
|
+
'[DllImport("kernel32.dll",SetLastError=true)]public static extern bool AttachConsole(uint p);' +
|
|
271
|
+
'[DllImport("kernel32.dll",SetLastError=true)]public static extern bool GenerateConsoleCtrlEvent(uint e,uint g);}\';' +
|
|
272
|
+
`[void][CC]::FreeConsole();$ok=[CC]::AttachConsole(${Number(targetPid)});Write-Output $ok;if($ok){[void][CC]::GenerateConsoleCtrlEvent(0,0)};exit 0`;
|
|
273
|
+
// 注意:信号发出后 PowerShell 自己也会收到 Ctrl+C 而退出(execFile 报错),
|
|
274
|
+
// 因此手动包装只取 stdout(True=attach 成功=信号已发出),忽略 exit code。
|
|
275
|
+
const { stdout } = await new Promise((resolve) => {
|
|
276
|
+
execFile(
|
|
277
|
+
"powershell.exe",
|
|
278
|
+
["-NoProfile", "-Command", ps],
|
|
279
|
+
{ timeout: 15000, windowsHide: true },
|
|
280
|
+
(err, out) => resolve({ stdout: String(out || "") }),
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
return /True/i.test(stdout);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 轮询探测进程是否退出(不发送任何信号) */
|
|
287
|
+
async function waitProcessExit(pid, waitMs) {
|
|
288
|
+
if (!pid) return true;
|
|
289
|
+
const deadline = Date.now() + waitMs;
|
|
290
|
+
while (Date.now() < deadline) {
|
|
291
|
+
try {
|
|
292
|
+
process.kill(pid, 0);
|
|
293
|
+
} catch {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** 向上遍历父链,返回 cmd.exe 祖先进程 PID(旧 watchdog 所在窗口,升级后关窗用) */
|
|
302
|
+
function listCmdAncestorPids(rows, pid) {
|
|
303
|
+
const byId = new Map(rows.map((r) => [r.pid, r]));
|
|
304
|
+
const out = [];
|
|
305
|
+
const seen = new Set();
|
|
306
|
+
let cur = byId.get(pid)?.ppid;
|
|
307
|
+
while (cur && !seen.has(cur)) {
|
|
308
|
+
seen.add(cur);
|
|
309
|
+
const r = byId.get(cur);
|
|
310
|
+
if (!r) break;
|
|
311
|
+
if (/cmd(\.exe)?\b/i.test(r.cmd || "")) out.push(cur);
|
|
312
|
+
cur = r.ppid;
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** 一次拉取全进程表 [{pid, ppid, cmd}](Windows: CIM;POSIX: ps) */
|
|
318
|
+
async function snapshotAllProcesses() {
|
|
319
|
+
const rows = [];
|
|
261
320
|
try {
|
|
262
321
|
if (process.platform === "win32") {
|
|
263
322
|
const ps =
|
|
264
|
-
|
|
265
|
-
|
|
323
|
+
"Get-CimInstance Win32_Process | ForEach-Object { Write-Output (" +
|
|
324
|
+
"$_.ProcessId.ToString() + '|' + $_.ParentProcessId.ToString() + '|' + $_.CommandLine) }";
|
|
266
325
|
const { stdout } = await execFileP(
|
|
267
326
|
"powershell.exe",
|
|
268
327
|
["-NoProfile", "-Command", ps],
|
|
269
|
-
{ timeout:
|
|
328
|
+
{ timeout: 20000, windowsHide: true },
|
|
270
329
|
);
|
|
271
|
-
|
|
272
|
-
.
|
|
273
|
-
|
|
274
|
-
|
|
330
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
331
|
+
const m = line.match(/^(\d+)\|(\d+)\|(.*)$/);
|
|
332
|
+
if (m)
|
|
333
|
+
rows.push({
|
|
334
|
+
pid: parseInt(m[1], 10),
|
|
335
|
+
ppid: parseInt(m[2], 10),
|
|
336
|
+
cmd: m[3] || "",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
const { stdout } = await execFileP("ps", ["-eo", "pid=,ppid=,args="], {
|
|
341
|
+
timeout: 20000,
|
|
342
|
+
});
|
|
343
|
+
for (const line of String(stdout).split(/\n/)) {
|
|
344
|
+
const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);
|
|
345
|
+
if (m)
|
|
346
|
+
rows.push({
|
|
347
|
+
pid: parseInt(m[1], 10),
|
|
348
|
+
ppid: parseInt(m[2], 10),
|
|
349
|
+
cmd: m[3] || "",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
275
352
|
}
|
|
276
|
-
const { stdout } = await execFileP("pgrep", ["-P", String(parentPid)], {
|
|
277
|
-
timeout: 15000,
|
|
278
|
-
});
|
|
279
|
-
return String(stdout)
|
|
280
|
-
.split(/\r?\n/)
|
|
281
|
-
.map((s) => parseInt(s.trim(), 10))
|
|
282
|
-
.filter((n) => n > 0);
|
|
283
353
|
} catch {
|
|
284
|
-
|
|
354
|
+
/* 快照失败时返回空,清理环节自然跳过 */
|
|
355
|
+
}
|
|
356
|
+
return rows;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* BFS 枚举 rootPid 的所有后代 PID(含孙进程),排除 excludePid。
|
|
361
|
+
* 必须基于【杀旧前】的全表快照:Windows 上业务命令经 cmd 中间层启动,
|
|
362
|
+
* 杀旧后中间层死亡导致父链断裂,再枚举会漏掉孙进程(211 实测:
|
|
363
|
+
* webserver 成孤儿占端口,新版拉起 EADDRINUSE)。
|
|
364
|
+
*/
|
|
365
|
+
function listDescendantPids(rows, rootPid, excludePid = 0) {
|
|
366
|
+
if (!rootPid) return [];
|
|
367
|
+
const result = new Set();
|
|
368
|
+
const queue = [rootPid];
|
|
369
|
+
while (queue.length > 0) {
|
|
370
|
+
const cur = queue.shift();
|
|
371
|
+
for (const r of rows) {
|
|
372
|
+
if (r.ppid === cur && r.pid !== excludePid && !result.has(r.pid)) {
|
|
373
|
+
result.add(r.pid);
|
|
374
|
+
queue.push(r.pid);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return [...result];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 快照中「父进程已死」的孤儿业务进程(本包 node 业务,排除 watchdog/upgrader 自身) */
|
|
382
|
+
function listOrphanBusinessPids(rows) {
|
|
383
|
+
const alive = new Set(rows.map((r) => r.pid));
|
|
384
|
+
return rows
|
|
385
|
+
.filter(
|
|
386
|
+
(r) =>
|
|
387
|
+
r.cmd.includes("tt-help-cli-ycl") &&
|
|
388
|
+
!/watchdog|upgrader/.test(r.cmd) &&
|
|
389
|
+
!alive.has(r.ppid),
|
|
390
|
+
)
|
|
391
|
+
.map((r) => r.pid);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 探测进程是否存活(Windows kill(pid,0);EPERM 也视为存活) */
|
|
395
|
+
async function isProcessAlive(pid) {
|
|
396
|
+
try {
|
|
397
|
+
process.kill(pid, 0);
|
|
398
|
+
return true;
|
|
399
|
+
} catch (e) {
|
|
400
|
+
return !!(e && e.code === "EPERM");
|
|
285
401
|
}
|
|
286
402
|
}
|
|
287
403
|
|
|
@@ -289,11 +405,10 @@ async function listChildPids(parentPid) {
|
|
|
289
405
|
async function forceKillTree(pid) {
|
|
290
406
|
try {
|
|
291
407
|
if (process.platform === "win32") {
|
|
292
|
-
await execFileP(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
408
|
+
await execFileP("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
|
|
409
|
+
timeout: 15000,
|
|
410
|
+
windowsHide: true,
|
|
411
|
+
});
|
|
297
412
|
} else {
|
|
298
413
|
process.kill(pid, "SIGKILL");
|
|
299
414
|
}
|
|
@@ -302,34 +417,24 @@ async function forceKillTree(pid) {
|
|
|
302
417
|
}
|
|
303
418
|
}
|
|
304
419
|
|
|
305
|
-
/**
|
|
306
|
-
|
|
307
|
-
* 流程:枚举直接子进程(排除 excludePid)→ 逐个强杀整棵子树 → 等 1s → 复查残留再杀。
|
|
308
|
-
* 背景:Windows 上 SIGTERM=强杀,旧 watchdog 的业务子进程(webserver 等)会变孤儿;
|
|
309
|
-
* macOS/Linux 上优雅退出未完成(SIGKILL 兜底)时同样需要清理。
|
|
310
|
-
* @returns {Promise<number[]>} 被清理的 PID 列表
|
|
311
|
-
*/
|
|
312
|
-
async function killChildProcesses(parentPid, excludePid = 0) {
|
|
420
|
+
/** 按 PID 列表逐个强杀(存活才杀),等 1s 后复查再杀一轮 */
|
|
421
|
+
async function killPidList(pids, label) {
|
|
313
422
|
const killed = [];
|
|
314
|
-
const
|
|
315
|
-
(
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
killed.push(pid);
|
|
423
|
+
for (const pid of pids) {
|
|
424
|
+
if (await isProcessAlive(pid)) {
|
|
425
|
+
await forceKillTree(pid);
|
|
426
|
+
killed.push(pid);
|
|
427
|
+
}
|
|
320
428
|
}
|
|
321
429
|
if (killed.length > 0) {
|
|
322
|
-
console.error(
|
|
323
|
-
`[upgrade-proc] 已清理旧 watchdog 残留子进程: ${killed.join(", ")}`,
|
|
324
|
-
);
|
|
430
|
+
console.error(`[upgrade-proc] ${label}: ${killed.join(", ")}`);
|
|
325
431
|
await new Promise((r) => setTimeout(r, 1000));
|
|
326
|
-
//
|
|
327
|
-
const
|
|
328
|
-
(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
killed.push(pid);
|
|
432
|
+
// 复查:仍存活的再杀一轮(防杀树竞态)
|
|
433
|
+
for (const pid of pids) {
|
|
434
|
+
if (await isProcessAlive(pid)) {
|
|
435
|
+
await forceKillTree(pid);
|
|
436
|
+
if (!killed.includes(pid)) killed.push(pid);
|
|
437
|
+
}
|
|
333
438
|
}
|
|
334
439
|
}
|
|
335
440
|
return killed;
|
|
@@ -437,18 +542,46 @@ export async function runUpgradeMode(argv) {
|
|
|
437
542
|
}
|
|
438
543
|
console.error(`[upgrade-proc] 新版本验证通过 (${verify.version})`);
|
|
439
544
|
|
|
440
|
-
// 3)
|
|
441
|
-
// Windows 上 SIGTERM
|
|
442
|
-
//
|
|
545
|
+
// 3) 杀旧:先优雅退出(等效 Ctrl+C)→ 等待 → 强杀兜底 → 按快照清理进程树 → 关旧窗口
|
|
546
|
+
// Windows 上 process.kill(SIGTERM)=强杀(无优雅),但用户在 cmd 窗口按 Ctrl+C
|
|
547
|
+
// 会触发旧 watchdog 的 SIGINT → gracefulShutdown 优雅停业务。因此先向旧 watchdog
|
|
548
|
+
// 的控制台发送 CTRL_C_EVENT(等效手动按键),8s 未退再强杀兜底;随后按【杀旧前】
|
|
549
|
+
// 快照强杀整棵后代树(排除 U 自己;杀旧后中间层死亡会断父链,必须提前快照),
|
|
550
|
+
// 再兜底清理「父已死」的孤儿业务进程,最后关闭旧 watchdog 所在的 cmd 窗口。
|
|
443
551
|
console.error(`[upgrade-proc] 通知旧 watchdog (pid=${oldPid}) 退出 ...`);
|
|
444
|
-
const
|
|
552
|
+
const procSnapshot = await snapshotAllProcesses();
|
|
553
|
+
const descPids = listDescendantPids(procSnapshot, oldPid, process.pid);
|
|
554
|
+
const cmdAncestors = listCmdAncestorPids(procSnapshot, oldPid);
|
|
555
|
+
|
|
556
|
+
let exited = false;
|
|
557
|
+
const gracefulSent =
|
|
558
|
+
process.platform === "win32" && (await sendConsoleCtrlC(oldPid));
|
|
559
|
+
if (gracefulSent) {
|
|
560
|
+
console.error(
|
|
561
|
+
`[upgrade-proc] 已发送 Ctrl+C(等效手动按键),等待旧 watchdog 优雅停止业务...`,
|
|
562
|
+
);
|
|
563
|
+
exited = await waitProcessExit(oldPid, 8000);
|
|
564
|
+
}
|
|
445
565
|
if (exited) {
|
|
446
|
-
console.error(`[upgrade-proc] 旧 watchdog
|
|
566
|
+
console.error(`[upgrade-proc] 旧 watchdog 已优雅退出`);
|
|
447
567
|
} else {
|
|
448
|
-
|
|
449
|
-
|
|
568
|
+
if (gracefulSent) {
|
|
569
|
+
console.error(`[upgrade-proc] 旧 watchdog 未在 8s 内优雅退出,强制终止`);
|
|
570
|
+
}
|
|
571
|
+
exited = await terminateProcess(oldPid, 5000);
|
|
572
|
+
if (exited) {
|
|
573
|
+
console.error(`[upgrade-proc] 旧 watchdog 已退出`);
|
|
574
|
+
} else {
|
|
575
|
+
console.error(`[upgrade-proc] 旧 watchdog 未在 5s 内退出,已强制终止`);
|
|
576
|
+
await new Promise((r) => setTimeout(r, 1000)); // 给强杀一点生效时间
|
|
577
|
+
}
|
|
450
578
|
}
|
|
451
|
-
await
|
|
579
|
+
await killPidList(descPids, "已清理旧 watchdog 进程树");
|
|
580
|
+
// 兜底:快照时父链已断(中间层先死)的孤儿业务进程,杀旧后按特征清理
|
|
581
|
+
const orphanPids = listOrphanBusinessPids(await snapshotAllProcesses());
|
|
582
|
+
await killPidList(orphanPids, "已清理孤儿业务进程");
|
|
583
|
+
// 关闭旧 watchdog 所在的 cmd 窗口(业务已停,窗口只剩空壳提示符)
|
|
584
|
+
await killPidList(cmdAncestors, "已关闭旧 cmd 窗口");
|
|
452
585
|
|
|
453
586
|
// 4) 旧进程已退,启新版 watchdog
|
|
454
587
|
// Windows:显式弹独立交互 cmd 窗口(start → cmd /k),日志直接显示在窗口,
|