tt-help-cli-ycl 1.4.92 → 1.4.93

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.92",
3
+ "version": "1.4.93",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@ import {
16
16
  import { ProcessManager } from "./process-manager.js";
17
17
  import {
18
18
  getVersion,
19
- spawnUpgraderAndWait,
19
+ spawnUpgrader,
20
20
  isGlobalInstall,
21
21
  startUpgradeChecker,
22
22
  } from "./upgrader.js";
@@ -574,23 +574,13 @@ export class WatchdogAgent {
574
574
 
575
575
  this.upgradeChecker = startUpgradeChecker({
576
576
  onUpdateAvailable: async ({ current, latest }) => {
577
- // 简化升级:spawn 独立升级器下载,下载期间业务/心跳完全不受影响;
578
- // 下载成功后才停业务并退出,由新版接管(避免"边停边起"与二次 taskkill 竞态)
577
+ // 独立升级器方案(无中间文件):spawn 升级进程 U(fire-and-forget),U 负责「下载 / 起新版 / 信号通知退出」。
578
+ // 下载期间业务/心跳不受影响;下载失败 U 自行退出,本进程保持运行;
579
+ // 新版起来后 U 发 SIGTERM → 本进程 gracefulShutdown 停业务/daemon 后退出,新版接管。
579
580
  console.error(
580
581
  `[upgrade] ${current} → ${latest},启动升级程序(下载期间业务不受影响)`,
581
582
  );
582
- const outcome = await spawnUpgraderAndWait(this._buildRestartParams());
583
- if (outcome !== "ok") {
584
- console.error(
585
- `[upgrade] 升级未完成(${outcome === "fail" ? "下载失败" : "升级程序无响应"}),保持当前版本运行`,
586
- );
587
- return;
588
- }
589
- console.error(
590
- `[upgrade] 新版本已下载,停止业务与 daemon 后退出,新版将接管`,
591
- );
592
- await this.stop();
593
- process.exit(0);
583
+ spawnUpgrader(this._buildRestartParams());
594
584
  },
595
585
  });
596
586
  }
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * 自动升级模块 — 检测 tt-help-cli-ycl 新版本并执行升级
3
3
  *
4
- * 仅全局安装模式启用。每 10 分钟查询 npm registry,
5
- * 发现新版本 → 回调通知 agent → agent 负责停进程 → 升级 → 重启。
4
+ * 仅全局安装模式启用。每 10 分钟查询 npm registry 发现新版本,
5
+ * 由旧 watchdog spawn 独立升级进程 U:U 负责「下载 → 验证代码 → 杀旧 → 启新」,
6
+ * 见 runUpgradeMode。旧 watchdog 收到 SIGTERM 后优雅退出,新版接管。
6
7
  */
7
8
 
8
9
  import { exec, spawn } from "child_process";
9
- import { readFileSync, writeFileSync, unlinkSync, openSync } from "fs";
10
+ import { readFileSync, openSync } from "fs";
10
11
  import { fileURLToPath } from "url";
11
12
  import { dirname, join } from "path";
12
13
  import { homedir, tmpdir } from "os";
@@ -113,6 +114,60 @@ export function performUpgrade() {
113
114
  });
114
115
  }
115
116
 
