cicy-desktop 2.1.290 → 2.1.292

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.290",
3
+ "version": "2.1.292",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -629,20 +629,21 @@ async function wslMissing() {
629
629
  });
630
630
  }
631
631
 
632
- // True iff a Windows optional feature reports State : Enabled. NOTE: dism
633
- // /get-featureinfo prints NOTHING when the caller is not elevated (the desktop
634
- // normally runs at medium integrity even for an admin account), so an empty
635
- // answer must not be read as "disabled" fall back to the functional probe.
632
+ // True iff a Windows optional feature is enabled. Primary source: WMI
633
+ // Win32_OptionalFeature (InstallState 1=Enabled, 2=Disabled) readable WITHOUT
634
+ // elevation, unlike `dism /get-featureinfo`, which prints nothing at medium
635
+ // integrity (the desktop normally runs unelevated even for admin accounts) and
636
+ // was being misread as "disabled". dism stays as the fallback when WMI is unavailable.
636
637
  function featureEnabled(feature) {
637
638
  return new Promise((resolve) => {
638
- execFile("dism", ["/english", "/online", "/get-featureinfo", `/featurename:${feature}`],
639
- { timeout: 30000, windowsHide: true },
640
- async (_e, out) => {
641
- const text = String(out || "");
642
- if (/State\s*:\s*Enabled/i.test(text)) return resolve(true);
643
- if (/State\s*:\s*Disabled/i.test(text)) return resolve(false);
644
- resolve(await wslFunctional()); // no answer (not elevated) → ask WSL itself
645
- });
639
+ const ps = `(Get-CimInstance Win32_OptionalFeature -Filter "Name='${feature}'").InstallState`;
640
+ execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ps], { timeout: 30000, windowsHide: true }, (err, out) => {
641
+ const v = String(out || "").trim();
642
+ if (!err && /^[0-9]+$/.test(v)) return resolve(v === "1");
643
+ execFile("dism", ["/english", "/online", "/get-featureinfo", `/featurename:${feature}`],
644
+ { timeout: 30000, windowsHide: true },
645
+ (_e, out2) => resolve(/State\s*:\s*Enabled/i.test(String(out2 || ""))));
646
+ });
646
647
  });
647
648
  }
648
649
 
@@ -240,8 +240,20 @@ function importTarball(dest, installDir) {
240
240
  // `wsl` call then blocks and the app goes 未响应). Bound it short so we FAIL FAST
241
241
  // and the caller can `wsl --shutdown` + retry instead of hanging 10 minutes.
242
242
  execFile("wsl", ["--import", DISTRO, installDir, dest, "--version", "2"],
243
- { timeout: 240000, windowsHide: true },
244
- (err, _so, se) => { if (err) { err.stderr = String(se || ""); return reject(err); } resolve(); });
243
+ { timeout: 240000, windowsHide: true, encoding: "buffer" },
244
+ (err, _so, se) => {
245
+ if (err) {
246
+ // wsl.exe 的报错是 UTF-16 输出(常见:「请启用'虚拟机平台'Windows 功能并确保在 BIOS 中
247
+ // 启用虚拟化」);解码后拼进 message,抽屉才能显示真正原因而不是「Command failed」。
248
+ let text = "";
249
+ try { text = Buffer.isBuffer(se) ? se.toString("utf16le") : String(se || ""); } catch {}
250
+ text = text.replace(/\u0000/g, "").replace(/\s+/g, " ").trim();
251
+ if (text) err.message = `${err.message.split("\n")[0]} — ${text.slice(0, 300)}`;
252
+ err.stderr = text;
253
+ return reject(err);
254
+ }
255
+ resolve();
256
+ });
245
257
  });
246
258
  }
247
259
 
