cicy-desktop 2.1.122 → 2.1.124

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.122",
3
+ "version": "2.1.124",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -556,16 +556,19 @@ function launchElevated(exe, args, { emit } = {}) {
556
556
  // hangs on "正在启动 Docker Desktop". Detect a missing WSL. `wsl` prints UTF-16
557
557
  // and a fresh Windows without the feature says "未安装 / not installed / can be
558
558
  // installed by running wsl.exe --install".
559
- function wslMissing() {
559
+ async function wslMissing() {
560
560
  if (process.platform !== "win32") return false;
561
- try {
562
- const out = execFileSync("wsl", ["--status"], { timeout: 8000, windowsHide: true, encoding: "utf16le", stdio: ["ignore", "pipe", "pipe"] });
563
- return /未安装|not installed|--install/i.test(String(out));
564
- } catch (e) {
565
- const s = String((e.stdout || "") + (e.stderr || ""));
566
- if (/未安装|not installed|--install/i.test(s)) return true;
567
- return false; // wsl present but errored for another reason assume OK
568
- }
561
+ // ASYNC execFile, NOT execFileSync: this runs on the homepage's status poll,
562
+ // and a cold post-reboot WSL can take many seconds to answer `wsl --status`.
563
+ // A sync call there blocked the whole Electron main process → the window went
564
+ // "未响应". Capture stdout+stderr even on a non-zero exit (a fresh Windows
565
+ // without WSL prints the "not installed / --install" hint and exits non-zero).
566
+ return await new Promise((resolve) => {
567
+ execFile("wsl", ["--status"], { timeout: 8000, windowsHide: true, encoding: "utf16le" }, (err, stdout, stderr) => {
568
+ const s = String((stdout || "") + (stderr || "") + (err && err.message ? err.message : ""));
569
+ resolve(/未安装|not installed|--install/i.test(s));
570
+ });
571
+ });
569
572
  }
570
573
 
571
574
  // Read-only, works without elevation. True iff a Windows optional feature
@@ -601,7 +604,7 @@ async function dismEnableFeature(feature, label, { emit } = {}) {
601
604
  // features are verified-enabled (a Windows reboot is then needed before Docker
602
605
  // can use WSL2), or { failed } if a feature couldn't be enabled.
603
606
  async function ensureWsl({ emit } = {}) {
604
- if (!wslMissing()) return { ok: true };
607
+ if (!(await wslMissing())) return { ok: true };
605
608
  emit && emit({ phase: "install-docker", status: "running", message: "Docker 需要 WSL2 后端,开始启用所需的 Windows 功能…" });
606
609
  const a = await dismEnableFeature("Microsoft-Windows-Subsystem-Linux", "启用 WSL 功能 1/2 · Linux 子系统", { emit });
607
610
  const b = await dismEnableFeature("VirtualMachinePlatform", "启用 WSL 功能 2/2 · 虚拟机平台", { emit });
@@ -16,6 +16,7 @@ const path = require("path");
16
16
  const os = require("os");
17
17
  const fs = require("fs");
18
18
  const docker = require("./docker"); // shared: downloads, waitUntil, probeHealth, launchElevated, ensureWsl…
19
+ const log = require("electron-log"); // persisted main.log — bootstrap timing/failures land here
19
20
 
20
21
  // Dedicated distro name — NEVER reuse/clobber a user's own "Ubuntu" distro.
21
22
  const DISTRO = process.env.CICY_WSL_DISTRO || "cicy-code-wsl";
@@ -88,13 +89,18 @@ function wslRunStream(cmd, { emit, phase = "install-docker", timeout = 900000, d
88
89
  }
89
90
 
90
91
  // Is `DISTRO` registered? `wsl -l -q` lists installed distros (UTF-16LE).
91
- function distroInstalled(distro = DISTRO) {
92
+ async function distroInstalled(distro = DISTRO) {
92
93
  if (process.platform !== "win32") return false;
93
- try {
94
- const out = execFileSync("wsl", ["-l", "-q"], { timeout: 8000, windowsHide: true, encoding: "utf16le" });
95
- return String(out).split(/\r?\n/).map((s) => s.replace(/\0/g, "").trim()).filter(Boolean)
96
- .some((d) => d.toLowerCase() === distro.toLowerCase());
97
- } catch { return false; }
94
+ // ASYNC execFile, NOT execFileSync: this runs on the homepage's status poll,
95
+ // and a sync `wsl -l -q` on a cold post-reboot WSL blocked the Electron main
96
+ // process for seconds → window "未响应". (waitUntil awaits the promise fine.)
97
+ return await new Promise((resolve) => {
98
+ execFile("wsl", ["-l", "-q"], { timeout: 8000, windowsHide: true, encoding: "utf16le" }, (err, stdout) => {
99
+ if (err) return resolve(false);
100
+ resolve(String(stdout || "").split(/\r?\n/).map((s) => s.replace(/\0/g, "").trim()).filter(Boolean)
101
+ .some((d) => d.toLowerCase() === distro.toLowerCase()));
102
+ });
103
+ });
98
104
  }
99
105
 
100
106
  // Install the Ubuntu distro WITHOUT launching its interactive first-run setup
@@ -182,6 +188,8 @@ async function startEngine() {
182
188
  // runtime files, (re)launches dockerd, and waits for the socket; between
183
189
  // attempts we hard-kill any half-dead daemon so the next launch is clean.
184
190
  for (let attempt = 1; attempt <= 3; attempt++) {
191
+ const at0 = Date.now();
192
+ log.info(`[startEngine] attempt ${attempt}/3 — (re)launch dockerd + wait for socket`);
185
193
  try {
186
194
  await wslRun(
187
195
  "update-alternatives --set iptables /usr/sbin/iptables-legacy >/dev/null 2>&1; " +
@@ -199,8 +207,9 @@ async function startEngine() {
199
207
  // cold again (a restart loop → 「引擎没起来」even though dockerd was fine).
200
208
  "for i in $(seq 1 120); do [ -S /var/run/docker.sock ] && docker version >/dev/null 2>&1 && break; sleep 1; done",
201
209
  { timeout: 150000 });
202
- } catch {}
203
- if (await dockerEngineUp()) return true;
210
+ } catch (e) { log.warn(`[startEngine] attempt ${attempt} launch/wait errored: ${e.message}`); }
211
+ if (await dockerEngineUp()) { log.info(`[startEngine] ✓ dockerd up on attempt ${attempt} (${((Date.now() - at0) / 1000).toFixed(1)}s)`); return true; }
212
+ log.warn(`[startEngine] attempt ${attempt} dockerd still not up after ${((Date.now() - at0) / 1000).toFixed(1)}s`);
204
213
  // Not up after the wait. Only hard-reset when dockerd actually DIED (crash) —
205
214
  // if it's still alive it's just slow/mid-init, so leave it and let the next
206
215
  // attempt's wait loop keep polling instead of killing a healthy daemon.
@@ -210,6 +219,7 @@ async function startEngine() {
210
219
  { timeout: 15000 });
211
220
  } catch {}
212
221
  }
222
+ log.error("[startEngine] ✗ dockerd NOT up after 3 attempts");
213
223
  return false;
214
224
  }
215
225
 
@@ -310,7 +320,11 @@ function ensureAutostart() {
310
320
  return new Promise((res) => {
311
321
  const tr = `wsl.exe -d ${DISTRO} -u root -e /usr/local/sbin/start-dockerd.sh`;
312
322
  execFile("schtasks", ["/create", "/tn", "cicy-docker-autostart", "/tr", tr, "/sc", "onlogon", "/rl", "HIGHEST", "/f"],
313
- { windowsHide: true }, () => res());
323
+ { windowsHide: true }, (err, _stdout, stderr) => {
324
+ if (err) log.warn(`[ensureAutostart] schtasks create FAILED (dockerd won't auto-start after reboot): ${err.message}${stderr ? ` / ${String(stderr).trim()}` : ""}`);
325
+ else log.info("[ensureAutostart] logon task 'cicy-docker-autostart' registered → dockerd starts on next logon");
326
+ res();
327
+ });
314
328
  });
315
329
  }
316
330
 
@@ -350,8 +364,8 @@ function ensureDesktopShortcut(volume = "cicy-team-8009", port = 8009) {
350
364
 
351
365
  // Composite status for the card.
352
366
  async function status(port = 8009) {
353
- const wsl = !docker.wslMissing();
354
- const distro = wsl && distroInstalled();
367
+ const wsl = !(await docker.wslMissing());
368
+ const distro = wsl && (await distroInstalled());
355
369
  const engineUp = distro && (await dockerEngineUp());
356
370
  const running = engineUp && (await probeHealth(port));
357
371
  return { wsl, distro, engineUp, running };
@@ -375,65 +389,104 @@ async function bootstrap(opts = {}) {
375
389
  async function _bootstrap({ onProgress, port = 8009, container = "cicy-code-docker", volume = "cicy-team", env = {} } = {}) {
376
390
  const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
377
391
 
392
+ // Structured, PERSISTED trace of the whole run (electron-log → main.log) so a
393
+ // failed "重启后起不来" is diagnosable AFTER the fact instead of staring at the
394
+ // ephemeral progress modal. Each step logs ▶start / ✓done(+duration, or "skip"
395
+ // when already satisfied) / ✗fail(+reason+duration); the final line gives the
396
+ // total time and the SLOWEST step, plus a [name,ms] breakdown of every step.
397
+ const BT0 = Date.now();
398
+ const secs = (t) => `${((Date.now() - t) / 1000).toFixed(1)}s`;
399
+ const steps = [];
400
+ let _pt = BT0, _pname = "";
401
+ const begin = (name) => { _pname = name; _pt = Date.now(); log.info(`[bootstrap] ▶ ${name}`); };
402
+ const done = (skip) => { steps.push([_pname, Date.now() - _pt]); log.info(`[bootstrap] ✓ ${_pname}${skip ? " (skip)" : ""} ${secs(_pt)}`); };
403
+ const fail = (reason, extra) => { steps.push([`${_pname}:FAIL`, Date.now() - _pt]); log.error(`[bootstrap] ✗ ${_pname} reason=${reason} ${secs(_pt)}${extra ? `\n${extra}` : ""}`); };
404
+ const finish = (ok, reason) => {
405
+ const slow = steps.slice().sort((a, b) => b[1] - a[1])[0];
406
+ log.info(`[bootstrap] ${ok ? "DONE ✓" : `ABORT ✗ reason=${reason}`} total=${secs(BT0)} slowest=${slow ? `${slow[0]} ${(slow[1] / 1000).toFixed(1)}s` : "n/a"} steps=${JSON.stringify(steps.map(([n, d]) => [n, Math.round(d)]))}`);
407
+ };
408
+ log.info(`[bootstrap] START port=${port} container=${container} volume=${volume}`);
409
+
378
410
  // 0) Fast path: already healthy → instant no-op (idempotent one-shot).
411
+ begin("probe-healthy");
379
412
  if (await probeHealth(port)) {
413
+ done();
380
414
  emit({ phase: "done", status: "done", message: "Docker cicy-code 已就绪 🎉" });
415
+ finish(true);
381
416
  return { ok: true, container };
382
417
  }
418
+ done();
383
419
 
384
420
  // 1) WSL2 platform
385
- if (docker.wslMissing()) {
421
+ begin("ensure-wsl");
422
+ if (await docker.wslMissing()) {
386
423
  const w = await docker.ensureWsl({ emit });
387
- if (w.needsReboot) { emit({ phase: "done", status: "reboot", message: "WSL2 正在安装——请【重启 Windows】后回来点「重试」继续。" }); return { ok: false, reason: "wsl_reboot_required" }; }
388
- }
424
+ if (w.needsReboot) { fail("wsl_reboot_required"); emit({ phase: "done", status: "reboot", message: "WSL2 正在安装——请【重启 Windows】后回来点「重试」继续。" }); finish(false, "wsl_reboot_required"); return { ok: false, reason: "wsl_reboot_required" }; }
425
+ done();
426
+ } else done(true);
389
427
 
390
428
  // 2) Ubuntu distro
391
- if (!distroInstalled()) {
392
- try { await installDistro({ emit }); } catch (e) { emit({ phase: "install-docker", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)` }); return { ok: false, reason: "distro_install_failed" }; }
429
+ begin("ensure-distro");
430
+ if (!(await distroInstalled())) {
431
+ try { await installDistro({ emit }); } catch (e) { fail("distro_install_failed", e.message); emit({ phase: "install-docker", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)` }); finish(false, "distro_install_failed"); return { ok: false, reason: "distro_install_failed" }; }
393
432
  const t0 = Date.now();
394
433
  const ok = await docker.waitUntil(() => distroInstalled(), { totalMs: 600000, everyMs: 5000, onTick: () => emit({ phase: "install-docker", status: "running", message: `正在下载/注册 Ubuntu…(已 ${Math.round((Date.now() - t0) / 1000)}s,首次较慢请耐心)` }) });
395
- if (!ok) { emit({ phase: "install-docker", status: "error", message: "Ubuntu 还没装好——稍等或点「重试」" }); return { ok: false, reason: "distro_not_ready" }; }
396
- }
434
+ if (!ok) { fail("distro_not_ready"); emit({ phase: "install-docker", status: "error", message: "Ubuntu 还没装好——稍等或点「重试」" }); finish(false, "distro_not_ready"); return { ok: false, reason: "distro_not_ready" }; }
435
+ done();
436
+ } else done(true);
397
437
 
398
438
  // 3) Docker Engine inside Ubuntu
439
+ begin("install-docker-engine");
399
440
  if (!(await dockerInstalled())) {
400
- try { await installDockerEngine({ emit }); } catch (e) { emit({ phase: "install-docker", status: "error", message: `Docker 安装失败:${e.message}(点重试)` }); return { ok: false, reason: "docker_install_failed" }; }
401
- }
441
+ try { await installDockerEngine({ emit }); } catch (e) { fail("docker_install_failed", e.message); emit({ phase: "install-docker", status: "error", message: `Docker 安装失败:${e.message}(点重试)` }); finish(false, "docker_install_failed"); return { ok: false, reason: "docker_install_failed" }; }
442
+ done();
443
+ } else done(true);
402
444
 
403
445
  // 4) dockerd up (phase "container" = 启动服务)
446
+ begin("start-dockerd");
404
447
  if (!(await dockerEngineUp())) {
405
448
  emit({ phase: "container", status: "running", message: "启动 Docker 引擎(首次较慢,请耐心)…" });
406
449
  const started = await startEngine(); // 3 clean attempts internally
407
450
  const up = started || await docker.waitUntil(dockerEngineUp, { totalMs: 120000, everyMs: 3000 });
408
451
  if (!up) {
409
- const log = await dockerdLogTail();
410
- emit({ phase: "container", status: "error", message: "Docker 引擎没起来——点「重试」" + (log ? `\n\ndockerd 日志(最后几行):\n${log}` : "") });
452
+ const dlog = await dockerdLogTail();
453
+ fail("dockerd_not_up", dlog ? `dockerd log:\n${dlog}` : "");
454
+ emit({ phase: "container", status: "error", message: "Docker 引擎没起来——点「重试」" + (dlog ? `\n\ndockerd 日志(最后几行):\n${dlog}` : "") });
455
+ finish(false, "dockerd_not_up");
411
456
  return { ok: false, reason: "dockerd_not_up" };
412
457
  }
413
- }
458
+ done();
459
+ } else done(true);
414
460
 
415
461
  // 5) Base image — pre-baked into the package, so this normally just confirms.
416
462
  // The download-tarball path is a fallback for a non-pre-baked rootfs.
463
+ begin("ensure-image");
417
464
  if (!(await imagePresent())) {
418
465
  let tarball;
419
466
  try { tarball = await docker.downloadImageTarball({ emit }); }
420
- catch (e) { emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}(点重试续传)` }); return { ok: false, reason: "image_download_failed" }; }
467
+ catch (e) { fail("image_download_failed", e.message); emit({ phase: "image", status: "error", message: `镜像下载失败:${e.message}(点重试续传)` }); finish(false, "image_download_failed"); return { ok: false, reason: "image_download_failed" }; }
421
468
  try { await loadImage(tarball, { emit }); }
422
- catch (e) { emit({ phase: "image", status: "error", message: `镜像导入失败:${e.message}(点重试)` }); return { ok: false, reason: "image_load_failed" }; }
423
- }
469
+ catch (e) { fail("image_load_failed", e.message); emit({ phase: "image", status: "error", message: `镜像导入失败:${e.message}(点重试)` }); finish(false, "image_load_failed"); return { ok: false, reason: "image_load_failed" }; }
470
+ done();
471
+ } else done(true);
424
472
 
425
473
  // 6) Container (phase "container" = 启动服务)
474
+ begin("run-container");
426
475
  if (!(await probeHealth(port))) {
427
476
  emit({ phase: "container", status: "running", message: "启动 cicy-code 服务…" });
428
477
  try { await runContainer({ port, container, volume, env }); }
429
- catch (e) { emit({ phase: "container", status: "error", message: `服务启动失败:${e.message}(点重试)` }); return { ok: false, reason: "container_start_failed" }; }
430
- }
478
+ catch (e) { fail("container_start_failed", e.message); emit({ phase: "container", status: "error", message: `服务启动失败:${e.message}(点重试)` }); finish(false, "container_start_failed"); return { ok: false, reason: "container_start_failed" }; }
479
+ done();
480
+ } else done(true);
431
481
 
432
482
  // 7) Health — the ONLY path to ok:true.
483
+ begin("wait-health");
433
484
  emit({ phase: "container", status: "running", message: "等待 cicy-code 就绪…" });
434
485
  const healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
435
- if (healthy) { await ensureAutostart(); await ensureDesktopShortcut(volume, port); } // survive reboot + desktop shortcut
486
+ if (healthy) { done(); await ensureAutostart(); await ensureDesktopShortcut(volume, port); } // survive reboot + desktop shortcut
487
+ else fail("health_timeout");
436
488
  emit({ phase: healthy ? "done" : "container", status: healthy ? "done" : "error", message: healthy ? "Docker cicy-code 已就绪 🎉" : `服务起来了但 :${port} 还没响应——稍等或点「重试」` });
489
+ finish(healthy, healthy ? null : "health_timeout");
437
490
  return { ok: healthy, container };
438
491
  }
439
492