cicy-desktop 2.1.107 → 2.1.109

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.107",
3
+ "version": "2.1.109",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -504,15 +504,51 @@ function wslMissing() {
504
504
  }
505
505
  }
506
506
 
507
+ // Read-only, works without elevation. True iff a Windows optional feature
508
+ // reports State : Enabled.
509
+ function featureEnabled(feature) {
510
+ return new Promise((resolve) => {
511
+ execFile("dism", ["/english", "/online", "/get-featureinfo", `/featurename:${feature}`],
512
+ { timeout: 30000, windowsHide: true },
513
+ (_e, out) => resolve(/State\s*:\s*Enabled/i.test(String(out || ""))));
514
+ });
515
+ }
516
+
517
+ // Enable ONE Windows optional feature, elevated, and WAIT until it actually
518
+ // reports Enabled. The old `wsl --install` was fire-and-forget — it could
519
+ // silently no-op, leaving the card stuck on "reboot required" forever. Here we
520
+ // drive DISM and verify, streaming every step to the install drawer.
521
+ async function dismEnableFeature(feature, label, { emit } = {}) {
522
+ if (await featureEnabled(feature)) {
523
+ emit && emit({ phase: "install-docker", status: "running", message: `${label}:已启用,跳过` });
524
+ return true;
525
+ }
526
+ emit && emit({ phase: "install-docker", status: "running", message: `${label}:正在启用(约 1–2 分钟,请稍候)…` });
527
+ await launchElevated("dism", ["/online", "/enable-feature", `/featurename:${feature}`, "/all", "/norestart"], { emit }).catch(() => {});
528
+ // launchElevated fires the elevated task and returns immediately; poll the
529
+ // real feature state (DISM exits 3010 = success + reboot-required).
530
+ const ok = await waitUntil(() => featureEnabled(feature), { totalMs: 240000, everyMs: 5000 });
531
+ emit && emit({ phase: "install-docker", status: ok ? "running" : "error", message: ok ? `${label}:已启用 ✓` : `${label}:未能确认启用(点「重试」)` });
532
+ return ok;
533
+ }
534
+
507
535
  // Ensure the WSL2 backend exists; install it (elevated) if missing. Returns
508
- // { ok } when present, or { needsReboot } after kicking off `wsl --install`
509
- // (which requires admin + a Windows reboot before Docker can use it).
536
+ // { ok } when already present, { needsReboot } after the two required Windows
537
+ // features are verified-enabled (a Windows reboot is then needed before Docker
538
+ // can use WSL2), or { failed } if a feature couldn't be enabled.
510
539
  async function ensureWsl({ emit } = {}) {
511
540
  if (!wslMissing()) return { ok: true };
512
- emit && emit({ phase: "install-docker", status: "running", message: "Docker 需要 WSL2 后端,正在安装 WSL(请在管理员授权框点「是」,装完需重启 Windows)…" });
513
- // --no-distribution: just the WSL2 platform (Docker brings its own distro);
514
- // falls back to plain `wsl --install` on older builds that reject the flag.
515
- await launchElevated("wsl", ["--install", "--no-distribution"], { emit });
541
+ emit && emit({ phase: "install-docker", status: "running", message: "Docker 需要 WSL2 后端,开始启用所需的 Windows 功能…" });
542
+ const a = await dismEnableFeature("Microsoft-Windows-Subsystem-Linux", "启用 WSL 功能 1/2 · Linux 子系统", { emit });
543
+ const b = await dismEnableFeature("VirtualMachinePlatform", "启用 WSL 功能 2/2 · 虚拟机平台", { emit });
544
+ if (!a || !b) {
545
+ emit && emit({ phase: "done", status: "error", message: "WSL 功能未能全部启用——请点「重试」" });
546
+ return { ok: false, needsReboot: false, failed: true };
547
+ }
548
+ // Best-effort: also pull the WSL2 kernel/plumbing (no-op until reboot on some
549
+ // builds; harmless if it errors).
550
+ await launchElevated("wsl", ["--install", "--no-distribution"], { emit }).catch(() => {});
551
+ emit && emit({ phase: "install-docker", status: "running", message: "WSL2 功能已启用 ✓,需【重启 Windows】后回来点「重试」继续。" });
516
552
  return { ok: false, needsReboot: true };
517
553
  }
518
554
 
@@ -11,12 +11,21 @@
11
11
  // → health-probe :8009 from Windows. Every step checks-then-acts and is
12
12
  // idempotent, so 重试 resumes.
13
13
 
