cicy-desktop 2.1.314 → 2.1.315
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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// In-app cicy-code update over the container's OWN API — cicy-code's native
|
|
2
|
+
// upgrade path (POST /api/cicy-update). The updater runs INSIDE the container
|
|
3
|
+
// (setsid'd, survives the supervisor restart), so the desktop needs neither
|
|
4
|
+
// `docker exec` nor a script push into /usr/local/bin — the two steps that kept
|
|
5
|
+
// failing ("cannot execute: required file not found", EACCES, wrong container).
|
|
6
|
+
//
|
|
7
|
+
// The host still resolves the version (fast, host network) and PINS it via
|
|
8
|
+
// `target`, so the container never runs its own slow `npm view`.
|
|
9
|
+
const http = require("node:http");
|
|
10
|
+
|
|
11
|
+
function httpJson(method, port, urlPath, { token = "", body = null, timeoutMs = 15000 } = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const payload = body == null ? null : Buffer.from(JSON.stringify(body));
|
|
14
|
+
const req = http.request({
|
|
15
|
+
host: "127.0.0.1", port, path: urlPath, method, timeout: timeoutMs,
|
|
16
|
+
headers: {
|
|
17
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
18
|
+
...(payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : {}),
|
|
19
|
+
},
|
|
20
|
+
}, (res) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
res.on("data", (c) => chunks.push(c));
|
|
23
|
+
res.on("end", () => {
|
|
24
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
25
|
+
let json = null;
|
|
26
|
+
try { json = text ? JSON.parse(text) : null; } catch {}
|
|
27
|
+
resolve({ status: res.statusCode || 0, json, text });
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on("error", reject);
|
|
31
|
+
req.on("timeout", () => { req.destroy(new Error("timeout")); });
|
|
32
|
+
if (payload) req.write(payload);
|
|
33
|
+
req.end();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// GET /api/health → running version (or null while the server is restarting).
|
|
38
|
+
async function healthVersion(port) {
|
|
39
|
+
try {
|
|
40
|
+
const r = await httpJson("GET", port, "/api/health", { timeoutMs: 3000 });
|
|
41
|
+
return r.status === 200 && r.json && r.json.version ? String(r.json.version) : null;
|
|
42
|
+
} catch { return null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
|
|
47
|
+
// Returns { started, ok, reason, version }.
|
|
48
|
+
// started=false → caller should fall back to its legacy path (API unreachable,
|
|
49
|
+
// rejected token, updater missing in this image, …).
|
|
50
|
+
// started=true → the container is updating; ok tells whether the new version
|
|
51
|
+
// came back healthy within waitMs.
|
|
52
|
+
async function inAppUpdate({ port = 8008, token, target, registry = "", emit = () => {}, waitMs = 300000, pollMs = 3000, sleep = defaultSleep, now = Date.now } = {}) {
|
|
53
|
+
if (!token) return { started: false, reason: "no_token" };
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await httpJson("POST", port, "/api/cicy-update", { token, body: { target: target || "", registry }, timeoutMs: 20000 });
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { started: false, reason: `api_unreachable: ${e.message}` };
|
|
59
|
+
}
|
|
60
|
+
if (res.status === 401 || res.status === 403) return { started: false, reason: `unauthorized (${res.status})` };
|
|
61
|
+
if (res.status !== 200 || !res.json) return { started: false, reason: `http ${res.status}` };
|
|
62
|
+
if (res.json.started !== true) {
|
|
63
|
+
const err = String(res.json.error || "");
|
|
64
|
+
if (/already up to date/i.test(err)) return { started: true, ok: true, alreadyLatest: true, version: String(res.json.current || target || "") };
|
|
65
|
+
return { started: false, reason: err || "not started" };
|
|
66
|
+
}
|
|
67
|
+
const want = String(res.json.target || target || "");
|
|
68
|
+
emit({ phase: "image", status: "running", message: `cicy-code → v${want} (in-app)` });
|
|
69
|
+
const deadline = now() + waitMs;
|
|
70
|
+
let sawDown = false;
|
|
71
|
+
while (now() < deadline) {
|
|
72
|
+
const v = await healthVersion(port);
|
|
73
|
+
if (v === null) sawDown = true;
|
|
74
|
+
else if (v === want) return { started: true, ok: true, version: v };
|
|
75
|
+
// Same old version still answering: the updater is installing (npm) — keep
|
|
76
|
+
// waiting; once it repoints + restarts, health drops and comes back new.
|
|
77
|
+
await sleep(pollMs);
|
|
78
|
+
}
|
|
79
|
+
return { started: true, ok: false, reason: sawDown ? "restarted but not healthy in time" : "updater did not switch version in time", version: want };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { inAppUpdate, healthVersion, httpJson };
|
|
@@ -23,6 +23,7 @@ const log = require("electron-log"); // persisted main.log — bootstrap timing/
|
|
|
23
23
|
const { gatewayKeyPresentInEnv } = require("./gateway-key-health");
|
|
24
24
|
const { t } = require("../i18n"); // 打开/读 token 的可见日志走 i18n
|
|
25
25
|
const { shouldSkipCicyUpdate } = require("./cicy-runtime-health");
|
|
26
|
+
const { inAppUpdate } = require("./cicy-inapp-update");
|
|
26
27
|
const protect = require("./docker-protect"); // 容器保护:自动流程禁止 rm/shutdown
|
|
27
28
|
|
|
28
29
|
// Dedicated distro name — NEVER reuse/clobber a user's own "Ubuntu" distro.
|
|
@@ -1513,6 +1514,28 @@ async function update({ onProgress, container = "cicy-code-docker", port = 8008
|
|
|
1513
1514
|
}
|
|
1514
1515
|
// 宿主机没解析出版本 → 给个可见提示(诊断:让用户/我们知道是 host 网络问题,而非容器)。
|
|
1515
1516
|
if (!latest) emit({ phase: "image", status: "running", message: t("docker.updating.hostResolveFail") });
|
|
1517
|
+
// 3) 首选:cicy-code 自己的升级方式 —— 容器内 API `POST /api/cicy-update`,把宿主机解析好
|
|
1518
|
+
// 的版本 pin 进去。更新脚本在容器里自己跑(setsid,扛得住 supervisor 重启),desktop
|
|
1519
|
+
// 不再 docker exec、不再往 /usr/local/bin 推脚本(这两步就是"required file not found"
|
|
1520
|
+
// / EACCES / 打错容器 的来源)。API 打不通或被拒才回落到下面的老路径。
|
|
1521
|
+
const apiRegistry = net === "global" ? "https://registry.npmjs.org" : net === "cn" ? "https://registry.npmmirror.com" : "";
|
|
1522
|
+
try {
|
|
1523
|
+
const token = await readContainerToken(port, container, `cicy-team-${port}`, { onLog: (ev) => emit({ phase: "image", status: ev.status === "error" ? "running" : ev.status, message: ev.message }) });
|
|
1524
|
+
const r = await inAppUpdate({ port, token, target: latest || "", registry: apiRegistry, emit });
|
|
1525
|
+
log.info(`[wsl-docker] update via API: ${JSON.stringify(r)}`);
|
|
1526
|
+
if (r.started) {
|
|
1527
|
+
if (r.alreadyLatest) {
|
|
1528
|
+
emit({ phase: "done", status: "done", message: t("docker.updating.alreadyLatest", { v: r.version || current }) });
|
|
1529
|
+
return { ok: true, alreadyLatest: true, version: r.version || current };
|
|
1530
|
+
}
|
|
1531
|
+
const doneMsg = r.ok ? t("docker.updating.doneVersion", { v: r.version }) : t("docker.updating.notReady");
|
|
1532
|
+
emit({ phase: "done", status: r.ok ? "done" : "error", message: doneMsg });
|
|
1533
|
+
return { ok: r.ok, version: r.version || latest || null, inApp: true };
|
|
1534
|
+
}
|
|
1535
|
+
emit({ phase: "image", status: "running", message: `in-app update unavailable (${r.reason}) → docker exec` });
|
|
1536
|
+
} catch (e) {
|
|
1537
|
+
emit({ phase: "image", status: "running", message: `in-app update failed (${e.message}) → docker exec` });
|
|
1538
|
+
}
|
|
1516
1539
|
// 3) 真要装:cp desktop 自带的脚本进容器(随 desktop 发版下发,不依赖镜像),把**已解析
|
|
1517
1540
|
// 的具体版本**作参数传进去 → 脚本跳过自己的 npm view,容器里不再有版本查询的卡顿。
|
|
1518
1541
|
emit({ phase: "image", status: "running", message: latest ? t("docker.updating.toVersion", { v: latest }) : t("docker.updating.pulling") });
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const http = require("node:http");
|
|
4
|
+
const { inAppUpdate } = require("../src/sidecar/cicy-inapp-update");
|
|
5
|
+
|
|
6
|
+
function serve(handler) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const srv = http.createServer(handler);
|
|
9
|
+
srv.listen(0, "127.0.0.1", () => resolve({ srv, port: srv.address().port }));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("inAppUpdate pins the host-resolved target, then waits for the new version to come back", async () => {
|
|
14
|
+
const seen = [];
|
|
15
|
+
let version = "2.3.571";
|
|
16
|
+
const { srv, port } = await serve((req, res) => {
|
|
17
|
+
let body = "";
|
|
18
|
+
req.on("data", (c) => { body += c; });
|
|
19
|
+
req.on("end", () => {
|
|
20
|
+
if (req.method === "POST" && req.url === "/api/cicy-update") {
|
|
21
|
+
seen.push({ auth: req.headers.authorization, body: JSON.parse(body) });
|
|
22
|
+
res.setHeader("content-type", "application/json");
|
|
23
|
+
res.end(JSON.stringify({ started: true, current: version, target: "2.3.573" }));
|
|
24
|
+
setTimeout(() => { version = "2.3.573"; }, 30);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (req.url === "/api/health") { res.setHeader("content-type", "application/json"); res.end(JSON.stringify({ status: "ok", version })); return; }
|
|
28
|
+
res.statusCode = 404; res.end();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
try {
|
|
32
|
+
const r = await inAppUpdate({ port, token: "cicy_tok", target: "2.3.573", registry: "https://registry.npmmirror.com", pollMs: 10, waitMs: 5000 });
|
|
33
|
+
assert.equal(r.started, true);
|
|
34
|
+
assert.equal(r.ok, true);
|
|
35
|
+
assert.equal(r.version, "2.3.573");
|
|
36
|
+
assert.equal(seen[0].auth, "Bearer cicy_tok");
|
|
37
|
+
assert.deepEqual(seen[0].body, { target: "2.3.573", registry: "https://registry.npmmirror.com" });
|
|
38
|
+
} finally { srv.close(); }
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("inAppUpdate reports not-started so the caller can fall back", async () => {
|
|
42
|
+
const { srv, port } = await serve((req, res) => {
|
|
43
|
+
res.setHeader("content-type", "application/json");
|
|
44
|
+
res.end(JSON.stringify({ started: false, error: "updater not found: /usr/local/bin/cicy-code-update.sh" }));
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const r = await inAppUpdate({ port, token: "t", target: "2.3.573" });
|
|
48
|
+
assert.equal(r.started, false);
|
|
49
|
+
assert.match(r.reason, /updater not found/);
|
|
50
|
+
} finally { srv.close(); }
|
|
51
|
+
const unreachable = await inAppUpdate({ port: 1, token: "t", target: "2.3.573" });
|
|
52
|
+
assert.equal(unreachable.started, false);
|
|
53
|
+
assert.match(unreachable.reason, /api_unreachable/);
|
|
54
|
+
assert.equal((await inAppUpdate({ port, token: "", target: "x" })).reason, "no_token");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("inAppUpdate treats 'already up to date' as success", async () => {
|
|
58
|
+
const { srv, port } = await serve((req, res) => {
|
|
59
|
+
res.setHeader("content-type", "application/json");
|
|
60
|
+
res.end(JSON.stringify({ started: false, current: "2.3.573", latest: "2.3.573", error: "already up to date" }));
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const r = await inAppUpdate({ port, token: "t", target: "2.3.573" });
|
|
64
|
+
assert.equal(r.started, true); assert.equal(r.ok, true); assert.equal(r.alreadyLatest, true);
|
|
65
|
+
} finally { srv.close(); }
|
|
66
|
+
});
|