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 +1 -1
- package/src/sidecar/docker.js +13 -10
- package/src/sidecar/wsl-docker.js +82 -29
package/package.json
CHANGED
package/src/sidecar/docker.js
CHANGED
|
@@ -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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 }, () =>
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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
|
|
410
|
-
|
|
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
|
|