cicy-desktop 2.1.136 → 2.1.138
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
|
@@ -16,6 +16,10 @@ const { ipcMain } = require("electron");
|
|
|
16
16
|
const fs = require("fs");
|
|
17
17
|
const os = require("os");
|
|
18
18
|
const path = require("path");
|
|
19
|
+
const log = require("electron-log");
|
|
20
|
+
// Background-computed docker status lives here; the homepage READS this (never
|
|
21
|
+
// probes WSL live → never blocks the UI / strands it on 「重试检测」).
|
|
22
|
+
const DOCKER_STATUS_FILE = path.join(os.homedir(), "cicy-ai", "db", "docker-status.json");
|
|
19
23
|
const sidecar = require("../sidecar/cicy-code");
|
|
20
24
|
const docker = require("../sidecar/docker");
|
|
21
25
|
const wslDocker = require("../sidecar/wsl-docker"); // Docker-版 via WSL2+Ubuntu (方案 A, win32)
|
|
@@ -110,6 +114,46 @@ function register({ sidecarLogPath } = {}) {
|
|
|
110
114
|
if (registered) return;
|
|
111
115
|
registered = true;
|
|
112
116
|
|
|
117
|
+
// ---- Docker status: computed by a dedicated NON-BLOCKING background daemon ----
|
|
118
|
+
// The homepage's 「重试检测」 / 「未响应」 came from probing WSL live on the UI's
|
|
119
|
+
// status call: a cold/busy WSL2 VM takes 10-20s to answer, timed out, and got
|
|
120
|
+
// mis-read as unknown. Instead the daemon below detects status off the UI thread,
|
|
121
|
+
// caches it (memory + file), AND auto-starts whatever is installed-but-down. The
|
|
122
|
+
// docker:app-status handler just returns the cache — instant, never blocks.
|
|
123
|
+
let _dockerStatusCache = null;
|
|
124
|
+
let _dockerDaemonBusy = false;
|
|
125
|
+
async function refreshDockerStatus() {
|
|
126
|
+
try {
|
|
127
|
+
const s = await appDocker.status(APP_PORT); // { wsl, distro, engineUp, running, unknown }
|
|
128
|
+
_dockerStatusCache = { installed: !!s.distro, dockerRunning: !!s.engineUp, running: !!s.running, unknown: !!s.unknown, port: APP_PORT, platform: process.platform, ts: Date.now() };
|
|
129
|
+
} catch (e) {
|
|
130
|
+
_dockerStatusCache = { installed: false, dockerRunning: false, running: false, unknown: true, port: APP_PORT, platform: process.platform, error: e.message, ts: Date.now() };
|
|
131
|
+
}
|
|
132
|
+
try { fs.mkdirSync(path.dirname(DOCKER_STATUS_FILE), { recursive: true }); fs.writeFileSync(DOCKER_STATUS_FILE, JSON.stringify(_dockerStatusCache)); } catch {}
|
|
133
|
+
return _dockerStatusCache;
|
|
134
|
+
}
|
|
135
|
+
async function reconcileDocker() {
|
|
136
|
+
if (_dockerDaemonBusy) return _dockerStatusCache;
|
|
137
|
+
_dockerDaemonBusy = true;
|
|
138
|
+
try {
|
|
139
|
+
const s = await refreshDockerStatus();
|
|
140
|
+
// 没启动的给我启动: distro installed but :8009 not healthy (and WSL not unknown)
|
|
141
|
+
// → bring it up. bootstrap is idempotent: it skips done steps and just runs
|
|
142
|
+
// startEngine + the container. Skip when not installed (would silently pull
|
|
143
|
+
// the 444MB rootfs) or unknown (WSL not answering — let the next tick retry).
|
|
144
|
+
if (s.installed && !s.running && !s.unknown) {
|
|
145
|
+
log.info("[docker-daemon] installed but :8009 down → auto-starting (bootstrap idempotent)");
|
|
146
|
+
try { await appDocker.bootstrap(appOpts()); } catch (e) { log.warn(`[docker-daemon] auto-start failed: ${e.message}`); }
|
|
147
|
+
await refreshDockerStatus();
|
|
148
|
+
}
|
|
149
|
+
} finally { _dockerDaemonBusy = false; }
|
|
150
|
+
return _dockerStatusCache;
|
|
151
|
+
}
|
|
152
|
+
function startDockerStatusDaemon() {
|
|
153
|
+
setTimeout(() => { reconcileDocker().catch(() => {}); }, 2000); // shortly after startup
|
|
154
|
+
setInterval(() => { reconcileDocker().catch(() => {}); }, 60000); // keep fresh + self-heal
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
ipcMain.handle("sidecar:status", async () => {
|
|
114
158
|
const running = await sidecar.probeExisting(PORT);
|
|
115
159
|
return { running };
|
|
@@ -170,13 +214,14 @@ function register({ sidecarLogPath } = {}) {
|
|
|
170
214
|
// ---- Docker-版 cicy-code on :8009 — WSL2 + Ubuntu + Docker Engine (方案 A) ----
|
|
171
215
|
// Card states (主人: 状态分清楚): running(:8009 healthy)→打开 / dockerRunning
|
|
172
216
|
// (engine up)→启动 / installed(Ubuntu present)→启动 Docker / else→下载安装.
|
|
173
|
-
ipcMain.handle("docker:app-status",
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
217
|
+
ipcMain.handle("docker:app-status", () => {
|
|
218
|
+
// NON-BLOCKING: return what the background daemon already computed (memory →
|
|
219
|
+
// file). NEVER probe WSL live here — that's what froze the UI / stranded the
|
|
220
|
+
// card on 「重试检测」. Kick a refresh if we have nothing cached yet.
|
|
221
|
+
if (_dockerStatusCache) return _dockerStatusCache;
|
|
222
|
+
try { return JSON.parse(fs.readFileSync(DOCKER_STATUS_FILE, "utf8")); } catch {}
|
|
223
|
+
reconcileDocker().catch(() => {});
|
|
224
|
+
return { installed: false, dockerRunning: false, running: false, unknown: true, port: APP_PORT, platform: process.platform };
|
|
180
225
|
});
|
|
181
226
|
|
|
182
227
|
// Common run options for the :8009 instance: its own container/volume + the
|
|
@@ -422,6 +467,10 @@ function register({ sidecarLogPath } = {}) {
|
|
|
422
467
|
});
|
|
423
468
|
});
|
|
424
469
|
});
|
|
470
|
+
|
|
471
|
+
// Kick the non-blocking docker status daemon: detect on startup + every 60s,
|
|
472
|
+
// cache to file, and auto-start what's installed-but-down. The UI just reads it.
|
|
473
|
+
startDockerStatusDaemon();
|
|
425
474
|
}
|
|
426
475
|
|
|
427
476
|
module.exports = { register };
|
package/src/sidecar/docker.js
CHANGED
|
@@ -567,7 +567,7 @@ async function wslMissing() {
|
|
|
567
567
|
// (wsl didn't answer / timed out). null lets status() report `unknown` instead
|
|
568
568
|
// of falsely concluding "not installed" when WSL is merely stuck.
|
|
569
569
|
return await new Promise((resolve) => {
|
|
570
|
-
execFile("wsl", ["--status"], { timeout:
|
|
570
|
+
execFile("wsl", ["--status"], { timeout: 25000, windowsHide: true, encoding: "utf16le" }, (err, stdout, stderr) => {
|
|
571
571
|
const s = String((stdout || "") + (stderr || "") + (err && err.message ? err.message : ""));
|
|
572
572
|
if (/未安装|not installed|--install/i.test(s)) return resolve(true); // definitely missing
|
|
573
573
|
if (err && (err.killed || err.signal || err.code === "ETIMEDOUT")) return resolve(null); // timed out → unknown
|
|
@@ -98,7 +98,11 @@ async function distroInstalled(distro = DISTRO) {
|
|
|
98
98
|
if (process.platform !== "win32") return false;
|
|
99
99
|
// ASYNC execFile (sync froze the main process on a cold/stuck WSL → "未响应").
|
|
100
100
|
return await new Promise((resolve) => {
|
|
101
|
-
|
|
101
|
+
// 25s, NOT 8s: a COLD WSL2 VM takes 10-20s just to boot before it answers the
|
|
102
|
+
// first `wsl -l -q`, so an 8s timeout falsely returns null(unknown) → the
|
|
103
|
+
// homepage gets stuck on 「重试检测」 instead of offering 「安装」. A warm WSL
|
|
104
|
+
// answers in <1s, so the longer ceiling never bites in the normal case.
|
|
105
|
+
execFile("wsl", ["-l", "-q"], { timeout: 25000, windowsHide: true, encoding: "utf16le" }, (err, stdout) => {
|
|
102
106
|
// Our timeout killed it / it was signalled → WSL didn't answer → UNKNOWN.
|
|
103
107
|
if (err && (err.killed || err.signal || err.code === "ETIMEDOUT")) return resolve(null);
|
|
104
108
|
// Other errors (wsl missing / non-zero exit) → definitively not our distro.
|
|
@@ -115,12 +119,26 @@ async function distroInstalled(distro = DISTRO) {
|
|
|
115
119
|
// Raw `wsl --import` of the rootfs as an ISOLATED v2 distro.
|
|
116
120
|
function importTarball(dest, installDir) {
|
|
117
121
|
return new Promise((resolve, reject) => {
|
|
122
|
+
// 240s, NOT 600s: a 444MB import finishes in well under 4min on a healthy WSL.
|
|
123
|
+
// If it runs longer it has WEDGED (the whole WSL subsystem hangs — every later
|
|
124
|
+
// `wsl` call then blocks and the app goes 未响应). Bound it short so we FAIL FAST
|
|
125
|
+
// and the caller can `wsl --shutdown` + retry instead of hanging 10 minutes.
|
|
118
126
|
execFile("wsl", ["--import", DISTRO, installDir, dest, "--version", "2"],
|
|
119
|
-
{ timeout:
|
|
127
|
+
{ timeout: 240000, windowsHide: true },
|
|
120
128
|
(err, _so, se) => { if (err) { err.stderr = String(se || ""); return reject(err); } resolve(); });
|
|
121
129
|
});
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
// `wsl --shutdown`: hard-reset the ENTIRE WSL VM. This is the only reliable cure
|
|
133
|
+
// when WSL wedges (a hung `wsl --import` leaves every subsequent `wsl` call
|
|
134
|
+
// blocking → the app freezes). Unlike `wsl --list/-d` it doesn't query the wedged
|
|
135
|
+
// VM, so it returns even when WSL is stuck. Use it as recovery between retries.
|
|
136
|
+
function wslShutdown() {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
execFile("wsl", ["--shutdown"], { timeout: 30000, windowsHide: true }, () => resolve());
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
// `wsl --terminate <distro>`: stop the distro so the NEXT `wsl -d` cold-boots it
|
|
125
143
|
// clean. This is the fix for the「引擎没起来」-on-fresh-install failure: a distro
|
|
126
144
|
// straight out of `wsl --import` is frequently half-initialized / unresponsive,
|
|
@@ -153,10 +171,17 @@ async function installDistro({ emit } = {}) {
|
|
|
153
171
|
try {
|
|
154
172
|
await importTarball(dest, installDir);
|
|
155
173
|
} catch (e) {
|
|
156
|
-
//
|
|
157
|
-
// (
|
|
158
|
-
|
|
159
|
-
|
|
174
|
+
// Import failed or WEDGED (our 4-min timeout fired). Recover hard, then retry once:
|
|
175
|
+
// 1) `wsl --shutdown` — clears a wedged WSL VM (the real cause of the 8-min hang
|
|
176
|
+
// + app freeze). Without this the retry hits the same stuck VM and hangs again.
|
|
177
|
+
// 2) ensure the WSL2 kernel (import also fails when it's missing).
|
|
178
|
+
// 3) unregister any half-registered distro a wedged import left behind, so the
|
|
179
|
+
// retry imports clean.
|
|
180
|
+
log.warn(`[installDistro] import failed/wedged (${e.message}) → wsl --shutdown + retry`);
|
|
181
|
+
emit && emit({ phase: "container", status: "running", message: "导入卡住,重置 WSL(--shutdown)后重试…" });
|
|
182
|
+
try { await wslShutdown(); } catch {}
|
|
183
|
+
try { await ensureWslKernel({ emit }); } catch {}
|
|
184
|
+
try { await new Promise((r) => execFile("wsl", ["--unregister", DISTRO], { timeout: 30000, windowsHide: true }, () => r())); } catch {}
|
|
160
185
|
await importTarball(dest, installDir);
|
|
161
186
|
}
|
|
162
187
|
// 3) Force a clean cold boot. A freshly-imported distro is often wedged, so the
|