cicy-desktop 2.1.315 → 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
|
@@ -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,6 +279,37 @@ 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; }
|
|
282
|
+
// 已重置过一次 WSL 仍不通:bootstrap 对此无能为力(以前它会跑「修复=更新」把 cicy-code
|
|
283
|
+
// 重启掉)。自愈继续往下走:
|
|
284
|
+
// 1) 一次 UAC 修 Windows 端口(net stop winnat → 重设动态端口范围 → net start winnat),30s 复探;
|
|
285
|
+
// 2) 修完仍连续 3 轮不通 → 自动重启 Windows(沿用上限 2 次)。
|
|
286
|
+
if (_relayResetDone) {
|
|
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`;
|
|
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}`); }
|
|
310
|
+
await refreshDockerStatus();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
280
313
|
} else { _relayMisses = 0; }
|
|
281
314
|
} else if (s.running) { _relayMisses = 0; }
|
|
282
315
|
if (!s.running && !s.unknown && _autoBootstrapPaused && Date.now() < _autoBootstrapRetryAt) {
|
package/src/sidecar/docker.js
CHANGED
|
@@ -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,
|
|
@@ -656,6 +656,13 @@ async function ensureFreshImage({ emit } = {}) {
|
|
|
656
656
|
// 容器内部视角的健康:docker exec 进容器用 node 探 127.0.0.1:8008。用来区分「容器真挂了」和
|
|
657
657
|
// 「容器好好的、只是 WSL 的 localhost 转发坏了」(后者 Windows 侧 :8008 打不通,实测
|
|
658
658
|
// `wsl --shutdown` 后 75 秒恢复)。
|
|
659
|
+
// 容器内健康、Windows 却连不上 :port 时给用户/日志的统一说明(bootstrap 与守护循环共用)。
|
|
660
|
+
function RELAY_UNREACHABLE_HINT(port = 8008) {
|
|
661
|
+
return `容器内 cicy-code 正常,但 Windows 连不上 127.0.0.1:${port}(WSL localhost 转发或 Windows TCP 端口异常;` +
|
|
662
|
+
`若桌面端日志里出现 connect EADDRINUSE,多半是 Windows 动态端口被占满/被 Hyper-V 保留段吞掉)。` +
|
|
663
|
+
`已自动重置过一次 WSL 仍不通时,请在管理员 PowerShell 执行:` +
|
|
664
|
+
` netsh int ipv4 show excludedportrange protocol=tcp; netsh int ipv4 set dynamicport tcp start=49152 num=16384; 然后重启 Windows。`;
|
|
665
|
+
}
|
|
659
666
|
async function insideHealthy(container = "cicy-code-docker-8008", port = 8008) {
|
|
660
667
|
try {
|
|
661
668
|
await wslRun(`docker exec ${container} node -e "fetch('http://127.0.0.1:${port}/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`, { timeout: 20000 });
|
|
@@ -1374,7 +1381,17 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
|
|
|
1374
1381
|
if (!healthy && !protect.guard(log, "auto-update after health miss")) {
|
|
1375
1382
|
healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 }); // 保护开启:只等,不自动改容器
|
|
1376
1383
|
}
|
|
1384
|
+
// Windows 侧探不到 ≠ 容器坏了。先从容器内部探一次:内部健康 = WSL localhost 转发 / Windows 端口
|
|
1385
|
+
// 出了问题(实测 connect EADDRINUSE 连外网也报),这时绝不能再跑 update()「修复」——它会
|
|
1386
|
+
// SIGTERM 重启 cicy-code,把正在跑的 agent 全部打断,而且对转发问题毫无帮助。
|
|
1387
|
+
let relayBroken = false;
|
|
1377
1388
|
if (!healthy) {
|
|
1389
|
+
try { relayBroken = await insideHealthy(container, port); } catch {}
|
|
1390
|
+
}
|
|
1391
|
+
if (!healthy && relayBroken) {
|
|
1392
|
+
log.warn(`[bootstrap] container healthy inside but 127.0.0.1:${port} unreachable from Windows → not touching the container`);
|
|
1393
|
+
emit({ phase: "container", status: "running", message: RELAY_UNREACHABLE_HINT(port) });
|
|
1394
|
+
} else if (!healthy) {
|
|
1378
1395
|
emit({ phase: "container", status: "running", message: "服务尚未响应,正在检查并修复运行时完整性…" });
|
|
1379
1396
|
try {
|
|
1380
1397
|
const repaired = await update({ onProgress: emit, container, port });
|
|
@@ -1383,11 +1400,13 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
|
|
|
1383
1400
|
log.warn(`[bootstrap] runtime auto-repair failed: ${e.message}`);
|
|
1384
1401
|
}
|
|
1385
1402
|
}
|
|
1386
|
-
if (!healthy) healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 105000, everyMs: 3000 });
|
|
1403
|
+
if (!healthy) healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: relayBroken ? 30000 : 105000, everyMs: 3000 });
|
|
1387
1404
|
if (healthy) { done(); await ensureAutostart(); await ensureDesktopShortcut(volume, port); } // survive reboot + desktop shortcut
|
|
1388
|
-
else fail("health_timeout");
|
|
1405
|
+
else fail(relayBroken ? "relay_unreachable" : "health_timeout", relayBroken ? RELAY_UNREACHABLE_HINT(port) : "");
|
|
1389
1406
|
let healthMessage = "Docker cicy-code 已就绪 🎉";
|
|
1390
|
-
if (!healthy) {
|
|
1407
|
+
if (!healthy && relayBroken) {
|
|
1408
|
+
healthMessage = RELAY_UNREACHABLE_HINT(port);
|
|
1409
|
+
} else if (!healthy) {
|
|
1391
1410
|
let tail = "";
|
|
1392
1411
|
try {
|
|
1393
1412
|
const r = await wslRun(`docker logs --tail 60 ${container}`, { timeout: 15000 });
|
|
@@ -1473,7 +1492,10 @@ async function cicyRuntimePlatformReady(container) {
|
|
|
1473
1492
|
const check = `target=$(readlink -f "$HOME/.local/bin/cicy-code" 2>/dev/null || true); ` +
|
|
1474
1493
|
`dest=$(dirname "$(dirname "$target")" 2>/dev/null); ` +
|
|
1475
1494
|
`case "$(uname -m)" in x86_64) p=cicy-code-linux-x64;; aarch64|arm64) p=cicy-code-linux-arm64;; *) exit 1;; esac; ` +
|
|
1476
|
-
|
|
1495
|
+
// readlink -f 会一路解析到 <prefix>/lib/node_modules/cicy-code/bin/cicy-code.js,此时 $dest 就是
|
|
1496
|
+
// 包目录本身,平台包在 $dest/node_modules/<p>(实际布局)。之前只查 $dest/lib/... 两种 → 永远 false
|
|
1497
|
+
// → 每轮 bootstrap 都把「更新 cicy-code」当修复跑一遍 → 更新脚本 SIGTERM 重启 cicy-code,打断所有 agent。
|
|
1498
|
+
`[ -x "$target" ] && { [ -f "$dest/node_modules/$p/package.json" ] || [ -f "$dest/lib/node_modules/$p/package.json" ] || [ -f "$dest/lib/node_modules/cicy-code/node_modules/$p/package.json" ]; }`;
|
|
1477
1499
|
try { await wslRun(`docker exec ${container} bash -lc '${check}'`, { timeout: 15000 }); return true; }
|
|
1478
1500
|
catch { return false; }
|
|
1479
1501
|
}
|
|
@@ -1685,7 +1707,7 @@ async function readMihomoSelections(container = "cicy-code-docker-8008") {
|
|
|
1685
1707
|
}
|
|
1686
1708
|
|
|
1687
1709
|
module.exports = {
|
|
1688
|
-
insideHealthy, wslShutdown,
|
|
1710
|
+
insideHealthy, wslShutdown, RELAY_UNREACHABLE_HINT,
|
|
1689
1711
|
bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
|
|
1690
1712
|
distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun, hasGatewayKey,
|
|
1691
1713
|
readMihomoConfig, readMihomoSelections, parseMihomoSelections,
|
|
@@ -10,3 +10,38 @@ test("does not skip an update when the recorded version matches but the platform
|
|
|
10
10
|
test("skips an update only when the recorded version matches and the runtime is complete", () => {
|
|
11
11
|
assert.equal(shouldSkipCicyUpdate({ latest: "2.3.563", current: "2.3.563", platformReady: true }), true);
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const readSrc = (p) => fs.readFileSync(path.join(__dirname, "..", p), "utf8");
|
|
17
|
+
|
|
18
|
+
test("platform-ready check covers the real npm layout (<pkg>/node_modules/cicy-code-linux-x64)", () => {
|
|
19
|
+
const src = readSrc("src/sidecar/wsl-docker.js");
|
|
20
|
+
assert.match(src, /\[ -f "\$dest\/node_modules\/\$p\/package\.json" \]/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("bootstrap never runs the updater when the container is healthy inside (relay problem)", () => {
|
|
24
|
+
const src = readSrc("src/sidecar/wsl-docker.js");
|
|
25
|
+
const i = src.indexOf('begin("wait-health")');
|
|
26
|
+
const block = src.slice(i, i + 3500);
|
|
27
|
+
assert.match(block, /relayBroken = await insideHealthy\(container, port\)/);
|
|
28
|
+
assert.match(block, /else if \(!healthy\) \{[\s\S]*?await update\(/);
|
|
29
|
+
assert.match(block, /fail\(relayBroken \? "relay_unreachable" : "health_timeout"/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("daemon loop stops re-bootstrapping after the one-shot relay reset", () => {
|
|
33
|
+
const src = readSrc("src/backends/sidecar-ipc.js");
|
|
34
|
+
assert.match(src, /if \(_relayResetDone\) \{[\s\S]*?reason: "relay_unreachable"[\s\S]*?return;/);
|
|
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
|
+
});
|