@amd-gaia/agent-ui 0.25.3 → 0.25.4-rc.10
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/bin/gaia-ui.cjs +2 -23
- package/dist/assets/AdvancedSettings-DXf2pogI.js +1 -0
- package/dist/assets/ConnectorsPane-CzLYXGMl.css +1 -0
- package/dist/assets/ConnectorsPane-R1yvKMNT.js +1 -0
- package/dist/assets/DocumentLibrary-CM6o8j2T.css +1 -0
- package/dist/assets/DocumentLibrary-DSOiP7Ft.js +4 -0
- package/dist/assets/FileBrowser-BlVSPjD2.js +4 -0
- package/dist/assets/FileBrowser-Cna3BsT7.css +1 -0
- package/dist/assets/MemoryDashboard-D_ZVZbS_.js +1 -0
- package/dist/assets/MemoryDashboard-DwrWAXi6.css +1 -0
- package/dist/assets/MobileAccessModal-BkVDm3YG.css +1 -0
- package/dist/assets/MobileAccessModal-CPjDeIIk.js +2 -0
- package/dist/assets/ScheduleManager-CjHJ4NbD.css +1 -0
- package/dist/assets/ScheduleManager-D1DOYVQo.js +1 -0
- package/dist/assets/api-Co-B9E0t.js +4 -0
- package/dist/assets/index-CMVmIBZ8.js +78 -0
- package/dist/assets/index-Y93uvqe7.css +1 -0
- package/dist/assets/useT-tWXlG-cT.js +3 -0
- package/dist/assets/zap-D-WCZ6dX.js +1 -0
- package/dist/icons/apple-touch-icon.png +0 -0
- package/dist/icons/icon-192.png +0 -0
- package/dist/icons/icon-512.png +0 -0
- package/dist/icons/icon-maskable-192.png +0 -0
- package/dist/icons/icon-maskable-512.png +0 -0
- package/dist/index.html +8 -3
- package/dist/manifest.json +16 -0
- package/dist/sw.js +49 -0
- package/main-safety-net.cjs +106 -8
- package/main.cjs +145 -162
- package/package.json +5 -3
- package/preload.cjs +19 -9
- package/services/agent-seeder.cjs +22 -13
- package/services/auto-updater.cjs +780 -216
- package/services/backend-installer.cjs +523 -365
- package/services/engine-install.cjs +233 -0
- package/services/index-query.cjs +7 -1
- package/services/lock-holders.cjs +200 -0
- package/services/quit-guard.cjs +36 -3
- package/services/tray-manager.cjs +12 -7
- package/dist/assets/AdvancedSettings-DERzP9Jc.js +0 -1
- package/dist/assets/ConnectorsPane-Dzu2DRTc.css +0 -1
- package/dist/assets/ConnectorsPane-s0fFxmus.js +0 -1
- package/dist/assets/DocumentLibrary-CIKp6qld.css +0 -1
- package/dist/assets/DocumentLibrary-DqapN_Tr.js +0 -4
- package/dist/assets/FileBrowser-Dil1lZJ4.css +0 -1
- package/dist/assets/FileBrowser-vAJV41RT.js +0 -4
- package/dist/assets/MemoryDashboard-CP5wVWkY.js +0 -1
- package/dist/assets/MemoryDashboard-Mo721-BI.css +0 -1
- package/dist/assets/MobileAccessModal-B31mPs50.js +0 -2
- package/dist/assets/MobileAccessModal-DuVy9F1u.css +0 -1
- package/dist/assets/ScheduleManager-D5c4gVIt.css +0 -1
- package/dist/assets/ScheduleManager-b0gTAf2R.js +0 -2
- package/dist/assets/apiBase-CS6zLsJY.js +0 -1
- package/dist/assets/index-C-EM_5yl.css +0 -1
- package/dist/assets/index-Cae5Fje-.js +0 -78
- package/dist/assets/zap-BogJn9oN.js +0 -1
- package/services/backend-installer-progress-dialog.cjs +0 -510
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* - Auto-update is OFF by default (`autoUpdate` in ~/.gaia/update-config.json,
|
|
25
25
|
* shared with the Terminal UI). Off, a found update is only offered: the
|
|
26
26
|
* renderer's "Update now" calls downloadUpdate(). On, it downloads silently.
|
|
27
|
-
* - On `update-downloaded`,
|
|
27
|
+
* - On `update-downloaded`, the chip offers the restart; nothing interrupts
|
|
28
28
|
* - Renderer integration via IPC channel `gaia:update:status`
|
|
29
29
|
* - Disabled entirely via GAIA_DISABLE_UPDATE=1 env var (CI / dev / corp)
|
|
30
30
|
* - Rollback: listReleases() → installVersion(tag) → pin persisted to
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
* - listReleases() → fetch available releases from GitHub
|
|
39
39
|
* - installVersion(tag) → downgrade/install a specific tagged release
|
|
40
40
|
* - clearPin() → resume auto-updates (clear the version pin)
|
|
41
|
+
* - revertUpdate() → go back to the version before the last update
|
|
41
42
|
* - setAutoUpdate(enabled) → persist the auto-update preference
|
|
42
43
|
* - downloadUpdate() → download the offered update ("Update now")
|
|
43
44
|
* - restartToInstall() → restart into a downloaded update
|
|
@@ -56,6 +57,8 @@
|
|
|
56
57
|
const path = require("path");
|
|
57
58
|
const fs = require("fs");
|
|
58
59
|
const os = require("os");
|
|
60
|
+
const crypto = require("crypto");
|
|
61
|
+
const zlib = require("zlib");
|
|
59
62
|
|
|
60
63
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
61
64
|
|
|
@@ -70,13 +73,6 @@ const UPDATE_CONFIG_PATH = path.join(os.homedir(), ".gaia", "update-config.json"
|
|
|
70
73
|
// that split is the A6/A7 resolution (#1724).
|
|
71
74
|
const DEFAULT_CHANNEL = "latest";
|
|
72
75
|
|
|
73
|
-
// per_page=100 is the GitHub API max for a single page; we fetch broadly so
|
|
74
|
-
// draft/prerelease/platform filtering still yields a full page of installable
|
|
75
|
-
// releases before the display cap below. >100 releases would need pagination
|
|
76
|
-
// (follow the Link rel="next" header). Not built yet.
|
|
77
|
-
const GITHUB_API_RELEASES =
|
|
78
|
-
"https://api.github.com/repos/amd/gaia/releases?per_page=100";
|
|
79
|
-
|
|
80
76
|
// The picker shows only the N most-recent installable releases (rolling back
|
|
81
77
|
// more than a few versions is rare). Older versions stay reachable via the
|
|
82
78
|
// "browse all on GitHub" link in the picker, so this display cap never makes a
|
|
@@ -84,21 +80,47 @@ const GITHUB_API_RELEASES =
|
|
|
84
80
|
const MAX_RELEASES_SHOWN = 10;
|
|
85
81
|
|
|
86
82
|
const RELEASES_PAGE = "https://github.com/amd/gaia/releases";
|
|
83
|
+
const RELEASE_DOWNLOADS = `${RELEASES_PAGE}/download`;
|
|
84
|
+
|
|
85
|
+
// The atom feed and download URLs don't count against the REST API's 60
|
|
86
|
+
// anonymous calls an hour, which one busy network can use up for everyone.
|
|
87
|
+
const RELEASES_ATOM = `${RELEASES_PAGE}.atom`;
|
|
88
|
+
const PICKER_FEED_PAGES = 3;
|
|
89
|
+
|
|
90
|
+
// Where a restart notes the update it started: a silent install that fails
|
|
91
|
+
// shows nothing, so the next launch is what finds out.
|
|
92
|
+
const INSTALL_ATTEMPT_PATH = path.join(os.homedir(), ".gaia", "update-attempt.json");
|
|
93
|
+
const PREVIOUS_VERSION_KEY = "previousAppVersion";
|
|
94
|
+
const LAUNCHES_SINCE_UPDATE_KEY = "launchesSinceUpdate";
|
|
95
|
+
// Same cap as the TUI's sumsAtOnce, to go easy on proxies.
|
|
96
|
+
const RELEASE_CHECKS_AT_ONCE = 8;
|
|
97
|
+
// A backend that dies weeks after an update isn't the update's fault.
|
|
98
|
+
const START_FAILURE_REVERT_LAUNCHES = 3;
|
|
99
|
+
|
|
100
|
+
// The installer removes the old version before unpacking the new one.
|
|
101
|
+
const MIN_INSTALL_FREE_BYTES = 1024 * 1024 * 1024;
|
|
87
102
|
|
|
88
103
|
// A black-holed connection must fail, not leave "Checking…" up forever.
|
|
89
104
|
const GITHUB_TIMEOUT_MS = 20 * 1000;
|
|
90
105
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
/** The generic feed for one release's assets. */
|
|
107
|
+
function _releaseFeed(tag, extra = {}) {
|
|
108
|
+
return {
|
|
109
|
+
provider: "generic",
|
|
110
|
+
url: `${RELEASE_DOWNLOADS}/${tag}/`,
|
|
111
|
+
// GitHub answers multi-range requests 501, which loses the delta download.
|
|
112
|
+
useMultipleRangeRequest: false,
|
|
113
|
+
...extra,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** fetch() with a timeout; rejects with `.kind` "timeout" or "network". */
|
|
118
|
+
async function _githubFetch(url, options = {}) {
|
|
98
119
|
try {
|
|
99
|
-
|
|
100
|
-
headers: {
|
|
120
|
+
return await fetch(url, {
|
|
121
|
+
headers: { "User-Agent": "GAIA-Agent-UI" },
|
|
101
122
|
signal: AbortSignal.timeout(GITHUB_TIMEOUT_MS),
|
|
123
|
+
...options,
|
|
102
124
|
});
|
|
103
125
|
} catch (err) {
|
|
104
126
|
const timedOut = err && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
@@ -106,17 +128,147 @@ async function _fetchReleases() {
|
|
|
106
128
|
kind: timedOut ? "timeout" : "network",
|
|
107
129
|
});
|
|
108
130
|
}
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A delta update diffs the installer electron-updater cached at install time
|
|
135
|
+
* against the new one, using the running version's blockmap. It guesses that
|
|
136
|
+
* blockmap's URL by swapping version strings inside the new one, which misses
|
|
137
|
+
* every release candidate's tag ("v0.25.4-rc3" holds no "0.25.4-rc.3"), and a
|
|
138
|
+
* fresh install has no blockmap cached. Without one the update downloads in
|
|
139
|
+
* full, so fetch it from the running version's own release ahead of time.
|
|
140
|
+
*/
|
|
141
|
+
async function _seedInstalledBlockmap() {
|
|
142
|
+
if (process.platform !== "win32" || typeof autoUpdaterRef.getOrCreateDownloadHelper !== "function") {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const version = state.currentVersion;
|
|
146
|
+
const { cacheDir } = await autoUpdaterRef.getOrCreateDownloadHelper();
|
|
147
|
+
const installer = path.join(cacheDir, "installer.exe");
|
|
148
|
+
const blockmap = path.join(cacheDir, "current.blockmap");
|
|
149
|
+
const seededFor = path.join(cacheDir, "gaia-blockmap-version");
|
|
150
|
+
if (!version || !fs.existsSync(installer)) return;
|
|
151
|
+
const seeded = _readText(seededFor);
|
|
152
|
+
if (seeded === `${version}:not-this-release`) return;
|
|
153
|
+
if (fs.existsSync(blockmap) && seeded === version) return;
|
|
154
|
+
|
|
155
|
+
const base = `${RELEASE_DOWNLOADS}/${_tagForVersion(version)}`;
|
|
156
|
+
const manifest = await _githubFetch(`${base}/${_platformManifest()}`);
|
|
157
|
+
if (!manifest.ok) throw _httpError(manifest.status);
|
|
158
|
+
const yml = await manifest.text();
|
|
159
|
+
const file = /^path:\s*(\S+)/m.exec(yml);
|
|
160
|
+
const sha512 = /^sha512:\s*(\S+)/m.exec(yml);
|
|
161
|
+
if (!file || !sha512) throw new Error(`${_platformManifest()} for ${version} names no installer`);
|
|
162
|
+
// The cached installer must be this release's, or the diff comes out wrong.
|
|
163
|
+
if ((await _sha512File(installer)) !== sha512[1]) {
|
|
164
|
+
log("info", `The cached installer isn't GAIA ${version}'s release file: the next update downloads in full`);
|
|
165
|
+
fs.writeFileSync(seededFor, `${version}:not-this-release`);
|
|
166
|
+
return;
|
|
111
167
|
}
|
|
168
|
+
const resp = await _githubFetch(`${base}/${file[1]}.blockmap`);
|
|
169
|
+
if (!resp.ok) throw _httpError(resp.status);
|
|
170
|
+
const data = Buffer.from(await resp.arrayBuffer());
|
|
171
|
+
JSON.parse(zlib.gunzipSync(data).toString());
|
|
172
|
+
fs.writeFileSync(blockmap, data);
|
|
173
|
+
fs.writeFileSync(seededFor, version);
|
|
174
|
+
log("info", `Cached GAIA ${version}'s blockmap, so the next update downloads only what changed`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _readText(file) {
|
|
112
178
|
try {
|
|
113
|
-
return
|
|
114
|
-
} catch
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
179
|
+
return fs.readFileSync(file, "utf8").trim();
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function _sha512File(file) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const hash = crypto.createHash("sha512");
|
|
188
|
+
fs.createReadStream(file)
|
|
189
|
+
.on("error", reject)
|
|
190
|
+
.on("data", (chunk) => hash.update(chunk))
|
|
191
|
+
.on("end", () => resolve(hash.digest("base64")));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _httpError(status) {
|
|
196
|
+
return Object.assign(new Error(`HTTP ${status}`), { kind: "http", status });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** One page of releases.atom as [{tag_name, published_at, html_url, prerelease}]. */
|
|
200
|
+
async function _fetchFeedPage(after) {
|
|
201
|
+
const url = after ? `${RELEASES_ATOM}?after=${encodeURIComponent(after)}` : RELEASES_ATOM;
|
|
202
|
+
const resp = await _githubFetch(url);
|
|
203
|
+
if (!resp.ok) throw _httpError(resp.status);
|
|
204
|
+
const xml = await resp.text();
|
|
205
|
+
if (!xml.includes("<feed")) {
|
|
206
|
+
throw Object.assign(new Error("not an atom feed"), { kind: "garbled" });
|
|
207
|
+
}
|
|
208
|
+
const releases = [];
|
|
209
|
+
for (const [, entry] of xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)) {
|
|
210
|
+
const id = /<id>[^<]*\/([^/<]+)<\/id>/.exec(entry);
|
|
211
|
+
if (!id) continue;
|
|
212
|
+
const updated = /<updated>([^<]+)<\/updated>/.exec(entry);
|
|
213
|
+
releases.push({
|
|
214
|
+
tag_name: id[1],
|
|
215
|
+
published_at: updated ? updated[1] : null,
|
|
216
|
+
html_url: `${RELEASES_PAGE}/tag/${id[1]}`,
|
|
217
|
+
prerelease: id[1].includes("-"),
|
|
118
218
|
});
|
|
119
219
|
}
|
|
220
|
+
return releases;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The newest releases from the atom feed, plus the newest final release, which
|
|
225
|
+
* a run of candidates can push off the feed's first page. Rejects with `.kind`
|
|
226
|
+
* set to "timeout", "network", "http" (with `.status`) or "garbled", so each
|
|
227
|
+
* caller can word its own message.
|
|
228
|
+
*/
|
|
229
|
+
async function _fetchReleases(pages = 1) {
|
|
230
|
+
const releases = [];
|
|
231
|
+
let after = null;
|
|
232
|
+
for (let page = 0; page < pages; page++) {
|
|
233
|
+
const batch = await _fetchFeedPage(after);
|
|
234
|
+
releases.push(...batch);
|
|
235
|
+
if (batch.length === 0) break;
|
|
236
|
+
after = batch[batch.length - 1].tag_name;
|
|
237
|
+
}
|
|
238
|
+
const latest = await _githubFetch(`${RELEASES_PAGE}/latest/download/${_platformManifest()}`);
|
|
239
|
+
if (latest.ok) {
|
|
240
|
+
const version = /^version:\s*(\S+)/m.exec(await latest.text());
|
|
241
|
+
const tag = version && `v${version[1].replace(/^v/, "")}`;
|
|
242
|
+
if (tag && !releases.some((r) => r.tag_name === tag)) {
|
|
243
|
+
releases.push({
|
|
244
|
+
tag_name: tag,
|
|
245
|
+
published_at: null,
|
|
246
|
+
html_url: `${RELEASES_PAGE}/tag/${tag}`,
|
|
247
|
+
prerelease: false,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
} else if (latest.status !== 404) {
|
|
251
|
+
throw _httpError(latest.status);
|
|
252
|
+
}
|
|
253
|
+
return releases;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Whether `tag` carries this platform's manifest and the installer it names.
|
|
258
|
+
* A release is listed before its assets finish uploading, and macOS manifests
|
|
259
|
+
* can name a zip that was never attached, so the file itself is checked.
|
|
260
|
+
*/
|
|
261
|
+
async function _releaseHasUpdate(tag) {
|
|
262
|
+
const base = `${RELEASE_DOWNLOADS}/${tag}`;
|
|
263
|
+
const manifest = await _githubFetch(`${base}/${_platformManifest()}`);
|
|
264
|
+
if (manifest.status === 404) return false;
|
|
265
|
+
if (!manifest.ok) throw _httpError(manifest.status);
|
|
266
|
+
const path = /^path:\s*(\S+)/m.exec(await manifest.text());
|
|
267
|
+
if (!path) return false;
|
|
268
|
+
const installer = await _githubFetch(`${base}/${path[1]}`, { method: "HEAD" });
|
|
269
|
+
if (installer.status === 404) return false;
|
|
270
|
+
if (!installer.ok) throw _httpError(installer.status);
|
|
271
|
+
return true;
|
|
120
272
|
}
|
|
121
273
|
|
|
122
274
|
/** The user-facing sentence for a _fetchReleases failure while `doing`. */
|
|
@@ -163,6 +315,8 @@ const state = {
|
|
|
163
315
|
error: null,
|
|
164
316
|
currentVersion: null,
|
|
165
317
|
pinnedVersion: null,
|
|
318
|
+
previousVersion: null,
|
|
319
|
+
startFailed: false,
|
|
166
320
|
autoUpdate: false,
|
|
167
321
|
// Offer release candidates too; off by default. A release-candidate build
|
|
168
322
|
// is offered newer candidates without it.
|
|
@@ -170,12 +324,43 @@ const state = {
|
|
|
170
324
|
// update-config.json could not be read; kept apart from `error` so a later
|
|
171
325
|
// check's status never hides it.
|
|
172
326
|
configError: null,
|
|
327
|
+
// The last restart didn't install; stays until an install works.
|
|
328
|
+
installFailed: null,
|
|
329
|
+
// Why the last unasked check or download failed; Settings shows it quietly.
|
|
330
|
+
lastCheckError: null,
|
|
331
|
+
// The update has downloaded and its backend is being fetched before the restart.
|
|
332
|
+
preparing: false,
|
|
173
333
|
};
|
|
174
334
|
|
|
175
335
|
let mainWindowRef = null;
|
|
176
336
|
/** Asks before an update restart would cut off running work (set by init). */
|
|
177
337
|
let confirmRestartRef = null;
|
|
338
|
+
// Put the update's backend in the local cache before the restart (set by init).
|
|
339
|
+
let prefetchBackendRef = null;
|
|
340
|
+
let isBackendPrefetchedRef = null;
|
|
178
341
|
let checkInProgress = false;
|
|
342
|
+
// True while a check nobody asked for runs, so its failure stays out of the UI.
|
|
343
|
+
let backgroundCheck = false;
|
|
344
|
+
// The last check was asked for, so the download it starts may report failure.
|
|
345
|
+
let askedCheck = false;
|
|
346
|
+
// A routine check is running with this version already downloaded.
|
|
347
|
+
let heldDownloadVersion = null;
|
|
348
|
+
// The downloaded update was given up on, so quitting must not install it.
|
|
349
|
+
let abandonedDownload = false;
|
|
350
|
+
// The silent install failed, so the next try shows the installer's window.
|
|
351
|
+
let wizardNextInstall = false;
|
|
352
|
+
// Picking a version asks for the restart too, so no second click is needed.
|
|
353
|
+
let restartWhenDownloaded = false;
|
|
354
|
+
// Settles once the restart a picked version asked for has run or been refused.
|
|
355
|
+
let pendingRestart = null;
|
|
356
|
+
let restartRefusal = null;
|
|
357
|
+
// Settles once the last update-downloaded is handled, prefetch and restart
|
|
358
|
+
// included. electron-updater doesn't wait for its listeners, so callers do.
|
|
359
|
+
let downloadHandled = null;
|
|
360
|
+
// Bumped when a download is given up on, so a prefetch that outlives it can tell.
|
|
361
|
+
let downloadEpoch = 0;
|
|
362
|
+
// A prefetch per version, so a repeat download waits on it instead of racing it.
|
|
363
|
+
const prefetching = new Map();
|
|
179
364
|
// The user asked for this download, so it may install on quit with auto-update off.
|
|
180
365
|
let installRequested = false;
|
|
181
366
|
// The file was unreadable at launch, so a pin it holds was never loaded.
|
|
@@ -275,10 +460,7 @@ function _refreshPreferences() {
|
|
|
275
460
|
log("info", `Settings file readable again — honouring its pin to ${pin}`);
|
|
276
461
|
setState({ pinnedVersion: pin });
|
|
277
462
|
if (autoUpdaterRef) {
|
|
278
|
-
autoUpdaterRef.setFeedURL(
|
|
279
|
-
provider: "generic",
|
|
280
|
-
url: `https://github.com/amd/gaia/releases/download/${pin}/`,
|
|
281
|
-
});
|
|
463
|
+
autoUpdaterRef.setFeedURL(_releaseFeed(pin));
|
|
282
464
|
}
|
|
283
465
|
}
|
|
284
466
|
}
|
|
@@ -288,7 +470,8 @@ function _refreshPreferences() {
|
|
|
288
470
|
function _applyInstallPolicy() {
|
|
289
471
|
if (!autoUpdaterRef) return;
|
|
290
472
|
autoUpdaterRef.autoDownload = state.autoUpdate && !state.pinnedVersion;
|
|
291
|
-
autoUpdaterRef.autoInstallOnAppQuit =
|
|
473
|
+
autoUpdaterRef.autoInstallOnAppQuit =
|
|
474
|
+
(state.autoUpdate || installRequested) && !abandonedDownload && !wizardNextInstall && !state.preparing;
|
|
292
475
|
autoUpdaterRef.allowPrerelease = _prereleasesAllowed();
|
|
293
476
|
}
|
|
294
477
|
|
|
@@ -314,6 +497,42 @@ function _savePin(pinnedVersion) {
|
|
|
314
497
|
_writeConfig({ pinnedVersion });
|
|
315
498
|
}
|
|
316
499
|
|
|
500
|
+
/**
|
|
501
|
+
* An error the UI shows in the user's language: `code` names the message
|
|
502
|
+
* update.err.<code> and `params` fills it. The message is the English, for logs.
|
|
503
|
+
*/
|
|
504
|
+
function _userError(code, params, message) {
|
|
505
|
+
return Object.assign(new Error(message), { code, params: params || {} });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Marks an error message as a coded one, for the renderer to translate.
|
|
509
|
+
const USER_ERROR_MARK = "gaia-update-error:";
|
|
510
|
+
|
|
511
|
+
// Codes the UI has an update.err.<code> message for; any other code is a cause.
|
|
512
|
+
const USER_ERROR_CODES = new Set([
|
|
513
|
+
"checkBusy",
|
|
514
|
+
"configBusy",
|
|
515
|
+
"diskSpace",
|
|
516
|
+
"downloadFailed",
|
|
517
|
+
"failedWith",
|
|
518
|
+
"noEarlier",
|
|
519
|
+
"noInstaller",
|
|
520
|
+
"offline",
|
|
521
|
+
"pinFailed",
|
|
522
|
+
"saveFailed",
|
|
523
|
+
]);
|
|
524
|
+
|
|
525
|
+
function _isUserCode(code) {
|
|
526
|
+
return USER_ERROR_CODES.has(code);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** IPC keeps only an error's message, so the code travels inside it. */
|
|
530
|
+
function _forRenderer(err) {
|
|
531
|
+
if (!err || !_isUserCode(err.code)) return err;
|
|
532
|
+
const { code, params, note } = err;
|
|
533
|
+
return new Error(`${USER_ERROR_MARK}${JSON.stringify({ code, params, note, message: err.message })}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
317
536
|
/**
|
|
318
537
|
* Merge `patch` into update-config.json. Propagates write errors so a failed
|
|
319
538
|
* pin write can abort a rollback (AC2 must not be silently broken). Callers
|
|
@@ -414,7 +633,9 @@ function _withConfigLock(fn) {
|
|
|
414
633
|
`${path.dirname(UPDATE_CONFIG_PATH)}.`
|
|
415
634
|
);
|
|
416
635
|
}
|
|
417
|
-
throw
|
|
636
|
+
throw _userError(
|
|
637
|
+
"configBusy",
|
|
638
|
+
{},
|
|
418
639
|
`${UPDATE_CONFIG_PATH} is busy (another GAIA app holds ${lock}) — try again in a moment.`
|
|
419
640
|
);
|
|
420
641
|
}
|
|
@@ -508,6 +729,19 @@ function _platformManifest() {
|
|
|
508
729
|
return process.arch === "arm64" ? "latest-linux-arm64.yml" : "latest-linux.yml";
|
|
509
730
|
}
|
|
510
731
|
|
|
732
|
+
/** GAIA releases on this install's channel, newest first. */
|
|
733
|
+
function _channelReleases(releases) {
|
|
734
|
+
const allowPrerelease = _prereleasesAllowed();
|
|
735
|
+
return releases
|
|
736
|
+
.filter(
|
|
737
|
+
(r) =>
|
|
738
|
+
TAG_RE.test(r.tag_name || "") &&
|
|
739
|
+
!Number.isNaN(_compareVersions(r.tag_name, "0.0.0")) &&
|
|
740
|
+
(allowPrerelease || !r.prerelease)
|
|
741
|
+
)
|
|
742
|
+
.sort((a, b) => _compareVersions(b.tag_name, a.tag_name));
|
|
743
|
+
}
|
|
744
|
+
|
|
511
745
|
/**
|
|
512
746
|
* Point the feed at the newest GitHub release on this install's channel that
|
|
513
747
|
* carries this platform's installer and update manifest; returns its tag.
|
|
@@ -515,31 +749,19 @@ function _platformManifest() {
|
|
|
515
749
|
* NO_CHANNEL state, when no release has an update for this platform.
|
|
516
750
|
*/
|
|
517
751
|
async function _applyGithubFeed() {
|
|
518
|
-
const
|
|
519
|
-
let
|
|
752
|
+
const manifest = _platformManifest();
|
|
753
|
+
let tag = null;
|
|
520
754
|
try {
|
|
521
|
-
|
|
755
|
+
for (const release of _channelReleases(await _fetchReleases())) {
|
|
756
|
+
if (await _releaseHasUpdate(release.tag_name)) {
|
|
757
|
+
tag = release.tag_name;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
522
761
|
} catch (err) {
|
|
523
762
|
throw new Error(_releasesErrorMessage(err, "check for updates"));
|
|
524
763
|
}
|
|
525
|
-
if (!
|
|
526
|
-
throw new Error(
|
|
527
|
-
`GitHub sent a response GAIA couldn't read when it tried to check for updates (not a list) — try again later, or see ${RELEASES_PAGE}.`
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
const manifest = _platformManifest();
|
|
531
|
-
const candidates = releases.filter(
|
|
532
|
-
(r) =>
|
|
533
|
-
r &&
|
|
534
|
-
!r.draft &&
|
|
535
|
-
TAG_RE.test(r.tag_name || "") &&
|
|
536
|
-
!Number.isNaN(_compareVersions(r.tag_name, "0.0.0")) &&
|
|
537
|
-
(allowPrerelease || !r.prerelease) &&
|
|
538
|
-
_releaseHasPlatformAsset(r) &&
|
|
539
|
-
Array.isArray(r.assets) &&
|
|
540
|
-
r.assets.some((a) => a && a.name === manifest)
|
|
541
|
-
);
|
|
542
|
-
if (candidates.length === 0) {
|
|
764
|
+
if (!tag) {
|
|
543
765
|
const error =
|
|
544
766
|
`No GAIA release publishes a desktop update for this platform yet (it needs ${manifest} ` +
|
|
545
767
|
`and its installer), so GAIA can't update itself here — download an installer from ${RELEASES_PAGE}.`;
|
|
@@ -547,13 +769,7 @@ async function _applyGithubFeed() {
|
|
|
547
769
|
log("warn", error);
|
|
548
770
|
return null;
|
|
549
771
|
}
|
|
550
|
-
|
|
551
|
-
const tag = candidates[0].tag_name;
|
|
552
|
-
autoUpdaterRef.setFeedURL({
|
|
553
|
-
provider: "generic",
|
|
554
|
-
url: `https://github.com/amd/gaia/releases/download/${tag}/`,
|
|
555
|
-
channel: DEFAULT_CHANNEL,
|
|
556
|
-
});
|
|
772
|
+
autoUpdaterRef.setFeedURL(_releaseFeed(tag, { channel: DEFAULT_CHANNEL }));
|
|
557
773
|
log("info", `Update feed set from GitHub: ${tag}`);
|
|
558
774
|
return tag;
|
|
559
775
|
}
|
|
@@ -587,23 +803,6 @@ function isDisabled() {
|
|
|
587
803
|
return process.env.GAIA_DISABLE_UPDATE === "1";
|
|
588
804
|
}
|
|
589
805
|
|
|
590
|
-
// ── Platform asset detection ─────────────────────────────────────────────────
|
|
591
|
-
|
|
592
|
-
function _platformAssetPattern() {
|
|
593
|
-
if (process.platform === "win32") return /setup\.exe$/i;
|
|
594
|
-
if (process.platform === "darwin") return /\.zip$/i;
|
|
595
|
-
// Linux: AppImage (deb is apt-managed, electron-updater can't downgrade it)
|
|
596
|
-
return process.arch === "arm64" ? /(arm64|aarch64)\.AppImage$/i : /(x86_64|amd64)\.AppImage$/i;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function _releaseHasPlatformAsset(release) {
|
|
600
|
-
const pattern = _platformAssetPattern();
|
|
601
|
-
return (
|
|
602
|
-
Array.isArray(release.assets) &&
|
|
603
|
-
release.assets.some((a) => a && a.name && pattern.test(a.name))
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
806
|
// ── listReleases ──────────────────────────────────────────────────────────────
|
|
608
807
|
|
|
609
808
|
/**
|
|
@@ -637,42 +836,34 @@ async function listReleases() {
|
|
|
637
836
|
}
|
|
638
837
|
}
|
|
639
838
|
|
|
640
|
-
let
|
|
839
|
+
let installable;
|
|
641
840
|
try {
|
|
642
|
-
releases
|
|
841
|
+
// Twice the display cap, so releases with no update here still fill the list.
|
|
842
|
+
const candidates = _channelReleases(await _fetchReleases(PICKER_FEED_PAGES)).slice(
|
|
843
|
+
0,
|
|
844
|
+
MAX_RELEASES_SHOWN * 2
|
|
845
|
+
);
|
|
846
|
+
const ready = await _mapLimited(candidates, RELEASE_CHECKS_AT_ONCE, (r) =>
|
|
847
|
+
_releaseHasUpdate(r.tag_name)
|
|
848
|
+
);
|
|
849
|
+
installable = candidates.filter((_, i) => ready[i]).slice(0, MAX_RELEASES_SHOWN);
|
|
643
850
|
} catch (err) {
|
|
644
851
|
return { error: _releasesErrorMessage(err, "list releases") };
|
|
645
852
|
}
|
|
646
|
-
|
|
853
|
+
|
|
854
|
+
return installable.map((r) => {
|
|
855
|
+
const tag = r.tag_name;
|
|
856
|
+
// Version is tag without leading 'v'
|
|
857
|
+
const version = tag.startsWith("v") ? tag.slice(1) : tag;
|
|
647
858
|
return {
|
|
648
|
-
|
|
859
|
+
version,
|
|
860
|
+
tag,
|
|
861
|
+
date: r.published_at,
|
|
862
|
+
notesUrl: r.html_url,
|
|
863
|
+
isCurrent: currentVersion ? _compareVersions(version, currentVersion) === 0 : false,
|
|
864
|
+
isPinned: pinnedVersion ? tag === pinnedVersion : false,
|
|
649
865
|
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const result = releases
|
|
653
|
-
.filter(
|
|
654
|
-
(r) =>
|
|
655
|
-
!r.draft &&
|
|
656
|
-
(allowPrerelease || !r.prerelease) &&
|
|
657
|
-
_releaseHasPlatformAsset(r)
|
|
658
|
-
)
|
|
659
|
-
.map((r) => {
|
|
660
|
-
const tag = r.tag_name || "";
|
|
661
|
-
// Version is tag without leading 'v'
|
|
662
|
-
const version = tag.startsWith("v") ? tag.slice(1) : tag;
|
|
663
|
-
return {
|
|
664
|
-
version,
|
|
665
|
-
tag,
|
|
666
|
-
date: r.published_at || null,
|
|
667
|
-
notesUrl: r.html_url || null,
|
|
668
|
-
isCurrent: currentVersion ? version === currentVersion : false,
|
|
669
|
-
isPinned: pinnedVersion ? tag === pinnedVersion : false,
|
|
670
|
-
};
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
// Cap to the most-recent installable releases; older ones remain reachable
|
|
674
|
-
// via the "browse all on GitHub" link in the picker.
|
|
675
|
-
return result.slice(0, MAX_RELEASES_SHOWN);
|
|
866
|
+
});
|
|
676
867
|
}
|
|
677
868
|
|
|
678
869
|
// ── installVersion ────────────────────────────────────────────────────────────
|
|
@@ -686,7 +877,7 @@ async function listReleases() {
|
|
|
686
877
|
* folder lets electron-updater treat it as an available update.
|
|
687
878
|
*
|
|
688
879
|
* Persists the version pin BEFORE starting the download so that even if the
|
|
689
|
-
* user
|
|
880
|
+
* user puts off the restart, the pin is already in place for AC2.
|
|
690
881
|
*
|
|
691
882
|
* @param {string} tag The git tag, e.g. "v0.20.0"
|
|
692
883
|
*/
|
|
@@ -702,7 +893,9 @@ async function installVersion(tag) {
|
|
|
702
893
|
);
|
|
703
894
|
}
|
|
704
895
|
if (checkInProgress) {
|
|
705
|
-
throw
|
|
896
|
+
throw _userError(
|
|
897
|
+
"checkBusy",
|
|
898
|
+
{},
|
|
706
899
|
"An update check is already in progress — try again in a moment."
|
|
707
900
|
);
|
|
708
901
|
}
|
|
@@ -718,22 +911,23 @@ async function installVersion(tag) {
|
|
|
718
911
|
_savePin(tag);
|
|
719
912
|
} catch (err) {
|
|
720
913
|
const msg = (err && err.message) || String(err);
|
|
721
|
-
throw
|
|
914
|
+
if (err && err.code === "configBusy") throw err;
|
|
915
|
+
throw _userError(
|
|
916
|
+
"pinFailed",
|
|
917
|
+
{ path: UPDATE_CONFIG_PATH, reason: msg },
|
|
722
918
|
`Failed to persist version pin — rollback aborted: ${msg}`
|
|
723
919
|
);
|
|
724
920
|
}
|
|
725
|
-
setState({ pinnedVersion: tag });
|
|
921
|
+
setState({ pinnedVersion: tag, previousVersion: null, startFailed: false });
|
|
726
922
|
// A pin always stops background downloads; this one is downloaded below.
|
|
727
923
|
installRequested = true;
|
|
924
|
+
restartWhenDownloaded = true;
|
|
728
925
|
_applyInstallPolicy();
|
|
729
926
|
|
|
730
927
|
checkInProgress = true;
|
|
731
928
|
try {
|
|
732
929
|
autoUpdaterRef.allowDowngrade = true;
|
|
733
|
-
autoUpdaterRef.setFeedURL(
|
|
734
|
-
provider: "generic",
|
|
735
|
-
url: `https://github.com/amd/gaia/releases/download/${tag}/`,
|
|
736
|
-
});
|
|
930
|
+
autoUpdaterRef.setFeedURL(_releaseFeed(tag));
|
|
737
931
|
|
|
738
932
|
// Known limitation: the generic/tagged feed + allowDowngrade stay set
|
|
739
933
|
// until clearPin(). While pinned, autoDownload=false (set at init) keeps
|
|
@@ -746,26 +940,42 @@ async function installVersion(tag) {
|
|
|
746
940
|
|
|
747
941
|
// Targeted check → update-available. autoDownload is off (default, and
|
|
748
942
|
// always while pinned), so download explicitly; update-downloaded then
|
|
749
|
-
//
|
|
943
|
+
// restarts into it.
|
|
750
944
|
await autoUpdaterRef.checkForUpdates();
|
|
751
945
|
if (state.status !== STATES.AVAILABLE) {
|
|
752
|
-
throw
|
|
946
|
+
throw _userError(
|
|
947
|
+
"noInstaller",
|
|
948
|
+
{ tag, url: RELEASES_PAGE },
|
|
753
949
|
`GAIA ${tag} has no installer for this platform at its release feed` +
|
|
754
950
|
(state.error ? `: ${state.error}` : ".") +
|
|
755
|
-
|
|
951
|
+
` Download it from ${RELEASES_PAGE} instead.`
|
|
756
952
|
);
|
|
757
953
|
}
|
|
758
954
|
setState({ status: STATES.DOWNLOADING, progress: 0, error: null });
|
|
955
|
+
downloadHandled = null;
|
|
759
956
|
await autoUpdaterRef.downloadUpdate();
|
|
760
957
|
} catch (err) {
|
|
958
|
+
restartWhenDownloaded = false;
|
|
761
959
|
// Don't leave the app pinned to a version that never installed.
|
|
762
960
|
const pinNote = _restorePinAfterFailedInstall(previousPin, tag);
|
|
763
961
|
const msg = (err && err.message) || String(err);
|
|
764
962
|
setState({ status: STATES.ERROR, error: msg });
|
|
765
|
-
|
|
963
|
+
// A coded cause keeps its code; any other shows its own text.
|
|
964
|
+
const coded = err && _isUserCode(err.code);
|
|
965
|
+
throw Object.assign(new Error(`${msg} ${pinNote.message}`), {
|
|
966
|
+
code: coded ? err.code : "failedWith",
|
|
967
|
+
params: coded ? err.params : { reason: msg },
|
|
968
|
+
note: { code: pinNote.code, params: pinNote.params },
|
|
969
|
+
});
|
|
766
970
|
} finally {
|
|
767
971
|
checkInProgress = false;
|
|
768
972
|
}
|
|
973
|
+
await downloadHandled;
|
|
974
|
+
if (restartRefusal) {
|
|
975
|
+
const refusal = restartRefusal;
|
|
976
|
+
restartRefusal = null;
|
|
977
|
+
throw refusal;
|
|
978
|
+
}
|
|
769
979
|
}
|
|
770
980
|
|
|
771
981
|
/** Undo the pin a failed install wrote; returns the sentence saying where things stand. */
|
|
@@ -775,21 +985,22 @@ function _restorePinAfterFailedInstall(previousPin, failedTag) {
|
|
|
775
985
|
} catch (err) {
|
|
776
986
|
// The file still pins failedTag; say so rather than claim otherwise.
|
|
777
987
|
log("error", "Failed to restore the pin after a failed install:", err && err.message);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
988
|
+
const reason = (err && err.message) || String(err);
|
|
989
|
+
return {
|
|
990
|
+
code: "pinStuck",
|
|
991
|
+
params: { tag: failedTag, path: UPDATE_CONFIG_PATH, reason },
|
|
992
|
+
message:
|
|
993
|
+
`The pin to ${failedTag} could not be undone in ${UPDATE_CONFIG_PATH} ` +
|
|
994
|
+
`(${reason}) — fix that file, then use Unpin in Settings → General → Updates.`,
|
|
995
|
+
};
|
|
782
996
|
}
|
|
783
997
|
if (!previousPin) {
|
|
784
998
|
clearPin();
|
|
785
|
-
return "Updates are not paused.";
|
|
999
|
+
return { code: "notPaused", params: {}, message: "Updates are not paused." };
|
|
786
1000
|
}
|
|
787
1001
|
setState({ pinnedVersion: previousPin });
|
|
788
|
-
autoUpdaterRef.setFeedURL(
|
|
789
|
-
|
|
790
|
-
url: `https://github.com/amd/gaia/releases/download/${previousPin}/`,
|
|
791
|
-
});
|
|
792
|
-
return `Still pinned to ${previousPin}.`;
|
|
1002
|
+
autoUpdaterRef.setFeedURL(_releaseFeed(previousPin));
|
|
1003
|
+
return { code: "stillPinned", params: { tag: previousPin }, message: `Still pinned to ${previousPin}.` };
|
|
793
1004
|
}
|
|
794
1005
|
|
|
795
1006
|
// ── clearPin / resumeUpdates ──────────────────────────────────────────────────
|
|
@@ -810,8 +1021,9 @@ function clearPin() {
|
|
|
810
1021
|
} catch (err) {
|
|
811
1022
|
log("warn", "Failed to clear pin file:", err && err.message);
|
|
812
1023
|
}
|
|
813
|
-
setState({ pinnedVersion: null });
|
|
1024
|
+
setState({ pinnedVersion: null, previousVersion: previousVersion() });
|
|
814
1025
|
installRequested = false;
|
|
1026
|
+
if (state.status === STATES.DOWNLOADED) _abandonDownload();
|
|
815
1027
|
|
|
816
1028
|
if (autoUpdaterRef) {
|
|
817
1029
|
_applyInstallPolicy();
|
|
@@ -837,8 +1049,12 @@ async function setAutoUpdate(enabled) {
|
|
|
837
1049
|
try {
|
|
838
1050
|
_writeConfig({ autoUpdate: enabled });
|
|
839
1051
|
} catch (err) {
|
|
840
|
-
throw
|
|
841
|
-
|
|
1052
|
+
if (err && err.code === "configBusy") throw err;
|
|
1053
|
+
const reason = (err && err.message) || String(err);
|
|
1054
|
+
throw _userError(
|
|
1055
|
+
"saveFailed",
|
|
1056
|
+
{ path: UPDATE_CONFIG_PATH, reason },
|
|
1057
|
+
`Couldn't save the auto-update setting to ${UPDATE_CONFIG_PATH}: ${reason}`
|
|
842
1058
|
);
|
|
843
1059
|
}
|
|
844
1060
|
setState({ autoUpdate: enabled });
|
|
@@ -861,18 +1077,322 @@ async function setIncludePrereleases(enabled) {
|
|
|
861
1077
|
try {
|
|
862
1078
|
_writeConfig({ includePrereleases: enabled });
|
|
863
1079
|
} catch (err) {
|
|
864
|
-
throw
|
|
865
|
-
|
|
1080
|
+
if (err && err.code === "configBusy") throw err;
|
|
1081
|
+
const reason = (err && err.message) || String(err);
|
|
1082
|
+
throw _userError(
|
|
1083
|
+
"saveFailed",
|
|
1084
|
+
{ path: UPDATE_CONFIG_PATH, reason },
|
|
1085
|
+
`Couldn't save the release-candidate setting to ${UPDATE_CONFIG_PATH}: ${reason}`
|
|
866
1086
|
);
|
|
867
1087
|
}
|
|
868
1088
|
setState({ includePrereleases: enabled });
|
|
869
1089
|
if (autoUpdaterRef) {
|
|
870
1090
|
autoUpdaterRef.allowPrerelease = _prereleasesAllowed();
|
|
871
1091
|
}
|
|
1092
|
+
const offered = state.status === STATES.DOWNLOADED || state.status === STATES.AVAILABLE;
|
|
1093
|
+
if (offered && _isPrereleaseVersion(state.version) && !_prereleasesAllowed()) {
|
|
1094
|
+
_abandonDownload();
|
|
1095
|
+
}
|
|
872
1096
|
log("info", `Release-candidate updates ${enabled ? "enabled" : "disabled"}`);
|
|
873
1097
|
}
|
|
874
1098
|
|
|
875
|
-
/**
|
|
1099
|
+
/** Stop offering the current update, and don't let a quit install it. */
|
|
1100
|
+
function _abandonDownload() {
|
|
1101
|
+
log("info", `No longer offering GAIA ${state.version}`);
|
|
1102
|
+
abandonedDownload = true;
|
|
1103
|
+
downloadEpoch += 1;
|
|
1104
|
+
installRequested = false;
|
|
1105
|
+
restartWhenDownloaded = false;
|
|
1106
|
+
setState({ status: STATES.IDLE, version: null, progress: 0, releaseNotes: null });
|
|
1107
|
+
_applyInstallPolicy();
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/** Promise.all over items, with at most `limit` calls in flight. */
|
|
1111
|
+
async function _mapLimited(items, limit, fn) {
|
|
1112
|
+
const results = new Array(items.length);
|
|
1113
|
+
let next = 0;
|
|
1114
|
+
async function worker() {
|
|
1115
|
+
while (next < items.length) {
|
|
1116
|
+
const i = next++;
|
|
1117
|
+
results[i] = await fn(items[i]);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
1121
|
+
return results;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function _readJsonFile(file) {
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) || {};
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
if (err && err.code !== "ENOENT") log("warn", `Couldn't read ${file}:`, err.message);
|
|
1129
|
+
return {};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function _runningVersion() {
|
|
1134
|
+
if (state.currentVersion) return state.currentVersion;
|
|
1135
|
+
// eslint-disable-next-line global-require
|
|
1136
|
+
return require("electron").app.getVersion();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* The older version a bad update can go back to, or null. Works before init():
|
|
1141
|
+
* a backend that won't set up stops the launch before the updater starts.
|
|
1142
|
+
* A pin means the version was chosen, so there's nothing to undo.
|
|
1143
|
+
*/
|
|
1144
|
+
function previousVersion() {
|
|
1145
|
+
if (isDisabled()) return null;
|
|
1146
|
+
const running = _runningVersion();
|
|
1147
|
+
const config = _readJsonFile(UPDATE_CONFIG_PATH);
|
|
1148
|
+
if (config.pinnedVersion) return null;
|
|
1149
|
+
const attempt = _readJsonFile(INSTALL_ATTEMPT_PATH);
|
|
1150
|
+
const earlier = attempt.from && attempt.from !== running ? attempt.from : config[PREVIOUS_VERSION_KEY];
|
|
1151
|
+
return earlier && _compareVersions(earlier, running) < 0 ? earlier : null;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** "0.25.4-rc.2" → "v0.25.4-rc2": candidate builds carry a dot their tags don't. */
|
|
1155
|
+
function _tagForVersion(version) {
|
|
1156
|
+
return `v${version.replace(/-rc\.(\d+)$/, "-rc$1")}`;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Go back to the version before the last update. Pins it, so the update that
|
|
1161
|
+
* went wrong isn't reinstalled, and resolves once the restart has run or been
|
|
1162
|
+
* refused (state then says why).
|
|
1163
|
+
*/
|
|
1164
|
+
async function revertUpdate() {
|
|
1165
|
+
const version = previousVersion();
|
|
1166
|
+
if (!version) {
|
|
1167
|
+
throw _userError(
|
|
1168
|
+
"noEarlier",
|
|
1169
|
+
{ url: RELEASES_PAGE },
|
|
1170
|
+
`There is no earlier GAIA version to go back to on this PC. Pick one in Settings → General → Updates, or download it from ${RELEASES_PAGE}.`
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
log("info", `Going back to GAIA ${version}`);
|
|
1174
|
+
const tag = _tagForVersion(version);
|
|
1175
|
+
const { startFailed } = state;
|
|
1176
|
+
try {
|
|
1177
|
+
await installVersion(tag);
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
// A refused restart leaves the pin and the download: neither may outlive it.
|
|
1180
|
+
if (state.pinnedVersion === tag) clearPin();
|
|
1181
|
+
setState({ startFailed });
|
|
1182
|
+
throw err;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/** True for the first few launches after an update landed, the only time a failed start blames it. */
|
|
1187
|
+
function _updateIsRecent() {
|
|
1188
|
+
// An attempt from the running version is a failed install, not an update.
|
|
1189
|
+
const { from } = _readJsonFile(INSTALL_ATTEMPT_PATH);
|
|
1190
|
+
if (from && from !== _runningVersion()) return true;
|
|
1191
|
+
const launches = _readJsonFile(UPDATE_CONFIG_PATH)[LAUNCHES_SINCE_UPDATE_KEY];
|
|
1192
|
+
return Number.isInteger(launches) && launches <= START_FAILURE_REVERT_LAUNCHES;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/** The app opened soon after an update but its backend didn't come up: offer the way back. */
|
|
1196
|
+
function reportStartFailure() {
|
|
1197
|
+
const previous = previousVersion();
|
|
1198
|
+
if (previous && _updateIsRecent()) setState({ startFailed: true, previousVersion: previous });
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/** Count this launch toward the window in which a failed start offers to go back. */
|
|
1202
|
+
function _countLaunchSinceUpdate() {
|
|
1203
|
+
const config = _readJsonFile(UPDATE_CONFIG_PATH);
|
|
1204
|
+
const launches = config[LAUNCHES_SINCE_UPDATE_KEY];
|
|
1205
|
+
if (!config[PREVIOUS_VERSION_KEY] || !Number.isInteger(launches)) return;
|
|
1206
|
+
if (launches > START_FAILURE_REVERT_LAUNCHES) return;
|
|
1207
|
+
try {
|
|
1208
|
+
_writeConfig({ [LAUNCHES_SINCE_UPDATE_KEY]: launches + 1 });
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
log("warn", "Couldn't count this launch since the update:", err && err.message);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** A slow backend came up after all, so there's nothing to offer a way back from. */
|
|
1215
|
+
function clearStartFailure() {
|
|
1216
|
+
if (state.startFailed) setState({ startFailed: false });
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** Note the install a quit is about to start, for the next launch to judge. */
|
|
1220
|
+
function _recordInstallAttempt() {
|
|
1221
|
+
try {
|
|
1222
|
+
fs.mkdirSync(path.dirname(INSTALL_ATTEMPT_PATH), { recursive: true });
|
|
1223
|
+
fs.writeFileSync(
|
|
1224
|
+
INSTALL_ATTEMPT_PATH,
|
|
1225
|
+
JSON.stringify({ from: state.currentVersion, target: state.version })
|
|
1226
|
+
);
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
log("warn", "Couldn't note the update attempt:", err && err.message);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Running the version an install was meant to replace means it failed. */
|
|
1233
|
+
function _readInstallOutcome() {
|
|
1234
|
+
let attempt;
|
|
1235
|
+
try {
|
|
1236
|
+
attempt = JSON.parse(fs.readFileSync(INSTALL_ATTEMPT_PATH, "utf8"));
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
if (err && err.code !== "ENOENT") {
|
|
1239
|
+
log("warn", "Couldn't read the last update attempt:", err.message);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
_countLaunchSinceUpdate();
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (attempt.from !== state.currentVersion) {
|
|
1246
|
+
try {
|
|
1247
|
+
_writeConfig({ [PREVIOUS_VERSION_KEY]: attempt.from, [LAUNCHES_SINCE_UPDATE_KEY]: 1 });
|
|
1248
|
+
} catch (err) {
|
|
1249
|
+
// The attempt file stays, so previousVersion() still knows the way back.
|
|
1250
|
+
log("warn", "Couldn't save the previous version:", err && err.message);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
fs.rmSync(INSTALL_ATTEMPT_PATH, { force: true });
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
wizardNextInstall = true;
|
|
1257
|
+
state.installFailed = { target: attempt.target, from: attempt.from };
|
|
1258
|
+
// Never stay pinned to a version that didn't land.
|
|
1259
|
+
if (state.pinnedVersion === _tagForVersion(attempt.target)) {
|
|
1260
|
+
try {
|
|
1261
|
+
_savePin(null);
|
|
1262
|
+
state.pinnedVersion = null;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
log("warn", "Couldn't clear the pin to the failed install:", err && err.message);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
log("error", `GAIA ${attempt.target} didn't install; still on ${attempt.from}`);
|
|
1268
|
+
// Report the failure once: a stale attempt would keep install-on-quit off for good.
|
|
1269
|
+
try {
|
|
1270
|
+
fs.rmSync(INSTALL_ATTEMPT_PATH, { force: true });
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
log("warn", "Couldn't clear the failed update attempt:", err && err.message);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** The quit path's check: a quit can't wait for the async one. */
|
|
1277
|
+
function _diskTooFullToInstall() {
|
|
1278
|
+
try {
|
|
1279
|
+
const stats = fs.statfsSync(os.homedir());
|
|
1280
|
+
return stats.bavail * stats.bsize < MIN_INSTALL_FREE_BYTES;
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
log("warn", "Couldn't read free disk space:", err && err.message);
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** Refuse a restart the installer can't finish, while the app still works. */
|
|
1288
|
+
async function _requireInstallSpace() {
|
|
1289
|
+
let free;
|
|
1290
|
+
try {
|
|
1291
|
+
const stats = await fs.promises.statfs(os.homedir());
|
|
1292
|
+
free = stats.bavail * stats.bsize;
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
log("warn", "Couldn't read free disk space:", err && err.message);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (free < MIN_INSTALL_FREE_BYTES) {
|
|
1298
|
+
const mb = Math.round(free / (1024 * 1024));
|
|
1299
|
+
throw _userError(
|
|
1300
|
+
"diskSpace",
|
|
1301
|
+
{ need: "1 GB", free: `${mb} MB` },
|
|
1302
|
+
`GAIA needs about 1 GB free to install this update, and this disk has ` +
|
|
1303
|
+
`${mb} MB. Free some space, then restart from the update chip.`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Setup on the next launch needs PyPI unless the backend was prefetched, so
|
|
1310
|
+
* restarting offline would leave no working app until the connection returns.
|
|
1311
|
+
* Only a PC with no network at all counts: probing a host would block people
|
|
1312
|
+
* behind proxies.
|
|
1313
|
+
*/
|
|
1314
|
+
function _isOffline() {
|
|
1315
|
+
return Boolean(electronApi && electronApi.net && !electronApi.net.isOnline());
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function _backendCached() {
|
|
1319
|
+
return Boolean(isBackendPrefetchedRef && state.version && isBackendPrefetchedRef(state.version));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function _requireNetwork() {
|
|
1323
|
+
if (_isOffline() && !_backendCached()) {
|
|
1324
|
+
throw _userError(
|
|
1325
|
+
"offline",
|
|
1326
|
+
{},
|
|
1327
|
+
"GAIA needs the internet to finish this update. Connect, then restart from the update chip."
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Fetch the update's backend while the app still runs. A failure only means the
|
|
1334
|
+
* first launch downloads it instead, as without a prefetch, so it is logged and
|
|
1335
|
+
* the restart keeps its internet check.
|
|
1336
|
+
*/
|
|
1337
|
+
async function _prefetchBackend(version) {
|
|
1338
|
+
if (!prefetchBackendRef || !version) return;
|
|
1339
|
+
if (isBackendPrefetchedRef && isBackendPrefetchedRef(version)) return;
|
|
1340
|
+
if (!prefetching.has(version)) {
|
|
1341
|
+
prefetching.set(
|
|
1342
|
+
version,
|
|
1343
|
+
Promise.resolve()
|
|
1344
|
+
.then(() => prefetchBackendRef(version))
|
|
1345
|
+
.finally(() => prefetching.delete(version))
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
try {
|
|
1349
|
+
await prefetching.get(version);
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
log("warn", `Couldn't fetch the backend for ${version} ahead of the restart:`, err && err.message);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** An update finished downloading: fetch its backend, then offer the restart. */
|
|
1356
|
+
async function _onDownloaded(info) {
|
|
1357
|
+
abandonedDownload = false;
|
|
1358
|
+
const version = (info && info.version) || state.version;
|
|
1359
|
+
const epoch = downloadEpoch;
|
|
1360
|
+
if (prefetchBackendRef) {
|
|
1361
|
+
// electron-updater has armed install-on-quit; a quit now would skip the checks below.
|
|
1362
|
+
setState({ status: STATES.DOWNLOADING, version, progress: 100, preparing: true });
|
|
1363
|
+
_applyInstallPolicy();
|
|
1364
|
+
await _prefetchBackend(version);
|
|
1365
|
+
if (epoch !== downloadEpoch) {
|
|
1366
|
+
setState({ preparing: false });
|
|
1367
|
+
_applyInstallPolicy();
|
|
1368
|
+
log("info", `GAIA ${version} was given up on while its backend was fetched`);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
setState({
|
|
1373
|
+
status: STATES.DOWNLOADED,
|
|
1374
|
+
version,
|
|
1375
|
+
progress: 100,
|
|
1376
|
+
preparing: false,
|
|
1377
|
+
error: null,
|
|
1378
|
+
lastCheckError: null,
|
|
1379
|
+
});
|
|
1380
|
+
_applyInstallPolicy();
|
|
1381
|
+
log("info", `Update downloaded: ${version}`);
|
|
1382
|
+
|
|
1383
|
+
// The chip offers the restart. Picking a version already asked for one.
|
|
1384
|
+
if (restartWhenDownloaded) {
|
|
1385
|
+
restartWhenDownloaded = false;
|
|
1386
|
+
// The download stays on offer: the chip can restart once this is cleared up.
|
|
1387
|
+
pendingRestart = restartToInstall().catch((err) => {
|
|
1388
|
+
log("warn", "Restart refused:", err && err.message);
|
|
1389
|
+
restartRefusal = err;
|
|
1390
|
+
});
|
|
1391
|
+
await pendingRestart;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** "Update now": download the update a check found; the chip then offers the restart. */
|
|
876
1396
|
async function downloadUpdate() {
|
|
877
1397
|
if (!autoUpdaterRef) {
|
|
878
1398
|
throw new Error("downloadUpdate called before auto-updater was initialised");
|
|
@@ -893,7 +1413,11 @@ async function downloadUpdate() {
|
|
|
893
1413
|
installRequested = false;
|
|
894
1414
|
_applyInstallPolicy();
|
|
895
1415
|
setState({ status: STATES.ERROR, error: message });
|
|
896
|
-
throw
|
|
1416
|
+
throw _userError(
|
|
1417
|
+
"downloadFailed",
|
|
1418
|
+
{ version: state.version || "", reason: message },
|
|
1419
|
+
`Downloading GAIA ${state.version || ""} failed: ${message}`
|
|
1420
|
+
);
|
|
897
1421
|
}
|
|
898
1422
|
}
|
|
899
1423
|
|
|
@@ -902,17 +1426,26 @@ async function restartToInstall() {
|
|
|
902
1426
|
if (!autoUpdaterRef || state.status !== STATES.DOWNLOADED) {
|
|
903
1427
|
throw new Error(`No downloaded update to install (status: ${state.status}).`);
|
|
904
1428
|
}
|
|
1429
|
+
await _requireInstallSpace();
|
|
1430
|
+
_requireNetwork();
|
|
905
1431
|
if (!(await confirmRestartRef())) {
|
|
906
1432
|
log("info", "Restart put off: work is still running — will install on next quit");
|
|
907
1433
|
return;
|
|
908
1434
|
}
|
|
909
|
-
log("info", "User chose to restart
|
|
910
|
-
|
|
1435
|
+
log("info", "User chose to restart — calling quitAndInstall");
|
|
1436
|
+
_recordInstallAttempt();
|
|
1437
|
+
// Silent: the installer wizard would otherwise wait for clicks. Force-run
|
|
1438
|
+
// brings the app straight back.
|
|
1439
|
+
autoUpdaterRef.quitAndInstall(!wizardNextInstall, true);
|
|
911
1440
|
}
|
|
912
1441
|
|
|
913
1442
|
// ── Core check ───────────────────────────────────────────────────────────────
|
|
914
1443
|
|
|
915
|
-
|
|
1444
|
+
/**
|
|
1445
|
+
* Check for an update. A `background` check that fails only logs and waits for
|
|
1446
|
+
* the next one: nobody asked for it, and nothing is wrong with the app.
|
|
1447
|
+
*/
|
|
1448
|
+
async function checkForUpdates({ background = false } = {}) {
|
|
916
1449
|
if (isDisabled()) {
|
|
917
1450
|
setState({ status: STATES.DISABLED });
|
|
918
1451
|
log("info", "Update check skipped — GAIA_DISABLE_UPDATE=1");
|
|
@@ -937,7 +1470,11 @@ async function checkForUpdates() {
|
|
|
937
1470
|
return;
|
|
938
1471
|
}
|
|
939
1472
|
checkInProgress = true;
|
|
940
|
-
|
|
1473
|
+
backgroundCheck = background;
|
|
1474
|
+
askedCheck = !background;
|
|
1475
|
+
// A routine check may replace the restart the chip offers, never remove it.
|
|
1476
|
+
heldDownloadVersion = background && state.status === STATES.DOWNLOADED ? state.version : null;
|
|
1477
|
+
if (!heldDownloadVersion) setState({ status: STATES.CHECKING, error: null });
|
|
941
1478
|
try {
|
|
942
1479
|
// No configured feed: aim at the newest GitHub release on this channel,
|
|
943
1480
|
// re-read every check so a new release is found.
|
|
@@ -947,14 +1484,36 @@ async function checkForUpdates() {
|
|
|
947
1484
|
log("info", "Checking for updates...");
|
|
948
1485
|
await autoUpdaterRef.checkForUpdates();
|
|
949
1486
|
} catch (err) {
|
|
950
|
-
|
|
951
|
-
status: STATES.ERROR,
|
|
952
|
-
error: (err && err.message) || String(err),
|
|
953
|
-
});
|
|
954
|
-
log("error", "Update check failed:", err && err.message);
|
|
1487
|
+
_recordFailure("Update check failed:", err);
|
|
955
1488
|
} finally {
|
|
956
1489
|
checkInProgress = false;
|
|
1490
|
+
backgroundCheck = false;
|
|
1491
|
+
heldDownloadVersion = null;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/** Show a failure the user asked to hear about; otherwise note it and go idle. */
|
|
1496
|
+
function _recordFailure(what, err) {
|
|
1497
|
+
const message = (err && err.message) || String(err);
|
|
1498
|
+
// electron-updater starts an automatic download without awaiting it, so the
|
|
1499
|
+
// failure can land before any progress and after the check has returned.
|
|
1500
|
+
const autoDownloading =
|
|
1501
|
+
!installRequested &&
|
|
1502
|
+
!askedCheck &&
|
|
1503
|
+
(state.status === STATES.DOWNLOADING ||
|
|
1504
|
+
(state.status === STATES.AVAILABLE && state.autoUpdate));
|
|
1505
|
+
if (heldDownloadVersion) {
|
|
1506
|
+
setState({ status: STATES.DOWNLOADED, version: heldDownloadVersion, lastCheckError: message });
|
|
1507
|
+
log("warn", `${what} ${message} — keeping the downloaded update`);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
if (backgroundCheck || autoDownloading) {
|
|
1511
|
+
setState({ status: STATES.IDLE, progress: 0, error: null, lastCheckError: message, preparing: false });
|
|
1512
|
+
log("warn", `${what} ${message} — will try again at the next check`);
|
|
1513
|
+
return;
|
|
957
1514
|
}
|
|
1515
|
+
setState({ status: STATES.ERROR, error: message });
|
|
1516
|
+
log("error", what, message);
|
|
958
1517
|
}
|
|
959
1518
|
|
|
960
1519
|
function scheduleNextCheck() {
|
|
@@ -964,7 +1523,7 @@ function scheduleNextCheck() {
|
|
|
964
1523
|
}
|
|
965
1524
|
scheduledTimeout = setTimeout(async () => {
|
|
966
1525
|
try {
|
|
967
|
-
await checkForUpdates();
|
|
1526
|
+
await checkForUpdates({ background: true });
|
|
968
1527
|
} catch (err) {
|
|
969
1528
|
log("error", "Scheduled check threw:", err && err.message);
|
|
970
1529
|
}
|
|
@@ -976,22 +1535,30 @@ function scheduleNextCheck() {
|
|
|
976
1535
|
|
|
977
1536
|
function wireAutoUpdaterEvents() {
|
|
978
1537
|
autoUpdaterRef.on("checking-for-update", () => {
|
|
979
|
-
setState({ status: STATES.CHECKING });
|
|
1538
|
+
if (!heldDownloadVersion) setState({ status: STATES.CHECKING });
|
|
980
1539
|
});
|
|
981
1540
|
|
|
982
1541
|
autoUpdaterRef.on("update-available", (info) => {
|
|
983
1542
|
const releaseNotes =
|
|
984
1543
|
typeof info.releaseNotes === "string" ? info.releaseNotes : null;
|
|
1544
|
+
if (heldDownloadVersion && info.version === heldDownloadVersion) {
|
|
1545
|
+
setState({ status: STATES.DOWNLOADED, lastCheckError: null });
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
985
1548
|
setState({
|
|
986
1549
|
status: STATES.AVAILABLE,
|
|
987
1550
|
version: info.version || null,
|
|
988
1551
|
releaseNotes,
|
|
989
1552
|
error: null,
|
|
1553
|
+
lastCheckError: null,
|
|
990
1554
|
});
|
|
991
1555
|
log("info", `Update available: ${info.version}`);
|
|
992
1556
|
});
|
|
993
1557
|
|
|
994
1558
|
autoUpdaterRef.on("update-not-available", (info) => {
|
|
1559
|
+
// A release withdrawn since it downloaded must not install on quit.
|
|
1560
|
+
if (heldDownloadVersion) _abandonDownload();
|
|
1561
|
+
downloadEpoch += 1;
|
|
995
1562
|
// Reset to idle so the UI hides any stale "available" chip.
|
|
996
1563
|
setState({
|
|
997
1564
|
status: STATES.IDLE,
|
|
@@ -999,6 +1566,7 @@ function wireAutoUpdaterEvents() {
|
|
|
999
1566
|
progress: 0,
|
|
1000
1567
|
releaseNotes: null,
|
|
1001
1568
|
error: null,
|
|
1569
|
+
lastCheckError: null,
|
|
1002
1570
|
});
|
|
1003
1571
|
log("info", `No update available (current ${info && info.version})`);
|
|
1004
1572
|
});
|
|
@@ -1014,79 +1582,29 @@ function wireAutoUpdaterEvents() {
|
|
|
1014
1582
|
});
|
|
1015
1583
|
});
|
|
1016
1584
|
|
|
1017
|
-
autoUpdaterRef.on("update-downloaded",
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
progress: 100,
|
|
1022
|
-
error: null,
|
|
1023
|
-
});
|
|
1024
|
-
log("info", `Update downloaded: ${info && info.version}`);
|
|
1025
|
-
|
|
1026
|
-
if (!electronApi || !electronApi.dialog) {
|
|
1027
|
-
log("warn", "No dialog available — skipping restart prompt");
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
try {
|
|
1031
|
-
const isRollback =
|
|
1032
|
-
state.pinnedVersion !== null &&
|
|
1033
|
-
info &&
|
|
1034
|
-
info.version;
|
|
1035
|
-
const title = isRollback ? "Roll back ready" : "Update ready";
|
|
1036
|
-
const message = isRollback
|
|
1037
|
-
? `GAIA ${info.version} is ready to install.`
|
|
1038
|
-
: `GAIA ${info && info.version ? info.version : ""} has been downloaded.`;
|
|
1039
|
-
const detail = isRollback
|
|
1040
|
-
? `The app will restart into v${info.version}. Your chat history will be preserved.`
|
|
1041
|
-
: "Restart the app to apply the update. Your chat history will be preserved.";
|
|
1042
|
-
|
|
1043
|
-
const choice = await electronApi.dialog.showMessageBox(
|
|
1044
|
-
mainWindowRef && !mainWindowRef.isDestroyed() ? mainWindowRef : null,
|
|
1045
|
-
{
|
|
1046
|
-
type: "info",
|
|
1047
|
-
buttons: ["Restart now", "Later"],
|
|
1048
|
-
defaultId: 0,
|
|
1049
|
-
cancelId: 1,
|
|
1050
|
-
title,
|
|
1051
|
-
message,
|
|
1052
|
-
detail,
|
|
1053
|
-
}
|
|
1054
|
-
);
|
|
1055
|
-
if (choice && choice.response === 0) {
|
|
1056
|
-
// quitAndInstall starts the installer at once, so ask about running
|
|
1057
|
-
// work here; the quit that follows doesn't ask again.
|
|
1058
|
-
if (!(await confirmRestartRef())) {
|
|
1059
|
-
log("info", "Restart put off: work is still running — will install on next quit");
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
log("info", "User chose to restart — calling quitAndInstall");
|
|
1063
|
-
autoUpdaterRef.quitAndInstall(false, true);
|
|
1064
|
-
} else {
|
|
1065
|
-
log("info", "User deferred restart — will install on next quit");
|
|
1066
|
-
}
|
|
1067
|
-
} catch (err) {
|
|
1068
|
-
const message = (err && err.message) || String(err);
|
|
1069
|
-
log("error", "Failed to show restart dialog:", message);
|
|
1070
|
-
electronApi.dialog.showErrorBox(
|
|
1071
|
-
"GAIA update",
|
|
1072
|
-
`GAIA couldn't finish asking about the update restart: ${message}\n\n` +
|
|
1073
|
-
"The update installs the next time you quit GAIA."
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1585
|
+
autoUpdaterRef.on("update-downloaded", (info) => {
|
|
1586
|
+
// Set before anything waits, so installVersion sees the restart it asked for.
|
|
1587
|
+
downloadHandled = _onDownloaded(info);
|
|
1588
|
+
return downloadHandled;
|
|
1076
1589
|
});
|
|
1077
1590
|
|
|
1078
1591
|
autoUpdaterRef.on("error", (err) => {
|
|
1079
|
-
|
|
1080
|
-
status: STATES.ERROR,
|
|
1081
|
-
error: (err && err.message) || String(err),
|
|
1082
|
-
});
|
|
1083
|
-
log("error", "electron-updater error:", err && err.message);
|
|
1592
|
+
_recordFailure("electron-updater error:", err);
|
|
1084
1593
|
});
|
|
1085
1594
|
}
|
|
1086
1595
|
|
|
1087
1596
|
function registerIpcHandlers() {
|
|
1088
1597
|
if (ipcHandlersRegistered || !electronApi || !electronApi.ipcMain) return;
|
|
1089
|
-
const
|
|
1598
|
+
const ipcMain = {
|
|
1599
|
+
handle: (channel, fn) =>
|
|
1600
|
+
electronApi.ipcMain.handle(channel, async (...args) => {
|
|
1601
|
+
try {
|
|
1602
|
+
return await fn(...args);
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
throw _forRenderer(err);
|
|
1605
|
+
}
|
|
1606
|
+
}),
|
|
1607
|
+
};
|
|
1090
1608
|
|
|
1091
1609
|
ipcMain.handle("gaia:update:get-status", () => getState());
|
|
1092
1610
|
ipcMain.handle("gaia:update:check", async () => {
|
|
@@ -1100,16 +1618,22 @@ function registerIpcHandlers() {
|
|
|
1100
1618
|
await installVersion(tag);
|
|
1101
1619
|
return getState();
|
|
1102
1620
|
});
|
|
1621
|
+
ipcMain.handle("gaia:update:revert", async () => {
|
|
1622
|
+
await revertUpdate();
|
|
1623
|
+
return getState();
|
|
1624
|
+
});
|
|
1103
1625
|
ipcMain.handle("gaia:update:resume", async () => {
|
|
1104
1626
|
clearPin();
|
|
1105
1627
|
return getState();
|
|
1106
1628
|
});
|
|
1107
1629
|
ipcMain.handle("gaia:update:set-auto-update", async (_event, enabled) => {
|
|
1108
1630
|
await setAutoUpdate(enabled);
|
|
1631
|
+
if (enabled) await checkForUpdates();
|
|
1109
1632
|
return getState();
|
|
1110
1633
|
});
|
|
1111
1634
|
ipcMain.handle("gaia:update:set-include-prereleases", async (_event, enabled) => {
|
|
1112
1635
|
await setIncludePrereleases(enabled);
|
|
1636
|
+
if (enabled) await checkForUpdates();
|
|
1113
1637
|
return getState();
|
|
1114
1638
|
});
|
|
1115
1639
|
ipcMain.handle("gaia:update:download", async () => {
|
|
@@ -1132,6 +1656,7 @@ function unregisterIpcHandlers() {
|
|
|
1132
1656
|
"gaia:update:check",
|
|
1133
1657
|
"gaia:update:list-releases",
|
|
1134
1658
|
"gaia:update:install-version",
|
|
1659
|
+
"gaia:update:revert",
|
|
1135
1660
|
"gaia:update:resume",
|
|
1136
1661
|
"gaia:update:set-auto-update",
|
|
1137
1662
|
"gaia:update:set-include-prereleases",
|
|
@@ -1162,6 +1687,10 @@ function unregisterIpcHandlers() {
|
|
|
1162
1687
|
* @param {() => Promise<boolean>} options.confirmRestart - Whether "Restart now"
|
|
1163
1688
|
* may go ahead; asked before the installer starts. Required: without it an
|
|
1164
1689
|
* update restart would silently cut off a running answer.
|
|
1690
|
+
* @param {(version: string) => Promise<void>} [options.prefetchBackend] - Fetch
|
|
1691
|
+
* the backend `version` needs before the restart, so its first launch is offline.
|
|
1692
|
+
* @param {(version: string) => boolean} [options.isBackendPrefetched] - Whether
|
|
1693
|
+
* that fetch finished, so a restart no longer needs the internet.
|
|
1165
1694
|
*/
|
|
1166
1695
|
function init(mainWindow, options = {}) {
|
|
1167
1696
|
if (typeof options.confirmRestart !== "function") {
|
|
@@ -1171,6 +1700,10 @@ function init(mainWindow, options = {}) {
|
|
|
1171
1700
|
}
|
|
1172
1701
|
mainWindowRef = mainWindow || null;
|
|
1173
1702
|
confirmRestartRef = options.confirmRestart;
|
|
1703
|
+
if (options.prefetchBackend) {
|
|
1704
|
+
prefetchBackendRef = options.prefetchBackend;
|
|
1705
|
+
isBackendPrefetchedRef = options.isBackendPrefetched;
|
|
1706
|
+
}
|
|
1174
1707
|
|
|
1175
1708
|
// Disabled path — short-circuit BEFORE touching any Electron APIs so
|
|
1176
1709
|
// this works in plain Node tests where require('electron') returns a
|
|
@@ -1220,10 +1753,11 @@ function init(mainWindow, options = {}) {
|
|
|
1220
1753
|
dialog: electron.dialog,
|
|
1221
1754
|
ipcMain: electron.ipcMain,
|
|
1222
1755
|
app: electron.app,
|
|
1756
|
+
net: electron.net,
|
|
1223
1757
|
};
|
|
1224
1758
|
autoUpdaterRef = electronUpdater.autoUpdater;
|
|
1225
1759
|
// The Terminal UI may have turned auto-update off since the last check.
|
|
1226
|
-
electron.app.on("before-quit",
|
|
1760
|
+
electron.app.on("before-quit", _onBeforeQuit);
|
|
1227
1761
|
|
|
1228
1762
|
// Record the running version for getState() / listReleases().
|
|
1229
1763
|
try {
|
|
@@ -1231,6 +1765,8 @@ function init(mainWindow, options = {}) {
|
|
|
1231
1765
|
} catch {
|
|
1232
1766
|
// ignore
|
|
1233
1767
|
}
|
|
1768
|
+
_readInstallOutcome();
|
|
1769
|
+
state.previousVersion = previousVersion();
|
|
1234
1770
|
} catch (err) {
|
|
1235
1771
|
log("error", "Failed to load electron-updater:", err && err.message);
|
|
1236
1772
|
setState({
|
|
@@ -1272,10 +1808,11 @@ function init(mainWindow, options = {}) {
|
|
|
1272
1808
|
}
|
|
1273
1809
|
|
|
1274
1810
|
// Point the updater at the configured R2 generic feed (#1724), or at GitHub
|
|
1275
|
-
// releases when none is configured.
|
|
1276
|
-
// owns the feed (rollback path), so skip.
|
|
1811
|
+
// releases when none is configured. A pin stays on its own release's feed.
|
|
1277
1812
|
let feedReady = true;
|
|
1278
|
-
if (
|
|
1813
|
+
if (state.pinnedVersion) {
|
|
1814
|
+
autoUpdaterRef.setFeedURL(_releaseFeed(state.pinnedVersion));
|
|
1815
|
+
} else {
|
|
1279
1816
|
feedReady = _applyForwardFeed();
|
|
1280
1817
|
}
|
|
1281
1818
|
|
|
@@ -1289,7 +1826,12 @@ function init(mainWindow, options = {}) {
|
|
|
1289
1826
|
// First check after CHECK_DELAY_MS, then every CHECK_INTERVAL_MS.
|
|
1290
1827
|
initialCheckTimeout = setTimeout(async () => {
|
|
1291
1828
|
try {
|
|
1292
|
-
await
|
|
1829
|
+
await _seedInstalledBlockmap();
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
log("warn", "Couldn't cache the installed version's blockmap; the next update downloads in full:", err && err.message);
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
await checkForUpdates({ background: true });
|
|
1293
1835
|
} catch (err) {
|
|
1294
1836
|
log("error", "Initial check threw:", err && err.message);
|
|
1295
1837
|
}
|
|
@@ -1302,6 +1844,22 @@ function init(mainWindow, options = {}) {
|
|
|
1302
1844
|
);
|
|
1303
1845
|
}
|
|
1304
1846
|
|
|
1847
|
+
function _onBeforeQuit(event) {
|
|
1848
|
+
_refreshPreferences();
|
|
1849
|
+
if (state.status !== STATES.DOWNLOADED || !autoUpdaterRef || !autoUpdaterRef.autoInstallOnAppQuit) {
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
// The chip's checks, for an install nobody clicks.
|
|
1853
|
+
if ((_isOffline() && !_backendCached()) || _diskTooFullToInstall()) {
|
|
1854
|
+
log("info", "Can't finish an install now: leaving the update for a later quit");
|
|
1855
|
+
autoUpdaterRef.autoInstallOnAppQuit = false;
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
// The quit guard kept the app open, so nothing is installing.
|
|
1859
|
+
if (event && event.defaultPrevented) return;
|
|
1860
|
+
_recordInstallAttempt();
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1305
1863
|
/** Tear down timers and IPC handlers. Safe to call multiple times. */
|
|
1306
1864
|
function destroy() {
|
|
1307
1865
|
if (initialCheckTimeout) {
|
|
@@ -1313,7 +1871,7 @@ function destroy() {
|
|
|
1313
1871
|
scheduledTimeout = null;
|
|
1314
1872
|
}
|
|
1315
1873
|
unregisterIpcHandlers();
|
|
1316
|
-
if (electronApi) electronApi.app.removeListener("before-quit",
|
|
1874
|
+
if (electronApi) electronApi.app.removeListener("before-quit", _onBeforeQuit);
|
|
1317
1875
|
mainWindowRef = null;
|
|
1318
1876
|
// Keep `initialized` true — calling init() again after destroy() is not a
|
|
1319
1877
|
// supported lifecycle and we'd need to re-wire electron-updater events
|
|
@@ -1327,10 +1885,16 @@ module.exports = {
|
|
|
1327
1885
|
getState,
|
|
1328
1886
|
listReleases,
|
|
1329
1887
|
installVersion,
|
|
1888
|
+
previousVersion,
|
|
1889
|
+
revertUpdate,
|
|
1890
|
+
reportStartFailure,
|
|
1891
|
+
clearStartFailure,
|
|
1330
1892
|
clearPin,
|
|
1331
1893
|
setAutoUpdate,
|
|
1332
1894
|
setIncludePrereleases,
|
|
1333
1895
|
downloadUpdate,
|
|
1334
1896
|
restartToInstall,
|
|
1335
1897
|
STATES,
|
|
1898
|
+
// For tests: init() runs it on a timer.
|
|
1899
|
+
_seedInstalledBlockmap,
|
|
1336
1900
|
};
|