cicy-desktop 2.1.294 → 2.1.296
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
|
@@ -148,14 +148,14 @@ function register({ sidecarLogPath } = {}) {
|
|
|
148
148
|
let _autoBootstrapRetryAt = 0; // 硬失败后的自动重试时间点(退避,不是永久暂停:用户不会自己修)
|
|
149
149
|
// 退避:子进程被拦(安全软件开机后一段时间会自己放行)2 分钟一试;其余 15 分钟一试。
|
|
150
150
|
const AUTO_RETRY_MS = { spawn_blocked: 2 * 60 * 1000, default: 15 * 60 * 1000 };
|
|
151
|
-
const HARD_BOOTSTRAP_REASONS = new Set(["spawn_blocked", "virtualization_disabled", "wsl_enable_failed", "wsl_reboot_required"]);
|
|
151
|
+
const HARD_BOOTSTRAP_REASONS = new Set(["spawn_blocked", "virtualization_disabled", "wsl_enable_failed", "wsl_kernel_missing", "wsl_reboot_required"]);
|
|
152
152
|
function recordBootstrapResult(result, err) {
|
|
153
153
|
if (err) {
|
|
154
154
|
const code = err && err.code;
|
|
155
155
|
const reason = code === "EPERM" || /spawn EPERM/.test(err.message || "") ? "spawn_blocked" : "bootstrap_exception";
|
|
156
156
|
_lastBootstrapError = { reason, message: err.message, ts: Date.now() };
|
|
157
157
|
} else if (result && result.ok) {
|
|
158
|
-
_lastBootstrapError = null; _autoBootstrapPaused = null; return;
|
|
158
|
+
_lastBootstrapError = null; _autoBootstrapPaused = null; try { if (rebootCount() > 0) rebootCount(-99); } catch {} return;
|
|
159
159
|
} else if (result) {
|
|
160
160
|
_lastBootstrapError = { reason: result.reason || result.error || "bootstrap_failed", message: result.message || "", ts: Date.now() };
|
|
161
161
|
} else return;
|
|
@@ -169,9 +169,19 @@ function register({ sidecarLogPath } = {}) {
|
|
|
169
169
|
// WSL 功能启用后必须重启 Windows 才能继续。不等人:自动安排 90 秒后重启(桌面上会有
|
|
170
170
|
// 系统倒计时提示),UI 可「取消」或「立即重启」。每次开机只安排一次。
|
|
171
171
|
let _rebootScheduled = false;
|
|
172
|
+
// 防重启循环:WSL 始终起不来时不能每次开机都重启。计数存 db/wsl-reboots.json,最多自动重启 2 次
|
|
173
|
+
// (成功后 :8008 起来时清零)。
|
|
174
|
+
const REBOOT_COUNT_FILE = path.join(os.homedir(), "cicy-ai", "db", "wsl-reboots.json");
|
|
175
|
+
function rebootCount(delta) {
|
|
176
|
+
let c = 0; try { c = Number(JSON.parse(fs.readFileSync(REBOOT_COUNT_FILE, "utf8")).count) || 0; } catch {}
|
|
177
|
+
if (delta) { c = Math.max(0, c + delta); try { fs.mkdirSync(path.dirname(REBOOT_COUNT_FILE), { recursive: true }); fs.writeFileSync(REBOOT_COUNT_FILE, JSON.stringify({ count: c, ts: Date.now() })); } catch {} }
|
|
178
|
+
return c;
|
|
179
|
+
}
|
|
172
180
|
function scheduleReboot(delaySec = 90) {
|
|
173
181
|
if (_rebootScheduled || process.platform !== "win32") return false;
|
|
182
|
+
if (delaySec > 10 && rebootCount() >= 2) { log.warn("[docker] auto-reboot skipped: already rebooted 2× for WSL without success"); return false; }
|
|
174
183
|
_rebootScheduled = true;
|
|
184
|
+
rebootCount(+1);
|
|
175
185
|
try {
|
|
176
186
|
require("child_process").execFile("shutdown", ["/r", "/t", String(delaySec), "/c", "CiCy Desktop: WSL2 功能已启用,需要重启 Windows 才能继续安装 Docker;登录后会自动继续。可在 CiCy Desktop 里取消。"], { windowsHide: true }, (err) => {
|
|
177
187
|
if (err) { _rebootScheduled = false; log.warn(`[docker] schedule reboot failed: ${err.message}`); }
|
package/src/sidecar/docker.js
CHANGED
|
@@ -640,7 +640,9 @@ async function wslMissing() {
|
|
|
640
640
|
// wsl.exe 不存在(ENOENT):功能刚启用但还没重启 Windows 时就是这样 —— 按「缺失」处理,
|
|
641
641
|
// 让 ensureWsl 走 needsReboot,而不是当成已就绪去 --import 然后 spawn ENOENT。
|
|
642
642
|
if (err && (err.code === "ENOENT" || /ENOENT/.test(err.message || ""))) return resolve(true);
|
|
643
|
-
|
|
643
|
+
// 旧版内置 wsl.exe 存根:任何子命令都只打印用法帮助("wsl.exe [Argument]" / "--install")。
|
|
644
|
+
// 功能刚启用还没重启时就是这个状态 → 按缺失处理(ensureWsl 走 needsReboot)。
|
|
645
|
+
if (/未安装|not installed|--install|\[Argument\]|\[参数\]/i.test(s)) return resolve(true);
|
|
644
646
|
if (err && (err.killed || err.signal || err.code === "ETIMEDOUT")) return resolve(null); // timed out → unknown
|
|
645
647
|
resolve(false); // wsl present (errored for another reason → assume OK)
|
|
646
648
|
});
|
|
@@ -665,6 +667,18 @@ function featureEnabled(feature) {
|
|
|
665
667
|
});
|
|
666
668
|
}
|
|
667
669
|
|
|
670
|
+
const WSL_KERNEL_MSI_URL = process.env.CICY_WSL_KERNEL_URL || "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi";
|
|
671
|
+
// WSL2 kernel present? The inbox (feature-based) WSL keeps it at System32\lxss\tools\kernel;
|
|
672
|
+
// the Store WSL bundles its own, in which case the file is absent but `wsl --status`
|
|
673
|
+
// reports a kernel version — callers treat "functional + version" as present too.
|
|
674
|
+
function wslKernelPresent() {
|
|
675
|
+
if (process.platform !== "win32") return true;
|
|
676
|
+
const sysroot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
677
|
+
try { if (fs.existsSync(path.join(sysroot, "System32", "lxss", "tools", "kernel"))) return true; } catch {}
|
|
678
|
+
try { if (fs.existsSync(path.join(process.env.LOCALAPPDATA || "", "Microsoft", "WindowsApps", "wsl.exe")) && fs.existsSync(path.join(process.env.ProgramFiles || "C:\\Program Files", "WSL", "wsl.exe"))) return true; } catch {}
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
|
|
668
682
|
// Functional WSL probe that needs no elevation: `wsl -l -v` on a machine with
|
|
669
683
|
// the features enabled (and rebooted) either lists distros or says "no installed
|
|
670
684
|
// distributions"; with the features off it prints the "--install" help text.
|
|
@@ -673,7 +687,7 @@ function wslFunctional() {
|
|
|
673
687
|
return new Promise((resolve) => {
|
|
674
688
|
execFile(wslExe(), ["-l", "-v"], { timeout: 25000, windowsHide: true, encoding: "utf16le" }, (err, stdout, stderr) => {
|
|
675
689
|
const s = String((stdout || "") + (stderr || "")).replace(/\u0000/g, "");
|
|
676
|
-
if (/NAME\s+STATE|没有已安装的分发版|no installed distributions|aka\.ms\/wslstore
|
|
690
|
+
if (/NAME\s+STATE|没有已安装的分发版|no installed distributions|aka\.ms\/wslstore|名称\s+状态/i.test(s)) return resolve(true);
|
|
677
691
|
resolve(false);
|
|
678
692
|
});
|
|
679
693
|
});
|
|
@@ -717,17 +731,18 @@ function elevatedWslSetup({ emit, need = {} } = {}) {
|
|
|
717
731
|
`$R = "${resultFile.replace(/"/g, '""')}"`,
|
|
718
732
|
'function Say($m) { Add-Content -Path $R -Value ("[" + (Get-Date -Format "HH:mm:ss") + "] " + $m) }',
|
|
719
733
|
'function St($n) { try { (Get-CimInstance Win32_OptionalFeature -Filter ("Name=\'" + $n + "\'")).InstallState } catch { 0 } }',
|
|
720
|
-
'
|
|
734
|
+
'$S32 = Join-Path $env:SystemRoot "System32"; $DISM = Join-Path $S32 "dism.exe"; $SFC = Join-Path $S32 "sfc.exe"; $MSIEXEC = Join-Path $S32 "msiexec.exe"',
|
|
735
|
+
'function En($n) { & $DISM /online /enable-feature /featurename:$n /all /norestart | Out-Null; $c = $LASTEXITCODE; Say ("enable " + $n + " exit=" + $c); return $c }',
|
|
721
736
|
'Say "start admin=$(([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(\'Administrators\'))"',
|
|
722
737
|
'$feats = @("Microsoft-Windows-Subsystem-Linux","VirtualMachinePlatform")',
|
|
723
738
|
'$bad = @(); foreach ($f in $feats) { if ((St $f) -ne 1) { $c = En $f; if ($c -ne 0 -and $c -ne 3010) { $bad += $f } } }',
|
|
724
739
|
'if ($bad.Count -gt 0) {',
|
|
725
740
|
' Say "repair: DISM /RestoreHealth (component store refused: $($bad -join \',\')) — this can take 10-30 min"',
|
|
726
|
-
'
|
|
727
|
-
'
|
|
741
|
+
' & $DISM /online /cleanup-image /restorehealth /norestart | Out-Null; Say ("restorehealth exit=" + $LASTEXITCODE)',
|
|
742
|
+
' & $SFC /scannow | Out-Null; Say ("sfc exit=" + $LASTEXITCODE)',
|
|
728
743
|
' foreach ($f in $bad) { $c = En $f }',
|
|
729
744
|
'}',
|
|
730
|
-
`if (Test-Path "${msi.replace(/"/g, '""')}") { Start-Process
|
|
745
|
+
`if (Test-Path "${msi.replace(/"/g, '""')}") { $p = Start-Process $MSIEXEC -ArgumentList "/i","\`"${msi.replace(/"/g, '""')}\`"","/qn","/norestart" -Wait -PassThru; Say ("kernel msi exit=" + $p.ExitCode) }`,
|
|
731
746
|
'Say ("final subsystem=" + (St "Microsoft-Windows-Subsystem-Linux") + " vmPlatform=" + (St "VirtualMachinePlatform"))',
|
|
732
747
|
'Say "DONE"',
|
|
733
748
|
].join("\r\n");
|
|
@@ -814,7 +829,31 @@ async function ensureWsl({ emit } = {}) {
|
|
|
814
829
|
const subsystemEnabled = await featureEnabled("Microsoft-Windows-Subsystem-Linux");
|
|
815
830
|
const vmPlatformEnabled = await featureEnabled("VirtualMachinePlatform");
|
|
816
831
|
const state = classifyWslPrerequisites({ executableMissing, subsystemEnabled, vmPlatformEnabled });
|
|
817
|
-
if (state.ready)
|
|
832
|
+
if (state.ready) {
|
|
833
|
+
// 功能都已启用、wsl.exe 也在,但 WSL 本身还不能用(`wsl -l -v` 只打印帮助)= 启用后还没
|
|
834
|
+
// 重启。别去 --import(必失败),直接进自动重启流程。
|
|
835
|
+
if (!(await wslFunctional())) {
|
|
836
|
+
log.warn("[bootstrap] ensure-wsl: features enabled but WSL not functional yet (pending reboot)");
|
|
837
|
+
emit && emit({ phase: "install-docker", status: "running", message: "WSL 功能已启用但尚未生效,需要重启 Windows…" });
|
|
838
|
+
return { ok: false, needsReboot: true };
|
|
839
|
+
}
|
|
840
|
+
// WSL2 内核(lxss\tools\kernel)缺失时 --import 也必失败。先把 MSI 下到 Downloads,再用
|
|
841
|
+
// 同一个提权脚本装(功能已启用的话脚本只做装内核这一件事)。
|
|
842
|
+
if (!wslKernelPresent()) {
|
|
843
|
+
const msi = path.join(os.homedir(), "Downloads", "wsl_update_x64.msi");
|
|
844
|
+
try { await ensureDownloaded(WSL_KERNEL_MSI_URL, msi, null, { emit, phase: "install-docker", label: "下载 WSL2 内核" }); }
|
|
845
|
+
catch (e) { log.warn(`[bootstrap] kernel msi download failed: ${e.message}`); }
|
|
846
|
+
emit && emit({ phase: "install-docker", status: "running", message: "WSL2 内核未安装,开始安装(会弹一次 UAC,请点「是」)…" });
|
|
847
|
+
const r = await elevatedWslSetup({ emit, need: {} });
|
|
848
|
+
if (!wslKernelPresent()) {
|
|
849
|
+
const message = `WSL2 内核未能安装${r.detail ? `(${r.detail})` : "(UAC 未确认或 MSI 安装失败)"}——会自动重试。`;
|
|
850
|
+
log.error(`[bootstrap] ✗ ensure-wsl reason=wsl_kernel_missing ${r.detail || ""}`);
|
|
851
|
+
emit && emit({ phase: "done", status: "error", message });
|
|
852
|
+
return { ok: false, needsReboot: false, failed: true, reason: "wsl_kernel_missing", message };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return { ok: true };
|
|
856
|
+
}
|
|
818
857
|
|
|
819
858
|
emit && emit({ phase: "install-docker", status: "running", message: "Docker 需要 WSL2 后端,开始启用所需的 Windows 功能(会弹一次 UAC,请点「是」)…" });
|
|
820
859
|
const r = await elevatedWslSetup({ emit, need: { subsystem: !subsystemEnabled, vmPlatform: !vmPlatformEnabled } });
|
|
@@ -930,7 +969,7 @@ module.exports = {
|
|
|
930
969
|
start, stop, stopContainer, restart, checkStatus, loadImage, loadImageFromTarball,
|
|
931
970
|
downloadImageTarball, imagePresent, dockerOk, installDocker,
|
|
932
971
|
bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir, downloadsDir, imageTarballPath,
|
|
933
|
-
launchElevated, elevatedWslSetup, spawnProbe, wslFunctional, wslExe, wslMissing, ensureWsl, virtualizationStatus,
|
|
972
|
+
launchElevated, elevatedWslSetup, spawnProbe, wslFunctional, wslKernelPresent, wslExe, wslMissing, ensureWsl, virtualizationStatus,
|
|
934
973
|
// platform-agnostic download/retry primitives, reused by native.js
|
|
935
974
|
ensureDownloaded, curlDownload, withRetry, waitUntil, run, headSize,
|
|
936
975
|
// image freshness (修「重建仍用旧镜像」—— 校验 OSS ETag 变了才重下重载)
|
|
@@ -241,12 +241,14 @@ function importTarball(dest, installDir) {
|
|
|
241
241
|
// and the caller can `wsl --shutdown` + retry instead of hanging 10 minutes.
|
|
242
242
|
execFile(docker.wslExe(), ["--import", DISTRO, installDir, dest, "--version", "2"],
|
|
243
243
|
{ timeout: 240000, windowsHide: true, encoding: "buffer" },
|
|
244
|
-
(err,
|
|
244
|
+
(err, so, se) => {
|
|
245
245
|
if (err) {
|
|
246
|
-
// wsl.exe 的报错是 UTF-16
|
|
247
|
-
//
|
|
246
|
+
// wsl.exe 的报错是 UTF-16 输出,而且多半写在 stdout(不是 stderr)(常见:「请启用'虚拟机
|
|
247
|
+
// 平台'Windows 功能并确保在 BIOS 中启用虚拟化」);两路都解码后拼进 message,抽屉才能显示
|
|
248
|
+
// 真正原因而不是「Command failed」。
|
|
248
249
|
let text = "";
|
|
249
|
-
try {
|
|
250
|
+
const dec = (b) => { try { return Buffer.isBuffer(b) ? b.toString("utf16le") : String(b || ""); } catch { return ""; } };
|
|
251
|
+
text = (dec(so) + " " + dec(se));
|
|
250
252
|
text = text.replace(/\u0000/g, "").replace(/\s+/g, " ").trim();
|
|
251
253
|
if (text) err.message = `${err.message.split("\n")[0]} — ${text.slice(0, 300)}`;
|
|
252
254
|
err.stderr = text;
|
|
@@ -28,7 +28,7 @@ test("featureEnabled reads Win32_OptionalFeature (works unelevated), dism only a
|
|
|
28
28
|
test("wsl --import failures carry wsl.exe's decoded message into the drawer", () => {
|
|
29
29
|
const src = read("src/sidecar/wsl-docker.js");
|
|
30
30
|
assert.match(src, /encoding: "buffer"/);
|
|
31
|
-
assert.match(src, /
|
|
31
|
+
assert.match(src, /toString\("utf16le"\)/);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
test("keepalive logon task degrades to LeastPrivilege / HKCU Run when not elevated", () => {
|
|
@@ -64,3 +64,18 @@ test("auto-update defaults to on", () => {
|
|
|
64
64
|
test("a missing wsl.exe (ENOENT, features enabled but not yet rebooted) counts as WSL missing", () => {
|
|
65
65
|
assert.match(read("src/sidecar/docker.js"), /err\.code === "ENOENT"[^\n]*return resolve\(true\)/);
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
test("features enabled but WSL still the pre-reboot stub → needsReboot; auto-reboot capped at 2", () => {
|
|
69
|
+
const d = read("src/sidecar/docker.js");
|
|
70
|
+
assert.match(d, /if \(!\(await wslFunctional\(\)\)\) \{/);
|
|
71
|
+
assert.match(d, /\[Argument\\\]/);
|
|
72
|
+
const ipc = read("src/backends/sidecar-ipc.js");
|
|
73
|
+
assert.match(ipc, /rebootCount\(\) >= 2/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("missing WSL2 kernel is installed through the same single elevation", () => {
|
|
77
|
+
const d = read("src/sidecar/docker.js");
|
|
78
|
+
assert.match(d, /function wslKernelPresent\(\)/);
|
|
79
|
+
assert.match(d, /if \(!wslKernelPresent\(\)\) \{[^]*elevatedWslSetup\(\{ emit, need: \{\} \}\)/);
|
|
80
|
+
assert.match(d, /reason: "wsl_kernel_missing"/);
|
|
81
|
+
});
|