cicy-desktop 2.1.142 → 2.1.144

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.
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
7
7
  <link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
8
8
  <title>CiCy Desktop</title>
9
- <script type="module" crossorigin src="./assets/index-DVdvubOy.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-LZDX5apT.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CKhSx1lp.css">
11
11
  </head>
12
12
  <body>
@@ -722,5 +722,5 @@ module.exports = {
722
722
  bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir, downloadsDir, imageTarballPath,
723
723
  launchElevated, wslMissing, ensureWsl,
724
724
  // platform-agnostic download/retry primitives, reused by native.js
725
- ensureDownloaded, curlDownload, withRetry, waitUntil, run,
725
+ ensureDownloaded, curlDownload, withRetry, waitUntil, run, headSize,
726
726
  };
@@ -155,10 +155,24 @@ async function installDistro({ emit } = {}) {
155
155
  // 1) Download the PRE-BAKED rootfs (Ubuntu+Docker+image baked in, ~444MB) with
156
156
  // a real progress bar. curl is ~10× faster than node's downloader on OSS.
157
157
  const dest = rootfsPath();
158
- try { await docker.curlDownload(ROOTFS_URL, dest, { emit, phase: "image", label: "下载运行环境" }); }
159
- catch (e) {
160
- emit && emit({ phase: "image", status: "running", message: `下载器异常(${e.message}),改用备用下载…` });
161
- await docker.ensureDownloaded(ROOTFS_URL, dest, null, { emit, phase: "image", label: "下载运行环境" });
158
+ // REUSE a complete rootfs already on disk (pre-staged into ~/Downloads, or left by
159
+ // a prior run) BEFORE curlDownload touches it. The bug: curlDownload doesn't skip a
160
+ // complete file (and on a failed curl it truncated the staged 447MB), so a staged
161
+ // rootfs got re-downloaded. headSize uses node http (works even when curl can't
162
+ // resolve the host — the `curl exit 6` we saw), so this skip is reliable.
163
+ let haveComplete = false;
164
+ try {
165
+ const expected = await docker.headSize(ROOTFS_URL);
166
+ const have = fs.statSync(dest).size;
167
+ haveComplete = expected > 0 && have === expected;
168
+ if (haveComplete) emit && emit({ phase: "image", status: "skip", message: "下载运行环境:已有完整包,跳过下载", progress: 100, received: have, total: expected });
169
+ } catch {}
170
+ if (!haveComplete) {
171
+ try { await docker.curlDownload(ROOTFS_URL, dest, { emit, phase: "image", label: "下载运行环境" }); }
172
+ catch (e) {
173
+ emit && emit({ phase: "image", status: "running", message: `下载器异常(${e.message}),改用备用下载…` });
174
+ await docker.ensureDownloaded(ROOTFS_URL, dest, null, { emit, phase: "image", label: "下载运行环境" });
175
+ }
162
176
  }
163
177
  // 2) Import as an ISOLATED WSL2 distro: its OWN VHDX under a dedicated dir, so
164
178
  // it never touches the user's existing distros. `--version 2` sets just THIS
@@ -252,6 +266,25 @@ const BOOT_SCRIPT_B64 = Buffer.from(
252
266
  "pgrep dockerd >/dev/null 2>&1 || setsid sh -c 'exec dockerd >/var/log/dockerd.log 2>&1 </dev/null' &\n"
253
267
  ).toString("base64");
254
268
 
269
+ // Hold the distro OPEN. The flip side of detaching dockerd: once the launching
270
+ // wsl.exe exits, WSL tears the distro down if no session holds it — killing the
271
+ // just-launched dockerd (the "启动引擎 转圈但 dockerd=none" we saw). A detached,
272
+ // long-lived `wsl … sleep infinity` keeps the distro alive so dockerd (and the
273
+ // container) survive. unref'd so it never blocks the app; survives app exit too.
274
+ let _keepalive = null;
275
+ function ensureKeepalive() {
276
+ if (process.platform !== "win32") return;
277
+ if (_keepalive && _keepalive.exitCode == null && !_keepalive.killed) return;
278
+ try {
279
+ _keepalive = spawn("wsl", ["-d", DISTRO, "-u", "root", "--", "sh", "-c", "exec sleep infinity"],
280
+ { detached: true, stdio: "ignore", windowsHide: true });
281
+ _keepalive.on("error", (e) => { log.warn(`[keepalive] ${e.message}`); _keepalive = null; });
282
+ _keepalive.on("exit", () => { _keepalive = null; });
283
+ _keepalive.unref();
284
+ log.info("[keepalive] holding distro open (sleep infinity)");
285
+ } catch (e) { log.warn(`[keepalive] spawn failed: ${e.message}`); }
286
+ }
287
+
255
288
  async function launchDockerd() {
256
289
  await wslRun(
257
290
  `echo ${BOOT_SCRIPT_B64} | base64 -d > /usr/local/sbin/start-dockerd.sh 2>/dev/null; chmod +x /usr/local/sbin/start-dockerd.sh 2>/dev/null; ` +
@@ -260,8 +293,10 @@ async function launchDockerd() {
260
293
  // Stale /var/run/docker.{pid,sock} from the pre-baked rootfs make a fresh dockerd
261
294
  // refuse to start; clear them when dockerd isn't already running.
262
295
  "if ! pgrep dockerd >/dev/null 2>&1; then rm -f /var/run/docker.pid /run/docker.pid /var/run/docker.sock /run/docker.sock; fi; " +
263
- "pgrep dockerd >/dev/null 2>&1 || setsid bash -c 'exec dockerd >/var/log/cicy-dockerd.log 2>&1 </dev/null' & " +
264
- "true",
296
+ // setsid --fork: fork dockerd into a NEW session and have setsid EXIT immediately,
297
+ // so this wsl call returns at once and dockerd is fully orphaned (no `& true`
298
+ // backgrounding race). It only survives because ensureKeepalive() holds the distro.
299
+ "pgrep dockerd >/dev/null 2>&1 || setsid --fork bash -c 'exec dockerd >/var/log/cicy-dockerd.log 2>&1 </dev/null'",
265
300
  { timeout: 20000 });
266
301
  }
267
302
 
@@ -273,7 +308,8 @@ async function startEngine() {
273
308
  for (let attempt = 1; attempt <= 3; attempt++) {
274
309
  const at0 = Date.now();
275
310
  let stuck = false;
276
- log.info(`[startEngine] attempt ${attempt}/3 — launch dockerd (detached) + poll socket`);
311
+ log.info(`[startEngine] attempt ${attempt}/3 — keepalive + launch dockerd (detached) + poll socket`);
312
+ ensureKeepalive(); // hold the distro open BEFORE launching, or the teardown kills dockerd
277
313
  try { await launchDockerd(); }
278
314
  catch (e) { stuck = true; log.warn(`[startEngine] attempt ${attempt} launch errored — WSL stuck? ${e.message}`); }
279
315
  if (!stuck) {
@@ -1709,7 +1709,6 @@ function DockerInstallDrawerHost() {
1709
1709
  function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
1710
1710
  const [status, setStatus] = useState(null);
1711
1711
  const [busy, setBusy] = useState(""); // "" | bootstrap | restart | stop | upgrade | probe
1712
- const [probeNote, setProbeNote] = useState(""); // 「重试检测」后给用户的明确反馈(WSL 仍卡 → 提示重启)
1713
1712
  const [menuOpen, setMenuOpen] = useState(false);
1714
1713
  const [confirmRecreate, setConfirmRecreate] = useState(false); // 重建容器 in-app 确认弹窗(不用 native confirm)
1715
1714
  // Inline rename (mirrors LocalTeamCard): double-click the title to edit.
@@ -1733,9 +1732,7 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
1733
1732
 
1734
1733
  const checkStatus = useCallback(async () => {
1735
1734
  try {
1736
- const s = await window.cicy?.docker?.appStatus?.();
1737
- setStatus(s);
1738
- if (s && !s.unknown) setProbeNote(""); // WSL came back → clear the stuck note
1735
+ setStatus(await window.cicy?.docker?.appStatus?.());
1739
1736
  } catch (e) { console.warn("[DockerCard]", e); }
1740
1737
  }, []);
1741
1738
 
@@ -1914,9 +1911,8 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
1914
1911
  try {
1915
1912
  const s = await (window.cicy?.docker?.appRedetect?.() ?? window.cicy?.docker?.appStatus?.());
1916
1913
  if (s) setStatus(s);
1917
- if (s?.unknown) setProbeNote(tr("docker.wslStillStuck", "WSL 仍无响应 —— 多半要重启 Windows 后再试"));
1918
- else setProbeNote("");
1919
- } catch (e) { setProbeNote(tr("docker.probeFailed", "检测失败,请重试")); }
1914
+ if (s?.unknown) toast.show({ id: "docker-probe", status: "error", message: tr("docker.wslStillStuck", "WSL 仍无响应 —— 多半要重启 Windows 后再试"), ttl: 6000 });
1915
+ } catch (e) { toast.show({ id: "docker-probe", status: "error", message: tr("docker.probeFailed", "检测失败,请重试"), ttl: 5000 }); }
1920
1916
  finally { setBusy(""); }
1921
1917
  return;
1922
1918
  }
@@ -2019,11 +2015,6 @@ function DockerCard({ dockerTeam, cloudTitle, onOpen, onRename, onRefresh }) {
2019
2015
  )}
2020
2016
  </div>
2021
2017
  <div className="bcard__meta"><span className="bcard__chip">Docker</span></div>
2022
- {(probeNote || (!running && stateText)) && (
2023
- <div data-id="DockerCard-statusline" style={{ marginTop: 4, fontSize: 11, lineHeight: 1.4, color: probeNote ? "#f0a020" : "rgba(255,255,255,0.55)" }}>
2024
- {probeNote || stateText}
2025
- </div>
2026
- )}
2027
2018
  </div>
2028
2019
  <button
2029
2020
  type="button"