tt-help-cli-ycl 1.4.126 → 1.4.130
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/scripts/test-daemon-failover.mjs +197 -0
- package/scripts/test-daemon-switch-history.mjs +141 -0
- package/scripts/test-wmi-free-upgrade.mjs +98 -0
- package/scripts/verify-kill-edge.mjs +108 -0
- package/scripts/verify-stop-command.mjs +159 -0
- package/src/clash/daemon.js +151 -25
- package/src/cli/refresh-mini.js +40 -13
- package/src/watchdog/agent.js +34 -1
- package/src/watchdog/process-manager.js +29 -13
- package/src/watchdog/upgrader.js +154 -49
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 } from "fs";
|
|
11
|
+
import { readFileSync, openSync, unlinkSync } from "fs";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { dirname, join } from "path";
|
|
14
14
|
import { homedir, tmpdir } from "os";
|
|
@@ -21,6 +21,12 @@ const homeDir = homedir() || process.env.HOME || process.env.USERPROFILE || "";
|
|
|
21
21
|
const TT_HELP_CONFIG = join(homeDir, ".tt-help.json");
|
|
22
22
|
const execFileP = promisify(execFile);
|
|
23
23
|
|
|
24
|
+
// 升级链绕开 WMI 的配套常量(与 agent.js 保持一致):
|
|
25
|
+
// 部分 Windows 机器 WMI(WinMgmt)损坏,Get-CimInstance 查不到进程表 →
|
|
26
|
+
// 关旧窗改用窗口标题匹配(tasklist /V),存活校验改用新版 watchdog 写出的标记文件
|
|
27
|
+
const STARTED_FILE = join(tmpdir(), "tt-help-watchdog-started.json");
|
|
28
|
+
const WINDOW_TITLE_TAG = "tt-help-watchdog";
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* 升级检测源:TT_HELP_REGISTRY 环境变量 > ~/.tt-help.json 的 npmRegistry > 官方 npmjs.org
|
|
26
32
|
* 注意:检测源应与本机 npm install 使用的 registry 保持一致(默认 npm 自身配置)
|
|
@@ -298,22 +304,101 @@ async function waitProcessExit(pid, waitMs) {
|
|
|
298
304
|
return false;
|
|
299
305
|
}
|
|
300
306
|
|
|
301
|
-
/**
|
|
302
|
-
function
|
|
303
|
-
const byId = new Map(rows.map((r) => [r.pid, r]));
|
|
307
|
+
/** 解析一行 CSV(处理引号包裹的逗号/转义引号),用于解析 tasklist /FO CSV 输出 */
|
|
308
|
+
export function parseCsvLine(line) {
|
|
304
309
|
const out = [];
|
|
305
|
-
|
|
306
|
-
let
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
let cur = "";
|
|
311
|
+
let inQ = false;
|
|
312
|
+
for (let i = 0; i < line.length; i++) {
|
|
313
|
+
const c = line[i];
|
|
314
|
+
if (inQ) {
|
|
315
|
+
if (c === '"') {
|
|
316
|
+
if (line[i + 1] === '"') {
|
|
317
|
+
cur += '"';
|
|
318
|
+
i++;
|
|
319
|
+
} else {
|
|
320
|
+
inQ = false;
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
cur += c;
|
|
324
|
+
}
|
|
325
|
+
} else if (c === '"') {
|
|
326
|
+
inQ = true;
|
|
327
|
+
} else if (c === ",") {
|
|
328
|
+
out.push(cur);
|
|
329
|
+
cur = "";
|
|
330
|
+
} else {
|
|
331
|
+
cur += c;
|
|
332
|
+
}
|
|
313
333
|
}
|
|
334
|
+
out.push(cur);
|
|
314
335
|
return out;
|
|
315
336
|
}
|
|
316
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Windows:按窗口标题匹配 tasklist /V 输出中的控制台宿主进程 PID(绕开 WMI)。
|
|
340
|
+
* 窗口标题在 conhost.exe 行(Win10/11)或 cmd.exe 行(旧系统),两者都返回;
|
|
341
|
+
* 其余进程(含升级器 U 自身的隐藏 conhost,标题为空)不会误匹配。
|
|
342
|
+
*/
|
|
343
|
+
async function listWindowPidsByTitle(titleSubstr) {
|
|
344
|
+
if (process.platform !== "win32") return [];
|
|
345
|
+
try {
|
|
346
|
+
const { stdout } = await execFileP(
|
|
347
|
+
"tasklist.exe",
|
|
348
|
+
["/V", "/FO", "CSV"],
|
|
349
|
+
{ timeout: 20000, windowsHide: true },
|
|
350
|
+
);
|
|
351
|
+
const pids = [];
|
|
352
|
+
for (const line of String(stdout).split(/\r?\n/)) {
|
|
353
|
+
if (!line.toLowerCase().includes(titleSubstr.toLowerCase())) continue;
|
|
354
|
+
const fields = parseCsvLine(line);
|
|
355
|
+
if (fields.length < 2) continue;
|
|
356
|
+
const name = (fields[0] || "").toLowerCase();
|
|
357
|
+
if (!name.includes("conhost") && !name.includes("cmd")) continue;
|
|
358
|
+
const pid = parseInt(fields[1], 10);
|
|
359
|
+
if (pid > 0 && pid !== process.pid) pids.push(pid);
|
|
360
|
+
}
|
|
361
|
+
return [...new Set(pids)];
|
|
362
|
+
} catch (e) {
|
|
363
|
+
console.error(`[upgrade-proc] tasklist 查窗口失败: ${e.message}`);
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 关闭旧 watchdog 所在的 cmd 窗口(绕开 WMI):
|
|
370
|
+
* 旧 watchdog 启动时已把窗口标题设为含 WINDOW_TITLE_TAG 的串(agent.js),
|
|
371
|
+
* 此时新版窗口尚未弹出,匹配到的窗口即旧窗口 → taskkill 杀 conhost/cmd 关窗。
|
|
372
|
+
* 旧 watchdog 版本过旧(未设标题)或非窗口运行时匹配不到,跳过。
|
|
373
|
+
*/
|
|
374
|
+
async function killOldWatchdogWindow() {
|
|
375
|
+
if (process.platform !== "win32") return;
|
|
376
|
+
const pids = await listWindowPidsByTitle(WINDOW_TITLE_TAG);
|
|
377
|
+
if (pids.length === 0) {
|
|
378
|
+
console.error(
|
|
379
|
+
`[upgrade-proc] 未找到标题含 "${WINDOW_TITLE_TAG}" 的窗口(旧 watchdog 非窗口运行或版本过旧未设标题),跳过关旧窗`,
|
|
380
|
+
);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await killPidList(pids, "已关闭旧 cmd 窗口");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** 读存活标记文件(新版 watchdog 启动后写入 {pid, version, ts}) */
|
|
387
|
+
function readStartedFile() {
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(readFileSync(STARTED_FILE, "utf-8"));
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** 删存活标记文件(spawn 新版前清掉旧 watchdog 的残留,确保读到的是新进程的) */
|
|
396
|
+
function deleteStartedFile() {
|
|
397
|
+
try {
|
|
398
|
+
unlinkSync(STARTED_FILE);
|
|
399
|
+
} catch {}
|
|
400
|
+
}
|
|
401
|
+
|
|
317
402
|
/** 一次拉取全进程表 [{pid, ppid, cmd}](Windows: CIM;POSIX: ps) */
|
|
318
403
|
async function snapshotAllProcesses() {
|
|
319
404
|
const rows = [];
|
|
@@ -441,24 +526,13 @@ async function killPidList(pids, label) {
|
|
|
441
526
|
return killed;
|
|
442
527
|
}
|
|
443
528
|
|
|
444
|
-
/**
|
|
529
|
+
/**
|
|
530
|
+
* 枚举当前所有 watchdog 进程 PID(仅 POSIX 存活校验前后对比用)。
|
|
531
|
+
* Windows 不再走这里——部分机器 WMI 损坏时枚举会静默返回空,已改为读
|
|
532
|
+
* 新版 watchdog 写出的存活标记文件(见 runUpgradeMode 第 5 步)。
|
|
533
|
+
*/
|
|
445
534
|
async function listWatchdogPids() {
|
|
446
535
|
try {
|
|
447
|
-
if (process.platform === "win32") {
|
|
448
|
-
const ps =
|
|
449
|
-
`Get-CimInstance Win32_Process -Filter "Name='node.exe'" ` +
|
|
450
|
-
`| Where-Object { $_.CommandLine -match 'cli\\.js watchdog' } ` +
|
|
451
|
-
`| Select-Object -ExpandProperty ProcessId`;
|
|
452
|
-
const { stdout } = await execFileP(
|
|
453
|
-
"powershell.exe",
|
|
454
|
-
["-NoProfile", "-Command", ps],
|
|
455
|
-
{ timeout: 15000, windowsHide: true },
|
|
456
|
-
);
|
|
457
|
-
return String(stdout)
|
|
458
|
-
.split(/\r?\n/)
|
|
459
|
-
.map((s) => parseInt(s.trim(), 10))
|
|
460
|
-
.filter((n) => n > 0);
|
|
461
|
-
}
|
|
462
536
|
const { stdout } = await execFileP("pgrep", ["-f", "cli.js watchdog"], {
|
|
463
537
|
timeout: 15000,
|
|
464
538
|
});
|
|
@@ -467,9 +541,7 @@ async function listWatchdogPids() {
|
|
|
467
541
|
.map((s) => parseInt(s.trim(), 10))
|
|
468
542
|
.filter((n) => n > 0);
|
|
469
543
|
} catch (e) {
|
|
470
|
-
console.error(
|
|
471
|
-
`[upgrader] 枚举 watchdog 进程失败(WMI 不可用?): ${e.message}`,
|
|
472
|
-
);
|
|
544
|
+
console.error(`[upgrader] 枚举 watchdog 进程失败: ${e.message}`);
|
|
473
545
|
return [];
|
|
474
546
|
}
|
|
475
547
|
}
|
|
@@ -551,11 +623,11 @@ export async function runUpgradeMode(argv) {
|
|
|
551
623
|
// 会触发旧 watchdog 的 SIGINT → gracefulShutdown 优雅停业务。因此先向旧 watchdog
|
|
552
624
|
// 的控制台发送 CTRL_C_EVENT(等效手动按键),8s 未退再强杀兜底;随后按【杀旧前】
|
|
553
625
|
// 快照强杀整棵后代树(排除 U 自己;杀旧后中间层死亡会断父链,必须提前快照),
|
|
554
|
-
// 再兜底清理「父已死」的孤儿业务进程,最后关闭旧 watchdog 所在的 cmd
|
|
626
|
+
// 再兜底清理「父已死」的孤儿业务进程,最后关闭旧 watchdog 所在的 cmd 窗口
|
|
627
|
+
// (按窗口标题匹配 tasklist /V,绕开 WMI——部分机器 WMI 损坏时快照为空)。
|
|
555
628
|
console.error(`[upgrade-proc] 通知旧 watchdog (pid=${oldPid}) 退出 ...`);
|
|
556
629
|
const procSnapshot = await snapshotAllProcesses();
|
|
557
630
|
const descPids = listDescendantPids(procSnapshot, oldPid, process.pid);
|
|
558
|
-
const cmdAncestors = listCmdAncestorPids(procSnapshot, oldPid);
|
|
559
631
|
|
|
560
632
|
let exited = false;
|
|
561
633
|
const gracefulSent =
|
|
@@ -584,8 +656,9 @@ export async function runUpgradeMode(argv) {
|
|
|
584
656
|
// 兜底:快照时父链已断(中间层先死)的孤儿业务进程,杀旧后按特征清理
|
|
585
657
|
const orphanPids = listOrphanBusinessPids(await snapshotAllProcesses());
|
|
586
658
|
await killPidList(orphanPids, "已清理孤儿业务进程");
|
|
587
|
-
// 关闭旧 watchdog 所在的 cmd
|
|
588
|
-
|
|
659
|
+
// 关闭旧 watchdog 所在的 cmd 窗口(业务已停,窗口只剩空壳提示符);
|
|
660
|
+
// 按窗口标题匹配(不依赖 WMI),旧 watchdog 已设标题含 WINDOW_TITLE_TAG
|
|
661
|
+
await killOldWatchdogWindow();
|
|
589
662
|
|
|
590
663
|
// 4) 旧进程已退,启新版 watchdog
|
|
591
664
|
// Windows:显式弹独立交互 cmd 窗口(start → cmd /k),日志直接显示在窗口,
|
|
@@ -602,9 +675,12 @@ export async function runUpgradeMode(argv) {
|
|
|
602
675
|
process.platform === "win32"
|
|
603
676
|
? undefined
|
|
604
677
|
: openLogFile("tt-help-watchdog", "upgrade-proc", "新版 watchdog 日志");
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
678
|
+
// Windows 存活校验改读新版 watchdog 写出的标记文件(WMI 损坏时枚举进程表失效),
|
|
679
|
+
// spawn 前先删掉旧 watchdog 的残留,确保读到的是新进程写的
|
|
680
|
+
if (process.platform === "win32") deleteStartedFile();
|
|
681
|
+
// POSIX:记录启动前的 watchdog 进程集合,用于存活校验前后对比
|
|
682
|
+
const beforePids =
|
|
683
|
+
process.platform === "win32" ? [] : await listWatchdogPids();
|
|
608
684
|
let child;
|
|
609
685
|
try {
|
|
610
686
|
child = spawnNewWatchdog(cliPath, args, logFd);
|
|
@@ -620,20 +696,49 @@ export async function runUpgradeMode(argv) {
|
|
|
620
696
|
process.exit(1);
|
|
621
697
|
}
|
|
622
698
|
|
|
623
|
-
// 5) 存活校验:3s
|
|
699
|
+
// 5) 存活校验:3s 后确认新版 watchdog 存活;未存活则明确报错,不静默
|
|
624
700
|
await new Promise((r) => setTimeout(r, 3000));
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
701
|
+
if (process.platform === "win32") {
|
|
702
|
+
// Windows:轮询存活标记文件(新版 watchdog 启动即写,含 pid/version),
|
|
703
|
+
// 版本匹配且 pid 可探测存活才算确认。15s 超时未出现则报错。
|
|
704
|
+
const deadline = Date.now() + 15_000;
|
|
705
|
+
let st = null;
|
|
706
|
+
while (Date.now() < deadline && !st) {
|
|
707
|
+
const cur = readStartedFile();
|
|
708
|
+
if (
|
|
709
|
+
cur &&
|
|
710
|
+
cur.pid > 0 &&
|
|
711
|
+
String(cur.version) === String(verify.version) &&
|
|
712
|
+
(await isProcessAlive(cur.pid))
|
|
713
|
+
) {
|
|
714
|
+
st = cur;
|
|
715
|
+
}
|
|
716
|
+
if (!st) await new Promise((r) => setTimeout(r, 1000));
|
|
717
|
+
}
|
|
718
|
+
if (st) {
|
|
719
|
+
console.error(
|
|
720
|
+
`[upgrade-proc] 新版 watchdog 存活确认 (pid=${st.pid}, v${st.version}),升级完成`,
|
|
721
|
+
);
|
|
722
|
+
} else {
|
|
723
|
+
console.error(
|
|
724
|
+
`[upgrade-proc] 新版 watchdog 启动后未存活,请查看窗口/日志排障`,
|
|
725
|
+
);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
632
728
|
} else {
|
|
633
|
-
|
|
634
|
-
|
|
729
|
+
const newPids = (await listWatchdogPids()).filter(
|
|
730
|
+
(p) => !beforePids.includes(p),
|
|
635
731
|
);
|
|
636
|
-
|
|
732
|
+
if (newPids.length > 0) {
|
|
733
|
+
console.error(
|
|
734
|
+
`[upgrade-proc] 新版 watchdog 存活确认 (pid=${newPids.join(",")}),升级完成`,
|
|
735
|
+
);
|
|
736
|
+
} else {
|
|
737
|
+
console.error(
|
|
738
|
+
`[upgrade-proc] 新版 watchdog 启动后未存活,请查看窗口/日志排障`,
|
|
739
|
+
);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
637
742
|
}
|
|
638
743
|
process.exit(0);
|
|
639
744
|
}
|