@@ -1081,6 +1093,13 @@ async function status(port = 8008) {
1081
1093
  // downloads and exit-100. A second caller just attaches to the in-flight run.
1082
1094
  let _bootstrapInFlight = null;
1083
1095
  let _bootstrapBeat = 0; // 上次进度事件时间 —— 心跳,用于判定 in-flight 是否卡死
1096
+ // 所有跟随同一次安装的进度订阅者 + 最近事件缓存:守护循环先起的安装没有 UI,用户随后点
1097
+ // 「安装」打开抽屉时,把已发生的步骤回放给它,之后的事件也同时推给它,而不是只有一句
1098
+ // 「正在跟随同一进度…」。
1099
+ const _bootstrapListeners = new Set();
1100
+ let _bootstrapRecent = [];
1101
+ let _bootstrapT0 = 0;
1102
+ const BOOTSTRAP_RECENT_MAX = 60;
1084
1103
  // 没有任何进度事件超过这个时长 = wedged(`wsl --import` 静默最久 ~1-4 分钟,留足余量)。
1085
1104
  const BOOTSTRAP_STALL_MS = 6 * 60 * 1000;
1086
1105
 
@@ -1092,7 +1111,11 @@ async function bootstrap(opts = {}) {
1092
1111
  // 丢弃它、重起一次;否则正常并发跟随同一进度。
1093
1112
  const stalled = Date.now() - _bootstrapBeat > BOOTSTRAP_STALL_MS;
1094
1113
  if (!stalled) {
1095
- try { opts.onProgress && opts.onProgress({ phase: "install-docker", status: "running", message: "安装已在进行中,正在跟随同一进度…" }); } catch {}
1114
+ if (opts.onProgress) {
1115
+ try { opts.onProgress({ phase: "install-docker", status: "running", message: `安装已在进行中(${Math.round((Date.now() - _bootstrapT0) / 1000)}s 前开始),正在跟随同一进度…` }); } catch {}
1116
+ for (const ev of _bootstrapRecent) { try { opts.onProgress(ev); } catch {} }
1117
+ _bootstrapListeners.add(opts.onProgress);
1118
+ }
1096
1119
  return _bootstrapInFlight;
1097
1120
  }
1098
1121
  log.warn(`[bootstrap] in-flight 卡死(${Math.round((Date.now() - _bootstrapBeat) / 1000)}s 无进度)→ 丢弃僵死的,重起一次`);
@@ -1101,9 +1124,16 @@ async function bootstrap(opts = {}) {
1101
1124
  }
1102
1125
  // 包一层 onProgress:每来一个事件就刷新心跳,卡死检测才准。
1103
1126
  _bootstrapBeat = Date.now();
1104
- const userOnProgress = opts.onProgress;
1105
- const wrapped = { ...opts, onProgress: (ev) => { _bootstrapBeat = Date.now(); try { userOnProgress && userOnProgress(ev); } catch {} } };
1106
- _bootstrapInFlight = _bootstrap(wrapped).finally(() => { _bootstrapInFlight = null; });
1127
+ _bootstrapT0 = Date.now();
1128
+ _bootstrapListeners.clear();
1129
+ _bootstrapRecent = [];
1130
+ if (opts.onProgress) _bootstrapListeners.add(opts.onProgress);
1131
+ const wrapped = { ...opts, onProgress: (ev) => {
1132
+ _bootstrapBeat = Date.now();
1133
+ _bootstrapRecent.push(ev); if (_bootstrapRecent.length > BOOTSTRAP_RECENT_MAX) _bootstrapRecent.shift();
1134
+ for (const fn of _bootstrapListeners) { try { fn(ev); } catch {} }
1135
+ } };
1136
+ _bootstrapInFlight = _bootstrap(wrapped).finally(() => { _bootstrapInFlight = null; _bootstrapListeners.clear(); });
1107
1137
  return _bootstrapInFlight;
1108
1138
  }
1109
1139
 
@@ -19,10 +19,16 @@ test("bootstrap failures land in lastError and pause the auto-bootstrap loop unt
19
19
  assert.match(src, /logFile: _logFile/);
20
20
  });
21
21
 
22
- test("featureEnabled does not treat dism's empty (unelevated) output as disabled", () => {
22
+ test("featureEnabled reads Win32_OptionalFeature (works unelevated), dism only as fallback", () => {
23
23
  const src = read("src/sidecar/docker.js");
24
- assert.match(src, /if \(\/State\\s\*:\\s\*Disabled\/i\.test\(text\)\) return resolve\(false\);\n\s+resolve\(await wslFunctional\(\)\)/);
25
- assert.match(src, /function wslFunctional\(\)/);
24
+ assert.match(src, /Get-CimInstance Win32_OptionalFeature -Filter "Name='\$\{feature\}'"\)\.InstallState/);
25
+ assert.match(src, /resolve\(v === "1"\)/);
26
+ });
27
+
28
+ test("wsl --import failures carry wsl.exe's decoded message into the drawer", () => {
29
+ const src = read("src/sidecar/wsl-docker.js");
30
+ assert.match(src, /encoding: "buffer"/);
31
+ assert.match(src, /se\.toString\("utf16le"\)/);
26
32
  });
27
33
 
28
34
  test("keepalive logon task degrades to LeastPrivilege / HKCU Run when not elevated", () => {
@@ -30,3 +36,9 @@ test("keepalive logon task degrades to LeastPrivilege / HKCU Run when not elevat
30
36
  assert.match(src, /writeKeepaliveFiles\(\{ runLevel: "LeastPrivilege" \}\)/);
31
37
  assert.match(src, /CurrentVersion\\\\Run/);
32
38
  });
39
+
40
+ test("a second bootstrap caller gets the in-flight run's progress (replay + live)", () => {
41
+ const src = read("src/sidecar/wsl-docker.js");
42
+ assert.match(src, /for \(const ev of _bootstrapRecent\) \{ try \{ opts\.onProgress\(ev\); \} catch \{\} \}\n\s+_bootstrapListeners\.add\(opts\.onProgress\);/);
43
+ assert.match(src, /for \(const fn of _bootstrapListeners\) \{ try \{ fn\(ev\); \} catch \{\} \}/);
44
+ });