dsh-selfupdater 0.3.4 → 0.4.5

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.
@@ -266,6 +266,28 @@ async function dshPluginAdd(tgzFile) {
266
266
  return result.stdout;
267
267
  }
268
268
 
269
+ /**
270
+ * 从 npm 在线重装"当前已装版本"的 dsh-selfupdater(备份丢失时的回滚兜底)。
271
+ * 与常规回滚不同:没有 bak 可还原时,直接重装同版本也能让服务恢复可用。
272
+ */
273
+ async function dshPluginAddSelf() {
274
+ const pkgJson = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
275
+ const curVer = String(pkgJson?.dependencies?.['dsh-selfupdater'] ?? '').replace(/^[~^]/, '');
276
+ if (curVer === '') throw new Error('profile 中找不到 dsh-selfupdater 版本记录,无法重装');
277
+ const { tgzUrl } = await fetchPackument(
278
+ registryCandidates().find((b) => b !== undefined) ?? 'https://registry.npmjs.org', 'dsh-selfupdater',
279
+ ).catch(() => ({ tgzUrl: '' }));
280
+ // 拼指定版本的下载地址(fetchPackument 返回的是 latest 的地址,这里按需覆盖版本号)。
281
+ const url = tgzUrl !== ''
282
+ ? tgzUrl.replace(/-\d+\.\d+\.\d+[^/]*\.tgz$/, `-${curVer}.tgz`)
283
+ : `${registryCandidates()[0]}/dsh-selfupdater/-/dsh-selfupdater-${curVer}.tgz`;
284
+ const tgzFile = join(stagingDir, `dsh-selfupdater-${curVer}.tgz`);
285
+ mkdirSync(stagingDir, { recursive: true });
286
+ await downloadTgz(url, tgzFile);
287
+ await dshPluginAdd(tgzFile);
288
+ log(`已从 npm 重装 dsh-selfupdater@${curVer}`);
289
+ }
290
+
269
291
  /* ------------------------------------------------------------------ *
270
292
  * 重启链与健康检查
271
293
  * ------------------------------------------------------------------ */
@@ -304,6 +326,22 @@ async function waitHealthy(timeoutMs) {
304
326
  /** 失败回滚:还原整个 profile 目录并再次拉起服务。 */
305
327
  async function rollback() {
306
328
  setState('rollback', '安装失败,正在回滚插件 profile …');
329
+ // 防御:备份目录不存在时绝不能 rename(否则就是用户遇到的 ENOENT)。
330
+ if (!existsSync(bakProfileDir)) {
331
+ log(`回滚中止:备份目录不存在 (${bakProfileDir}),尝试直接重建可用 profile`);
332
+ try {
333
+ mkdirSync(profileDir, { recursive: true });
334
+ await dshPluginAddSelf();
335
+ launchRunnerChain();
336
+ const ok = await waitHealthy(60000);
337
+ setState(ok ? 'done_failed' : 'error',
338
+ ok ? '已重装插件并恢复服务' : '恢复后服务仍未就绪,请查看容器日志');
339
+ return ok;
340
+ } catch (err) {
341
+ setState('error', `回滚失败且重装也不成功:${err.message}`);
342
+ return false;
343
+ }
344
+ }
307
345
  try {
308
346
  rmSync(profileDir, { recursive: true, force: true });
309
347
  renameSync(bakProfileDir, profileDir);
@@ -325,6 +363,21 @@ async function rollback() {
325
363
  async function main() {
326
364
  mkdirSync(dshStateDir, { recursive: true });
327
365
 
366
+ // 【孤儿 bak 自愈】上次更新若在"bak 已生成但还没回滚/清理"的窗口内被
367
+ // 中断(进程被杀、断电、看门狗超时直接 exit),会留下一个孤儿备份:
368
+ // profiles/web.pluginupdate-bak ← 真正完整的旧 profile 在这里
369
+ // profiles/web ← 可能是空壳或半成品新版本
370
+ // 若不处理,下次 rollback 会因 bak 已不在而报 ENOENT。这里在动手前先救:
371
+ if (!existsSync(profileDir) && existsSync(bakProfileDir)) {
372
+ log('检测到孤儿备份(profile 缺失),自动从 bak 恢复');
373
+ renameSync(bakProfileDir, profileDir);
374
+ } else if (existsSync(bakProfileDir)) {
375
+ // profile 存在且不是本次会话创建的 bak:说明上一次更新已成功结束,
376
+ // 这个 bak 是"成功后保留供排查"的那份;本次升级即将重建备份,清掉旧的。
377
+ log('清理上一次更新遗留的 bak 备份目录');
378
+ rmSync(bakProfileDir, { recursive: true, force: true });
379
+ }
380
+
328
381
  // 锁文件双端校验:路由触发前会检查;这里再补一道防手动重复执行。
329
382
  if (existsSync(lockFile)) throw new Error('已有一次插件更新在进行中(锁文件存在)');
330
383
 
@@ -435,3 +488,17 @@ main().catch(async (err) => {
435
488
  }
436
489
  finish(1);
437
490
  });
491
+
492
+ // 进程被外部信号杀死时(NAS 重启容器、kill 等)也要尽力把 profile 放回原位,
493
+ // 否则留下的孤儿 bak 会让下一次回滚撞 ENOENT(本次加固的重点场景)。
494
+ for (const sig of ['SIGTERM', 'SIGINT']) {
495
+ process.on(sig, () => {
496
+ try {
497
+ if (!existsSync(profileDir) && existsSync(bakProfileDir)) {
498
+ renameSync(bakProfileDir, profileDir);
499
+ log(`收到 ${sig},已将 bak 恢复到 profile 原位`);
500
+ }
501
+ } catch { /* 尽力而为 */ }
502
+ finish(143);
503
+ });
504
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfupdater",
3
- "version": "0.3.4",
3
+ "version": "0.4.5",
4
4
  "description": "Self-update plugin for DeepSeek Harness: check npm for newer @deepseek-ai/dsh, swap node_modules atomically, restart, health-check and roll back on failure; also supports one-click online update of installed DSH plugins. DSH 主程序与已装插件的一站式在线更新插件。",
5
5
  "license": "MIT",
6
6
  "type": "module",