tt-help-cli-ycl 1.4.99 → 1.4.102
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-upgrade-spawn.mjs +179 -0
- package/src/watchdog/upgrader.js +20 -9
package/package.json
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 升级器进程级模拟测试(新版 spawn 方案)
|
|
3
|
+
*
|
|
4
|
+
* 结构:假全局包 tt-help-cli-ycl(package.json version 9.9.9 + 假 cli.js 保持存活)
|
|
5
|
+
* → 假旧 watchdog A(SIGTERM 优雅退出)
|
|
6
|
+
* → 真 U(upgrader.js --upgrade --old-pid A)
|
|
7
|
+
* → 假 npm(FAKE_PKG/FAKE_VER 环境变量把 package.json version 改为 9.9.10)
|
|
8
|
+
* → U:下载(假 npm 改版本) → 验证(9.9.10) → 杀 A → spawn 新版 B(detached) → 存活确认
|
|
9
|
+
*
|
|
10
|
+
* 断言:
|
|
11
|
+
* - U 日志:验证通过 / 旧已退出 / 新版已启动 / 存活确认
|
|
12
|
+
* - A 已退出
|
|
13
|
+
* - B 进程存活(detached,U 退出后仍活)
|
|
14
|
+
*
|
|
15
|
+
* 用法:node scripts/test-upgrade-spawn.mjs
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
copyFileSync,
|
|
21
|
+
chmodSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
} from "fs";
|
|
26
|
+
import { tmpdir } from "os";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
import { spawn, execFileSync } from "child_process";
|
|
29
|
+
|
|
30
|
+
const SRC = "/Users/yinchengliang/soft/tiktok/tiktok-cli";
|
|
31
|
+
const REAL_NPM = execFileSync("which npm", { shell: true }).toString().trim();
|
|
32
|
+
const tmp = mkdtempSync(join(tmpdir(), "tt-upgrade-test-"));
|
|
33
|
+
const pkgDir = join(tmp, "pkg", "node_modules", "tt-help-cli-ycl");
|
|
34
|
+
mkdirSync(join(pkgDir, "src", "watchdog"), { recursive: true });
|
|
35
|
+
mkdirSync(join(tmp, "bin"), { recursive: true });
|
|
36
|
+
|
|
37
|
+
let passed = 0,
|
|
38
|
+
failed = 0;
|
|
39
|
+
function check(name, cond, detail = "") {
|
|
40
|
+
if (cond) {
|
|
41
|
+
passed++;
|
|
42
|
+
console.log(` ✅ ${name}`);
|
|
43
|
+
} else {
|
|
44
|
+
failed++;
|
|
45
|
+
console.log(` ❌ ${name} ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 准备假全局包 ──
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(pkgDir, "package.json"),
|
|
52
|
+
JSON.stringify({ name: "tt-help-cli-ycl", version: "9.9.9" }, null, 2),
|
|
53
|
+
);
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(pkgDir, "cli.js"),
|
|
56
|
+
`console.log("[fake-B] 新版 watchdog 启动 pid="+process.pid); setInterval(()=>{},1000); process.on("SIGTERM",()=>process.exit(0));`,
|
|
57
|
+
);
|
|
58
|
+
copyFileSync(
|
|
59
|
+
join(SRC, "src/watchdog/upgrader.js"),
|
|
60
|
+
join(pkgDir, "src/watchdog/upgrader.js"),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// ── 假旧 watchdog A ──
|
|
64
|
+
const fakeA = join(tmp, "fakeA.mjs");
|
|
65
|
+
writeFileSync(
|
|
66
|
+
fakeA,
|
|
67
|
+
`console.log("[fake-A] 启动 pid="+process.pid); process.on("SIGTERM",()=>{console.log("[fake-A] 收到 SIGTERM 优雅退出");process.exit(0)}); setInterval(()=>{},1000);`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// ── 假 npm ──
|
|
71
|
+
const fakeNpm = join(tmp, "bin", "npm");
|
|
72
|
+
writeFileSync(
|
|
73
|
+
fakeNpm,
|
|
74
|
+
`#!/bin/sh
|
|
75
|
+
if [ "$1" = "install" ]; then
|
|
76
|
+
node -e "const fs=require('fs');const p=process.env.FAKE_PKG;const j=JSON.parse(fs.readFileSync(p,'utf8'));j.version=process.env.FAKE_VER;fs.writeFileSync(p,JSON.stringify(j,null,2)+'\\n');console.log('[fake-npm] 版本改为 '+j.version)"
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
exec "${REAL_NPM}" "$@"
|
|
80
|
+
`,
|
|
81
|
+
);
|
|
82
|
+
chmodSync(fakeNpm, 0o755);
|
|
83
|
+
|
|
84
|
+
console.log("── 启动假旧 watchdog A ──");
|
|
85
|
+
const a = spawn(process.execPath, [fakeA], {
|
|
86
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
+
});
|
|
88
|
+
let aOut = "";
|
|
89
|
+
a.stdout.on("data", (d) => (aOut += d));
|
|
90
|
+
a.stderr.on("data", (d) => (aOut += d));
|
|
91
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
92
|
+
|
|
93
|
+
console.log("── 启动真升级器 U ──");
|
|
94
|
+
let uOut = "";
|
|
95
|
+
// mac 上 /var→/private/var 是 symlink:spawn 参数必须用 realpath,
|
|
96
|
+
// 否则 process.argv[1] ≠ import.meta.url 的真实路径,--upgrade 守卫不成立直接退出
|
|
97
|
+
const upgraderReal = realpathSync(join(pkgDir, "src/watchdog/upgrader.js"));
|
|
98
|
+
const u = spawn(
|
|
99
|
+
process.execPath,
|
|
100
|
+
[
|
|
101
|
+
upgraderReal,
|
|
102
|
+
"--upgrade",
|
|
103
|
+
"--old-pid",
|
|
104
|
+
String(a.pid),
|
|
105
|
+
"--server",
|
|
106
|
+
"http://127.0.0.1:3000",
|
|
107
|
+
"--client-id",
|
|
108
|
+
"test-client",
|
|
109
|
+
"--interval",
|
|
110
|
+
"15",
|
|
111
|
+
],
|
|
112
|
+
{
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
PATH: `${join(tmp, "bin")}:${process.env.PATH}`,
|
|
116
|
+
FAKE_PKG: join(pkgDir, "package.json"),
|
|
117
|
+
FAKE_VER: "9.9.10",
|
|
118
|
+
},
|
|
119
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
u.stdout.on("data", (d) => (uOut += d));
|
|
123
|
+
u.stderr.on("data", (d) => (uOut += d));
|
|
124
|
+
const uExit = await new Promise((res) => u.on("exit", (c) => res(c)));
|
|
125
|
+
await new Promise((r) => setTimeout(r, 500)); // 等 B 起来
|
|
126
|
+
|
|
127
|
+
console.log("\n──── 假 A 输出 ────\n" + aOut.trim());
|
|
128
|
+
console.log("\n──── 升级器 U 输出 ────\n" + uOut.trim());
|
|
129
|
+
console.log(`\nU 退出码: ${uExit}\n`);
|
|
130
|
+
|
|
131
|
+
// 提取新版 B 的 pid
|
|
132
|
+
const pidMatch = uOut.match(/新版 watchdog 已启动 \(pid=(\d+)\)/);
|
|
133
|
+
const bPid = pidMatch ? Number(pidMatch[1]) : null;
|
|
134
|
+
|
|
135
|
+
console.log("──── 断言 ────");
|
|
136
|
+
check("U 正常退出(exit 0)", uExit === 0, `(实际 ${uExit})`);
|
|
137
|
+
check("版本验证通过 (9.9.10)", /新版本验证通过 \(9\.9\.10\)/.test(uOut));
|
|
138
|
+
check(
|
|
139
|
+
"旧 watchdog 已退出",
|
|
140
|
+
/旧 watchdog 已退出/.test(uOut),
|
|
141
|
+
uOut.includes("已退出")
|
|
142
|
+
? ""
|
|
143
|
+
: uOut
|
|
144
|
+
.split("\n")
|
|
145
|
+
.filter((l) => l.includes("退出"))
|
|
146
|
+
.join("|"),
|
|
147
|
+
);
|
|
148
|
+
check("新版 watchdog 已启动", /新版 watchdog 已启动 \(pid=\d+\)/.test(uOut));
|
|
149
|
+
check("新版存活确认", /新版 watchdog 存活确认/.test(uOut));
|
|
150
|
+
check(
|
|
151
|
+
"假 A 已退出",
|
|
152
|
+
a.exitCode !== null || a.signalCode !== null,
|
|
153
|
+
`(exitCode=${a.exitCode} signal=${a.signalCode})`,
|
|
154
|
+
);
|
|
155
|
+
check(
|
|
156
|
+
"新版 B 进程存活",
|
|
157
|
+
bPid
|
|
158
|
+
? (() => {
|
|
159
|
+
try {
|
|
160
|
+
process.kill(bPid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
})()
|
|
166
|
+
: false,
|
|
167
|
+
`(bPid=${bPid})`,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// 清理:杀 B(如还活着)
|
|
171
|
+
if (bPid) {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(bPid, "SIGKILL");
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
177
|
+
|
|
178
|
+
console.log(`\n结果: ${passed} 通过 / ${failed} 失败`);
|
|
179
|
+
process.exit(failed ? 1 : 0);
|
package/src/watchdog/upgrader.js
CHANGED
|
@@ -199,12 +199,15 @@ function openLogFile(prefix, label, what) {
|
|
|
199
199
|
/**
|
|
200
200
|
* spawn 子进程(升级器 U / 新版 watchdog 共用)
|
|
201
201
|
*
|
|
202
|
-
* interactive
|
|
203
|
-
*
|
|
204
|
-
* U 与新版继承该窗口,运维可远程(RDP)在前端窗口看日志。
|
|
205
|
-
* 前提:watchdog 由 cmd /k 启动(保证窗口在杀旧后不关闭)。
|
|
202
|
+
* interactive(仅用于 U):stdio inherit,继承 cmd /k 前端窗口,
|
|
203
|
+
* 让运维在前端窗口看升级过程日志([upgrade-proc])。
|
|
206
204
|
*
|
|
207
|
-
*
|
|
205
|
+
* 新版 watchdog 一律以非交互方式 spawn(interactive: false):
|
|
206
|
+
* - detached: true → Windows 上子进程拥有【独立控制台窗口】(Node 文档语义),
|
|
207
|
+
* 即新版在【新开的前端 cmd 窗口】运行,运维远程(RDP/桌面)可看日志;
|
|
208
|
+
* 旧 watchdog 被杀不影响新版(不依赖旧窗口控制台,Windows 实测可靠)。
|
|
209
|
+
* - windowsHide: false → 允许弹出该控制台窗口(前端可见)。
|
|
210
|
+
* - stdio 独立日志文件 → 进程退出后日志可查。
|
|
208
211
|
*
|
|
209
212
|
* @param {string[]} args 子进程完整参数(node 脚本路径 + 参数)
|
|
210
213
|
* @param {{interactive?: boolean, logFd?: number}} opts
|
|
@@ -216,7 +219,9 @@ function spawnChild(args, { interactive = false, logFd } = {}) {
|
|
|
216
219
|
stdio: interactive
|
|
217
220
|
? "inherit"
|
|
218
221
|
: ["ignore", logFd || "ignore", logFd || "ignore"],
|
|
219
|
-
windowsHide
|
|
222
|
+
// 关键:windowsHide 恒为 false。新版 detached 时 Windows 自动分配独立控制台窗口
|
|
223
|
+
// (前端可见);U interactive 时 inherit 旧窗口也不隐藏。
|
|
224
|
+
windowsHide: false,
|
|
220
225
|
});
|
|
221
226
|
if (!interactive) child.unref();
|
|
222
227
|
return child;
|
|
@@ -309,8 +314,13 @@ export async function runUpgradeMode(argv) {
|
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
// 4) 旧进程已退,启新版 watchdog
|
|
312
|
-
//
|
|
313
|
-
//
|
|
317
|
+
// 新版永远 detached + windowsHide:false:
|
|
318
|
+
// - Windows 上 detached:true 时子进程拥有独立控制台窗口(Node 文档语义)
|
|
319
|
+
// → 新版在【新开的前端 cmd 窗口】运行,运维远程(RDP/桌面)可看日志
|
|
320
|
+
// - 不接管旧 cmd /k 窗口(interactive inherit 在 Windows 实测三次失败,
|
|
321
|
+
// 旧窗口控制台在旧 watchdog 被杀后不可复用)
|
|
322
|
+
// - 独立日志文件,进程退出后可查
|
|
323
|
+
// U 本身仍按 interactive 透传(旧窗口显示升级日志,不阻断)
|
|
314
324
|
console.error(`[upgrade-proc] 启动新版 watchdog ...`);
|
|
315
325
|
const cliPath = join(__dirname, "..", "..", "cli.js");
|
|
316
326
|
const args = buildWatchdogArgs({
|
|
@@ -326,7 +336,8 @@ export async function runUpgradeMode(argv) {
|
|
|
326
336
|
);
|
|
327
337
|
let child;
|
|
328
338
|
try {
|
|
329
|
-
|
|
339
|
+
// 新版永远独立控制台窗口,不传 interactive(不再继承旧 cmd /k 窗口)
|
|
340
|
+
child = spawnChild([cliPath, ...args], { interactive: false, logFd });
|
|
330
341
|
console.error(`[upgrade-proc] 新版 watchdog 已启动 (pid=${child.pid})`);
|
|
331
342
|
} catch (e) {
|
|
332
343
|
console.error(`[upgrade-proc] 新版 watchdog 启动失败: ${e.message}`);
|