117
+ /**
118
+ * 下载后验证:确认 npm install 真正换了版本,且新版主模块能加载(冒烟)
119
+ * 验证通过才允许杀旧 → 启新;失败则旧 watchdog 不受影响继续运行
120
+ */
121
+ async function verifyUpgradedPackage(beforeVersion) {
122
+ const now = getVersion();
123
+ if (now === "unknown")
124
+ return { ok: false, reason: "读不到全局包版本(package.json 缺失)" };
125
+ if (now === beforeVersion)
126
+ return {
127
+ ok: false,
128
+ reason: `版本未变化(仍 ${now}),疑似 npm install 未生效`,
129
+ };
130
+ const smoke = await smokeTestMainModule();
131
+ if (!smoke.ok) return { ok: false, reason: smoke.reason };
132
+ return { ok: true, version: now };
133
+ }
134
+
135
+ /** 冒烟:spawn 子进程加载新版主模块(仅加载不执行业务),能加载则代码/依赖基本可用 */
136
+ function smokeTestMainModule(timeoutMs = 15_000) {
137
+ return new Promise((resolve) => {
138
+ const mainModulePath = join(__dirname, "..", "..", "src", "main.js");
139
+ const code = `import(${JSON.stringify(
140
+ mainModulePath,
141
+ )}).then(()=>process.exit(0)).catch((e)=>{console.error(e&&e.message||e);process.exit(1)})`;
142
+ let child;
143
+ try {
144
+ child = spawn(process.execPath, ["--input-type=module", "-e", code], {
145
+ stdio: ["ignore", "ignore", "pipe"],
146
+ windowsHide: true,
147
+ });
148
+ } catch (e) {
149
+ resolve({ ok: false, reason: `冒烟启动失败: ${e.message}` });
150
+ return;
151
+ }
152
+ let err = "";
153
+ child.stderr?.on("data", (d) => (err += d.toString()));
154
+ const timer = setTimeout(() => {
155
+ try {
156
+ child.kill("SIGKILL");
157
+ } catch {}
158
+ resolve({ ok: false, reason: `冒烟超时 ${timeoutMs}ms` });
159
+ }, timeoutMs);
160
+ child.on("exit", (code) => {
161
+ clearTimeout(timer);
162
+ resolve(
163
+ code === 0
164
+ ? { ok: true }
165
+ : { ok: false, reason: `冒烟失败: ${err.slice(0, 200)}` },
166
+ );
167
+ });
168
+ });
169
+ }
170
+
116
171
  /**
117
172
  * 启动定时升级检测
118
173
  * @param {{intervalMs?: number, onUpdateAvailable?: function}} opts
@@ -161,194 +216,181 @@ export function startUpgradeChecker({
161
216
  // 独立升级器(直接运行模式)
162
217
  // ══════════════════════════════════════════════
163
218
  //
164
- // 设计目标:让升级变得简单、无竞态。
165
- // 旧 watchdog 检测到新版 → spawn 独立升级器(detached)→ 旧 watchdog 继续正常运行(业务不断)
166
- // 升级器:npm install → 写结果文件 → 等旧 watchdog 退出 → spawn 新版 watchdog → 自身退出
167
- // 旧 watchdog 看到结果 ok → 停心跳/停业务/停 daemon → 退出,由新版接管。
168
- //
169
- // 结果文件:tmpdir/tt-help-upgrade-result.json
170
- // - 由升级器写入 { status: "ok" | "fail", error?, at },旧 watchdog 轮询读取
171
- //
172
- // 用法(旧 watchdog 调用):
173
- // node upgrader.js --upgrade --old-pid <pid> [--interactive] [--server ...] [--client-id ...] [--interval ...] [--no-daemon]
174
-
175
- const UPGRADE_RESULT_FILE = join(tmpdir(), "tt-help-upgrade-result.json");
219
+ // 设计(定稿,无中间文件):旧 watchdog 检测到新版 → spawn 独立升级进程 U(detached)
220
+ // U:npm install 下载 →【验证代码】→【杀旧】→【启新】
221
+ // - 下载失败 / 验证未通过(版本未变、新版主模块加载失败)→ 不杀旧,旧 watchdog 继续跑
222
+ // - 验证通过 → SIGTERM 旧 watchdog(优雅,5s 未退强杀兜底)→ 无并存窗口 → 启新版一定能起来
223
+ // 关键:U 的 stdout/stderr 独立落盘(tmpdir/tt-help-upgrader-*.log),旧 watchdog 退出后日志不断,可定位。
176
224
 
177
- function writeUpgradeResult(state) {
225
+ /** 打开日志文件(追加模式)并打印路径;失败返回 undefined(stdio 回退到 ignore) */
226
+ function openLogFile(prefix, label, what) {
178
227
  try {
179
- writeFileSync(
180
- UPGRADE_RESULT_FILE,
181
- JSON.stringify({ ...state, at: Date.now() }, null, 2),
182
- "utf-8",
183
- );
184
- } catch {}
228
+ const logPath = join(tmpdir(), `${prefix}-${Date.now()}.log`);
229
+ console.error(`[${label}] ${what}: ${logPath}`);
230
+ return openSync(logPath, "a");
231
+ } catch {
232
+ return undefined;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * spawn 子进程(升级器 / 新版 watchdog 共用)
238
+ * interactive:继承控制台;否则 detached 后台运行,stdout/stderr 落到 logFd
239
+ * @param {string[]} args 子进程完整参数(node 脚本路径 + 参数)
240
+ * @returns {import("child_process").ChildProcess}
241
+ */
242
+ function spawnChild(args, { interactive = false, logFd } = {}) {
243
+ const child = spawn(process.execPath, args, {
244
+ detached: !interactive,
245
+ stdio: interactive
246
+ ? "inherit"
247
+ : ["ignore", logFd || "ignore", logFd || "ignore"],
248
+ windowsHide: !interactive,
249
+ });
250
+ if (!interactive) child.unref();
251
+ return child;
185
252
  }
186
253
 
187
- /** 轮询等待指定 pid 退出,返回是否已退出(超时返回 false) */
188
- async function waitProcessExit(
189
- pid,
190
- { timeoutMs = 60_000, pollMs = 1000 } = {},
191
- ) {
192
- const start = Date.now();
193
- while (Date.now() - start < timeoutMs) {
254
+ /**
255
+ * 终止进程:先 SIGTERM(优雅),轮询等待退出;窗口内未退出则 SIGKILL 兜底。
256
+ * Windows 注意:process.kill(pid, "SIGTERM") 在 Windows 上即强制终止(无优雅退出)。
257
+ * @returns {Promise<boolean>} 是否在窗口内退出(false = 已强杀兜底)
258
+ */
259
+ async function terminateProcess(pid, waitMs = 5000) {
260
+ if (!pid) return true;
261
+ try {
262
+ process.kill(pid, "SIGTERM");
263
+ } catch {
264
+ return true; // 进程已不存在
265
+ }
266
+ const deadline = Date.now() + waitMs;
267
+ while (Date.now() < deadline) {
194
268
  try {
195
- process.kill(pid, 0); // 存在则继续等
269
+ process.kill(pid, 0); // 0 仅探测存活;抛错 = 已退出
270
+ await new Promise((r) => setTimeout(r, 500));
196
271
  } catch {
197
- return true; // 已退出
272
+ return true;
198
273
  }
199
- await new Promise((r) => setTimeout(r, pollMs));
200
274
  }
275
+ try {
276
+ process.kill(pid, "SIGKILL");
277
+ } catch {}
201
278
  return false;
202
279
  }
203
280
 
281
+ /** 新版 watchdog 的 CLI 参数(透传 server/clientId/interval/noDaemon,保证与旧版一致) */
282
+ function buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon }) {
283
+ const args = ["watchdog"];
284
+ if (serverUrl) args.push("--server", serverUrl);
285
+ if (clientId) args.push("--client-id", clientId);
286
+ args.push("--interval", String(intervalSec));
287
+ if (noDaemon) args.push("--no-daemon");
288
+ return args;
289
+ }
290
+
204
291
  /**
205
292
  * 独立升级器主流程(由旧 watchdog 以子进程方式启动)
293
+ * 顺序:下载 → 验证代码 → 杀旧 → 启新。任一步失败都不杀旧,业务不断。
206
294
  * @param {string[]} argv - process.argv.slice(2)
207
295
  */
208
296
  export async function runUpgradeMode(argv) {
209
297
  const get = (f) => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : undefined);
210
- const oldPid = parseInt(get("--old-pid"), 10) || 0;
211
298
  const interactive = argv.includes("--interactive");
212
299
  const serverUrl = get("--server");
213
300
  const clientId = get("--client-id");
214
301
  const intervalSec = parseInt(get("--interval"), 10) || 15;
215
302
  const noDaemon = argv.includes("--no-daemon");
303
+ // 旧 watchdog pid:显式 --old-pid > 父进程 pid(默认由旧 watchdog spawn 时即父进程)
304
+ const oldPid =
305
+ (argv.includes("--old-pid") ? parseInt(get("--old-pid"), 10) : 0) ||
306
+ process.ppid;
216
307
 
308
+ // 1) 下载
217
309
  console.error(
218
310
  `[upgrade-proc] 开始下载新版: npm install -g ${PACKAGE}@latest ...`,
219
311
  );
312
+ const beforeVersion = getVersion(); // 下载前版本,用于验证 npm install 真正生效
220
313
  const result = await performUpgrade();
221
314
  if (!result.success) {
222
315
  const err = result.output.substring(0, 300);
223
- console.error(`[upgrade-proc] 下载失败: ${err}`);
224
- writeUpgradeResult({ status: "fail", error: err });
316
+ console.error(`[upgrade-proc] 下载失败,保持当前版本运行: ${err}`);
225
317
  process.exit(1);
226
318
  }
227
- console.error(`[upgrade-proc] 下载完成,通知旧 watchdog 退出`);
228
- writeUpgradeResult({ status: "ok" });
229
319
 
230
- // 等旧 watchdog 退出,避免与其 stopAll 竞态(它退出后才会让新版接管业务)
231
- if (oldPid) {
232
- const exited = await waitProcessExit(oldPid);
233
- if (!exited) {
234
- console.error(
235
- `[upgrade-proc] 等待旧 watchdog(pid=${oldPid}) 退出超时,继续启动新版`,
236
- );
237
- }
320
+ // 2) 下载后验证:版本确实更新 + 新版主模块能加载(冒烟);通过才允许杀旧
321
+ const verify = await verifyUpgradedPackage(beforeVersion);
322
+ if (!verify.ok) {
323
+ console.error(
324
+ `[upgrade-proc] 新版本验证未通过,不切换,保持当前版本运行: ${verify.reason}`,
325
+ );
326
+ process.exit(1);
238
327
  }
328
+ console.error(`[upgrade-proc] 新版本验证通过 (${verify.version})`);
239
329
 
240
- // 启动新版 watchdog(interactive 时继承控制台接管窗口;否则后台 + 日志文件)
241
- const cliPath = join(
242
- dirname(fileURLToPath(import.meta.url)),
243
- "..",
244
- "..",
245
- "cli.js",
246
- );
247
- const args = ["watchdog"];
248
- if (serverUrl) args.push("--server", serverUrl);
249
- if (clientId) args.push("--client-id", clientId);
250
- args.push("--interval", String(intervalSec));
251
- if (noDaemon) args.push("--no-daemon");
252
-
253
- let logFd = undefined;
254
- try {
255
- const logPath = join(tmpdir(), `tt-help-watchdog-${Date.now()}.log`);
256
- logFd = openSync(logPath, "a");
257
- console.error(`[upgrade-proc] 新版 watchdog 日志: ${logPath}`);
258
- } catch {}
330
+ // 3) 杀旧:SIGTERM 优雅优先,5s 未退强杀兜底(U 不卡死)
331
+ console.error(`[upgrade-proc] 通知旧 watchdog (pid=${oldPid}) 退出 ...`);
332
+ const exited = await terminateProcess(oldPid, 5000);
333
+ if (exited) {
334
+ console.error(`[upgrade-proc] 旧 watchdog 已退出`);
335
+ } else {
336
+ console.error(`[upgrade-proc] 旧 watchdog 未在 5s 内退出,已强制终止`);
337
+ await new Promise((r) => setTimeout(r, 1000)); // 给强杀一点生效时间
338
+ }
259
339
 
260
- let child;
340
+ // 4) 旧进程已退,启新版 watchdog(无并存窗口,一定能起来)
341
+ console.error(`[upgrade-proc] 启动新版 watchdog ...`);
342
+ const cliPath = join(__dirname, "..", "..", "cli.js");
343
+ const args = buildWatchdogArgs({ serverUrl, clientId, intervalSec, noDaemon });
344
+ const logFd = openLogFile(
345
+ "tt-help-watchdog",
346
+ "upgrade-proc",
347
+ "新版 watchdog 日志",
348
+ );
261
349
  try {
262
- child = spawn(process.execPath, [cliPath, ...args], {
263
- detached: !interactive,
264
- stdio: interactive
265
- ? "inherit"
266
- : ["ignore", logFd || "ignore", logFd || "ignore"],
267
- windowsHide: !interactive,
268
- });
269
- if (!interactive) child.unref();
350
+ const child = spawnChild([cliPath, ...args], { interactive, logFd });
351
+ console.error(`[upgrade-proc] 新版 watchdog 已启动 (pid=${child.pid})`);
270
352
  } catch (e) {
271
353
  console.error(`[upgrade-proc] 新版 watchdog 启动失败: ${e.message}`);
272
354
  process.exit(1);
273
355
  }
274
- console.error(`[upgrade-proc] 新版 watchdog 已启动 (pid=${child.pid})`);
275
356
  process.exit(0);
276
357
  }
277
358
 
278
359
  /**
279
- * 旧 watchdog 调用:spawn 独立升级器并轮询结果文件
280
- * @returns {Promise<"ok"|"fail"|"timeout">}
360
+ * 旧 watchdog 调用:spawn 独立升级器(fire-and-forget,不等待、不写中间文件)
361
+ * 升级器负责「下载 → 验证 → 杀旧 → 启新」(见 runUpgradeMode);本进程收到 SIGTERM 优雅退出。
362
+ * 非交互时升级器 stdout/stderr 落盘(进程退出后日志不断,可定位问题)
363
+ * @returns {import("child_process").ChildProcess|null} 升级器子进程(spawn 失败返回 null)
281
364
  */
282
- export function spawnUpgraderAndWait({
365
+ export function spawnUpgrader({
283
366
  serverUrl,
284
367
  clientId,
285
368
  intervalSec = 15,
286
369
  spawnDaemon = true,
287
370
  interactive = false,
288
371
  oldPid = 0,
289
- timeoutMs = 180_000,
290
372
  } = {}) {
291
- return new Promise((resolve) => {
292
- // 清理上次结果
293
- try {
294
- unlinkSync(UPGRADE_RESULT_FILE);
295
- } catch {}
296
-
297
- const upgraderPath = fileURLToPath(import.meta.url);
298
- const args = ["--upgrade", "--old-pid", String(oldPid || process.pid)];
299
- if (serverUrl) args.push("--server", serverUrl);
300
- if (clientId) args.push("--client-id", clientId);
301
- args.push("--interval", String(intervalSec));
302
- if (!spawnDaemon) args.push("--no-daemon");
303
- if (interactive) args.push("--interactive");
304
-
305
- let child;
306
- try {
307
- child = spawn(process.execPath, [upgraderPath, ...args], {
308
- // 交互:升级器接管控制台(后续新版 watchdog 也继承窗口);非交互:后台 + 日志
309
- detached: !interactive,
310
- stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
311
- windowsHide: !interactive,
312
- });
313
- if (!interactive) child.unref();
314
- } catch (e) {
315
- console.error(`[watchdog] 升级程序启动失败: ${e.message}`);
316
- resolve("fail");
317
- return;
318
- }
319
- child.stderr?.on("data", (d) => {
320
- const m = d.toString().trim();
321
- if (m) console.error(`[upgrade-proc] ${m.substring(0, 200)}`);
322
- });
373
+ const upgraderPath = fileURLToPath(import.meta.url);
374
+ const args = ["--upgrade", "--old-pid", String(oldPid || process.ppid)];
375
+ if (serverUrl) args.push("--server", serverUrl);
376
+ if (clientId) args.push("--client-id", clientId);
377
+ args.push("--interval", String(intervalSec));
378
+ if (!spawnDaemon) args.push("--no-daemon");
379
+ if (interactive) args.push("--interactive");
323
380
 
324
- const started = Date.now();
325
- const timer = setInterval(() => {
326
- try {
327
- const data = JSON.parse(readFileSync(UPGRADE_RESULT_FILE, "utf-8"));
328
- if (data && data.status) {
329
- clearInterval(timer);
330
- resolve(data.status === "ok" ? "ok" : "fail");
331
- }
332
- } catch {}
333
- if (Date.now() - started > timeoutMs) {
334
- clearInterval(timer);
335
- try {
336
- unlinkSync(UPGRADE_RESULT_FILE);
337
- } catch {}
338
- console.error(
339
- `[watchdog] 升级程序 ${timeoutMs / 1000}s 无结果,放弃等待`,
340
- );
341
- resolve("timeout");
342
- }
343
- }, 2000);
344
- });
381
+ const logFd = openLogFile("tt-help-upgrader", "watchdog", "升级程序日志");
382
+ try {
383
+ return spawnChild([upgraderPath, ...args], { interactive, logFd });
384
+ } catch (e) {
385
+ console.error(`[watchdog] 升级程序启动失败: ${e.message}`);
386
+ return null;
387
+ }
345
388
  }
346
389
 
347
390
  // 直接运行时自动进入独立升级器模式
348
391
  if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
349
392
  runUpgradeMode(process.argv.slice(2)).catch((e) => {
350
393
  console.error(`[upgrade-proc] 升级器异常: ${e.message}`);
351
- writeUpgradeResult({ status: "fail", error: e.message });
352
394
  process.exit(1);
353
395
  });
354
396
  }