cicy-desktop 2.1.329 → 2.1.331
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 +1 -1
- package/src/app-updater.js +78 -16
- package/src/main.js +33 -5
- package/src/sidecar/host-mihomo.js +13 -4
- package/test/app-updater-github-fallback.test.js +81 -0
- package/test/autostart-run-key.test.js +59 -0
package/package.json
CHANGED
package/src/app-updater.js
CHANGED
|
@@ -21,7 +21,7 @@ const os = require("os");
|
|
|
21
21
|
const { readGlobalConfig, updateGlobalConfig } = require("./utils/global-json");
|
|
22
22
|
const https = require("https");
|
|
23
23
|
const log = require("electron-log");
|
|
24
|
-
const { R2_RELEASES_BASE } = require("./sidecar/mirrors");
|
|
24
|
+
const { R2_RELEASES_BASE, buildUrlList } = require("./sidecar/mirrors");
|
|
25
25
|
|
|
26
26
|
// ── 版本比较:>0 a 新于 b ─────────────────────────────────────────────────────
|
|
27
27
|
function cmpVer(a, b) {
|
|
@@ -74,25 +74,76 @@ function headOk(url, redirects = 0) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// ── 最新版本号 ────────────────────────────────────────────────────────────────
|
|
77
|
+
// R2 优先(win/mac/linux 各自指针,两条 CI 版本可能不同步,所以各读各的),GitHub 兜底。
|
|
78
|
+
//
|
|
79
|
+
// 原先这里是「一律读 OSS,彻底不碰 GitHub」,理由是仓库可能私有。代价是 R2 一旦不可达,
|
|
80
|
+
// 自更新就整条断掉 —— 而 fleet 里确实有一批 Windows 节点连 r2.deepfetch.de5.net 直接
|
|
81
|
+
// ECONNRESET(GitHub 反而 ~300ms 可达),它们因此永远停在旧版、只能人工推包。
|
|
82
|
+
// 现在 R2 仍是首选(仓库若再转私有,这条路不受影响),失败才回退 GitHub Release;
|
|
83
|
+
// 两边都不通才算真失败。
|
|
84
|
+
const GH_REPO = "cicy-ai/cicy-desktop";
|
|
85
|
+
|
|
86
|
+
function fetchLatestVersionFromGitHub() {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const req = https.get(
|
|
89
|
+
`https://api.github.com/repos/${GH_REPO}/releases/latest`,
|
|
90
|
+
{ headers: { "User-Agent": "cicy-desktop-updater", Accept: "application/vnd.github+json" }, timeout: 10000 },
|
|
91
|
+
(res) => {
|
|
92
|
+
if (res.statusCode !== 200) { res.resume(); return reject(new Error(`GitHub HTTP ${res.statusCode}`)); }
|
|
93
|
+
let body = "";
|
|
94
|
+
res.setEncoding("utf8");
|
|
95
|
+
res.on("data", (d) => (body += d));
|
|
96
|
+
res.on("end", () => {
|
|
97
|
+
try {
|
|
98
|
+
const tag = String((JSON.parse(body) || {}).tag_name || "").replace(/^v/, "");
|
|
99
|
+
tag ? resolve(tag) : reject(new Error("no tag_name"));
|
|
100
|
+
} catch (e) { reject(e); }
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
req.on("error", reject);
|
|
105
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
async function fetchLatestVersion() {
|
|
78
|
-
// 版本清单**一律读 OSS**(win/mac/linux 各自指针),与网络判定无关 —— 彻底不碰 GitHub,
|
|
79
|
-
// 这样 cicy-desktop 仓库私有也不影响更新检查。win/mac 是两条独立 CI、版本可能不同步,读错
|
|
80
|
-
// 平台会 404,所以各读各的指针文件。
|
|
81
110
|
const file = process.platform === "win32" ? "win-latest-version.txt"
|
|
82
111
|
: process.platform === "darwin" ? "mac-latest-version.txt"
|
|
83
112
|
: "linux-latest-version.txt";
|
|
84
|
-
|
|
113
|
+
try {
|
|
114
|
+
return (await getText(`${R2_RELEASES_BASE}/${file}`)).trim().replace(/^v/, "");
|
|
115
|
+
} catch (e) {
|
|
116
|
+
log.warn(`[app-updater] R2 版本指针不可达(${e.message}),回退 GitHub Release`);
|
|
117
|
+
return await fetchLatestVersionFromGitHub();
|
|
118
|
+
}
|
|
85
119
|
}
|
|
86
120
|
|
|
87
|
-
// ──
|
|
121
|
+
// ── 安装包来源(有序候选)+ 本地文件名 ──────────────────────────────────────
|
|
122
|
+
// 返回 { file, urls } —— urls 是按优先级排好的候选:R2 → GitHub 直连 → ghproxy 镜像。
|
|
123
|
+
// 调用方依次尝试,第一个能下的算数(见 check() 的 HEAD 探测和 downloadUpdate 的重试)。
|
|
124
|
+
//
|
|
125
|
+
// 注意 Windows 两边**文件名不同**:R2 上是 cicy-desktop-<ver>.exe(CI 传的版本化副本),
|
|
126
|
+
// GitHub Release 里是 electron-builder 产出的 CiCy-Desktop-Setup-<ver>.exe。
|
|
127
|
+
// mac 的 pkg 和 linux 的 AppImage 两边同名。`file` 只是本地落盘名,保持原样不变。
|
|
88
128
|
function assetFor(version) {
|
|
89
129
|
const plat = process.platform;
|
|
90
130
|
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
91
|
-
// 三端安装包**全部从 OSS 拉**,与网络无关 —— 不再拼 GitHub 下载 URL,仓库私有也能更新。
|
|
92
131
|
const file = plat === "win32" ? `cicy-desktop-${version}.exe`
|
|
93
132
|
: plat === "darwin" ? `cicy-desktop-${version}-${arch}.pkg`
|
|
94
133
|
: `CiCy-Desktop-${version}.AppImage`;
|
|
95
|
-
|
|
134
|
+
const ghName = plat === "win32" ? `CiCy-Desktop-Setup-${version}.exe` : file;
|
|
135
|
+
const ghUrl = `https://github.com/${GH_REPO}/releases/download/v${version}/${ghName}`;
|
|
136
|
+
// buildUrlList("global") = [直连, ...镜像];R2 排在最前面。
|
|
137
|
+
const urls = [`${R2_RELEASES_BASE}/${file}`, ...buildUrlList(ghUrl, "global")];
|
|
138
|
+
return { file, urls, url: urls[0] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 第一个 HEAD 得到 200/206 的候选;都不行返回 null。
|
|
142
|
+
async function firstReachable(urls) {
|
|
143
|
+
for (const u of urls) {
|
|
144
|
+
if (await headOk(u)) return u;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
96
147
|
}
|
|
97
148
|
|
|
98
149
|
// ── 下载(带进度,跟随重定向)──────────────────────────────────────────────────
|
|
@@ -179,9 +230,10 @@ async function check() {
|
|
|
179
230
|
if (latest && cmpVer(latest, current) > 0) {
|
|
180
231
|
// 「先出包再更新版本」客户端保险:版本号涨了不代表包传完了。先 HEAD 确认本平台安装包
|
|
181
232
|
// 真能下(200),否则当成「还没就绪」继续显已是最新,避免用户点下载 404。
|
|
182
|
-
const {
|
|
183
|
-
const ready = await
|
|
233
|
+
const { urls } = assetFor(latest);
|
|
234
|
+
const ready = await firstReachable(urls);
|
|
184
235
|
if (ready) {
|
|
236
|
+
log.info(`[app-updater] ${latest} 安装包就绪:${ready}`);
|
|
185
237
|
broadcast({ status: "available", version: latest, current, progress: null, filePath: null, autoUpdate: getAutoUpdate(), auto: false });
|
|
186
238
|
if (getAutoUpdate()) {
|
|
187
239
|
log.info(`[app-updater] auto-update on → downloading ${latest} and installing without asking`);
|
|
@@ -189,7 +241,7 @@ async function check() {
|
|
|
189
241
|
await downloadUpdate();
|
|
190
242
|
if (_state.status === "ready") installNow();
|
|
191
243
|
}
|
|
192
|
-
} else { log.info(`[app-updater] ${latest}
|
|
244
|
+
} else { log.info(`[app-updater] ${latest} 版本号已更新但所有来源都拿不到安装包(HEAD 非 200):${urls.join(" , ")} — 暂不提示更新`); broadcast({ status: "up-to-date", version: current, current }); }
|
|
193
245
|
} else {
|
|
194
246
|
broadcast({ status: "up-to-date", version: latest || current, current });
|
|
195
247
|
}
|
|
@@ -208,16 +260,26 @@ async function downloadUpdate() {
|
|
|
208
260
|
if (!version) { broadcast({ status: "error", error: "no version" }); return _state; }
|
|
209
261
|
_downloading = true;
|
|
210
262
|
try {
|
|
211
|
-
const {
|
|
263
|
+
const { urls, file } = assetFor(version);
|
|
212
264
|
// 下到 ~/Downloads(用户能直接找到安装包);目录不存在则建。
|
|
213
265
|
const dir = app.getPath("downloads");
|
|
214
266
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
215
267
|
const dest = path.join(dir, file);
|
|
216
|
-
log.info(`[app-updater] downloading ${url} → ${dest}`);
|
|
217
268
|
broadcast({ status: "downloading", progress: { percent: 0, transferred: 0, total: 0 }, filePath: null });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
269
|
+
// 逐个候选试(R2 → GitHub → 镜像):一个源断了就换下一个,全断才算失败。
|
|
270
|
+
let lastErr = null;
|
|
271
|
+
for (const url of urls) {
|
|
272
|
+
try {
|
|
273
|
+
log.info(`[app-updater] downloading ${url} → ${dest}`);
|
|
274
|
+
await download(url, dest, (p) => broadcast({ status: "downloading", progress: p }));
|
|
275
|
+
broadcast({ status: "ready", filePath: dest, progress: { percent: 100 } });
|
|
276
|
+
return _state;
|
|
277
|
+
} catch (e) {
|
|
278
|
+
lastErr = e;
|
|
279
|
+
log.warn(`[app-updater] 源失败(${url}):${e.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
throw lastErr || new Error("no source available");
|
|
221
283
|
} catch (e) {
|
|
222
284
|
log.warn("[app-updater] download failed:", e.message);
|
|
223
285
|
broadcast({ status: "error", error: e.message });
|
package/src/main.js
CHANGED
|
@@ -949,22 +949,29 @@ function ensureAutoLaunch() {
|
|
|
949
949
|
// (它先起 master 再起 worker,直接起 electron.exe 会缺 master)。登录项指向一个
|
|
950
950
|
// wscript 隐藏启动的 VBS → `cmd /c node .\bin\cicy-desktop`,全程无控制台窗口。
|
|
951
951
|
const fromSource = !electronApp.isPackaged;
|
|
952
|
-
let opts;
|
|
952
|
+
let opts, runCmd;
|
|
953
953
|
if (fromSource) {
|
|
954
954
|
const vbs = writeSourceAutostartVbs();
|
|
955
955
|
opts = { openAtLogin: want, path: "wscript.exe", args: ["//B", "//Nologo", vbs] };
|
|
956
|
+
runCmd = `wscript.exe //B //Nologo "${vbs}"`;
|
|
956
957
|
} else {
|
|
957
958
|
opts = { openAtLogin: want, args: ["--hidden"] };
|
|
959
|
+
runCmd = `"${process.execPath}" --hidden`;
|
|
958
960
|
}
|
|
959
|
-
const
|
|
960
|
-
fromSource ? { path: opts.path, args: opts.args } : undefined
|
|
961
|
-
)
|
|
962
|
-
if (cur.openAtLogin !== want) {
|
|
961
|
+
const probe = () =>
|
|
962
|
+
electronApp.getLoginItemSettings(fromSource ? { path: opts.path, args: opts.args } : undefined);
|
|
963
|
+
if (probe().openAtLogin !== want) {
|
|
963
964
|
electronApp.setLoginItemSettings(opts);
|
|
964
965
|
log.info(
|
|
965
966
|
`[autostart] openAtLogin → ${want}${fromSource ? ` (source: ${opts.path} ${opts.args.join(" ")})` : ""}`
|
|
966
967
|
);
|
|
967
968
|
}
|
|
969
|
+
// Read it back. setLoginItemSettings() returns without throwing on every
|
|
970
|
+
// Windows node of this fleet and still leaves openAtLogin false, with
|
|
971
|
+
// HKCU\...\Run untouched — so autostart silently never existed, and a node
|
|
972
|
+
// whose app went away (e.g. an installer closed it) never came back, not
|
|
973
|
+
// even after a reboot. When the API did not take, write the Run key.
|
|
974
|
+
if (probe().openAtLogin !== want) ensureWindowsRunKey(want, runCmd);
|
|
968
975
|
} else if (process.platform === "linux") {
|
|
969
976
|
if (!electronApp.isPackaged) return;
|
|
970
977
|
ensureLinuxAutostart(want);
|
|
@@ -974,6 +981,27 @@ function ensureAutoLaunch() {
|
|
|
974
981
|
}
|
|
975
982
|
}
|
|
976
983
|
|
|
984
|
+
// Fallback for the silently-failing setLoginItemSettings (see ensureAutoLaunch):
|
|
985
|
+
// own the HKCU Run entry directly. Async so a slow reg.exe never delays startup;
|
|
986
|
+
// failures are logged, never thrown — autostart is best-effort.
|
|
987
|
+
const WIN_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
988
|
+
const WIN_RUN_NAME = "CiCy Desktop";
|
|
989
|
+
function ensureWindowsRunKey(want, command) {
|
|
990
|
+
try {
|
|
991
|
+
const { execFile } = require("child_process");
|
|
992
|
+
const args = want
|
|
993
|
+
? ["add", WIN_RUN_KEY, "/v", WIN_RUN_NAME, "/t", "REG_SZ", "/d", command, "/f"]
|
|
994
|
+
: ["delete", WIN_RUN_KEY, "/v", WIN_RUN_NAME, "/f"];
|
|
995
|
+
execFile("reg", args, { windowsHide: true }, (err) => {
|
|
996
|
+
// deleting an absent value exits non-zero — that is the wanted end state.
|
|
997
|
+
if (err && want) log.warn(`[autostart] Run-key fallback failed: ${err.message}`);
|
|
998
|
+
else if (want) log.info(`[autostart] Run-key fallback wrote ${WIN_RUN_NAME} → ${command}`);
|
|
999
|
+
});
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
log.warn(`[autostart] Run-key fallback threw: ${e.message}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
977
1005
|
// 源码模式 Windows 登录自启的隐藏启动器(VBS)。写到 %LOCALAPPDATA%\cicy-desktop,幂等覆盖。
|
|
978
1006
|
// 继承当前的 CICY_DEBUG(用 start-cicy-desktop-win.bat 起的就带 =1),其余环境用登录时的用户环境。
|
|
979
1007
|
function writeSourceAutostartVbs() {
|
|
@@ -233,14 +233,23 @@ function start({ force = false } = {}) {
|
|
|
233
233
|
|
|
234
234
|
// Full enable: ensure binary → copy+adapt container config → (re)start.
|
|
235
235
|
// containerYaml is fetched by the caller (sidecar-ipc) via appDocker.
|
|
236
|
+
// A missing container config is NOT fatal on its own: when the WSL/docker
|
|
237
|
+
// cicy-code isn't installed (or is down) we run STANDALONE on the host's own
|
|
238
|
+
// mihomo-host.yaml if one is already there, so the per-profile proxies keep
|
|
239
|
+
// working instead of dying with the container — that's what lets Windows run
|
|
240
|
+
// without WSL at all. Only "no container config AND no host config" leaves us
|
|
241
|
+
// nothing to start. Standalone has no authoritative selection source, so the
|
|
242
|
+
// selection sync is skipped rather than overriding whatever the host holds.
|
|
236
243
|
async function enable({ containerYaml, selections = {}, emit } = {}) {
|
|
237
244
|
await ensureBinary({ emit });
|
|
238
|
-
|
|
239
|
-
|
|
245
|
+
let changed = false, standalone = false;
|
|
246
|
+
if (containerYaml) changed = writeConfig(containerYaml);
|
|
247
|
+
else if (fs.existsSync(HOST_CONFIG)) standalone = true;
|
|
248
|
+
else throw new Error(tt("noContainerConfig"));
|
|
240
249
|
const res = start({ force: changed });
|
|
241
|
-
const synced = await syncSelections(selections);
|
|
250
|
+
const synced = standalone ? { updated: [] } : await syncSelections(selections);
|
|
242
251
|
emit && emit({ phase: "chrome-proxy", status: "running", message: tt("ready") });
|
|
243
|
-
return { ok: true, ...res, ...synced };
|
|
252
|
+
return { ok: true, standalone, ...res, ...synced };
|
|
244
253
|
}
|
|
245
254
|
|
|
246
255
|
module.exports = {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// The self-updater used to read BOTH the version pointer and the installer from
|
|
5
|
+
// R2 only ("彻底不碰 GitHub"). That made auto-update fail closed on every node
|
|
6
|
+
// that cannot reach r2.deepfetch.de5.net — a real slice of the fleet answers
|
|
7
|
+
// ECONNRESET there while GitHub responds in ~300ms, so those boxes sat on an old
|
|
8
|
+
// version until someone pushed a package by hand. R2 stays FIRST (a private repo
|
|
9
|
+
// must keep working); GitHub is the fallback.
|
|
10
|
+
//
|
|
11
|
+
// app-updater requires electron at load time, so this asserts on source shape —
|
|
12
|
+
// the same convention as app-updater-auto.test.js.
|
|
13
|
+
const test = require("node:test");
|
|
14
|
+
const assert = require("node:assert/strict");
|
|
15
|
+
const fs = require("node:fs");
|
|
16
|
+
const path = require("node:path");
|
|
17
|
+
|
|
18
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "app-updater.js"), "utf8");
|
|
19
|
+
|
|
20
|
+
test("version pointer falls back to the GitHub release when R2 is unreachable", () => {
|
|
21
|
+
assert.match(src, /function fetchLatestVersionFromGitHub\(\)/);
|
|
22
|
+
assert.match(src, /api\.github\.com\/repos\/\$\{GH_REPO\}\/releases\/latest/);
|
|
23
|
+
assert.match(src, /tag_name/);
|
|
24
|
+
// R2 first, GitHub only in the catch — not the other way round.
|
|
25
|
+
const fn = src.slice(
|
|
26
|
+
src.indexOf("async function fetchLatestVersion()"),
|
|
27
|
+
src.indexOf("// ── 安装包来源")
|
|
28
|
+
);
|
|
29
|
+
assert.match(fn, /R2_RELEASES_BASE\}\/\$\{file\}/);
|
|
30
|
+
assert.match(fn, /catch \(e\) \{[^]*fetchLatestVersionFromGitHub\(\)/);
|
|
31
|
+
assert.ok(
|
|
32
|
+
fn.indexOf("R2_RELEASES_BASE") < fn.indexOf("fetchLatestVersionFromGitHub"),
|
|
33
|
+
"R2 must be tried first"
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("assetFor returns ordered candidates: R2 → GitHub → mirrors", () => {
|
|
38
|
+
const fn = src.slice(
|
|
39
|
+
src.indexOf("function assetFor(version)"),
|
|
40
|
+
src.indexOf("async function firstReachable")
|
|
41
|
+
);
|
|
42
|
+
assert.match(
|
|
43
|
+
fn,
|
|
44
|
+
/const urls = \[`\$\{R2_RELEASES_BASE\}\/\$\{file\}`, \.\.\.buildUrlList\(ghUrl, "global"\)\]/
|
|
45
|
+
);
|
|
46
|
+
// Windows is the one platform whose asset is named differently on each source:
|
|
47
|
+
// R2 keeps the CI's versioned copy, GitHub has electron-builder's Setup exe.
|
|
48
|
+
assert.match(fn, /ghName = plat === "win32" \? `CiCy-Desktop-Setup-\$\{version\}\.exe` : file/);
|
|
49
|
+
assert.match(fn, /releases\/download\/v\$\{version\}\/\$\{ghName\}/);
|
|
50
|
+
assert.match(fn, /return \{ file, urls, url: urls\[0\] \}/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("check() probes every candidate before declaring the package missing", () => {
|
|
54
|
+
assert.match(src, /async function firstReachable\(urls\)/);
|
|
55
|
+
assert.match(src, /for \(const u of urls\) \{[^]*if \(await headOk\(u\)\) return u;/);
|
|
56
|
+
const chk = src.slice(
|
|
57
|
+
src.indexOf("async function check()"),
|
|
58
|
+
src.indexOf("async function downloadUpdate()")
|
|
59
|
+
);
|
|
60
|
+
assert.match(chk, /const \{ urls \} = assetFor\(latest\)/);
|
|
61
|
+
assert.match(chk, /const ready = await firstReachable\(urls\)/);
|
|
62
|
+
assert.doesNotMatch(chk, /await headOk\(url\)/); // the old single-source probe is gone
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("downloadUpdate() retries the next source instead of failing on the first", () => {
|
|
66
|
+
const dl = src.slice(
|
|
67
|
+
src.indexOf("async function downloadUpdate()"),
|
|
68
|
+
src.indexOf("function installNow()")
|
|
69
|
+
);
|
|
70
|
+
assert.match(dl, /const \{ urls, file \} = assetFor\(version\)/);
|
|
71
|
+
assert.match(dl, /for \(const url of urls\) \{/);
|
|
72
|
+
assert.match(dl, /catch \(e\) \{[^]*lastErr = e/);
|
|
73
|
+
assert.match(dl, /throw lastErr \|\| new Error\("no source available"\)/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("mirrors helper is imported so CN keeps its ghproxy fallback", () => {
|
|
77
|
+
assert.match(
|
|
78
|
+
src,
|
|
79
|
+
/const \{ R2_RELEASES_BASE, buildUrlList \} = require\("\.\/sidecar\/mirrors"\)/
|
|
80
|
+
);
|
|
81
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Windows autostart had no test and never worked. Measured across the fleet:
|
|
5
|
+
// app.setLoginItemSettings({openAtLogin:true}) returns without throwing, yet
|
|
6
|
+
// getLoginItemSettings() still reports false and HKCU\...\Run stays untouched
|
|
7
|
+
// (the key exists and is writable — OneDrive lives there). So every Windows node
|
|
8
|
+
// ran with no autostart at all, which is why one whose app went away never came
|
|
9
|
+
// back, not even after a reboot. ensureAutoLaunch() now reads the setting back
|
|
10
|
+
// and owns the Run entry itself when the API did not take.
|
|
11
|
+
const test = require("node:test");
|
|
12
|
+
const assert = require("node:assert/strict");
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
|
|
16
|
+
const src = fs.readFileSync(path.join(__dirname, "..", "src", "main.js"), "utf8");
|
|
17
|
+
const fn = src.slice(
|
|
18
|
+
src.indexOf("function ensureAutoLaunch()"),
|
|
19
|
+
src.indexOf("function ensureWindowsRunKey")
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
test("windows autostart verifies the setting instead of trusting the API", () => {
|
|
23
|
+
// A single getLoginItemSettings() call before the write is not enough — the
|
|
24
|
+
// regression is entirely in what happens AFTER setLoginItemSettings().
|
|
25
|
+
assert.match(fn, /const probe = \(\) =>/);
|
|
26
|
+
assert.match(fn, /if \(probe\(\)\.openAtLogin !== want\) \{[^]*setLoginItemSettings\(opts\)/);
|
|
27
|
+
assert.match(fn, /if \(probe\(\)\.openAtLogin !== want\) ensureWindowsRunKey\(want, runCmd\)/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the Run-key command differs for packaged vs source runs", () => {
|
|
31
|
+
// Packaged: the exe itself, hidden. Source: wscript running the VBS launcher,
|
|
32
|
+
// because starting electron.exe directly skips bin/cicy-desktop's master boot.
|
|
33
|
+
assert.match(fn, /runCmd = `"\$\{process\.execPath\}" --hidden`/);
|
|
34
|
+
assert.match(fn, /runCmd = `wscript\.exe \/\/B \/\/Nologo "\$\{vbs\}"`/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("ensureWindowsRunKey adds or removes the HKCU Run value, async and non-fatal", () => {
|
|
38
|
+
const helper = src.slice(
|
|
39
|
+
src.indexOf("function ensureWindowsRunKey"),
|
|
40
|
+
src.indexOf("// 源码模式 Windows 登录自启")
|
|
41
|
+
);
|
|
42
|
+
assert.match(
|
|
43
|
+
src,
|
|
44
|
+
/WIN_RUN_KEY = "HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run"/
|
|
45
|
+
);
|
|
46
|
+
assert.match(
|
|
47
|
+
helper,
|
|
48
|
+
/\["add", WIN_RUN_KEY, "\/v", WIN_RUN_NAME, "\/t", "REG_SZ", "\/d", command, "\/f"\]/
|
|
49
|
+
);
|
|
50
|
+
assert.match(helper, /\["delete", WIN_RUN_KEY, "\/v", WIN_RUN_NAME, "\/f"\]/);
|
|
51
|
+
assert.match(helper, /execFile\("reg", args, \{ windowsHide: true \}/); // no console flash, no startup stall
|
|
52
|
+
assert.match(helper, /catch \(e\) \{[^]*log\.warn/); // best-effort: never throws into startup
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("autostart still defaults to on and is honoured per-platform", () => {
|
|
56
|
+
assert.match(fn, /const want = prefs\.openAtLogin !== false/);
|
|
57
|
+
assert.match(fn, /if \(process\.platform === "darwin"\)/);
|
|
58
|
+
assert.match(fn, /else if \(process\.platform === "linux"\)/);
|
|
59
|
+
});
|