cicy-desktop 2.1.338 → 2.1.339
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
package/src/app-updater.js
CHANGED
|
@@ -229,6 +229,41 @@ async function download(url, dest, onProgress) {
|
|
|
229
229
|
throw lastErr || new Error("download failed");
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// ── 自动安装的循环闸 ─────────────────────────────────────────────────────────
|
|
233
|
+
// Auto-install kills the app by design (an installer cannot replace a running
|
|
234
|
+
// exe). So if an install does NOT land, the app comes back on the old version,
|
|
235
|
+
// the next check() tries the same thing, and the machine is stuck in a restart
|
|
236
|
+
// loop that closes the user's window every time. That is not hypothetical — it
|
|
237
|
+
// took the whole fleet down. The attempt is therefore remembered ACROSS
|
|
238
|
+
// restarts, and a version that has already failed twice is not auto-installed
|
|
239
|
+
// again until the cooldown passes; the update is still offered, just not forced.
|
|
240
|
+
const AUTO_TRY_MAX = 2;
|
|
241
|
+
const AUTO_TRY_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
|
242
|
+
|
|
243
|
+
function readAutoTry() {
|
|
244
|
+
try { return readGlobalConfig(GLOBAL_JSON)?.desktopAutoInstall || {}; } catch { return {}; }
|
|
245
|
+
}
|
|
246
|
+
function noteAutoTry(version) {
|
|
247
|
+
const cur = readAutoTry();
|
|
248
|
+
const count = cur.version === version ? (cur.count || 0) + 1 : 1;
|
|
249
|
+
try {
|
|
250
|
+
updateGlobalConfig(GLOBAL_JSON, (c) => ({
|
|
251
|
+
...(c || {}),
|
|
252
|
+
desktopAutoInstall: { version, count, at: Date.now() },
|
|
253
|
+
}));
|
|
254
|
+
} catch {}
|
|
255
|
+
return count;
|
|
256
|
+
}
|
|
257
|
+
function clearAutoTry() {
|
|
258
|
+
try { updateGlobalConfig(GLOBAL_JSON, (c) => { const n = { ...(c || {}) }; delete n.desktopAutoInstall; return n; }); } catch {}
|
|
259
|
+
}
|
|
260
|
+
function autoInstallAllowed(version) {
|
|
261
|
+
const t = readAutoTry();
|
|
262
|
+
if (t.version !== version) return true;
|
|
263
|
+
if ((t.count || 0) < AUTO_TRY_MAX) return true;
|
|
264
|
+
return Date.now() - (t.at || 0) > AUTO_TRY_COOLDOWN_MS;
|
|
265
|
+
}
|
|
266
|
+
|
|
232
267
|
// ── 状态机 + 广播 ─────────────────────────────────────────────────────────────
|
|
233
268
|
let _win = null;
|
|
234
269
|
let _state = { status: "idle", version: null, current: null, progress: null, filePath: null, error: null, autoUpdate: false, auto: false };
|
|
@@ -260,6 +295,9 @@ function init(mainWin) {
|
|
|
260
295
|
_win = mainWin;
|
|
261
296
|
_state.current = app.getVersion();
|
|
262
297
|
_state.autoUpdate = getAutoUpdate();
|
|
298
|
+
// Running the version we last tried to install = that attempt worked; forget
|
|
299
|
+
// it so a future update to the same number is never wrongly blocked.
|
|
300
|
+
try { if (readAutoTry().version === _state.current) clearAutoTry(); } catch {}
|
|
263
301
|
setTimeout(() => check().catch(() => {}), 15_000); // 启动后探一次
|
|
264
302
|
setInterval(() => check().catch(() => {}), 30 * 60 * 1000); // 每 30 分钟
|
|
265
303
|
}
|
|
@@ -279,10 +317,15 @@ async function check() {
|
|
|
279
317
|
log.info(`[app-updater] ${latest} 安装包就绪:${ready}`);
|
|
280
318
|
broadcast({ status: "available", version: latest, current, progress: null, filePath: null, autoUpdate: getAutoUpdate(), auto: false });
|
|
281
319
|
if (getAutoUpdate()) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
320
|
+
if (!autoInstallAllowed(latest)) {
|
|
321
|
+
log.warn(`[app-updater] ${latest} 已自动安装失败 ${AUTO_TRY_MAX} 次,冷却中 — 只提示不再自动装(避免反复关闭 app)`);
|
|
322
|
+
} else {
|
|
323
|
+
const n = noteAutoTry(latest);
|
|
324
|
+
log.info(`[app-updater] auto-update on → downloading ${latest} and installing without asking (attempt ${n}/${AUTO_TRY_MAX})`);
|
|
325
|
+
broadcast({ auto: true });
|
|
326
|
+
await downloadUpdate();
|
|
327
|
+
if (_state.status === "ready") installNow();
|
|
328
|
+
}
|
|
286
329
|
}
|
|
287
330
|
} else { log.info(`[app-updater] ${latest} 版本号已更新但所有来源都拿不到安装包(HEAD 非 200):${urls.join(" , ")} — 暂不提示更新`); broadcast({ status: "up-to-date", version: current, current }); }
|
|
288
331
|
} else {
|
|
@@ -364,8 +407,12 @@ function installWindows(installer) {
|
|
|
364
407
|
// VBS escapes a double quote by doubling it. cmd waits for the installer (no
|
|
365
408
|
// `start` on that leg), then the pause lets Windows release the replaced
|
|
366
409
|
// files before the new exe runs hidden.
|
|
410
|
+
// Wait for THIS process to exit before the installer starts: it cannot replace
|
|
411
|
+
// a running exe, and previously the chain and app.quit() raced — the installer
|
|
412
|
+
// could begin while the app was still up, fail, and leave the version
|
|
413
|
+
// unchanged, which is what fed the restart loop.
|
|
367
414
|
const cmd =
|
|
368
|
-
`cmd /c ""${installer}" /S & timeout /t 20 /nobreak >nul & start "" "${exe}" --hidden"`;
|
|
415
|
+
`cmd /c "timeout /t 8 /nobreak >nul & "${installer}" /S & timeout /t 20 /nobreak >nul & start "" "${exe}" --hidden"`;
|
|
369
416
|
const line = `sh.Run "${cmd.replace(/"/g, '""')}", 0, False`;
|
|
370
417
|
fs.writeFileSync(vbs, ['Set sh = CreateObject("WScript.Shell")', line, ""].join("\r\n"));
|
|
371
418
|
const ch = spawn("wscript.exe", ["//B", "//Nologo", vbs], {
|
|
@@ -375,8 +422,8 @@ function installWindows(installer) {
|
|
|
375
422
|
});
|
|
376
423
|
ch.unref();
|
|
377
424
|
log.info(`[app-updater] hidden install + relaunch chain started (pid ${ch.pid})`);
|
|
378
|
-
// Quit
|
|
379
|
-
setTimeout(() => { try { app.quit(); } catch {} },
|
|
425
|
+
// Quit promptly — the chain waits 8s for us before touching anything.
|
|
426
|
+
setTimeout(() => { try { app.quit(); } catch {} }, 500);
|
|
380
427
|
}
|
|
381
428
|
|
|
382
429
|
function installNow() {
|
|
@@ -46,18 +46,31 @@ test("the relaunch is part of the same detached chain, so it outlives the app",
|
|
|
46
46
|
assert.match(win, /windowsHide: true/);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test("the chain
|
|
50
|
-
//
|
|
51
|
-
//
|
|
49
|
+
test("the chain is quit → install → settle → relaunch, in that order", () => {
|
|
50
|
+
// Three waits, each load-bearing:
|
|
51
|
+
// 8s — this process must be GONE before the installer starts. Racing it is
|
|
52
|
+
// what made installs fail, and a failed install fed the restart loop
|
|
53
|
+
// that took the fleet down.
|
|
54
|
+
// 20s — let Windows release the replaced files before the new exe runs.
|
|
55
|
+
// No `start` on the installer leg, so cmd blocks until the install finishes.
|
|
52
56
|
assert.doesNotMatch(win, /start "" "\$\{installer\}"/);
|
|
57
|
+
assert.match(win, /timeout \/t 8 \/nobreak >nul & "\$\{installer\}" \/S/);
|
|
53
58
|
assert.match(win, /timeout \/t 20 \/nobreak/);
|
|
54
|
-
const
|
|
55
|
-
|
|
59
|
+
const preWait = win.indexOf("timeout /t 8");
|
|
60
|
+
const install = win.indexOf('"${installer}" /S');
|
|
61
|
+
const settle = win.indexOf("timeout /t 20");
|
|
62
|
+
const relaunch = win.indexOf('start "" "${exe}"');
|
|
63
|
+
assert.ok(
|
|
64
|
+
preWait < install && install < settle && settle < relaunch,
|
|
65
|
+
"quit → install → settle → relaunch"
|
|
66
|
+
);
|
|
56
67
|
});
|
|
57
68
|
|
|
58
|
-
test("the app quits
|
|
69
|
+
test("the app quits promptly, well inside the chain's 8s head start", () => {
|
|
59
70
|
assert.match(win, /app\.quit\(\)/);
|
|
60
|
-
|
|
71
|
+
const m = win.match(/\}, (\d+)\);/);
|
|
72
|
+
assert.ok(m, "quit is scheduled");
|
|
73
|
+
assert.ok(Number(m[1]) < 8000, `quit delay ${m[1]}ms must be under the 8s head start`);
|
|
61
74
|
});
|
|
62
75
|
|
|
63
76
|
test("mac and linux keep their existing behaviour", () => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Auto-install kills the app by design — an installer cannot replace a running
|
|
5
|
+
// exe. So when an install does NOT land, the app comes back on the old version,
|
|
6
|
+
// the next check() tries the same thing, and the machine sits in a restart loop
|
|
7
|
+
// that closes the user's window every time. That is exactly what happened: the
|
|
8
|
+
// fleet flapped up/down until every node was gone. A destructive automatic
|
|
9
|
+
// action needs a stop condition, and it had none.
|
|
10
|
+
const test = require("node:test");
|
|
11
|
+
const assert = require("node:assert/strict");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "app-updater.js"), "utf8");
|
|
16
|
+
|
|
17
|
+
test("the same version is not auto-installed forever", () => {
|
|
18
|
+
assert.match(src, /AUTO_TRY_MAX = 2/);
|
|
19
|
+
assert.match(src, /AUTO_TRY_COOLDOWN_MS = 6 \* 60 \* 60 \* 1000/);
|
|
20
|
+
const fn = src.slice(src.indexOf("function autoInstallAllowed"), src.indexOf("// ── 状态机"));
|
|
21
|
+
assert.match(fn, /if \(t\.version !== version\) return true/); // a new version always may
|
|
22
|
+
assert.match(fn, /if \(\(t\.count \|\| 0\) < AUTO_TRY_MAX\) return true/);
|
|
23
|
+
assert.match(fn, /Date\.now\(\) - \(t\.at \|\| 0\) > AUTO_TRY_COOLDOWN_MS/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("the attempt count survives the restart the install itself causes", () => {
|
|
27
|
+
// In-memory state would reset on every relaunch, which is precisely the thing
|
|
28
|
+
// the loop does — so the counter has to be on disk.
|
|
29
|
+
assert.match(src, /desktopAutoInstall: \{ version, count, at: Date\.now\(\) \}/);
|
|
30
|
+
assert.match(src, /readGlobalConfig\(GLOBAL_JSON\)\?\.desktopAutoInstall/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("check() gates the auto path and still offers the update", () => {
|
|
34
|
+
const chk = src.slice(
|
|
35
|
+
src.indexOf("async function check()"),
|
|
36
|
+
src.indexOf("async function downloadUpdate")
|
|
37
|
+
);
|
|
38
|
+
assert.match(chk, /if \(!autoInstallAllowed\(latest\)\) \{/);
|
|
39
|
+
assert.match(chk, /noteAutoTry\(latest\)/);
|
|
40
|
+
// blocked = do not install; the "available" broadcast above still happened, so
|
|
41
|
+
// the user can update by hand.
|
|
42
|
+
assert.ok(
|
|
43
|
+
chk.indexOf("autoInstallAllowed") < chk.indexOf("await downloadUpdate()"),
|
|
44
|
+
"the gate must come before the download"
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a landed install clears the record", () => {
|
|
49
|
+
const init = src.slice(
|
|
50
|
+
src.indexOf("function init(mainWin)"),
|
|
51
|
+
src.indexOf("async function check()")
|
|
52
|
+
);
|
|
53
|
+
assert.match(init, /readAutoTry\(\)\.version === _state\.current/);
|
|
54
|
+
assert.match(init, /clearAutoTry\(\)/);
|
|
55
|
+
});
|