14
- const { execFile, execFileSync } = require("child_process");
14
+ const { execFile, execFileSync, spawn } = require("child_process");
15
15
  const path = require("path");
16
+ const os = require("os");
17
+ const fs = require("fs");
16
18
  const docker = require("./docker"); // shared: downloads, waitUntil, probeHealth, launchElevated, ensureWsl…
17
19
 
18
20
  const DISTRO = process.env.CICY_WSL_DISTRO || "Ubuntu";
19
21
  const IMAGE = process.env.CICY_DOCKER_IMAGE || "cicybot/cicy-code:latest";
22
+ // Ubuntu WSL rootfs from the Tsinghua TUNA mirror (CN-fast). We download it
23
+ // ourselves (real progress bar) and `wsl --import` it, instead of
24
+ // `wsl --install -d Ubuntu` (no parseable progress, needs the MS Store).
25
+ const ROOTFS_URL = process.env.CICY_WSL_ROOTFS_URL ||
26
+ "https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/wsl/jammy/current/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz";
27
+
28
+ function rootfsPath() { return path.join(docker.downloadsDir(), "ubuntu-jammy-wsl-rootfs.tar.gz"); }
20
29
 
21
30
  // Run a bash command as root inside the distro. execFile (no host shell) → the
22
31
  // command string is passed verbatim to `bash -lc`, so only bash-level quoting
@@ -32,6 +41,34 @@ function wslRun(cmd, { timeout = 60000, distro = DISTRO } = {}) {
32
41
  });
33
42
  }
34
43
 
44
+ // Like wslRun, but STREAMS each output line to the install drawer so the user
45
+ // (and the customer) watch the real install proceed — apt fetching/unpacking,
46
+ // `docker load` layers — instead of staring at a frozen spinner. Throttled so
47
+ // rapid output doesn't flood the log; resolves { stdout } with the full tail.
48
+ function wslRunStream(cmd, { emit, phase = "install-docker", timeout = 900000, distro = DISTRO } = {}) {
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawn("wsl", ["-d", distro, "-u", "root", "--", "bash", "-lc", cmd], { windowsHide: true });
51
+ let buf = "", tail = "", last = 0;
52
+ const pump = (chunk) => {
53
+ buf += chunk.toString("utf8");
54
+ let nl;
55
+ while ((nl = buf.indexOf("\n")) >= 0) {
56
+ const line = buf.slice(0, nl).replace(/\r$/, "").trim();
57
+ buf = buf.slice(nl + 1);
58
+ if (!line) continue;
59
+ tail += line + "\n";
60
+ const now = Date.now();
61
+ if (emit && now - last > 350) { last = now; emit({ phase, status: "running", message: line.slice(0, 200) }); }
62
+ }
63
+ };
64
+ child.stdout.on("data", pump);
65
+ child.stderr.on("data", pump);
66
+ const timer = setTimeout(() => { try { child.kill("SIGKILL"); } catch {} reject(Object.assign(new Error("timeout"), { stdout: tail })); }, timeout);
67
+ child.on("error", (e) => { clearTimeout(timer); reject(e); });
68
+ child.on("close", (code) => { clearTimeout(timer); code === 0 ? resolve({ stdout: tail }) : reject(Object.assign(new Error(`exit ${code}`), { stdout: tail })); });
69
+ });
70
+ }
71
+
35
72
  // Is `DISTRO` registered? `wsl -l -q` lists installed distros (UTF-16LE).
