cicy-desktop 2.1.316 → 2.1.317

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.316",
3
+ "version": "2.1.317",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -170,6 +170,8 @@ function register({ sidecarLogPath } = {}) {
170
170
  // WSL 功能启用后必须重启 Windows 才能继续。不等人:自动安排 90 秒后重启(桌面上会有
171
171
  // 系统倒计时提示),UI 可「取消」或「立即重启」。每次开机只安排一次。
172
172
  let _rebootScheduled = false;
173
+ let _tcpRepairDone = false; // Windows 端口修复每次进程只做一次(需要 UAC)
174
+ let _relayMissesAfterRepair = 0; // 端口修复后仍连不上的轮数 → 3 轮自动重启
173
175
  let _relayMisses = 0, _relayResetDone = false; // WSL localhost 转发自愈(见 daemon 循环)
174
176
  let _unknownStreak = 0, _wslRepairDone = false; // WSL 卡死自愈(见 daemon 循环)
175
177
  let _softRetryAt = 0; // 非硬失败(如 docker_install_failed)也退避 2 分钟,别每 35 秒重跑一遍
@@ -181,13 +183,13 @@ function register({ sidecarLogPath } = {}) {
181
183
  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 {} }
182
184
  return c;
183
185
  }
184
- function scheduleReboot(delaySec = 90) {
186
+ function scheduleReboot(delaySec = 90, why = "") {
185
187
  if (_rebootScheduled || process.platform !== "win32") return false;
186
188
  if (delaySec > 10 && rebootCount() >= 2) { log.warn("[docker] auto-reboot skipped: already rebooted 2× for WSL without success"); return false; }
187
189
  _rebootScheduled = true;
188
190
  rebootCount(+1);
189
191
  try {
190
- require("child_process").execFile("shutdown", ["/r", "/t", String(delaySec), "/c", "CiCy Desktop: WSL2 功能已启用,需要重启 Windows 才能继续安装 Docker;登录后会自动继续。可在 CiCy Desktop 里取消。"], { windowsHide: true }, (err) => {
192
+ require("child_process").execFile("shutdown", ["/r", "/t", String(delaySec), "/c", why || "CiCy Desktop: WSL2 功能已启用,需要重启 Windows 才能继续安装 Docker;登录后会自动继续。可在 CiCy Desktop 里取消。"], { windowsHide: true }, (err) => {
191
193
  if (err) { _rebootScheduled = false; log.warn(`[docker] schedule reboot failed: ${err.message}`); }
192
194
  else log.info(`[docker] Windows reboot scheduled in ${delaySec}s (WSL features enabled)`);
193
195
  });
@@ -277,11 +279,34 @@ function register({ sidecarLogPath } = {}) {
277
279
  return;
278
280
  }
279
281
  if (_relayMisses < 2) { log.warn(`[docker-daemon] :${APP_PORT} unreachable from Windows but healthy inside (${_relayMisses}/2)`); return; }
280
- // 已重置过一次仍不通:bootstrap 对此无能为力(而且以前它会跑「修复=更新」把 cicy-code
281
- // 重启掉)。只把原因和修复提示写进卡片,每 10 分钟记一次日志,不再反复 bootstrap。
282
+ // 已重置过一次 WSL 仍不通:bootstrap 对此无能为力(以前它会跑「修复=更新」把 cicy-code
283
+ // 重启掉)。自愈继续往下走:
284
+ // 1) 一次 UAC 修 Windows 端口(net stop winnat → 重设动态端口范围 → net start winnat),30s 复探;
285
+ // 2) 修完仍连续 3 轮不通 → 自动重启 Windows(沿用上限 2 次)。
282
286
  if (_relayResetDone) {
283
287
  const hint = (appDocker.RELAY_UNREACHABLE_HINT && appDocker.RELAY_UNREACHABLE_HINT(APP_PORT)) || `container healthy inside but 127.0.0.1:${APP_PORT} unreachable from Windows`;
284
- if (!_lastBootstrapError || _lastBootstrapError.reason !== "relay_unreachable") { _lastBootstrapError = { reason: "relay_unreachable", message: hint, ts: Date.now() }; log.warn(`[docker-daemon] ${hint}`); }
288
+ if (!_tcpRepairDone && appDocker.elevatedTcpRepair) {
289
+ _tcpRepairDone = true;
290
+ _lastBootstrapError = { reason: "relay_unreachable", message: "容器正常但 Windows 连不上 :8008 → 正在修复 Windows TCP 端口(请在 UAC 弹窗点「是」)…", ts: Date.now() };
291
+ await refreshDockerStatus();
292
+ auditDestructiveIpc(log, "docker:self-heal-windows-tcp", null, { container: APP_CONTAINER });
293
+ let r = { ok: false, detail: "" };
294
+ try { r = await appDocker.elevatedTcpRepair({ emit: () => {} }); } catch (e) { r = { ok: false, detail: e.message }; }
295
+ log.warn(`[docker-daemon] windows tcp repair ok=${r.ok}: ${r.detail}`);
296
+ await new Promise((res) => setTimeout(res, 30000));
297
+ _relayMissesAfterRepair = 0;
298
+ await refreshDockerStatus();
299
+ return;
300
+ }
301
+ _relayMissesAfterRepair += 1;
302
+ if (_relayMissesAfterRepair >= 3 && !_rebootScheduled && rebootCount() < 2) {
303
+ _lastBootstrapError = { reason: "relay_unreachable", message: "容器正常但 Windows 连不上 :8008,端口修复无效 → 90 秒后自动重启 Windows 修复", ts: Date.now() };
304
+ log.warn("[docker-daemon] relay still unreachable after tcp repair → auto reboot");
305
+ scheduleReboot(90, "Windows 无法连接本机 :8008(端口异常),自动重启修复");
306
+ await refreshDockerStatus();
307
+ return;
308
+ }
309
+ if (!_lastBootstrapError || _lastBootstrapError.reason !== "relay_unreachable" || !/端口修复无效|正在修复/.test(_lastBootstrapError.message)) { _lastBootstrapError = { reason: "relay_unreachable", message: hint, ts: Date.now() }; log.warn(`[docker-daemon] ${hint}`); }
285
310
  await refreshDockerStatus();
286
311
  return;
287
312
  }
@@ -1013,7 +1013,50 @@ async function bootstrap({ onProgress, port = 8008, container = CONTAINER, volum
1013
1013
  return { ok: healthy, container };
1014
1014
  }
1015
1015
 
1016
+ // Windows 侧「连 127.0.0.1 都 connect EADDRINUSE」= 动态 TCP 端口被占满 / 被 Hyper-V(winnat)
1017
+ // 的保留段吞掉。软件可修:重启 winnat 释放保留段 + 把动态端口范围重设为默认(49152-65535)。
1018
+ // 一次 UAC,结果写文件回读。返回 { ok, detail }。
1019
+ function elevatedTcpRepair({ emit } = {}) {
1020
+ return new Promise(async (resolve) => {
1021
+ if (process.platform !== "win32") return resolve({ ok: false, detail: "win-only" });
1022
+ const dir = path.join(process.env.LOCALAPPDATA || os.tmpdir(), "cicy-desktop");
1023
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
1024
+ const ps1 = path.join(dir, "tcp-repair.ps1");
1025
+ const resultFile = path.join(dir, "tcp-repair.result.txt");
1026
+ try { fs.unlinkSync(resultFile); } catch {}
1027
+ const script = [
1028
+ '$ErrorActionPreference = "Continue"',
1029
+ `$R = "${resultFile.replace(/"/g, '""')}"`,
1030
+ 'function Say($m) { Add-Content -Path $R -Value ("[" + (Get-Date -Format "HH:mm:ss") + "] " + $m) }',
1031
+ '$S32 = Join-Path $env:SystemRoot "System32"; $NETSH = Join-Path $S32 "netsh.exe"; $NET = Join-Path $S32 "net.exe"',
1032
+ 'Say "start admin=$(([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(\'Administrators\'))"',
1033
+ 'try { $d = (& $NETSH int ipv4 show dynamicport tcp | Out-String) -replace "\s+"," "; Say ("before: " + $d.Trim()) } catch {}',
1034
+ 'try { $x = (& $NETSH int ipv4 show excludedportrange protocol=tcp | Out-String); $n = ([regex]::Matches($x, "^\s*\d+\s+\d+", "Multiline")).Count; Say ("excluded ranges: " + $n) } catch {}',
1035
+ 'try { $tw = (& (Join-Path $S32 "netstat.exe") -ano | Select-String "TIME_WAIT").Count; Say ("TIME_WAIT: " + $tw) } catch {}',
1036
+ '& $NET stop winnat | Out-Null; Say ("net stop winnat exit=" + $LASTEXITCODE)',
1037
+ '& $NETSH int ipv4 set dynamicport tcp start=49152 num=16384 | Out-Null; Say ("set dynamicport tcp exit=" + $LASTEXITCODE)',
1038
+ '& $NETSH int ipv4 set dynamicport udp start=49152 num=16384 | Out-Null; Say ("set dynamicport udp exit=" + $LASTEXITCODE)',
1039
+ '& $NET start winnat | Out-Null; Say ("net start winnat exit=" + $LASTEXITCODE)',
1040
+ 'try { $d = (& $NETSH int ipv4 show dynamicport tcp | Out-String) -replace "\s+"," "; Say ("after: " + $d.Trim()) } catch {}',
1041
+ 'Say "DONE"',
1042
+ ].join("\r\n");
1043
+ try { fs.writeFileSync(ps1, "\uFEFF" + script, "utf8"); } catch (e) { return resolve({ ok: false, detail: `写提权脚本失败: ${e.message}` }); }
1044
+ const launched = await launchElevated("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", ps1], { emit });
1045
+ if (!launched) return resolve({ ok: false, detail: "无法发起提权(UAC 被拒绝或不可用)" });
1046
+ const t0 = Date.now(); let lines = [];
1047
+ while (Date.now() - t0 < 3 * 60 * 1000) {
1048
+ await new Promise((r) => setTimeout(r, 3000));
1049
+ try { lines = fs.readFileSync(resultFile, "utf8").split(/\r?\n/).filter(Boolean); } catch {}
1050
+ if (lines.some((l) => /DONE/.test(l))) break;
1051
+ if (Date.now() - t0 > 90 * 1000 && lines.length === 0) break;
1052
+ }
1053
+ const detail = lines.map((l) => l.replace(/^\[[^\]]*\] /, "")).join(" | ");
1054
+ resolve({ ok: /set dynamicport tcp exit=0/.test(detail) && /net start winnat exit=0/.test(detail), detail: detail || "提权后 90 秒内没有任何进展(UAC 可能没有被点击「是」)" });
1055
+ });
1056
+ }
1057
+
1016
1058
  module.exports = {
1059
+ elevatedTcpRepair,
1017
1060
  start, stop, stopContainer, restart, checkStatus, loadImage, loadImageFromTarball,
1018
1061
  downloadImageTarball, imagePresent, dockerOk, installDocker,
1019
1062
  bootstrap, probeHealth, readContainerToken, dockerDesktopExe, desktopDir, downloadsDir, imageTarballPath,
@@ -33,3 +33,15 @@ test("daemon loop stops re-bootstrapping after the one-shot relay reset", () =>
33
33
  const src = readSrc("src/backends/sidecar-ipc.js");
34
34
  assert.match(src, /if \(_relayResetDone\) \{[\s\S]*?reason: "relay_unreachable"[\s\S]*?return;/);
35
35
  });
36
+
37
+ test("relay still broken after wsl reset → one-shot elevated Windows TCP repair, then auto reboot", () => {
38
+ const d = readSrc("src/sidecar/docker.js");
39
+ assert.match(d, /function elevatedTcpRepair/);
40
+ assert.match(d, /net stop winnat/);
41
+ assert.match(d, /set dynamicport tcp start=49152 num=16384/);
42
+ const s = readSrc("src/backends/sidecar-ipc.js");
43
+ const i = s.indexOf("if (_relayResetDone) {");
44
+ const block = s.slice(i, i + 3000);
45
+ assert.match(block, /appDocker\.elevatedTcpRepair\(/);
46
+ assert.match(block, /_relayMissesAfterRepair >= 3[\s\S]*?scheduleReboot\(90/);
47
+ });