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.
@@ -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
- /** 向上遍历父链,返回 cmd.exe 祖先进程 PID(旧 watchdog 所在窗口,升级后关窗用) */
302
- function listCmdAncestorPids(rows, pid) {
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
- 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;
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
- /** 枚举当前所有 watchdog 进程 PID(存活校验前后对比用;不含升级器 U 自身) */
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
- await killPidList(cmdAncestors, "已关闭旧 cmd 窗口");
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
- // 记录启动前的 watchdog 进程集合,用于存活校验前后对比(Windows 经 start 启动时
606
- // 中间 cmd 秒退,不能检查其 PID)
607
- const beforePids = await listWatchdogPids();
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 后确认出现新增的 watchdog 进程;未存活则明确报错,不静默
699
+ // 5) 存活校验:3s 后确认新版 watchdog 存活;未存活则明确报错,不静默
624
700
  await new Promise((r) => setTimeout(r, 3000));
625
- const newPids = (await listWatchdogPids()).filter(
626
- (p) => !beforePids.includes(p),
627
- );
628
- if (newPids.length > 0) {
629
- console.error(
630
- `[upgrade-proc] 新版 watchdog 存活确认 (pid=${newPids.join(",")}),升级完成`,
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
- console.error(
634
- `[upgrade-proc] 新版 watchdog 启动后未存活,请查看窗口/日志排障`,
729
+ const newPids = (await listWatchdogPids()).filter(
730
+ (p) => !beforePids.includes(p),
635
731
  );
636
- process.exit(1);
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
  }