36
73
  function distroInstalled(distro = DISTRO) {
37
74
  if (process.platform !== "win32") return false;
@@ -46,16 +83,24 @@ function distroInstalled(distro = DISTRO) {
46
83
  // (--no-launch). We always run commands as root afterwards, so no user account
47
84
  // is needed. Elevated via the scheduled-task path (reliable on these machines).
48
85
  async function installDistro({ emit } = {}) {
49
- emit && emit({ phase: "install-docker", status: "running", message: `安装 ${DISTRO} 子系统(首次下载较大,请耐心等待)…` });
50
- // Try non-elevated first (adding a distro to an existing WSL is per-user);
51
- // fall back to elevated if it errors.
52
- try {
53
- await new Promise((resolve, reject) => {
54
- execFile("wsl", ["--install", "-d", DISTRO, "--no-launch"], { timeout: 600000, windowsHide: true }, (err) => err ? reject(err) : resolve());
55
- });
56
- } catch {
57
- await docker.launchElevated("wsl", ["--install", "-d", DISTRO, "--no-launch"], { emit });
58
- }
86
+ // 1) Download the rootfs with a REAL progress bar (Tsinghua mirror, resumable).
87
+ const dest = rootfsPath();
88
+ await docker.ensureDownloaded(ROOTFS_URL, dest, null, { emit, phase: "install-docker", label: "下载 Ubuntu" });
89
+ // 2) Ensure the WSL2 kernel exists (a feature-enable + reboot leaves the kernel
90
+ // component missing `--import --version 2` errors). Best-effort, non-fatal.
91
+ emit && emit({ phase: "install-docker", status: "running", message: "检查/更新 WSL2 内核…" });
92
+ await new Promise((res) => execFile("wsl", ["--update", "--web-download"], { timeout: 180000, windowsHide: true }, () => res()));
93
+ try { await new Promise((res) => execFile("wsl", ["--set-default-version", "2"], { timeout: 15000, windowsHide: true }, () => res())); } catch {}
94
+ // 3) Import it as a WSL2 distro (creates the VHD under installDir; no MS Store,
95
+ // no interactive first-run — we run everything as root afterwards).
96
+ emit && emit({ phase: "install-docker", status: "running", message: "导入 Ubuntu 到 WSL2…" });
97
+ const installDir = path.join(process.env["LOCALAPPDATA"] || path.join(os.homedir(), "AppData", "Local"), "cicy-ubuntu");
98
+ try { fs.mkdirSync(installDir, { recursive: true }); } catch {}
99
+ await new Promise((resolve, reject) => {
100
+ execFile("wsl", ["--import", DISTRO, installDir, dest, "--version", "2"],
101
+ { timeout: 600000, windowsHide: true },
102
+ (err, _so, se) => err ? reject(Object.assign(err, { stderr: String(se || "") })) : resolve());
103
+ });
59
104
  }
60
105
 
61
106
  // docker CLI present inside the distro?
@@ -66,8 +111,8 @@ async function dockerInstalled() {
66
111
 
67
112
  // Install Docker Engine (docker.io) inside the distro.
68
113
  async function installDockerEngine({ emit } = {}) {
69
- emit && emit({ phase: "install-docker", status: "running", message: "在 Ubuntu 里安装 Docker(apt,几分钟)…" });
70
- await wslRun("apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io", { timeout: 900000 });
114
+ emit && emit({ phase: "install-docker", status: "running", message: "在 Ubuntu 里安装 Docker(apt,几分钟,下面是实时进度)…" });
115
+ await wslRunStream("apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io 2>&1", { emit, phase: "install-docker", timeout: 900000 });
71
116
  }
72
117
 
73
118
  // dockerd reachable inside the distro?
@@ -96,9 +141,9 @@ function toWslPath(winPath) {
96
141
 
97
142
  // docker load the (Windows-side) tarball into the distro's Docker + retag.
98
143
  async function loadImage(winTarballPath, { emit } = {}) {
99
- emit && emit({ phase: "image", status: "loading", message: "正在导入镜像到 Docker(较大,约 1-3 分钟,请稍候)…" });
144
+ emit && emit({ phase: "image", status: "loading", message: "正在导入镜像到 Docker(较大,约 1-3 分钟,下面是实时进度)…" });
100
145
  const p = toWslPath(winTarballPath);
101
- const { stdout } = await wslRun(`docker load -i "${p}"`, { timeout: 600000 });
146
+ const { stdout } = await wslRunStream(`docker load -i "${p}"`, { emit, phase: "image", timeout: 600000 });
102
147
  const m = String(stdout).match(/Loaded image:\s*(\S+)/i);
103
148
  if (m && m[1] !== IMAGE) { try { await wslRun(`docker tag ${m[1]} ${IMAGE}`, { timeout: 15000 }); } catch {} }
104
149
  }
@@ -155,7 +200,8 @@ async function bootstrap({ onProgress, port = 8009, container = "cicy-code-docke
155
200
  // 2) Ubuntu distro
156
201
  if (!distroInstalled()) {
157
202
  try { await installDistro({ emit }); } catch (e) { emit({ phase: "done", status: "error", message: `Ubuntu 安装失败:${e.message}(点重试)` }); return { ok: false, reason: "distro_install_failed" }; }
158
- const ok = await docker.waitUntil(() => distroInstalled(), { totalMs: 600000, everyMs: 5000 });
203
+ const t0 = Date.now();
204
+ 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,首次较慢请耐心)` }) });
159
205
  if (!ok) { emit({ phase: "done", status: "error", message: "Ubuntu 还没装好——稍等或点「重试」" }); return { ok: false, reason: "distro_not_ready" }; }
160
206
  }
161
207