tt-help-cli-ycl 1.4.159 → 1.4.161
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/agent.js +27 -0
- package/src/watchdog/upgrader.js +125 -10
package/package.json
CHANGED
package/src/watchdog/agent.js
CHANGED
|
@@ -296,6 +296,7 @@ export class WatchdogAgent {
|
|
|
296
296
|
this._switchRestartEnabled = false; // 代理切换重启默认不启用(后端下发开启才生效)
|
|
297
297
|
this._switchRestartConfigKey = null; // 上次配置指纹(幂等打印用)
|
|
298
298
|
this._autoUpgradeEnabled = true; // 自动升级默认开启(后端下发关闭才停用)
|
|
299
|
+
this._loadedVersion = getVersion(); // 启动时包版本(磁盘版本变化 = 被手动升级,需自动重启)
|
|
299
300
|
this._errorTriggerEnabled = false; // 默认不启用(后端下发开启才生效)
|
|
300
301
|
this._errorTriggerCount = ERROR_TRIGGER_COUNT;
|
|
301
302
|
this._errorTriggerMinMs = ERROR_MIN_MS;
|
|
@@ -1068,6 +1069,7 @@ export class WatchdogAgent {
|
|
|
1068
1069
|
_startUpgradeChecker() {
|
|
1069
1070
|
if (!isGlobalInstall()) return;
|
|
1070
1071
|
if (!this._autoUpgradeEnabled) return;
|
|
1072
|
+
this._startSelfVersionWatcher();
|
|
1071
1073
|
|
|
1072
1074
|
this.upgradeChecker = startUpgradeChecker({
|
|
1073
1075
|
onUpdateAvailable: async ({ current, latest }) => {
|
|
@@ -1082,6 +1084,31 @@ export class WatchdogAgent {
|
|
|
1082
1084
|
});
|
|
1083
1085
|
}
|
|
1084
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* 磁盘版本自检(每 60s):发现磁盘包版本 ≠ 本进程加载版本(被手动 npm i -g 升/降级)时,
|
|
1089
|
+
* 自动走 restart-only 重启(杀旧 → 启新 → 校验),无需人工重启。
|
|
1090
|
+
* 只比不等:手动降级同样重启,保证运行代码与磁盘一致。
|
|
1091
|
+
*/
|
|
1092
|
+
_startSelfVersionWatcher() {
|
|
1093
|
+
if (this._selfVersionTimer) return;
|
|
1094
|
+
this._selfVersionTimer = setInterval(() => {
|
|
1095
|
+
try {
|
|
1096
|
+
const disk = getVersion();
|
|
1097
|
+
if (disk && disk !== this._loadedVersion) {
|
|
1098
|
+
console.error(
|
|
1099
|
+
`[upgrade] 检测到包版本变化 ${this._loadedVersion} → ${disk}(手动升级),自动重启 watchdog...`,
|
|
1100
|
+
);
|
|
1101
|
+
clearInterval(this._selfVersionTimer);
|
|
1102
|
+
this._selfVersionTimer = null;
|
|
1103
|
+
spawnUpgrader({ ...this._buildRestartParams(), restartOnly: true });
|
|
1104
|
+
}
|
|
1105
|
+
} catch {
|
|
1106
|
+
/* 读版本失败,下轮重试 */
|
|
1107
|
+
}
|
|
1108
|
+
}, 60_000);
|
|
1109
|
+
if (this._selfVersionTimer.unref) this._selfVersionTimer.unref();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1085
1112
|
/** 构建新版 watchdog 重启所需参数(透传给独立升级器) */
|
|
1086
1113
|
_buildRestartParams() {
|
|
1087
1114
|
return {
|
package/src/watchdog/upgrader.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { exec, spawn, execFile } from "child_process";
|
|
10
10
|
import { promisify } from "util";
|
|
11
|
-
import { readFileSync, openSync, unlinkSync } from "fs";
|
|
11
|
+
import { readFileSync, writeFileSync, openSync, unlinkSync } from "fs";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { dirname, join } from "path";
|
|
14
14
|
import { homedir, tmpdir } from "os";
|
|
@@ -549,19 +549,38 @@ async function listWatchdogPids() {
|
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
/**
|
|
552
|
-
* 启动新版 watchdog
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
552
|
+
* 启动新版 watchdog(Windows 方式 2026-09-25 重写 — 214 实测教训):
|
|
553
|
+
*
|
|
554
|
+
* 旧实现(node spawn 直接嵌 start 命令,windowsHide:true)起的 watchdog 会「起而复死」:
|
|
555
|
+
* 外层 cmd 带 CREATE_NO_WINDOW 隐藏启动,其内 start 弹出的新控制台随外层生命周期
|
|
556
|
+
* 被系统回收,watchdog 写完存活标记后数秒内消失(1.4.159 自动升级后「看门狗没拉起来」实因)。
|
|
557
|
+
*
|
|
558
|
+
* 新实现:写临时 bat,由 cmd /c 执行;bat 内用 start 弹独立交互窗口运行新版 ——
|
|
559
|
+
* 与运维手动双击/命令行启动完全同构(213/214 实测该方式进程持久不灭)。
|
|
560
|
+
* 窗口标题含 WINDOW_TITLE_TAG,供下次升级 killOldWatchdogWindow 关窗匹配。
|
|
561
|
+
*
|
|
562
|
+
* 其他平台:直接 detached spawn node,日志落盘 logFd。
|
|
557
563
|
* @returns {import("child_process").ChildProcess}
|
|
558
564
|
*/
|
|
559
565
|
function spawnNewWatchdog(cliPath, args, logFd) {
|
|
560
566
|
if (process.platform === "win32") {
|
|
567
|
+
// start 直接执行 node(不经 cmd /k 中转):cmd /k 后带多个引号段会触发 cmd 的
|
|
568
|
+
// 引号剥离规则(剥整行首尾引号 → 带空格的 node 路径被截断成 C:\Program → 启动失败)
|
|
569
|
+
const cmdLine = [cliPath, ...args].map((a) => `"${a}"`).join(" ");
|
|
570
|
+
const batPath = join(
|
|
571
|
+
tmpdir(),
|
|
572
|
+
`tt-help-watchdog-start-${Date.now()}.bat`,
|
|
573
|
+
);
|
|
574
|
+
const bat = [
|
|
575
|
+
"@echo off",
|
|
576
|
+
`start "${WINDOW_TITLE_TAG}" "${process.execPath}" ${cmdLine}`,
|
|
577
|
+
"exit",
|
|
578
|
+
].join("\r\n");
|
|
579
|
+
writeFileSync(batPath, bat + "\r\n", "utf-8");
|
|
561
580
|
const child = spawn(
|
|
562
581
|
"cmd.exe",
|
|
563
|
-
["/c",
|
|
564
|
-
//
|
|
582
|
+
["/c", batPath],
|
|
583
|
+
// 外层 cmd 隐藏且一闪即退;新窗口由 start 创建,独立持久
|
|
565
584
|
{ detached: true, stdio: "ignore", windowsHide: true, cwd: homeDir },
|
|
566
585
|
);
|
|
567
586
|
child.unref();
|
|
@@ -583,12 +602,94 @@ function buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon }) {
|
|
|
583
602
|
return args;
|
|
584
603
|
}
|
|
585
604
|
|
|
605
|
+
/**
|
|
606
|
+
* 仅重启模式(--restart-only):由运行中的 watchdog 在检测到磁盘包版本变化时调用。
|
|
607
|
+
* 复用完整升级模式的 杀旧→启新→存活校验(第 3/4/5 步),跳过下载/验证。
|
|
608
|
+
*/
|
|
609
|
+
async function runRestartOnlyMode(argv) {
|
|
610
|
+
const get = (f) => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : undefined);
|
|
611
|
+
const serverUrl = get("--server");
|
|
612
|
+
const clientId = get("--client-id");
|
|
613
|
+
const intervalSec = parseInt(get("--interval"), 10) || 15;
|
|
614
|
+
const noDaemon = argv.includes("--no-daemon");
|
|
615
|
+
const oldPid =
|
|
616
|
+
(argv.includes("--old-pid") ? parseInt(get("--old-pid"), 10) : 0) ||
|
|
617
|
+
process.ppid;
|
|
618
|
+
if (!oldPid) {
|
|
619
|
+
console.error(`[restart-only] 无旧 pid,退出`);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
console.error(`[restart-only] 重启 watchdog (旧 pid=${oldPid})...`);
|
|
623
|
+
|
|
624
|
+
// 杀旧:与完整模式一致(Ctrl+C 优雅 → 强杀 → 清树 → 孤儿 → 关窗)
|
|
625
|
+
const procSnapshot = await snapshotAllProcesses();
|
|
626
|
+
const descPids = listDescendantPids(procSnapshot, oldPid, process.pid);
|
|
627
|
+
let exited = false;
|
|
628
|
+
const gracefulSent =
|
|
629
|
+
process.platform === "win32" && (await sendConsoleCtrlC(oldPid));
|
|
630
|
+
if (gracefulSent) {
|
|
631
|
+
console.error(`[restart-only] 已发送 Ctrl+C,等待旧 watchdog 优雅停止业务...`);
|
|
632
|
+
exited = await waitProcessExit(oldPid, 8000);
|
|
633
|
+
}
|
|
634
|
+
if (!exited) {
|
|
635
|
+
if (gracefulSent)
|
|
636
|
+
console.error(`[restart-only] 旧 watchdog 未在 8s 内优雅退出,强制终止`);
|
|
637
|
+
exited = await terminateProcess(oldPid, 5000);
|
|
638
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
639
|
+
}
|
|
640
|
+
await killPidList(descPids, "[restart-only] 已清理旧 watchdog 进程树");
|
|
641
|
+
const orphanPids = listOrphanBusinessPids(await snapshotAllProcesses());
|
|
642
|
+
await killPidList(orphanPids, "[restart-only] 已清理孤儿业务进程");
|
|
643
|
+
await killOldWatchdogWindow();
|
|
644
|
+
|
|
645
|
+
// 启新(与完整模式一致:Windows 弹独立 cmd /k 窗口)
|
|
646
|
+
console.error(`[restart-only] 启动新版 watchdog ...`);
|
|
647
|
+
const cliPath = join(__dirname, "..", "..", "cli.js");
|
|
648
|
+
const args = buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon });
|
|
649
|
+
const logFd =
|
|
650
|
+
process.platform === "win32"
|
|
651
|
+
? undefined
|
|
652
|
+
: openLogFile("tt-help-watchdog", "restart-only", "新版 watchdog 日志");
|
|
653
|
+
if (process.platform === "win32") deleteStartedFile();
|
|
654
|
+
spawnNewWatchdog(cliPath, args, logFd);
|
|
655
|
+
|
|
656
|
+
// 存活校验(与完整模式一致)
|
|
657
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
658
|
+
if (process.platform === "win32") {
|
|
659
|
+
const deadline = Date.now() + 15_000;
|
|
660
|
+
let st = null;
|
|
661
|
+
while (Date.now() < deadline && !st) {
|
|
662
|
+
const cur = readStartedFile();
|
|
663
|
+
if (cur && cur.pid > 0 && (await isProcessAlive(cur.pid))) st = cur;
|
|
664
|
+
if (!st) await new Promise((r) => setTimeout(r, 1000));
|
|
665
|
+
}
|
|
666
|
+
if (st) {
|
|
667
|
+
console.error(`[restart-only] 新版 watchdog 存活确认 (pid=${st.pid}),重启完成`);
|
|
668
|
+
} else {
|
|
669
|
+
console.error(`[restart-only] 新版 watchdog 启动后未存活,请查看窗口/日志排障`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
const newPids = (await listWatchdogPids()).filter((p) => p !== oldPid);
|
|
674
|
+
if (newPids.length > 0) {
|
|
675
|
+
console.error(`[restart-only] 新版 watchdog 存活确认 (pid=${newPids.join(",")}),重启完成`);
|
|
676
|
+
} else {
|
|
677
|
+
console.error(`[restart-only] 新版 watchdog 启动后未存活,请查看窗口/日志排障`);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
process.exit(0);
|
|
682
|
+
}
|
|
683
|
+
|
|
586
684
|
/**
|
|
587
685
|
* 独立升级器主流程(由旧 watchdog 以子进程方式启动)
|
|
588
686
|
* 顺序:下载 → 验证版本 → 杀旧 → 启新。任一步失败都不杀旧,业务不断。
|
|
589
687
|
* @param {string[]} argv - process.argv.slice(2)
|
|
590
688
|
*/
|
|
591
689
|
export async function runUpgradeMode(argv) {
|
|
690
|
+
// 仅重启模式:磁盘包被手动升级(npm i -g)后由运行中的 watchdog 调用,
|
|
691
|
+
// 跳过下载与版本验证(此时磁盘与线上一致,会误判"未升级"),直接杀旧→启新→校验
|
|
692
|
+
if (argv.includes("--restart-only")) return runRestartOnlyMode(argv);
|
|
592
693
|
const get = (f) => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : undefined);
|
|
593
694
|
const serverUrl = get("--server");
|
|
594
695
|
const clientId = get("--client-id");
|
|
@@ -604,10 +705,22 @@ export async function runUpgradeMode(argv) {
|
|
|
604
705
|
`[upgrade-proc] 开始下载新版: npm install -g ${PACKAGE}@latest ...`,
|
|
605
706
|
);
|
|
606
707
|
const beforeVersion = getVersion(); // 下载前版本,用于验证 npm install 真正生效
|
|
607
|
-
|
|
708
|
+
let result = await performUpgrade();
|
|
709
|
+
if (!result.success && /EBUSY|resource busy/i.test(result.output)) {
|
|
710
|
+
// Windows 实测:运行中的 node/watchdog 或其 cwd 在包目录内时,npm 覆盖文件报 EBUSY。
|
|
711
|
+
// 此刻升级已势在必行(版本差已确认),终止旧 watchdog 释放文件锁后重试安装。
|
|
712
|
+
console.error(
|
|
713
|
+
`[upgrade-proc] 包目录被运行进程锁定(EBUSY),终止旧 watchdog 释放锁后重试安装...`,
|
|
714
|
+
);
|
|
715
|
+
const lockSnapshot = await snapshotAllProcesses();
|
|
716
|
+
const lockDescPids = listDescendantPids(lockSnapshot, oldPid, process.pid);
|
|
717
|
+
await terminateProcess(oldPid, 5000);
|
|
718
|
+
await killPidList(lockDescPids, "[upgrade-proc] 已停止旧 watchdog 释放包目录锁");
|
|
719
|
+
result = await performUpgrade();
|
|
720
|
+
}
|
|
608
721
|
if (!result.success) {
|
|
609
722
|
const err = result.output.substring(0, 300);
|
|
610
|
-
console.error(`[upgrade-proc]
|
|
723
|
+
console.error(`[upgrade-proc] 下载失败,保持当前状态: ${err}`);
|
|
611
724
|
process.exit(1);
|
|
612
725
|
}
|
|
613
726
|
|
|
@@ -759,9 +872,11 @@ export function spawnUpgrader({
|
|
|
759
872
|
intervalSec = 15,
|
|
760
873
|
spawnDaemon = true,
|
|
761
874
|
oldPid = 0,
|
|
875
|
+
restartOnly = false,
|
|
762
876
|
} = {}) {
|
|
763
877
|
const upgraderPath = fileURLToPath(import.meta.url);
|
|
764
878
|
const args = ["--upgrade", "--old-pid", String(oldPid || process.ppid)];
|
|
879
|
+
if (restartOnly) args.push("--restart-only");
|
|
765
880
|
if (serverUrl) args.push("--server", serverUrl);
|
|
766
881
|
if (clientId) args.push("--client-id", clientId);
|
|
767
882
|
args.push("--interval", String(intervalSec));
|