cicy-desktop 2.1.143 → 2.1.145

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": "cicy-desktop",
3
- "version": "2.1.143",
3
+ "version": "2.1.145",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -360,6 +360,18 @@ function curlDownload(url, dest, { emit, phase = "image", label = "下载镜像"
360
360
  // (much faster here); falls back to the node downloader if curl is unavailable.
361
361
  async function downloadImageTarball({ emit } = {}) {
362
362
  const dest = imageTarballPath();
363
+ // REUSE a complete tarball already on disk (staged into ~/Downloads, or a prior
364
+ // run) BEFORE curlDownload touches it — curlDownload doesn't skip a complete file
365
+ // (and a failed curl truncates it). headSize uses node http so it works even when
366
+ // curl can't resolve the host. Skips the ~500MB re-download per new user.
367
+ try {
368
+ const expected = await headSize(R2_TARBALL);
369
+ const have = fs.statSync(dest).size;
370
+ if (expected > 0 && have === expected) {
371
+ emit && emit({ phase: "image", status: "skip", message: "镜像:已有完整包,跳过下载", progress: 100, received: have, total: expected });
372
+ return dest;
373
+ }
374
+ } catch {}
363
375
  try { await curlDownload(R2_TARBALL, dest, { emit, phase: "image", label: "下载镜像" }); }
364
376
  catch (e) {
365
377
  emit && emit({ phase: "image", status: "running", message: `curl 下载失败(${e.message}),改用内置下载…` });
@@ -722,5 +734,5 @@ module.exports = {
722
734
  bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir, downloadsDir, imageTarballPath,
723
735
  launchElevated, wslMissing, ensureWsl,
724
736
  // platform-agnostic download/retry primitives, reused by native.js
725
- ensureDownloaded, curlDownload, withRetry, waitUntil, run,
737
+ ensureDownloaded, curlDownload, withRetry, waitUntil, run, headSize,
726
738
  };
@@ -155,10 +155,24 @@ async function installDistro({ emit } = {}) {
155
155
  // 1) Download the PRE-BAKED rootfs (Ubuntu+Docker+image baked in, ~444MB) with
156
156
  // a real progress bar. curl is ~10× faster than node's downloader on OSS.
157
157
  const dest = rootfsPath();
158
- try { await docker.curlDownload(ROOTFS_URL, dest, { emit, phase: "image", label: "下载运行环境" }); }
159
- catch (e) {
160
- emit && emit({ phase: "image", status: "running", message: `下载器异常(${e.message}),改用备用下载…` });
161
- await docker.ensureDownloaded(ROOTFS_URL, dest, null, { emit, phase: "image", label: "下载运行环境" });
158
+ // REUSE a complete rootfs already on disk (pre-staged into ~/Downloads, or left by
159
+ // a prior run) BEFORE curlDownload touches it. The bug: curlDownload doesn't skip a
160
+ // complete file (and on a failed curl it truncated the staged 447MB), so a staged
161
+ // rootfs got re-downloaded. headSize uses node http (works even when curl can't
162
+ // resolve the host — the `curl exit 6` we saw), so this skip is reliable.
163
+ let haveComplete = false;
164
+ try {
165
+ const expected = await docker.headSize(ROOTFS_URL);
166
+ const have = fs.statSync(dest).size;
167
+ haveComplete = expected > 0 && have === expected;
168
+ if (haveComplete) emit && emit({ phase: "image", status: "skip", message: "下载运行环境:已有完整包,跳过下载", progress: 100, received: have, total: expected });
169
+ } catch {}
170
+ if (!haveComplete) {
171
+ try { await docker.curlDownload(ROOTFS_URL, dest, { emit, phase: "image", label: "下载运行环境" }); }
172
+ catch (e) {
173
+ emit && emit({ phase: "image", status: "running", message: `下载器异常(${e.message}),改用备用下载…` });
174
+ await docker.ensureDownloaded(ROOTFS_URL, dest, null, { emit, phase: "image", label: "下载运行环境" });
175
+ }
162
176
  }
163
177
  // 2) Import as an ISOLATED WSL2 distro: its OWN VHDX under a dedicated dir, so
164
178
  // it never touches the user's existing distros. `--version 2` sets just THIS
@@ -252,6 +266,25 @@ const BOOT_SCRIPT_B64 = Buffer.from(
252
266
  "pgrep dockerd >/dev/null 2>&1 || setsid sh -c 'exec dockerd >/var/log/dockerd.log 2>&1 </dev/null' &\n"
253
267
  ).toString("base64");
254
268
 
269
+ // Hold the distro OPEN. The flip side of detaching dockerd: once the launching
270
+ // wsl.exe exits, WSL tears the distro down if no session holds it — killing the
271
+ // just-launched dockerd (the "启动引擎 转圈但 dockerd=none" we saw). A detached,
272
+ // long-lived `wsl … sleep infinity` keeps the distro alive so dockerd (and the
273
+ // container) survive. unref'd so it never blocks the app; survives app exit too.
274
+ let _keepalive = null;
275
+ function ensureKeepalive() {
276
+ if (process.platform !== "win32") return;
277
+ if (_keepalive && _keepalive.exitCode == null && !_keepalive.killed) return;
278
+ try {
279
+ _keepalive = spawn("wsl", ["-d", DISTRO, "-u", "root", "--", "sh", "-c", "exec sleep infinity"],
280
+ { detached: true, stdio: "ignore", windowsHide: true });
281
+ _keepalive.on("error", (e) => { log.warn(`[keepalive] ${e.message}`); _keepalive = null; });
282
+ _keepalive.on("exit", () => { _keepalive = null; });
283
+ _keepalive.unref();
284
+ log.info("[keepalive] holding distro open (sleep infinity)");
285
+ } catch (e) { log.warn(`[keepalive] spawn failed: ${e.message}`); }
286
+ }
287
+
255
288
  async function launchDockerd() {
256
289
  await wslRun(
257
290
  `echo ${BOOT_SCRIPT_B64} | base64 -d > /usr/local/sbin/start-dockerd.sh 2>/dev/null; chmod +x /usr/local/sbin/start-dockerd.sh 2>/dev/null; ` +
@@ -260,8 +293,10 @@ async function launchDockerd() {
260
293
  // Stale /var/run/docker.{pid,sock} from the pre-baked rootfs make a fresh dockerd
261
294
  // refuse to start; clear them when dockerd isn't already running.
262
295
  "if ! pgrep dockerd >/dev/null 2>&1; then rm -f /var/run/docker.pid /run/docker.pid /var/run/docker.sock /run/docker.sock; fi; " +
263
- "pgrep dockerd >/dev/null 2>&1 || setsid bash -c 'exec dockerd >/var/log/cicy-dockerd.log 2>&1 </dev/null' & " +
264
- "true",
296
+ // setsid --fork: fork dockerd into a NEW session and have setsid EXIT immediately,
297
+ // so this wsl call returns at once and dockerd is fully orphaned (no `& true`
298
+ // backgrounding race). It only survives because ensureKeepalive() holds the distro.
299
+ "pgrep dockerd >/dev/null 2>&1 || setsid --fork bash -c 'exec dockerd >/var/log/cicy-dockerd.log 2>&1 </dev/null'",
265
300
  { timeout: 20000 });
266
301
  }
267
302
 
@@ -273,7 +308,8 @@ async function startEngine() {
273
308
  for (let attempt = 1; attempt <= 3; attempt++) {
274
309
  const at0 = Date.now();
275
310
  let stuck = false;
276
- log.info(`[startEngine] attempt ${attempt}/3 — launch dockerd (detached) + poll socket`);
311
+ log.info(`[startEngine] attempt ${attempt}/3 — keepalive + launch dockerd (detached) + poll socket`);
312
+ ensureKeepalive(); // hold the distro open BEFORE launching, or the teardown kills dockerd
277
313
  try { await launchDockerd(); }
278
314
  catch (e) { stuck = true; log.warn(`[startEngine] attempt ${attempt} launch errored — WSL stuck? ${e.message}`); }
279
315
  if (!stuck) {