tt-help-cli-ycl 1.4.105 → 1.4.107
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 +165 -17
package/package.json
CHANGED
package/src/watchdog/upgrader.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 见 runUpgradeMode。旧 watchdog 收到 SIGTERM 后优雅退出,新版接管。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { exec, spawn } from "child_process";
|
|
9
|
+
import { exec, spawn, execFile } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
10
11
|
import { readFileSync, openSync } from "fs";
|
|
11
12
|
import { fileURLToPath } from "url";
|
|
12
13
|
import { dirname, join } from "path";
|
|
@@ -18,6 +19,7 @@ const PACKAGE = "tt-help-cli-ycl";
|
|
|
18
19
|
const REGISTRY = "https://registry.npmjs.org";
|
|
19
20
|
const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
20
21
|
const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
|
|
22
|
+
const execFileP = promisify(execFile);
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* 升级检测源:TT_HELP_REGISTRY 环境变量 > ~/.tt-help.json 的 npmRegistry > 官方 npmjs.org
|
|
@@ -253,6 +255,140 @@ async function terminateProcess(pid, waitMs = 5000) {
|
|
|
253
255
|
return false;
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
/** 枚举指定父进程的直接子进程 PID(Windows: PowerShell;POSIX: pgrep) */
|
|
259
|
+
async function listChildPids(parentPid) {
|
|
260
|
+
if (!parentPid) return [];
|
|
261
|
+
try {
|
|
262
|
+
if (process.platform === "win32") {
|
|
263
|
+
const ps =
|
|
264
|
+
`Get-CimInstance Win32_Process -Filter "ParentProcessId=${parentPid}" ` +
|
|
265
|
+
`| Select-Object -ExpandProperty ProcessId`;
|
|
266
|
+
const { stdout } = await execFileP(
|
|
267
|
+
"powershell.exe",
|
|
268
|
+
["-NoProfile", "-Command", ps],
|
|
269
|
+
{ timeout: 15000, windowsHide: true },
|
|
270
|
+
);
|
|
271
|
+
return String(stdout)
|
|
272
|
+
.split(/\r?\n/)
|
|
273
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
274
|
+
.filter((n) => n > 0);
|
|
275
|
+
}
|
|
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
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 强杀单个进程及其子树(Windows: taskkill /T /F;POSIX: SIGKILL) */
|
|
289
|
+
async function forceKillTree(pid) {
|
|
290
|
+
try {
|
|
291
|
+
if (process.platform === "win32") {
|
|
292
|
+
await execFileP(
|
|
293
|
+
"taskkill.exe",
|
|
294
|
+
["/PID", String(pid), "/T", "/F"],
|
|
295
|
+
{ timeout: 15000, windowsHide: true },
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
process.kill(pid, "SIGKILL");
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
/* 进程可能已退出 */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 清理旧 watchdog 残留的子进程(升级后不留孤儿占端口/资源)。
|
|
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) {
|
|
313
|
+
const killed = [];
|
|
314
|
+
const firstRound = (await listChildPids(parentPid)).filter(
|
|
315
|
+
(p) => p !== excludePid,
|
|
316
|
+
);
|
|
317
|
+
for (const pid of firstRound) {
|
|
318
|
+
await forceKillTree(pid);
|
|
319
|
+
killed.push(pid);
|
|
320
|
+
}
|
|
321
|
+
if (killed.length > 0) {
|
|
322
|
+
console.error(
|
|
323
|
+
`[upgrade-proc] 已清理旧 watchdog 残留子进程: ${killed.join(", ")}`,
|
|
324
|
+
);
|
|
325
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
326
|
+
// 复查:仍有残留则再杀一轮
|
|
327
|
+
const remain = (await listChildPids(parentPid)).filter(
|
|
328
|
+
(p) => p !== excludePid,
|
|
329
|
+
);
|
|
330
|
+
for (const pid of remain) {
|
|
331
|
+
await forceKillTree(pid);
|
|
332
|
+
killed.push(pid);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return killed;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 枚举当前所有 watchdog 进程 PID(存活校验前后对比用;不含升级器 U 自身) */
|
|
339
|
+
async function listWatchdogPids() {
|
|
340
|
+
try {
|
|
341
|
+
if (process.platform === "win32") {
|
|
342
|
+
const ps =
|
|
343
|
+
`Get-CimInstance Win32_Process -Filter "Name='node.exe'" ` +
|
|
344
|
+
`| Where-Object { $_.CommandLine -match 'cli\\.js watchdog' } ` +
|
|
345
|
+
`| Select-Object -ExpandProperty ProcessId`;
|
|
346
|
+
const { stdout } = await execFileP(
|
|
347
|
+
"powershell.exe",
|
|
348
|
+
["-NoProfile", "-Command", ps],
|
|
349
|
+
{ timeout: 15000, windowsHide: true },
|
|
350
|
+
);
|
|
351
|
+
return String(stdout)
|
|
352
|
+
.split(/\r?\n/)
|
|
353
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
354
|
+
.filter((n) => n > 0);
|
|
355
|
+
}
|
|
356
|
+
const { stdout } = await execFileP("pgrep", ["-f", "cli.js watchdog"], {
|
|
357
|
+
timeout: 15000,
|
|
358
|
+
});
|
|
359
|
+
return String(stdout)
|
|
360
|
+
.split(/\r?\n/)
|
|
361
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
362
|
+
.filter((n) => n > 0);
|
|
363
|
+
} catch {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 启动新版 watchdog:
|
|
370
|
+
* - Windows:`cmd.exe /c start cmd /k <node> <cli.js> watchdog ...`
|
|
371
|
+
* start 弹出【独立交互 cmd 窗口】(与 U 完全解耦),watchdog 日志直接显示在窗口里,
|
|
372
|
+
* /k 保持窗口打开(watchdog 退出后提示符仍在,便于排障);中间 cmd 隐藏不闪窗。
|
|
373
|
+
* - 其他平台:直接 detached spawn node,日志落盘 logFd。
|
|
374
|
+
* @returns {import("child_process").ChildProcess}
|
|
375
|
+
*/
|
|
376
|
+
function spawnNewWatchdog(cliPath, args, logFd) {
|
|
377
|
+
if (process.platform === "win32") {
|
|
378
|
+
const child = spawn(
|
|
379
|
+
"cmd.exe",
|
|
380
|
+
["/c", "start", "cmd", "/k", process.execPath, cliPath, ...args],
|
|
381
|
+
{ detached: true, stdio: "ignore", windowsHide: true },
|
|
382
|
+
);
|
|
383
|
+
child.unref();
|
|
384
|
+
child.on("error", (err) => {
|
|
385
|
+
console.error(`[upgrade-proc] 新版 watchdog 启动失败: ${err.message}`);
|
|
386
|
+
});
|
|
387
|
+
return child;
|
|
388
|
+
}
|
|
389
|
+
return spawnChild([cliPath, ...args], { logFd });
|
|
390
|
+
}
|
|
391
|
+
|
|
256
392
|
/** 新版 watchdog 的 CLI 参数(透传 server/clientId/interval/noDaemon,保证与旧版一致) */
|
|
257
393
|
function buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon }) {
|
|
258
394
|
const args = ["watchdog"];
|
|
@@ -301,7 +437,9 @@ export async function runUpgradeMode(argv) {
|
|
|
301
437
|
}
|
|
302
438
|
console.error(`[upgrade-proc] 新版本验证通过 (${verify.version})`);
|
|
303
439
|
|
|
304
|
-
// 3)
|
|
440
|
+
// 3) 杀旧:先优雅发退出信号 → 等待 → 检测残留子进程 → 强杀
|
|
441
|
+
// Windows 上 SIGTERM=强杀(无优雅),旧 watchdog 的业务子进程(webserver 等)
|
|
442
|
+
// 会变孤儿占端口,因此杀旧后必须检测并强杀残留子进程(排除 U 自己)。
|
|
305
443
|
console.error(`[upgrade-proc] 通知旧 watchdog (pid=${oldPid}) 退出 ...`);
|
|
306
444
|
const exited = await terminateProcess(oldPid, 5000);
|
|
307
445
|
if (exited) {
|
|
@@ -310,9 +448,10 @@ export async function runUpgradeMode(argv) {
|
|
|
310
448
|
console.error(`[upgrade-proc] 旧 watchdog 未在 5s 内退出,已强制终止`);
|
|
311
449
|
await new Promise((r) => setTimeout(r, 1000)); // 给强杀一点生效时间
|
|
312
450
|
}
|
|
451
|
+
await killChildProcesses(oldPid, process.pid);
|
|
313
452
|
|
|
314
453
|
// 4) 旧进程已退,启新版 watchdog
|
|
315
|
-
//
|
|
454
|
+
// Windows:显式弹独立交互 cmd 窗口(start → cmd /k),日志直接显示在窗口,
|
|
316
455
|
// 与旧 cmd /k 窗口解耦(旧窗口控制台在旧 watchdog 被杀后不可复用)
|
|
317
456
|
console.error(`[upgrade-proc] 启动新版 watchdog ...`);
|
|
318
457
|
const cliPath = join(__dirname, "..", "..", "cli.js");
|
|
@@ -322,31 +461,40 @@ export async function runUpgradeMode(argv) {
|
|
|
322
461
|
intervalSec,
|
|
323
462
|
noDaemon,
|
|
324
463
|
});
|
|
325
|
-
const logFd =
|
|
326
|
-
"
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
464
|
+
const logFd =
|
|
465
|
+
process.platform === "win32"
|
|
466
|
+
? undefined
|
|
467
|
+
: openLogFile("tt-help-watchdog", "upgrade-proc", "新版 watchdog 日志");
|
|
468
|
+
// 记录启动前的 watchdog 进程集合,用于存活校验前后对比(Windows 经 start 启动时
|
|
469
|
+
// 中间 cmd 秒退,不能检查其 PID)
|
|
470
|
+
const beforePids = await listWatchdogPids();
|
|
330
471
|
let child;
|
|
331
472
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
473
|
+
child = spawnNewWatchdog(cliPath, args, logFd);
|
|
474
|
+
if (process.platform === "win32") {
|
|
475
|
+
console.error(
|
|
476
|
+
`[upgrade-proc] 已弹独立 cmd 窗口启动新版 watchdog(窗口内可见运行日志)`,
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
console.error(`[upgrade-proc] 新版 watchdog 已启动 (pid=${child.pid})`);
|
|
480
|
+
}
|
|
335
481
|
} catch (e) {
|
|
336
482
|
console.error(`[upgrade-proc] 新版 watchdog 启动失败: ${e.message}`);
|
|
337
483
|
process.exit(1);
|
|
338
484
|
}
|
|
339
485
|
|
|
340
|
-
// 5) 存活校验:3s
|
|
486
|
+
// 5) 存活校验:3s 后确认出现新增的 watchdog 进程;未存活则明确报错,不静默
|
|
341
487
|
await new Promise((r) => setTimeout(r, 3000));
|
|
342
|
-
|
|
343
|
-
|
|
488
|
+
const newPids = (await listWatchdogPids()).filter(
|
|
489
|
+
(p) => !beforePids.includes(p),
|
|
490
|
+
);
|
|
491
|
+
if (newPids.length > 0) {
|
|
344
492
|
console.error(
|
|
345
|
-
`[upgrade-proc] 新版 watchdog 存活确认 (pid=${
|
|
493
|
+
`[upgrade-proc] 新版 watchdog 存活确认 (pid=${newPids.join(",")}),升级完成`,
|
|
346
494
|
);
|
|
347
|
-
}
|
|
495
|
+
} else {
|
|
348
496
|
console.error(
|
|
349
|
-
`[upgrade-proc] 新版 watchdog
|
|
497
|
+
`[upgrade-proc] 新版 watchdog 启动后未存活,请查看窗口/日志排障`,
|
|
350
498
|
);
|
|
351
499
|
process.exit(1);
|
|
352
500
|
}
|