@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
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
* - ensureUv() → Promise<void>
|
|
17
17
|
* - installBackend(opts) → Promise<void>
|
|
18
18
|
* - ensureBackend(opts) → Promise<string> (returns gaia bin path)
|
|
19
|
+
* - isBackendCurrent() → boolean (nothing to install or refresh)
|
|
20
|
+
* - stopRunningDaemon(opts) → Promise<void> (before the venv is touched)
|
|
19
21
|
* - getInstalledVersion(gaiaBin) → string | null
|
|
20
22
|
* - findGaiaBin() → string | null
|
|
21
23
|
* - getState() / setState() → state machine helpers
|
|
22
24
|
* - getLogPath() / getStatePath() → path helpers
|
|
25
|
+
* - readLogTail(maxLines) → string[]
|
|
23
26
|
* - runPreChecks(opts) → Promise<PreCheckResult>
|
|
24
27
|
* - STATES → state name constants
|
|
25
28
|
*
|
|
@@ -54,10 +57,16 @@ const GAIA_PYTHON_BIN = IS_WINDOWS
|
|
|
54
57
|
|
|
55
58
|
const STATE_FILE = path.join(GAIA_HOME, "electron-install-state.json");
|
|
56
59
|
const LOG_FILE = path.join(GAIA_HOME, "electron-install.log");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
60
|
+
// hub/installer.py writes this once the flagship is installed; first-run setup installs it.
|
|
61
|
+
const FLAGSHIP_SENTINEL = path.join(GAIA_HOME, "agents", "gaia", ".installed");
|
|
62
|
+
/** `gaia init --agent-only` kept the installed agent: the hub couldn't be reached. */
|
|
63
|
+
const AGENT_KEPT_EXIT = 5;
|
|
64
|
+
|
|
65
|
+
// Room for what this installer writes: Python 3.12, uv's wheel cache and the
|
|
66
|
+
// `amd-gaia[ui]` venv (0.3 GB on macOS arm64 for 0.25.1rc5, under 1 GB in
|
|
67
|
+
// all). Models are not installed here; first-run setup checks the disk for
|
|
68
|
+
// the download the user picks.
|
|
69
|
+
const MIN_DISK_SPACE_BYTES = 2 * 1024 * 1024 * 1024; // 2 GB
|
|
61
70
|
const NETWORK_CHECK_HOSTS = Object.freeze([
|
|
62
71
|
"https://pypi.org/simple/",
|
|
63
72
|
"https://astral.sh",
|
|
@@ -131,24 +140,38 @@ const STAGES = Object.freeze({
|
|
|
131
140
|
ENSURE_UV: "ensure-uv",
|
|
132
141
|
CREATE_VENV: "create-venv",
|
|
133
142
|
INSTALL_PACKAGE: "install-package",
|
|
134
|
-
GAIA_INIT: "gaia-init",
|
|
135
143
|
VERIFY: "verify",
|
|
144
|
+
// After the package, and outside the weighted bar: it has no percent to report.
|
|
145
|
+
AGENT_REFRESH: "agent-refresh",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Why an install failed, for what the app tells the user to do about it.
|
|
149
|
+
const FAILURE_REASONS = Object.freeze({
|
|
150
|
+
DISK: "disk",
|
|
151
|
+
NETWORK: "network",
|
|
152
|
+
TLS: "tls",
|
|
153
|
+
LOCKED: "locked",
|
|
154
|
+
DAEMON: "daemon",
|
|
136
155
|
});
|
|
137
156
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
// `gaia daemon stop` can take about 90 s: up to 45 s for a graceful exit, then
|
|
158
|
+
// terminating the daemon and its agents, then up to 20 s for GAIA's Lemonade Server.
|
|
159
|
+
const DAEMON_STOP_TIMEOUT_MS = 120 * 1000;
|
|
160
|
+
/** `gaia daemon stop` exit code: the daemon stopped, GAIA's Lemonade Server did not. */
|
|
161
|
+
const DAEMON_STOP_LEMONADE_EXIT = 3;
|
|
162
|
+
const DAEMON_INSTANCE_FILE = path.join(GAIA_HOME, "host", "instance.json");
|
|
163
|
+
|
|
164
|
+
// Stage an older installer recorded while it ran `gaia init`; the package was
|
|
165
|
+
// already in place by then.
|
|
166
|
+
const LEGACY_SETUP_STAGE = "gaia-init";
|
|
143
167
|
|
|
144
168
|
// Weight each stage contributes to the overall 0-100 progress.
|
|
145
169
|
// Sum must equal 100.
|
|
146
170
|
const STAGE_WEIGHTS = {
|
|
147
171
|
[STAGES.PRE_CHECKS]: 2,
|
|
148
172
|
[STAGES.ENSURE_UV]: 8,
|
|
149
|
-
[STAGES.CREATE_VENV]:
|
|
150
|
-
[STAGES.INSTALL_PACKAGE]:
|
|
151
|
-
[STAGES.GAIA_INIT]: 28,
|
|
173
|
+
[STAGES.CREATE_VENV]: 15,
|
|
174
|
+
[STAGES.INSTALL_PACKAGE]: 73,
|
|
152
175
|
[STAGES.VERIFY]: 2,
|
|
153
176
|
};
|
|
154
177
|
|
|
@@ -157,7 +180,6 @@ const STAGE_ORDER = [
|
|
|
157
180
|
STAGES.ENSURE_UV,
|
|
158
181
|
STAGES.CREATE_VENV,
|
|
159
182
|
STAGES.INSTALL_PACKAGE,
|
|
160
|
-
STAGES.GAIA_INIT,
|
|
161
183
|
STAGES.VERIFY,
|
|
162
184
|
];
|
|
163
185
|
|
|
@@ -175,10 +197,6 @@ let logStream = null;
|
|
|
175
197
|
*/
|
|
176
198
|
let logRotatedThisSession = false;
|
|
177
199
|
|
|
178
|
-
function isTruthyEnv(value) {
|
|
179
|
-
return /^(1|true|yes|on)$/i.test(String(value || ""));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
200
|
function ensureGaiaHome() {
|
|
183
201
|
try {
|
|
184
202
|
if (!fs.existsSync(GAIA_HOME)) {
|
|
@@ -321,6 +339,16 @@ function getStatePath() {
|
|
|
321
339
|
return STATE_FILE;
|
|
322
340
|
}
|
|
323
341
|
|
|
342
|
+
/** The end of the install log, or nothing when no install has written one. */
|
|
343
|
+
function readLogTail(maxLines = 40) {
|
|
344
|
+
try {
|
|
345
|
+
return fs.readFileSync(LOG_FILE, "utf8").split(/\r?\n/).filter(Boolean).slice(-maxLines);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
if (err.code === "ENOENT") return [];
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
324
352
|
// ── State machine ────────────────────────────────────────────────────────────
|
|
325
353
|
|
|
326
354
|
/**
|
|
@@ -396,7 +424,8 @@ function computeOverallPercent(stage, withinStagePercent) {
|
|
|
396
424
|
function makeProgressReporter(onProgress) {
|
|
397
425
|
const safe = typeof onProgress === "function" ? onProgress : () => {};
|
|
398
426
|
return function report(stage, withinStagePercent, message) {
|
|
399
|
-
const percent =
|
|
427
|
+
const percent =
|
|
428
|
+
withinStagePercent === null ? null : computeOverallPercent(stage, withinStagePercent);
|
|
400
429
|
try {
|
|
401
430
|
safe(stage, percent, message || "");
|
|
402
431
|
} catch (err) {
|
|
@@ -405,148 +434,6 @@ function makeProgressReporter(onProgress) {
|
|
|
405
434
|
};
|
|
406
435
|
}
|
|
407
436
|
|
|
408
|
-
// ── gaia init progress (`gaia init --progress-json`) ─────────────────────────
|
|
409
|
-
|
|
410
|
-
// Where the plan and the end of the downloads land in the GAIA_INIT slice; the
|
|
411
|
-
// rest of the slice is Lemonade setup before and verification after.
|
|
412
|
-
const INIT_PLAN_PERCENT = 5;
|
|
413
|
-
const INIT_DOWNLOADED_PERCENT = 95;
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Read one line of `gaia init --progress-json` output. Returns `{ event }` for
|
|
417
|
-
* a progress event, `{ error }` for a line that looks like one but is not, and
|
|
418
|
-
* null for human output.
|
|
419
|
-
*/
|
|
420
|
-
function parseInitEventLine(line) {
|
|
421
|
-
const text = String(line || "").trim();
|
|
422
|
-
if (!text.startsWith("{")) return null;
|
|
423
|
-
let value;
|
|
424
|
-
try {
|
|
425
|
-
value = JSON.parse(text);
|
|
426
|
-
} catch (err) {
|
|
427
|
-
return { error: err.message };
|
|
428
|
-
}
|
|
429
|
-
if (!value || typeof value !== "object" || typeof value.event !== "string") {
|
|
430
|
-
return { error: 'no "event" field' };
|
|
431
|
-
}
|
|
432
|
-
return { event: value };
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function formatGB(bytes) {
|
|
436
|
-
return `${(Math.max(0, bytes || 0) / 1e9).toFixed(1)} GB`;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function formatSpeed(bytesPerSecond) {
|
|
440
|
-
return `${(bytesPerSecond / 1e6).toFixed(1)} MB/s`;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function formatTimeLeft(seconds) {
|
|
444
|
-
if (seconds < 60) return "less than a minute left";
|
|
445
|
-
const minutes = Math.round(seconds / 60);
|
|
446
|
-
if (minutes < 60) return `about ${minutes} min left`;
|
|
447
|
-
return `about ${Math.floor(minutes / 60)} h ${minutes % 60} min left`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Map `gaia init --progress-json` events onto the GAIA_INIT slice of the bar.
|
|
452
|
-
* `report(withinStagePercent, message)` receives a percent that never goes
|
|
453
|
-
* back. `onLine` returns true for the lines too frequent to log.
|
|
454
|
-
*/
|
|
455
|
-
function createInitProgress(report, logFn = log) {
|
|
456
|
-
const models = new Map();
|
|
457
|
-
let percent = 0;
|
|
458
|
-
|
|
459
|
-
const advance = (within, message) => {
|
|
460
|
-
percent = Math.max(percent, Math.round(within));
|
|
461
|
-
report(percent, message);
|
|
462
|
-
};
|
|
463
|
-
const totals = () => {
|
|
464
|
-
let done = 0;
|
|
465
|
-
let total = 0;
|
|
466
|
-
for (const m of models.values()) {
|
|
467
|
-
total += m.total;
|
|
468
|
-
done += Math.min(m.done, m.total);
|
|
469
|
-
}
|
|
470
|
-
return { done, total };
|
|
471
|
-
};
|
|
472
|
-
const downloadPercent = () => {
|
|
473
|
-
const { done, total } = totals();
|
|
474
|
-
const share = total > 0 ? done / total : 0;
|
|
475
|
-
return INIT_PLAN_PERCENT + (INIT_DOWNLOADED_PERCENT - INIT_PLAN_PERCENT) * share;
|
|
476
|
-
};
|
|
477
|
-
const model = (id) => {
|
|
478
|
-
if (!models.has(id)) models.set(id, { done: 0, total: 0 });
|
|
479
|
-
return models.get(id);
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
function handle(event) {
|
|
483
|
-
switch (event.event) {
|
|
484
|
-
case "step":
|
|
485
|
-
advance(percent, `Step ${event.step} of ${event.total}: ${event.message}`);
|
|
486
|
-
return true;
|
|
487
|
-
case "plan":
|
|
488
|
-
for (const m of event.models || []) {
|
|
489
|
-
if (!m.downloaded) model(m.id).total = m.bytes_total || 0;
|
|
490
|
-
}
|
|
491
|
-
advance(
|
|
492
|
-
INIT_PLAN_PERCENT,
|
|
493
|
-
event.bytes_total > 0
|
|
494
|
-
? `Downloading ${formatGB(event.bytes_total)} of models`
|
|
495
|
-
: "Models already downloaded"
|
|
496
|
-
);
|
|
497
|
-
return true;
|
|
498
|
-
case "progress": {
|
|
499
|
-
const m = model(event.model);
|
|
500
|
-
m.done = event.bytes_done || 0;
|
|
501
|
-
m.total = Math.max(event.bytes_total || 0, m.done);
|
|
502
|
-
const parts = [
|
|
503
|
-
`Downloading ${event.model} (${event.index} of ${event.count}): ` +
|
|
504
|
-
`${formatGB(m.done)} of ${formatGB(m.total)}`,
|
|
505
|
-
];
|
|
506
|
-
if (event.speed_bps > 0) {
|
|
507
|
-
const { done, total } = totals();
|
|
508
|
-
parts.push(
|
|
509
|
-
`at ${formatSpeed(event.speed_bps)}, ` +
|
|
510
|
-
formatTimeLeft((total - done) / event.speed_bps)
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
advance(downloadPercent(), parts.join(" "));
|
|
514
|
-
return true;
|
|
515
|
-
}
|
|
516
|
-
case "model_done": {
|
|
517
|
-
const m = model(event.model);
|
|
518
|
-
m.done = m.total;
|
|
519
|
-
advance(downloadPercent(), `${event.model} ready`);
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
case "done":
|
|
523
|
-
advance(
|
|
524
|
-
event.ok ? INIT_DOWNLOADED_PERCENT : percent,
|
|
525
|
-
event.ok ? "Models downloaded, finishing setup" : "Model download did not finish"
|
|
526
|
-
);
|
|
527
|
-
return true;
|
|
528
|
-
case "error":
|
|
529
|
-
advance(percent, String(event.message || "gaia init reported an error").trim());
|
|
530
|
-
return true;
|
|
531
|
-
default:
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function onLine(line) {
|
|
537
|
-
const parsed = parseInitEventLine(line);
|
|
538
|
-
if (!parsed) return false;
|
|
539
|
-
if (parsed.error) {
|
|
540
|
-
logFn(`Malformed gaia init progress line (${parsed.error}): ${line}`);
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
handle(parsed.event);
|
|
544
|
-
return parsed.event.event === "progress";
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return { onLine, handle };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
437
|
// ── Command helpers ──────────────────────────────────────────────────────────
|
|
551
438
|
|
|
552
439
|
/**
|
|
@@ -581,9 +468,13 @@ function findGaiaBin() {
|
|
|
581
468
|
*
|
|
582
469
|
* `onLine(line)` sees each whole stdout line as it arrives; a line it returns
|
|
583
470
|
* true for is left out of the log.
|
|
471
|
+
*
|
|
472
|
+
* When `signal` aborts, the child and everything it started are stopped and
|
|
473
|
+
* the Promise rejects with InstallCancelled once they are gone.
|
|
584
474
|
*/
|
|
585
|
-
function runCommand(cmd, args, { env, stageLabel, onLine } = {}) {
|
|
586
|
-
return new
|
|
475
|
+
function runCommand(cmd, args, { env, stageLabel, onLine, signal, timeoutMs } = {}) {
|
|
476
|
+
if (signal && signal.aborted) return Promise.reject(new InstallCancelled());
|
|
477
|
+
return new Promise((resolve, reject) => {
|
|
587
478
|
log(`$ ${cmd} ${args.join(" ")}`);
|
|
588
479
|
let proc;
|
|
589
480
|
try {
|
|
@@ -600,6 +491,21 @@ function runCommand(cmd, args, { env, stageLabel, onLine } = {}) {
|
|
|
600
491
|
return;
|
|
601
492
|
}
|
|
602
493
|
|
|
494
|
+
let stopping = false;
|
|
495
|
+
const onAbort = () => {
|
|
496
|
+
stopping = true;
|
|
497
|
+
stopTimer();
|
|
498
|
+
log(`Stopping ${cmd}: GAIA is quitting`);
|
|
499
|
+
require("./process-tree.cjs")
|
|
500
|
+
.killProcessTree(proc, { label: stageLabel || cmd, log, logError })
|
|
501
|
+
.then(() => reject(new InstallCancelled()), reject);
|
|
502
|
+
};
|
|
503
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
504
|
+
const finish = (result) => {
|
|
505
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
506
|
+
if (!stopping) resolve(result);
|
|
507
|
+
};
|
|
508
|
+
|
|
603
509
|
let stdout = "";
|
|
604
510
|
let stderr = "";
|
|
605
511
|
let partial = "";
|
|
@@ -647,18 +553,123 @@ function runCommand(cmd, args, { env, stageLabel, onLine } = {}) {
|
|
|
647
553
|
});
|
|
648
554
|
});
|
|
649
555
|
|
|
556
|
+
// From `timers`, not the globals: the installer tests run this file in a bare vm context.
|
|
557
|
+
let timer = null;
|
|
558
|
+
if (timeoutMs) {
|
|
559
|
+
timer = require("timers").setTimeout(() => {
|
|
560
|
+
logError(`${cmd} ${args.join(" ")} did not finish within ${Math.round(timeoutMs / 1000)}s — ending it`);
|
|
561
|
+
killTree(proc);
|
|
562
|
+
finish({ code: null, stdout, stderr, timedOut: true });
|
|
563
|
+
}, timeoutMs);
|
|
564
|
+
}
|
|
565
|
+
const stopTimer = () => {
|
|
566
|
+
if (timer) require("timers").clearTimeout(timer);
|
|
567
|
+
};
|
|
568
|
+
|
|
650
569
|
proc.on("error", (err) => {
|
|
570
|
+
stopTimer();
|
|
651
571
|
logError(`${cmd} error: ${err.message}`);
|
|
652
|
-
|
|
572
|
+
finish({ code: -1, stdout, stderr, error: err });
|
|
653
573
|
});
|
|
654
574
|
|
|
655
575
|
proc.on("exit", (code) => {
|
|
576
|
+
stopTimer();
|
|
656
577
|
log(` exit code: ${code}`);
|
|
657
|
-
|
|
578
|
+
finish({ code, stdout, stderr });
|
|
658
579
|
});
|
|
659
580
|
});
|
|
660
581
|
}
|
|
661
582
|
|
|
583
|
+
/** End `proc` and what it started: a Windows venv launcher runs Python as a child. */
|
|
584
|
+
function killTree(proc) {
|
|
585
|
+
if (IS_WINDOWS) {
|
|
586
|
+
const r = spawnSync("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
|
|
587
|
+
windowsHide: true,
|
|
588
|
+
timeout: 30 * 1000,
|
|
589
|
+
});
|
|
590
|
+
if (r.error || r.status !== 0) {
|
|
591
|
+
logError(`taskkill /PID ${proc.pid} /T /F failed: ${r.error ? r.error.message : `exit ${r.status}`}`);
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
proc.kill("SIGKILL");
|
|
597
|
+
} catch (err) {
|
|
598
|
+
logError(`Could not end pid ${proc.pid}: ${err.message}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** The pid of the GAIA daemon ~/.gaia/host/instance.json records, when that process is alive. */
|
|
603
|
+
function liveDaemonPid() {
|
|
604
|
+
let pid;
|
|
605
|
+
try {
|
|
606
|
+
pid = Number(JSON.parse(fs.readFileSync(DAEMON_INSTANCE_FILE, "utf8")).pid);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
if (err.code !== "ENOENT") log(`Could not read ${DAEMON_INSTANCE_FILE}: ${err.message}`);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
612
|
+
try {
|
|
613
|
+
process.kill(pid, 0);
|
|
614
|
+
return pid;
|
|
615
|
+
} catch (err) {
|
|
616
|
+
// EPERM: the process exists but belongs to someone else.
|
|
617
|
+
return err.code === "EPERM" ? pid : null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Stop the GAIA daemon (and the agents it runs) that the existing venv may be
|
|
623
|
+
* running, so the install doesn't trip over its open files. Throws an
|
|
624
|
+
* InstallError naming the daemon only when one is still alive afterwards: a
|
|
625
|
+
* stop that fails with no daemon running (a broken venv, say) must not block
|
|
626
|
+
* the reinstall that repairs it.
|
|
627
|
+
*/
|
|
628
|
+
async function stopRunningDaemon({ timeoutMs = DAEMON_STOP_TIMEOUT_MS, signal } = {}) {
|
|
629
|
+
if (!fs.existsSync(GAIA_BIN) || !fs.existsSync(GAIA_PYTHON_BIN)) return;
|
|
630
|
+
const result = await runCommand(GAIA_BIN, ["daemon", "stop"], {
|
|
631
|
+
stageLabel: "daemon",
|
|
632
|
+
timeoutMs,
|
|
633
|
+
signal,
|
|
634
|
+
// gaia prints emoji, which a Windows pipe's code page can't encode.
|
|
635
|
+
env: { ...process.env, PYTHONIOENCODING: "utf-8" },
|
|
636
|
+
});
|
|
637
|
+
if (result.code === 0) return;
|
|
638
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
639
|
+
if (result.code === 2 && /invalid choice: '?daemon'?/.test(output)) {
|
|
640
|
+
log("The installed GAIA predates the daemon — nothing to stop");
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const pid = liveDaemonPid();
|
|
644
|
+
if (pid === null) {
|
|
645
|
+
if (result.code === DAEMON_STOP_LEMONADE_EXIT) {
|
|
646
|
+
log(
|
|
647
|
+
"The GAIA daemon stopped, but GAIA's Lemonade Server did not (see the lines above). " +
|
|
648
|
+
"It lives outside the backend's files, so the install goes on."
|
|
649
|
+
);
|
|
650
|
+
} else {
|
|
651
|
+
log(
|
|
652
|
+
`gaia daemon stop ${result.timedOut ? "timed out" : `exited ${result.code}`}, ` +
|
|
653
|
+
"but no GAIA daemon is running — continuing"
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const why = result.timedOut
|
|
659
|
+
? `did not stop within ${Math.round(timeoutMs / 1000)} seconds`
|
|
660
|
+
: `could not be stopped (gaia daemon stop exit ${result.code})`;
|
|
661
|
+
log(`To stop it by hand: ${GAIA_BIN} daemon stop`);
|
|
662
|
+
throw new InstallError(
|
|
663
|
+
`GAIA's background service (the GAIA daemon, pid ${pid}) ${why}, so its files can't be updated.`,
|
|
664
|
+
{
|
|
665
|
+
stage: STAGES.CREATE_VENV,
|
|
666
|
+
code: result.code,
|
|
667
|
+
reason: FAILURE_REASONS.DAEMON,
|
|
668
|
+
suggestion: `It has no window to close. Restart your PC, then try again. The log at ${LOG_FILE} has the details.`,
|
|
669
|
+
}
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
662
673
|
/**
|
|
663
674
|
* Detect the Windows "file in use" signature in command output. An upgrade's
|
|
664
675
|
* `uv pip install --refresh` can't replace gaia.exe while a previous GAIA
|
|
@@ -669,10 +680,95 @@ function isFileLockedError(output) {
|
|
|
669
680
|
return /os error 32/i.test(output) || /being used by another process/i.test(output);
|
|
670
681
|
}
|
|
671
682
|
|
|
683
|
+
/** The files uv could not replace, as absolute paths, in the order it named them. */
|
|
684
|
+
function lockedFiles(output) {
|
|
685
|
+
const files = [];
|
|
686
|
+
for (const m of String(output || "").matchAll(/failed to \w+ (?:file|directory) `([^`]+)`/gi)) {
|
|
687
|
+
const file = path.resolve(m[1]);
|
|
688
|
+
if (!files.includes(file)) files.push(file);
|
|
689
|
+
}
|
|
690
|
+
return files;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const MOVED_ASIDE_SUFFIX = /\.old-\d+$/i;
|
|
694
|
+
|
|
695
|
+
function sameFile(a, b) {
|
|
696
|
+
return path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Rename each locked launcher in the venv's Scripts folder to <name>.old-<ts>
|
|
701
|
+
* so uv can write a fresh one (#6570). A launcher something is running can't
|
|
702
|
+
* be renamed; it is left alone. Returns [{ file, aside }] for those moved.
|
|
703
|
+
*/
|
|
704
|
+
function moveLockedAside(files) {
|
|
705
|
+
const scripts = path.join(GAIA_VENV, "Scripts");
|
|
706
|
+
const stamp = Date.now();
|
|
707
|
+
const moved = [];
|
|
708
|
+
for (const file of files) {
|
|
709
|
+
if (!/\.exe$/i.test(file) || !sameFile(path.dirname(file), scripts)) continue;
|
|
710
|
+
const aside = `${file}.old-${stamp}`;
|
|
711
|
+
try {
|
|
712
|
+
fs.renameSync(file, aside);
|
|
713
|
+
moved.push({ file, aside });
|
|
714
|
+
log(`Moved locked ${file} aside to ${path.basename(aside)}`);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
log(`Could not move locked ${file} aside (${err.code || err.message}); it stays where it is`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return moved;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Put back every moved launcher uv did not replace, so a failed upgrade leaves the venv whole. */
|
|
723
|
+
function putBackUnreplaced(moved) {
|
|
724
|
+
for (const { file, aside } of moved) {
|
|
725
|
+
if (fs.existsSync(file)) {
|
|
726
|
+
log(`${path.basename(file)} was reinstalled; ${path.basename(aside)} is deleted on a later install`);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
fs.renameSync(aside, file);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
throw new Error(`Could not put ${aside} back at ${file}: ${err.message}`);
|
|
733
|
+
}
|
|
734
|
+
log(`Put ${path.basename(file)} back from ${path.basename(aside)}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Tidy what an earlier upgrade moved aside. A .old file whose original was
|
|
740
|
+
* reinstalled is deleted unless still in use; one whose original is missing
|
|
741
|
+
* is put back, newest first.
|
|
742
|
+
*/
|
|
743
|
+
function removeMovedLaunchers() {
|
|
744
|
+
const dir = path.join(GAIA_VENV, "Scripts");
|
|
745
|
+
const stampOf = (name) => Number(/\.old-(\d+)$/i.exec(name)[1]);
|
|
746
|
+
const leftovers = listDir(dir)
|
|
747
|
+
.filter((name) => MOVED_ASIDE_SUFFIX.test(name))
|
|
748
|
+
.sort((a, b) => stampOf(b) - stampOf(a));
|
|
749
|
+
for (const name of leftovers) {
|
|
750
|
+
const aside = path.join(dir, name);
|
|
751
|
+
const file = aside.replace(MOVED_ASIDE_SUFFIX, "");
|
|
752
|
+
if (!fs.existsSync(file)) {
|
|
753
|
+
fs.renameSync(aside, file);
|
|
754
|
+
log(`Put ${path.basename(file)} back from ${name}: no newer copy was installed`);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
fs.unlinkSync(aside);
|
|
759
|
+
log(`Removed ${aside}, moved aside by an earlier upgrade`);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
// Windows refuses to delete a program that is running (EPERM) or open (EBUSY).
|
|
762
|
+
if (err.code !== "EBUSY" && err.code !== "EPERM") throw err;
|
|
763
|
+
log(`Left ${aside} in place: it is still in use (${err.code})`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
672
768
|
/**
|
|
673
769
|
* Detect a transient network failure in `uv pip install` output so the
|
|
674
770
|
* install stage can retry instead of failing the whole bootstrap. The
|
|
675
|
-
* install stage downloads transitive deps (numpy, faiss,
|
|
771
|
+
* install stage downloads transitive deps (numpy, faiss, pypdfium2, …) from
|
|
676
772
|
* PyPI; a single mid-stream hiccup ("stream closed because of a broken pipe")
|
|
677
773
|
* otherwise fails the entire backend install — and, in the release pipeline,
|
|
678
774
|
* the whole AppImage smoke test that gates publishing.
|
|
@@ -1078,15 +1174,34 @@ async function runPreChecks(opts = {}) {
|
|
|
1078
1174
|
// ── uv install ───────────────────────────────────────────────────────────────
|
|
1079
1175
|
|
|
1080
1176
|
class InstallError extends Error {
|
|
1081
|
-
constructor(message, { stage, code, suggestion } = {}) {
|
|
1177
|
+
constructor(message, { stage, code, suggestion, reason, lockedFiles } = {}) {
|
|
1082
1178
|
super(message);
|
|
1083
1179
|
this.name = "InstallError";
|
|
1084
1180
|
this.stage = stage || null;
|
|
1085
1181
|
this.code = code || null;
|
|
1086
1182
|
this.suggestion = suggestion || null;
|
|
1183
|
+
this.reason = reason || null;
|
|
1184
|
+
// The files another program held open, for main to find who holds them.
|
|
1185
|
+
this.lockedFiles = lockedFiles || null;
|
|
1087
1186
|
}
|
|
1088
1187
|
}
|
|
1089
1188
|
|
|
1189
|
+
/** The caller's signal stopped the install: the app is quitting, nothing failed. */
|
|
1190
|
+
class InstallCancelled extends Error {
|
|
1191
|
+
constructor() {
|
|
1192
|
+
super("The install stopped because GAIA is quitting; it continues on the next launch.");
|
|
1193
|
+
this.name = "InstallCancelled";
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/** Which FAILURE_REASONS a uv failure's output shows, or null. */
|
|
1198
|
+
function failureReason(output) {
|
|
1199
|
+
if (isFileLockedError(output)) return FAILURE_REASONS.LOCKED;
|
|
1200
|
+
if (isTlsCertError(output)) return FAILURE_REASONS.TLS;
|
|
1201
|
+
if (isTransientNetworkError(output)) return FAILURE_REASONS.NETWORK;
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1090
1205
|
/**
|
|
1091
1206
|
* Which `extraResources` subdirectory holds the bundled uv for this host.
|
|
1092
1207
|
* Returns null for platforms we don't yet ship a binary for (falls through
|
|
@@ -1303,7 +1418,7 @@ function addManagedBinToPath() {
|
|
|
1303
1418
|
*
|
|
1304
1419
|
* Throws InstallError on failure.
|
|
1305
1420
|
*/
|
|
1306
|
-
async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
1421
|
+
async function ensureUv({ onProgress, isPackaged, signal } = {}) {
|
|
1307
1422
|
const report = makeProgressReporter(onProgress);
|
|
1308
1423
|
report(STAGES.ENSURE_UV, 0, "Checking uv (Python package manager)");
|
|
1309
1424
|
|
|
@@ -1402,13 +1517,13 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
1402
1517
|
"-Command",
|
|
1403
1518
|
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
1404
1519
|
],
|
|
1405
|
-
{ stageLabel: "uv-install-dev" }
|
|
1520
|
+
{ stageLabel: "uv-install-dev", signal }
|
|
1406
1521
|
);
|
|
1407
1522
|
} else {
|
|
1408
1523
|
result = await runCommand(
|
|
1409
1524
|
"bash",
|
|
1410
1525
|
["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
|
|
1411
|
-
{ stageLabel: "uv-install-dev" }
|
|
1526
|
+
{ stageLabel: "uv-install-dev", signal }
|
|
1412
1527
|
);
|
|
1413
1528
|
}
|
|
1414
1529
|
|
|
@@ -1469,7 +1584,7 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
1469
1584
|
"-Command",
|
|
1470
1585
|
"irm https://astral.sh/uv/install.ps1 | iex",
|
|
1471
1586
|
],
|
|
1472
|
-
{ stageLabel: "uv-install-packaged-rescue" }
|
|
1587
|
+
{ stageLabel: "uv-install-packaged-rescue", signal }
|
|
1473
1588
|
);
|
|
1474
1589
|
if (rescue.code === 0) {
|
|
1475
1590
|
// Ensure common install locations are present on PATH for this
|
|
@@ -1495,6 +1610,7 @@ async function ensureUv({ onProgress, isPackaged } = {}) {
|
|
|
1495
1610
|
log(`Packaged Windows: automated uv installer exited ${rescue.code}`);
|
|
1496
1611
|
}
|
|
1497
1612
|
} catch (rescueErr) {
|
|
1613
|
+
if (rescueErr instanceof InstallCancelled) throw rescueErr;
|
|
1498
1614
|
log(`Packaged Windows: rescue installer threw: ${rescueErr.message}`);
|
|
1499
1615
|
}
|
|
1500
1616
|
}
|
|
@@ -1545,6 +1661,11 @@ function resolveBackendVersion(opts = {}) {
|
|
|
1545
1661
|
}
|
|
1546
1662
|
}
|
|
1547
1663
|
|
|
1664
|
+
/** The version the app ships with, whatever backend this run was asked to install. */
|
|
1665
|
+
function appVersion() {
|
|
1666
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1548
1669
|
/**
|
|
1549
1670
|
* The PEP 440 spelling of the app's npm version: `0.25.0-rc.1` -> `0.25.0rc1`.
|
|
1550
1671
|
* pip pins and `gaia --version` use this form; a final version is unchanged.
|
|
@@ -1554,142 +1675,16 @@ function toPep440(version) {
|
|
|
1554
1675
|
return version.replace(/^(\d+\.\d+\.\d+)-rc\.(\d+)$/, "$1rc$2");
|
|
1555
1676
|
}
|
|
1556
1677
|
|
|
1557
|
-
function shouldSkipGaiaInit(opts) {
|
|
1558
|
-
return Boolean(opts.skipGaiaInit) || isTruthyEnv(process.env.GAIA_SKIP_GAIA_INIT);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
/**
|
|
1562
|
-
* Run `gaia init` for the profile the app's own setup check asks about
|
|
1563
|
-
* (setup_runner.PROFILE), so the first launch finds setup done instead of
|
|
1564
|
-
* running it a second time.
|
|
1565
|
-
*
|
|
1566
|
-
* `chooseLemonade` is asked about a Lemonade Server already running here; see
|
|
1567
|
-
* lemonadeInitArgs.
|
|
1568
|
-
*
|
|
1569
|
-
* Returns `{ init }`, "done", "failed" or "skipped", plus `initError` when it
|
|
1570
|
-
* failed. The state file records both: a backend whose setup never finished is
|
|
1571
|
-
* not installed, so the next launch runs it, and the app shows the error.
|
|
1572
|
-
*/
|
|
1573
|
-
async function runGaiaInit(gaiaBin, report, version, skipGaiaInit, chooseLemonade) {
|
|
1574
|
-
if (skipGaiaInit) {
|
|
1575
|
-
log("Skipping gaia init (skipGaiaInit=true or GAIA_SKIP_GAIA_INIT set)");
|
|
1576
|
-
return { init: "skipped" };
|
|
1577
|
-
}
|
|
1578
|
-
setState(STATES.INSTALLING, { stage: STAGES.GAIA_INIT, version });
|
|
1579
|
-
report(STAGES.GAIA_INIT, 0, GAIA_INIT_MESSAGE);
|
|
1580
|
-
const lemonadeArgs = await lemonadeInitArgs(gaiaBin, chooseLemonade, runCommand, report);
|
|
1581
|
-
const progress = createInitProgress((within, message) =>
|
|
1582
|
-
report(STAGES.GAIA_INIT, within, message)
|
|
1583
|
-
);
|
|
1584
|
-
const initResult = await runCommand(
|
|
1585
|
-
gaiaBin,
|
|
1586
|
-
["init", "--profile", "gaia", "--yes", "--skip-webui-build", "--progress-json", ...lemonadeArgs],
|
|
1587
|
-
// Unwrapped, so an error line reaches initFailureReason whole.
|
|
1588
|
-
{ stageLabel: "gaia-init", env: { ...process.env, COLUMNS: "1000" }, onLine: progress.onLine }
|
|
1589
|
-
);
|
|
1590
|
-
if (initResult.code !== 0) {
|
|
1591
|
-
// Non-fatal: the app opens and its start screen shows initError, and the
|
|
1592
|
-
// next launch runs setup again, because the state file records the failure.
|
|
1593
|
-
log(
|
|
1594
|
-
`Warning: gaia init exited with code ${initResult.code}. Continuing; setup runs again next launch.`
|
|
1595
|
-
);
|
|
1596
|
-
report(STAGES.GAIA_INIT, 100, "Lemonade Server setup did not finish");
|
|
1597
|
-
return { init: "failed", initError: initFailureReason(initResult) };
|
|
1598
|
-
}
|
|
1599
|
-
report(STAGES.GAIA_INIT, 100, "Lemonade Server setup complete");
|
|
1600
|
-
return { init: "done" };
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
/** Why `gaia init` failed, in its own words: its last ❌ line, else its last line. */
|
|
1604
|
-
function initFailureReason({ code, stdout, stderr }) {
|
|
1605
|
-
const lines = `${stdout || ""}\n${stderr || ""}`
|
|
1606
|
-
.split(/\r?\n/)
|
|
1607
|
-
.map((line) => line.trim())
|
|
1608
|
-
// --progress-json event lines are for the progress bar, not the user.
|
|
1609
|
-
.filter((line) => line && !line.startsWith('{"event"'));
|
|
1610
|
-
const marked = lines.filter((line) => line.startsWith("❌"));
|
|
1611
|
-
const reason = marked.length
|
|
1612
|
-
? marked[marked.length - 1].replace(/^❌\s*/, "")
|
|
1613
|
-
: lines[lines.length - 1];
|
|
1614
|
-
return reason || `gaia init exited with code ${code} and printed nothing.`;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
/**
|
|
1618
|
-
* Run a `gaia ... --json` command and parse what it printed.
|
|
1619
|
-
* Returns undefined after logging a warning that names the failure.
|
|
1620
|
-
*/
|
|
1621
|
-
async function readGaiaJson(run, gaiaBin, args) {
|
|
1622
|
-
const shown = `gaia ${args.join(" ")}`;
|
|
1623
|
-
const result = await run(gaiaBin, args, { stageLabel: "lemonade" });
|
|
1624
|
-
if (result.code !== 0) {
|
|
1625
|
-
log(`Warning: ${shown} exited with code ${result.code}: ${(result.stderr || result.stdout || "").trim().slice(-400)}`);
|
|
1626
|
-
return undefined;
|
|
1627
|
-
}
|
|
1628
|
-
const lines = (result.stdout || "").trim().split(/\r?\n/);
|
|
1629
|
-
try {
|
|
1630
|
-
return JSON.parse(lines[lines.length - 1]);
|
|
1631
|
-
} catch (err) {
|
|
1632
|
-
log(`Warning: ${shown} printed no JSON (${err.message}): ${(result.stdout || "").trim().slice(-400)}`);
|
|
1633
|
-
return undefined;
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
/**
|
|
1638
|
-
* Extra `gaia init` args for a Lemonade Server the user already runs.
|
|
1639
|
-
*
|
|
1640
|
-
* Asks `chooseLemonade(found)` only on a first setup (nothing saved, no
|
|
1641
|
-
* LEMONADE_BASE_URL) when a usable server answers. "external" adds
|
|
1642
|
-
* `--lemonade-url <url>`, "gaia" adds `--gaia-lemonade`, null adds nothing:
|
|
1643
|
-
* init then sets up GAIA's own and prints how to switch, as it always has.
|
|
1644
|
-
* A server that needs a key or is too old is left to the app's setup screen.
|
|
1645
|
-
*
|
|
1646
|
-
* `report` is the progress reporter: while the question is open the progress
|
|
1647
|
-
* window says setup is waiting on the user, then goes back to GAIA_INIT_MESSAGE.
|
|
1648
|
-
*/
|
|
1649
|
-
async function lemonadeInitArgs(gaiaBin, chooseLemonade, run = runCommand, report = () => {}) {
|
|
1650
|
-
if (typeof chooseLemonade !== "function") return [];
|
|
1651
|
-
const unchanged = "gaia init will set up GAIA's own Lemonade Server; to use yours later run `gaia lemonade connect <url>`.";
|
|
1652
|
-
const status = await readGaiaJson(run, gaiaBin, ["lemonade", "status", "--json"]);
|
|
1653
|
-
if (status === undefined || status === null) {
|
|
1654
|
-
log(`Warning: could not read which Lemonade Server GAIA uses; ${unchanged}`);
|
|
1655
|
-
return [];
|
|
1656
|
-
}
|
|
1657
|
-
if (status.saved_mode != null || status.source === "env") return [];
|
|
1658
|
-
const found = await readGaiaJson(run, gaiaBin, ["lemonade", "detect", "--json"]);
|
|
1659
|
-
if (found === undefined) {
|
|
1660
|
-
log(`Warning: could not look for a Lemonade Server already running; ${unchanged}`);
|
|
1661
|
-
return [];
|
|
1662
|
-
}
|
|
1663
|
-
if (!found) return [];
|
|
1664
|
-
if (found.status !== "ok") {
|
|
1665
|
-
log(`Found a Lemonade Server at ${found.url} (${found.status}); the app's setup screen offers it.`);
|
|
1666
|
-
return [];
|
|
1667
|
-
}
|
|
1668
|
-
let answer;
|
|
1669
|
-
report(STAGES.GAIA_INIT, 0, WAITING_FOR_LEMONADE_CHOICE_MESSAGE);
|
|
1670
|
-
try {
|
|
1671
|
-
answer = await chooseLemonade(found);
|
|
1672
|
-
} catch (err) {
|
|
1673
|
-
log(`Warning: could not ask whether to use the Lemonade Server at ${found.url}: ${err.message}; ${unchanged}`);
|
|
1674
|
-
return [];
|
|
1675
|
-
} finally {
|
|
1676
|
-
report(STAGES.GAIA_INIT, 0, GAIA_INIT_MESSAGE);
|
|
1677
|
-
}
|
|
1678
|
-
log(`Lemonade Server at ${found.url}: user chose ${answer || "not to answer"}`);
|
|
1679
|
-
if (answer === "external") return ["--lemonade-url", found.url];
|
|
1680
|
-
if (answer === "gaia") return ["--gaia-lemonade"];
|
|
1681
|
-
return [];
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
1678
|
/**
|
|
1685
1679
|
* Install the GAIA Python backend from scratch.
|
|
1686
1680
|
*
|
|
1681
|
+
* Installs no model and runs no `gaia init`: the Lemonade Server and the models
|
|
1682
|
+
* are set up by the app's first-run setup, where the user chooses cloud or
|
|
1683
|
+
* local and sees what downloads.
|
|
1684
|
+
*
|
|
1687
1685
|
* opts:
|
|
1688
1686
|
* - onProgress(stage, percent, message)
|
|
1689
1687
|
* - version: string — override the pinned version
|
|
1690
|
-
* - skipGaiaInit: boolean — skip `gaia init` (for testing)
|
|
1691
|
-
* - chooseLemonade(found): Promise<"external"|"gaia"|null> — asked when a
|
|
1692
|
-
* Lemonade Server already runs here; see lemonadeInitArgs
|
|
1693
1688
|
*
|
|
1694
1689
|
* Throws InstallError on failure. The state file is updated to reflect
|
|
1695
1690
|
* the current stage so a subsequent launch can recover.
|
|
@@ -1707,7 +1702,6 @@ async function installBackend(opts = {}) {
|
|
|
1707
1702
|
const pipPackage = localWheel
|
|
1708
1703
|
? `${localWheel}[ui]`
|
|
1709
1704
|
: `amd-gaia[ui]==${toPep440(version)}`;
|
|
1710
|
-
const skipGaiaInit = shouldSkipGaiaInit(opts);
|
|
1711
1705
|
|
|
1712
1706
|
log("================================================");
|
|
1713
1707
|
log(" Installing GAIA backend");
|
|
@@ -1718,13 +1712,14 @@ async function installBackend(opts = {}) {
|
|
|
1718
1712
|
setState(STATES.INSTALLING, { stage: STAGES.ENSURE_UV, version });
|
|
1719
1713
|
|
|
1720
1714
|
// Stage 1: ensure uv
|
|
1721
|
-
await ensureUv({ onProgress: opts.onProgress, isPackaged: opts.isPackaged });
|
|
1715
|
+
await ensureUv({ onProgress: opts.onProgress, isPackaged: opts.isPackaged, signal: opts.signal });
|
|
1722
1716
|
|
|
1723
1717
|
// Stage 2: create venv
|
|
1724
1718
|
setState(STATES.INSTALLING, { stage: STAGES.CREATE_VENV, version });
|
|
1725
1719
|
report(STAGES.CREATE_VENV, 0, "Creating Python 3.12 environment");
|
|
1726
1720
|
|
|
1727
1721
|
ensureGaiaHome();
|
|
1722
|
+
await stopRunningDaemon({ signal: opts.signal });
|
|
1728
1723
|
|
|
1729
1724
|
// If the venv exists but the python binary is missing, treat as partial.
|
|
1730
1725
|
const venvLooksValid =
|
|
@@ -1741,7 +1736,7 @@ async function installBackend(opts = {}) {
|
|
|
1741
1736
|
}
|
|
1742
1737
|
|
|
1743
1738
|
const venvArgs = ["venv", GAIA_VENV, "--python", "3.12"];
|
|
1744
|
-
let venvResult = await runCommand("uv", venvArgs, { stageLabel: "venv" });
|
|
1739
|
+
let venvResult = await runCommand("uv", venvArgs, { stageLabel: "venv", signal: opts.signal });
|
|
1745
1740
|
// uv downloads Python 3.12 when none is installed; behind a corporate
|
|
1746
1741
|
// proxy that fails on uv's bundled roots exactly like the pip step below.
|
|
1747
1742
|
let venvNativeTls = false;
|
|
@@ -1764,6 +1759,7 @@ async function installBackend(opts = {}) {
|
|
|
1764
1759
|
}
|
|
1765
1760
|
venvResult = await runCommand("uv", [...venvArgs, "--native-tls"], {
|
|
1766
1761
|
stageLabel: "venv",
|
|
1762
|
+
signal: opts.signal,
|
|
1767
1763
|
});
|
|
1768
1764
|
}
|
|
1769
1765
|
if (venvResult.code !== 0) {
|
|
@@ -1775,6 +1771,7 @@ async function installBackend(opts = {}) {
|
|
|
1775
1771
|
{
|
|
1776
1772
|
stage: STAGES.CREATE_VENV,
|
|
1777
1773
|
code: venvResult.code,
|
|
1774
|
+
reason: failureReason(`${venvResult.stdout || ""}\n${venvResult.stderr || ""}`),
|
|
1778
1775
|
suggestion: `Try creating it manually:\n ${manual}\nThen restart GAIA.`,
|
|
1779
1776
|
}
|
|
1780
1777
|
);
|
|
@@ -1787,6 +1784,7 @@ async function installBackend(opts = {}) {
|
|
|
1787
1784
|
// Stage 3: pip install
|
|
1788
1785
|
setState(STATES.INSTALLING, { stage: STAGES.INSTALL_PACKAGE, version });
|
|
1789
1786
|
report(STAGES.INSTALL_PACKAGE, 0, `Installing ${pipPackage}`);
|
|
1787
|
+
if (IS_WINDOWS) removeMovedLaunchers();
|
|
1790
1788
|
|
|
1791
1789
|
const pipArgs = [
|
|
1792
1790
|
"pip",
|
|
@@ -1799,11 +1797,26 @@ async function installBackend(opts = {}) {
|
|
|
1799
1797
|
GAIA_PYTHON_BIN,
|
|
1800
1798
|
];
|
|
1801
1799
|
// Retry the install on transient PyPI/network failures. The transitive
|
|
1802
|
-
// deps (numpy, faiss,
|
|
1800
|
+
// deps (numpy, faiss, pypdfium2, …) are fetched live from PyPI even
|
|
1803
1801
|
// when the gaia wheel itself is local, so a single broken-pipe mid-download
|
|
1804
1802
|
// would otherwise fail the whole bootstrap (and block a release). File-lock
|
|
1805
|
-
// failures (Windows os-error-32)
|
|
1803
|
+
// failures (Windows os-error-32) get their own single retry after this loop.
|
|
1806
1804
|
let installResult;
|
|
1805
|
+
if (!localWheel && isBackendPrefetched(version)) {
|
|
1806
|
+
report(STAGES.INSTALL_PACKAGE, 0, `Installing ${pipPackage} from the downloaded update`);
|
|
1807
|
+
const offline = await runCommand(
|
|
1808
|
+
"uv",
|
|
1809
|
+
["pip", "install", pipPackage, "--offline", "--compile-bytecode", "--python", GAIA_PYTHON_BIN],
|
|
1810
|
+
{ stageLabel: "pip", signal: opts.signal }
|
|
1811
|
+
);
|
|
1812
|
+
fs.rmSync(_prefetchMarker(toPep440(version)), { force: true });
|
|
1813
|
+
if (offline.code === 0) {
|
|
1814
|
+
installResult = offline;
|
|
1815
|
+
} else {
|
|
1816
|
+
log(`The prefetched backend is no longer all in uv's cache (${_lastLines(offline)}) — installing from PyPI`);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const installedOffline = Boolean(installResult);
|
|
1807
1820
|
let useNativeTls = false;
|
|
1808
1821
|
let nativeTlsAttempted = false;
|
|
1809
1822
|
// The TLS branch flips useNativeTls and `continue`s, consuming an iteration.
|
|
@@ -1811,12 +1824,12 @@ async function installBackend(opts = {}) {
|
|
|
1811
1824
|
// so the promised --native-tls retry actually runs (issue #1693 review).
|
|
1812
1825
|
for (
|
|
1813
1826
|
let attempt = 1;
|
|
1814
|
-
attempt <= INSTALL_MAX_ATTEMPTS || (useNativeTls && !nativeTlsAttempted);
|
|
1827
|
+
!installedOffline && (attempt <= INSTALL_MAX_ATTEMPTS || (useNativeTls && !nativeTlsAttempted));
|
|
1815
1828
|
attempt++
|
|
1816
1829
|
) {
|
|
1817
1830
|
const attemptArgs = useNativeTls ? [...pipArgs, "--native-tls"] : pipArgs;
|
|
1818
1831
|
if (useNativeTls) nativeTlsAttempted = true;
|
|
1819
|
-
installResult = await runCommand("uv", attemptArgs, { stageLabel: "pip" });
|
|
1832
|
+
installResult = await runCommand("uv", attemptArgs, { stageLabel: "pip", signal: opts.signal });
|
|
1820
1833
|
if (installResult.code === 0) break;
|
|
1821
1834
|
const attemptOutput = `${installResult.stdout || ""}\n${installResult.stderr || ""}`;
|
|
1822
1835
|
// A corporate proxy's custom root CA isn't in uv's bundled webpki roots, so
|
|
@@ -1853,16 +1866,41 @@ async function installBackend(opts = {}) {
|
|
|
1853
1866
|
);
|
|
1854
1867
|
await sleep(backoffMs);
|
|
1855
1868
|
}
|
|
1869
|
+
// A launcher that is only open, not running, can be renamed out of uv's way (#6570).
|
|
1870
|
+
const firstOutput = `${installResult.stdout || ""}\n${installResult.stderr || ""}`;
|
|
1871
|
+
if (IS_WINDOWS && installResult.code !== 0 && isFileLockedError(firstOutput)) {
|
|
1872
|
+
const moved = moveLockedAside(lockedFiles(firstOutput));
|
|
1873
|
+
if (moved.length > 0) {
|
|
1874
|
+
report(STAGES.INSTALL_PACKAGE, 0, "Another program has GAIA's files open — retrying the install");
|
|
1875
|
+
try {
|
|
1876
|
+
installResult = await runCommand("uv", useNativeTls ? [...pipArgs, "--native-tls"] : pipArgs, {
|
|
1877
|
+
stageLabel: "pip",
|
|
1878
|
+
signal: opts.signal,
|
|
1879
|
+
});
|
|
1880
|
+
} finally {
|
|
1881
|
+
putBackUnreplaced(moved);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1856
1885
|
if (installResult.code !== 0) {
|
|
1857
|
-
// Windows: an upgrade can't replace gaia.exe while a previous GAIA
|
|
1858
|
-
// process still holds it open (issue #1388). The orphan cleanup in
|
|
1859
|
-
// main.cjs should have killed it first; if the lock persists, point the
|
|
1860
|
-
// user at the live process rather than the generic manual-install hint.
|
|
1861
1886
|
const output = `${installResult.stdout || ""}\n${installResult.stderr || ""}`;
|
|
1862
1887
|
let suggestion;
|
|
1863
1888
|
if (isFileLockedError(output)) {
|
|
1864
|
-
|
|
1865
|
-
|
|
1889
|
+
// pip's own line is already in the log, which the setup screen's details show.
|
|
1890
|
+
const files = lockedFiles(output);
|
|
1891
|
+
const names = files.map((file) => path.basename(file)).join(", ");
|
|
1892
|
+
throw new InstallError(
|
|
1893
|
+
`GAIA couldn't replace its own files because another program has them open${names ? ` (${names})` : ""}.`,
|
|
1894
|
+
{
|
|
1895
|
+
stage: STAGES.INSTALL_PACKAGE,
|
|
1896
|
+
code: installResult.code,
|
|
1897
|
+
reason: FAILURE_REASONS.LOCKED,
|
|
1898
|
+
lockedFiles: files,
|
|
1899
|
+
suggestion:
|
|
1900
|
+
"Close other GAIA apps and any terminal running gaia, or restart your PC, then try again. " +
|
|
1901
|
+
`The log at ${LOG_FILE} names the file.`,
|
|
1902
|
+
}
|
|
1903
|
+
);
|
|
1866
1904
|
} else if (isTlsCertError(output)) {
|
|
1867
1905
|
suggestion =
|
|
1868
1906
|
"PyPI's certificate isn't trusted on this network — usually a corporate proxy presenting its own root CA. " +
|
|
@@ -1871,6 +1909,14 @@ async function installBackend(opts = {}) {
|
|
|
1871
1909
|
: "") +
|
|
1872
1910
|
"Ask IT to install the proxy's root CA in your system's trusted certificate store, then relaunch GAIA. " +
|
|
1873
1911
|
"See https://amd-gaia.ai/docs/quickstart#cli-install";
|
|
1912
|
+
} else if (isTransientNetworkError(output)) {
|
|
1913
|
+
// The log file keeps pip's output; the box says what to do about it.
|
|
1914
|
+
throw new InstallError("GAIA couldn't reach the internet to finish installing.", {
|
|
1915
|
+
stage: STAGES.INSTALL_PACKAGE,
|
|
1916
|
+
code: installResult.code,
|
|
1917
|
+
suggestion:
|
|
1918
|
+
"Check your connection, then choose Retry. If it keeps failing, the log file below says why. Your chats and settings are safe.",
|
|
1919
|
+
});
|
|
1874
1920
|
} else {
|
|
1875
1921
|
suggestion = `Try installing manually:\n uv pip install ${pipPackage} --python ${
|
|
1876
1922
|
IS_WINDOWS ? `${GAIA_VENV_DISPLAY}/Scripts/python.exe` : `${GAIA_VENV_DISPLAY}/bin/python`
|
|
@@ -1891,6 +1937,7 @@ async function installBackend(opts = {}) {
|
|
|
1891
1937
|
{
|
|
1892
1938
|
stage: STAGES.INSTALL_PACKAGE,
|
|
1893
1939
|
code: installResult.code,
|
|
1940
|
+
reason: failureReason(output),
|
|
1894
1941
|
suggestion,
|
|
1895
1942
|
}
|
|
1896
1943
|
);
|
|
@@ -1907,16 +1954,7 @@ async function installBackend(opts = {}) {
|
|
|
1907
1954
|
}
|
|
1908
1955
|
report(STAGES.INSTALL_PACKAGE, 100, "GAIA package installed");
|
|
1909
1956
|
|
|
1910
|
-
// Stage 4:
|
|
1911
|
-
const setup = await runGaiaInit(
|
|
1912
|
-
GAIA_BIN,
|
|
1913
|
-
report,
|
|
1914
|
-
version,
|
|
1915
|
-
skipGaiaInit,
|
|
1916
|
-
opts.chooseLemonade
|
|
1917
|
-
);
|
|
1918
|
-
|
|
1919
|
-
// Stage 5: verify
|
|
1957
|
+
// Stage 4: verify
|
|
1920
1958
|
setState(STATES.INSTALLING, { stage: STAGES.VERIFY, version });
|
|
1921
1959
|
report(STAGES.VERIFY, 0, "Verifying installation");
|
|
1922
1960
|
|
|
@@ -1974,12 +2012,142 @@ async function installBackend(opts = {}) {
|
|
|
1974
2012
|
log(`Shim creation check failed: ${err.message}`);
|
|
1975
2013
|
}
|
|
1976
2014
|
|
|
1977
|
-
setState(STATES.READY, { stage: null, version, installedVersion
|
|
2015
|
+
setState(STATES.READY, { stage: null, version, installedVersion });
|
|
1978
2016
|
log("Backend install complete");
|
|
1979
2017
|
}
|
|
1980
2018
|
|
|
2019
|
+
// ── Agent refresh ────────────────────────────────────────────────────────────
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Update the flagship to this release, without Lemonade or models. A PC with
|
|
2023
|
+
* no flagship is skipped: first-run setup installs it.
|
|
2024
|
+
*
|
|
2025
|
+
* The stage stays recorded until this succeeds, so an update that failed or was
|
|
2026
|
+
* quit part way through refreshes again on the next launch.
|
|
2027
|
+
*/
|
|
2028
|
+
async function refreshAgent(gaiaBin, opts = {}) {
|
|
2029
|
+
const version = resolveBackendVersion(opts);
|
|
2030
|
+
if (!fs.existsSync(FLAGSHIP_SENTINEL)) {
|
|
2031
|
+
log("No GAIA agent installed yet — first-run setup installs it");
|
|
2032
|
+
// Nothing to refresh: a refresh recorded earlier is done, not pending forever.
|
|
2033
|
+
setState(STATES.READY, { version, installedVersion: getInstalledVersion(gaiaBin) });
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
setState(STATES.INSTALLING, { stage: STAGES.AGENT_REFRESH, version });
|
|
2037
|
+
makeProgressReporter(opts.onProgress)(STAGES.AGENT_REFRESH, null, "Updating the GAIA agent");
|
|
2038
|
+
|
|
2039
|
+
const result = await runCommand(gaiaBin, ["init", "--agent-only"], {
|
|
2040
|
+
stageLabel: "agent",
|
|
2041
|
+
env: { ...process.env, COLUMNS: "1000" },
|
|
2042
|
+
signal: opts.signal,
|
|
2043
|
+
});
|
|
2044
|
+
const lastLine = `${result.stderr || ""}`.trim().split(/\r?\n/).pop();
|
|
2045
|
+
if (result.code === AGENT_KEPT_EXIT) {
|
|
2046
|
+
// Offline: the working agent stays, and the next launch tries the update again.
|
|
2047
|
+
log(`Kept the installed GAIA agent; the update runs again next launch: ${lastLine}`);
|
|
2048
|
+
setState(STATES.PARTIAL, { stage: STAGES.AGENT_REFRESH, version, message: lastLine });
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (result.code !== 0) {
|
|
2052
|
+
throw new InstallError(
|
|
2053
|
+
lastLine ? lastLine.replace(/^Error:\s*/, "") : `The GAIA agent update exited ${result.code}.`,
|
|
2054
|
+
{ stage: STAGES.AGENT_REFRESH, code: result.code }
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
setState(STATES.READY, { version, installedVersion: getInstalledVersion(gaiaBin) });
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// ── Backend prefetch for an app update ──────────────────────────────────────
|
|
2061
|
+
|
|
2062
|
+
const PREFETCH_DIR = path.join(GAIA_HOME, "cache", "backend-prefetch");
|
|
2063
|
+
|
|
2064
|
+
function _prefetchMarker(pep440) {
|
|
2065
|
+
return path.join(PREFETCH_DIR, `${pep440}.ready`);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/** True when an earlier prefetch left everything `version` needs in uv's cache. */
|
|
2069
|
+
function isBackendPrefetched(version) {
|
|
2070
|
+
return Boolean(version) && fs.existsSync(_prefetchMarker(toPep440(version)));
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Before an app update restarts, put the backend it will need into uv's cache
|
|
2075
|
+
* by installing it into a throwaway environment. The first launch then
|
|
2076
|
+
* installs offline, so it neither waits on nor depends on PyPI.
|
|
2077
|
+
* Throws with uv's own words when the download fails.
|
|
2078
|
+
*/
|
|
2079
|
+
async function prefetchBackend(appVersion, { isPackaged } = {}) {
|
|
2080
|
+
if (process.env.GAIA_LOCAL_WHEEL) {
|
|
2081
|
+
log("GAIA_LOCAL_WHEEL is set: the next launch installs that wheel, so there is nothing to prefetch");
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
const version = toPep440(appVersion);
|
|
2085
|
+
const pipPackage = `amd-gaia[ui]==${version}`;
|
|
2086
|
+
const workDir = path.join(PREFETCH_DIR, version);
|
|
2087
|
+
const venv = path.join(workDir, "venv");
|
|
2088
|
+
const python = IS_WINDOWS ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python");
|
|
2089
|
+
log(`Prefetching ${pipPackage} for the update`);
|
|
2090
|
+
await ensureUv({ isPackaged });
|
|
2091
|
+
_removeScratchVenvs();
|
|
2092
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
2093
|
+
try {
|
|
2094
|
+
const venvResult = await runCommand("uv", ["venv", venv, "--python", "3.12"], { stageLabel: "prefetch" });
|
|
2095
|
+
if (venvResult.code !== 0) {
|
|
2096
|
+
throw new Error(`uv venv exited ${venvResult.code}: ${_lastLines(venvResult)}`);
|
|
2097
|
+
}
|
|
2098
|
+
const args = ["pip", "install", pipPackage, "--refresh", "--python", python];
|
|
2099
|
+
let result = await runCommand("uv", args, { stageLabel: "prefetch" });
|
|
2100
|
+
if (result.code !== 0 && isTlsCertError(`${result.stdout || ""}\n${result.stderr || ""}`)) {
|
|
2101
|
+
result = await runCommand("uv", [...args, "--native-tls"], { stageLabel: "prefetch" });
|
|
2102
|
+
}
|
|
2103
|
+
if (result.code !== 0) {
|
|
2104
|
+
throw new Error(`Couldn't download ${pipPackage} (uv exit ${result.code}): ${_lastLines(result)}`);
|
|
2105
|
+
}
|
|
2106
|
+
} finally {
|
|
2107
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
2108
|
+
}
|
|
2109
|
+
fs.writeFileSync(_prefetchMarker(version), new Date().toISOString());
|
|
2110
|
+
log(`Prefetched ${pipPackage}: the next launch installs it offline`);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/** Scratch venvs a quit cut short; the .ready markers beside them stay. */
|
|
2114
|
+
function _removeScratchVenvs() {
|
|
2115
|
+
let entries = [];
|
|
2116
|
+
try {
|
|
2117
|
+
entries = fs.readdirSync(PREFETCH_DIR, { withFileTypes: true });
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
if (err.code !== "ENOENT") throw err;
|
|
2120
|
+
}
|
|
2121
|
+
for (const entry of entries) {
|
|
2122
|
+
if (entry.isDirectory()) fs.rmSync(path.join(PREFETCH_DIR, entry.name), { recursive: true, force: true });
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function _lastLines({ stdout, stderr }) {
|
|
2127
|
+
return `${stdout || ""}\n${stderr || ""}`
|
|
2128
|
+
.split("\n")
|
|
2129
|
+
.map((line) => line.trimEnd())
|
|
2130
|
+
.filter(Boolean)
|
|
2131
|
+
.slice(-5)
|
|
2132
|
+
.join(" | ");
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1981
2135
|
// ── Version-aware ensure ─────────────────────────────────────────────────────
|
|
1982
2136
|
|
|
2137
|
+
/**
|
|
2138
|
+
* Whether the backend is finished at the app's version. A recorded failure or
|
|
2139
|
+
* unfinished stage says no, so it reaches ensureBackend and the user.
|
|
2140
|
+
*/
|
|
2141
|
+
function isBackendCurrent() {
|
|
2142
|
+
// A local wheel is reinstalled on every launch, as ensureBackend says.
|
|
2143
|
+
if (process.env.GAIA_LOCAL_WHEEL) return false;
|
|
2144
|
+
const bin = findGaiaBin();
|
|
2145
|
+
if (!bin) return false;
|
|
2146
|
+
const prev = getState();
|
|
2147
|
+
if (prev && prev.state !== STATES.READY) return false;
|
|
2148
|
+
return getInstalledVersion(bin) === toPep440(appVersion());
|
|
2149
|
+
}
|
|
2150
|
+
|
|
1983
2151
|
/**
|
|
1984
2152
|
* The installed backend version, or null when it is not installed.
|
|
1985
2153
|
* For the app's own venv this reads the package record instead of running
|
|
@@ -2049,12 +2217,13 @@ function venvPackageVersion() {
|
|
|
2049
2217
|
* opts:
|
|
2050
2218
|
* - onProgress(stage, percent, message)
|
|
2051
2219
|
* - version: override the pinned version
|
|
2052
|
-
* - skipGaiaInit: bool
|
|
2053
|
-
* - chooseLemonade: see installBackend
|
|
2054
2220
|
* - allowPartialRestart: bool (default true) — restart from scratch if
|
|
2055
2221
|
* the state file indicates a `partial` install.
|
|
2222
|
+
* - signal: AbortSignal — stops the running step (the app is quitting)
|
|
2056
2223
|
*
|
|
2057
|
-
* Throws InstallError on failure and updates the state file.
|
|
2224
|
+
* Throws InstallError on failure and updates the state file. Throws
|
|
2225
|
+
* InstallCancelled when `signal` stopped it; the state file then still says
|
|
2226
|
+
* installing, so the next launch runs the install again.
|
|
2058
2227
|
*/
|
|
2059
2228
|
async function ensureBackend(opts = {}) {
|
|
2060
2229
|
openLog({ truncate: true });
|
|
@@ -2097,6 +2266,7 @@ async function ensureBackend(opts = {}) {
|
|
|
2097
2266
|
}.`,
|
|
2098
2267
|
{
|
|
2099
2268
|
stage: STAGES.PRE_CHECKS,
|
|
2269
|
+
reason: FAILURE_REASONS.DISK,
|
|
2100
2270
|
suggestion: `Free at least ${requiredMb} MB at ${path.dirname(
|
|
2101
2271
|
GAIA_HOME
|
|
2102
2272
|
)} and try again.`,
|
|
@@ -2132,47 +2302,28 @@ async function ensureBackend(opts = {}) {
|
|
|
2132
2302
|
// reinstall from the local wheel so CI gets a fresh install each run.
|
|
2133
2303
|
const expectedVersion = resolveBackendVersion(opts);
|
|
2134
2304
|
const existingBin = findGaiaBin();
|
|
2135
|
-
// Installed means
|
|
2136
|
-
//
|
|
2305
|
+
// Installed means the package finished installing. No state file at all
|
|
2306
|
+
// is an install from before it existed.
|
|
2137
2307
|
const prev = preChecks.previousState;
|
|
2138
|
-
const setupFinished =
|
|
2139
|
-
!prev || (prev.state === STATES.READY && prev.init !== "failed");
|
|
2140
2308
|
const packageFinished =
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2309
|
+
!prev ||
|
|
2310
|
+
prev.state === STATES.READY ||
|
|
2311
|
+
prev.stage === LEGACY_SETUP_STAGE ||
|
|
2312
|
+
prev.stage === STAGES.VERIFY ||
|
|
2313
|
+
prev.stage === STAGES.AGENT_REFRESH;
|
|
2146
2314
|
if (existingBin) {
|
|
2147
2315
|
const installedVersion = getInstalledVersion(existingBin);
|
|
2148
2316
|
const versionMatches =
|
|
2149
2317
|
expectedVersion !== null && installedVersion === toPep440(expectedVersion);
|
|
2150
|
-
if (versionMatches &&
|
|
2151
|
-
|
|
2152
|
-
`GAIA ${installedVersion} is installed but its setup did not finish ` +
|
|
2153
|
-
`(state=${prev.state}, stage=${prev.stage || "-"}) — running gaia init`
|
|
2154
|
-
);
|
|
2155
|
-
const report = makeProgressReporter(opts.onProgress);
|
|
2156
|
-
const setup = await runGaiaInit(
|
|
2157
|
-
existingBin,
|
|
2158
|
-
report,
|
|
2159
|
-
expectedVersion,
|
|
2160
|
-
shouldSkipGaiaInit(opts),
|
|
2161
|
-
opts.chooseLemonade
|
|
2162
|
-
);
|
|
2163
|
-
setState(STATES.READY, { version: expectedVersion, installedVersion, ...setup });
|
|
2164
|
-
report(STAGES.VERIFY, 100, `GAIA ${installedVersion} ready`);
|
|
2318
|
+
if (versionMatches && packageFinished && prev && prev.stage === STAGES.AGENT_REFRESH) {
|
|
2319
|
+
await refreshAgent(existingBin, opts);
|
|
2165
2320
|
return existingBin;
|
|
2166
2321
|
}
|
|
2167
|
-
if (versionMatches &&
|
|
2322
|
+
if (versionMatches && packageFinished) {
|
|
2168
2323
|
log(
|
|
2169
2324
|
`GAIA backend already installed at version ${installedVersion} — nothing to do`
|
|
2170
2325
|
);
|
|
2171
|
-
setState(STATES.READY, {
|
|
2172
|
-
version: expectedVersion,
|
|
2173
|
-
installedVersion,
|
|
2174
|
-
init: prev ? prev.init : undefined,
|
|
2175
|
-
});
|
|
2326
|
+
setState(STATES.READY, { version: expectedVersion, installedVersion });
|
|
2176
2327
|
// Tell the UI we are instantly ready.
|
|
2177
2328
|
const report = makeProgressReporter(opts.onProgress);
|
|
2178
2329
|
report(STAGES.VERIFY, 100, `GAIA ${installedVersion} ready`);
|
|
@@ -2205,8 +2356,13 @@ async function ensureBackend(opts = {}) {
|
|
|
2205
2356
|
throw err;
|
|
2206
2357
|
}
|
|
2207
2358
|
|
|
2359
|
+
await refreshAgent(verified, opts);
|
|
2208
2360
|
return verified;
|
|
2209
2361
|
} catch (err) {
|
|
2362
|
+
if (err instanceof InstallCancelled) {
|
|
2363
|
+
log(err.message);
|
|
2364
|
+
throw err;
|
|
2365
|
+
}
|
|
2210
2366
|
if (err instanceof InstallError) {
|
|
2211
2367
|
setState(STATES.FAILED, {
|
|
2212
2368
|
stage: err.stage || null,
|
|
@@ -2232,11 +2388,13 @@ module.exports = {
|
|
|
2232
2388
|
ensureUv,
|
|
2233
2389
|
installBackend,
|
|
2234
2390
|
ensureBackend,
|
|
2391
|
+
isBackendCurrent,
|
|
2392
|
+
prefetchBackend,
|
|
2393
|
+
isBackendPrefetched,
|
|
2235
2394
|
getInstalledVersion,
|
|
2236
2395
|
findGaiaBin,
|
|
2237
2396
|
toPep440,
|
|
2238
|
-
|
|
2239
|
-
initFailureReason,
|
|
2397
|
+
stopRunningDaemon,
|
|
2240
2398
|
|
|
2241
2399
|
// Pre-checks
|
|
2242
2400
|
runPreChecks,
|
|
@@ -2255,9 +2413,6 @@ module.exports = {
|
|
|
2255
2413
|
setState,
|
|
2256
2414
|
clearState,
|
|
2257
2415
|
|
|
2258
|
-
// gaia init progress
|
|
2259
|
-
parseInitEventLine,
|
|
2260
|
-
createInitProgress,
|
|
2261
2416
|
computeOverallPercent,
|
|
2262
2417
|
|
|
2263
2418
|
// Logging
|
|
@@ -2267,12 +2422,14 @@ module.exports = {
|
|
|
2267
2422
|
logError,
|
|
2268
2423
|
getLogPath,
|
|
2269
2424
|
getStatePath,
|
|
2425
|
+
readLogTail,
|
|
2270
2426
|
|
|
2271
2427
|
// Constants
|
|
2272
2428
|
STATES,
|
|
2273
2429
|
STAGES,
|
|
2274
|
-
|
|
2275
|
-
|
|
2430
|
+
FAILURE_REASONS,
|
|
2431
|
+
DAEMON_STOP_TIMEOUT_MS,
|
|
2432
|
+
DAEMON_STOP_LEMONADE_EXIT,
|
|
2276
2433
|
GAIA_HOME,
|
|
2277
2434
|
GAIA_VENV,
|
|
2278
2435
|
GAIA_BIN,
|
|
@@ -2280,4 +2437,5 @@ module.exports = {
|
|
|
2280
2437
|
|
|
2281
2438
|
// Error
|
|
2282
2439
|
InstallError,
|
|
2440
|
+
InstallCancelled,
|
|
2283
2441
|
};
|