@scopebond/agent 0.5.6 → 0.6.1
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/dist/agent.d.ts +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -3
- package/dist/agent.js.map +1 -1
- package/dist/allowance-sender.js +10 -4
- package/dist/allowance-sender.js.map +1 -1
- package/dist/apps-entry.d.ts +46 -0
- package/dist/apps-entry.d.ts.map +1 -0
- package/dist/apps-entry.js +118 -0
- package/dist/apps-entry.js.map +1 -0
- package/dist/autostart.d.ts +11 -5
- package/dist/autostart.d.ts.map +1 -1
- package/dist/autostart.js +60 -6
- package/dist/autostart.js.map +1 -1
- package/dist/awake.d.ts +31 -0
- package/dist/awake.d.ts.map +1 -0
- package/dist/awake.js +57 -0
- package/dist/awake.js.map +1 -0
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/cli-main.js +28 -1
- package/dist/cli-main.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/ipc.d.ts +9 -4
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +15 -9
- package/dist/ipc.js.map +1 -1
- package/dist/log-file.js.map +1 -1
- package/dist/native-update.d.ts +83 -7
- package/dist/native-update.d.ts.map +1 -1
- package/dist/native-update.js +230 -33
- package/dist/native-update.js.map +1 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +1 -0
- package/dist/prompt.js.map +1 -1
- package/dist/selfcheck.d.ts.map +1 -1
- package/dist/selfcheck.js +4 -1
- package/dist/selfcheck.js.map +1 -1
- package/dist/service.d.ts +20 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +141 -44
- package/dist/service.js.map +1 -1
- package/dist/setup.d.ts +12 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +42 -13
- package/dist/setup.js.map +1 -1
- package/dist/tray-model.d.ts +2 -0
- package/dist/tray-model.d.ts.map +1 -1
- package/dist/tray-model.js +2 -0
- package/dist/tray-model.js.map +1 -1
- package/dist/update.d.ts +56 -12
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +209 -38
- package/dist/update.js.map +1 -1
- package/dist/upkeep.d.ts +16 -0
- package/dist/upkeep.d.ts.map +1 -0
- package/dist/upkeep.js +55 -0
- package/dist/upkeep.js.map +1 -0
- package/package.json +4 -4
package/dist/service.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// versions to run, and once a day it runs the end-to-end self-check.
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { hostname } from "node:os";
|
|
7
|
+
import { hostname, uptime } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { actOnBlocked, blockedQuestion, hookVersion, isSingleExecutable, isManaged, loadConnection, localActivity, readBlocked, readMeta, ruleReport,
|
|
9
|
+
import { actOnBlocked, blockedQuestion, hookVersion, isSingleExecutable, isManaged, loadConnection, localActivity, noteUnresolvableHooks, readBlocked, readMeta, ruleReport, writeDeliveryState, userHarnessFile, writeHarnessConfig } from "@scopebond/hook";
|
|
10
10
|
import { computerStatus, expectedHarnesses, missingHookEntries, runCycle } from "./agent.js";
|
|
11
11
|
import { agentCliPath } from "./self.js";
|
|
12
|
-
import { fetchVerifiedInstaller, installAfterExit, installKind } from "./native-update.js";
|
|
13
|
-
import { AGENT_LOG_ENV, launcherIsCurrent, launcherPath, refreshLauncher, startCommands } from "./autostart.js";
|
|
12
|
+
import { fetchVerifiedInstaller, installAfterExit, installerUnchanged, installKind, nativeTrayPath, takeInstallResult } from "./native-update.js";
|
|
13
|
+
import { AGENT_LOG_ENV, launcherIsCurrent, launcherPath, refreshLauncher, retireLauncherRunValue, startCommands } from "./autostart.js";
|
|
14
14
|
import { callAgent, startControl } from "./ipc.js";
|
|
15
15
|
import { runSelfCheck } from "./selfcheck.js";
|
|
16
16
|
import { flushReasons, queueReason } from "./override-reasons.js";
|
|
@@ -21,7 +21,9 @@ import { sendAllowancesAndRequests } from "./allowance-sender.js";
|
|
|
21
21
|
import { checkResult, trayModel } from "./tray-model.js";
|
|
22
22
|
import { fetchComputerSummary, openInBrowser, sameOrigin } from "./summary.js";
|
|
23
23
|
import { startReconnect } from "./reconnect.js";
|
|
24
|
-
import { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries, maintainedHookCommand } from "./update.js";
|
|
24
|
+
import { agentVersion, compareVersions, fetchClientVersion, installAgent, maintainHookEntries, maintainedHookCommand, recentlyFailedUpdate, recordFailedUpdate, startCheck, } from "./update.js";
|
|
25
|
+
import { runUpkeepApart } from "./upkeep.js";
|
|
26
|
+
import { awakeSinceAtStart, readAwake, writeAwake } from "./awake.js";
|
|
25
27
|
export const AGENT_VERSION = `agent/${agentVersion()}`;
|
|
26
28
|
const INTERVAL_MS = 60_000;
|
|
27
29
|
const MAX_BACKOFF_MS = 15 * 60_000;
|
|
@@ -29,6 +31,9 @@ const UPDATE_EVERY_MS = 6 * 60 * 60 * 1000;
|
|
|
29
31
|
const SELF_CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
30
32
|
const SUMMARY_EVERY_MS = 5 * 60 * 1000;
|
|
31
33
|
export const AFTER_PID_ENV = "SCOPEBOND_AGENT_AFTER_PID";
|
|
34
|
+
/** Left in the Scopebond folder when this computer's user stops the agent (`scopebond-agent stop`), and removed when it
|
|
35
|
+
* starts: the native tray then leaves it stopped instead of starting it again. */
|
|
36
|
+
export const STOPPED_FILE = "agent-stopped.json";
|
|
32
37
|
/** The person's tray settings (D143: notifications about problems by default, none about blocks). */
|
|
33
38
|
export const TRAY_SETTINGS_FILE = "agent-settings.json";
|
|
34
39
|
export function readTraySettings(dir) {
|
|
@@ -113,8 +118,11 @@ export async function takeOver(dir, cli, waitMs = 30_000) {
|
|
|
113
118
|
const refresh = process.env[REFRESH_LAUNCHER_ENV] === "1";
|
|
114
119
|
delete process.env[AFTER_PID_ENV];
|
|
115
120
|
delete process.env[REFRESH_LAUNCHER_ENV];
|
|
121
|
+
// The agent being replaced stops serving (control channel closed, lock released) and then waits for this one to answer
|
|
122
|
+
// before it exits, so it can go back to its own version if this one never starts. Unless the launcher must be rewritten
|
|
123
|
+
// (nothing may still run it), its stopping is enough.
|
|
116
124
|
if (Number.isInteger(previous) && previous > 0)
|
|
117
|
-
await waitForExit(previous, waitMs);
|
|
125
|
+
await waitForExit(previous, waitMs, refresh ? undefined : () => !lockHeldBy(dir, previous));
|
|
118
126
|
if (!refresh)
|
|
119
127
|
return;
|
|
120
128
|
// The old launcher reads its last lines after its agent exits; give it a moment to finish before its file changes.
|
|
@@ -127,7 +135,7 @@ export async function takeOver(dir, cli, waitMs = 30_000) {
|
|
|
127
135
|
console.log(`${new Date().toISOString()} could not rewrite the autostart launcher: ${error.message}`);
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
|
-
export async function waitForExit(pid, timeoutMs) {
|
|
138
|
+
export async function waitForExit(pid, timeoutMs, released) {
|
|
131
139
|
const until = Date.now() + timeoutMs;
|
|
132
140
|
while (Date.now() < until) {
|
|
133
141
|
try {
|
|
@@ -136,9 +144,33 @@ export async function waitForExit(pid, timeoutMs) {
|
|
|
136
144
|
catch {
|
|
137
145
|
return;
|
|
138
146
|
}
|
|
147
|
+
if (released?.())
|
|
148
|
+
return;
|
|
139
149
|
await new Promise((r) => setTimeout(r, 250));
|
|
140
150
|
}
|
|
141
151
|
}
|
|
152
|
+
/** Whether the one-agent lock names `pid`. */
|
|
153
|
+
export function lockHeldBy(dir, pid) {
|
|
154
|
+
try {
|
|
155
|
+
return readFileSync(join(dir, AGENT_LOCK), "utf8").trim().split(/\s+/)[0] === String(pid);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** How long the agent that handed over waits for its replacement to answer before going back to its own version. */
|
|
162
|
+
export const HANDOVER_CONFIRM_MS = 3 * 60_000;
|
|
163
|
+
/** Wait until an agent other than this process answers on the local channel reporting `version`. */
|
|
164
|
+
export async function waitForAgentVersion(dir, version, timeoutMs) {
|
|
165
|
+
const until = Date.now() + timeoutMs;
|
|
166
|
+
while (Date.now() < until) {
|
|
167
|
+
const status = await callAgent(dir, "GET", "/status", undefined, 2_000);
|
|
168
|
+
if (status?.agent && status.agent.pid !== process.pid && status.agent.version === `agent/${version}`)
|
|
169
|
+
return true;
|
|
170
|
+
await new Promise((r) => setTimeout(r, 1_000));
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
142
174
|
export const AGENT_LOCK = "agent.lock";
|
|
143
175
|
/** How long a lock whose agent never answered on its local channel still counts as an agent starting up. */
|
|
144
176
|
export const AGENT_LOCK_STARTING_MS = 60_000;
|
|
@@ -197,7 +229,13 @@ export async function startService(options) {
|
|
|
197
229
|
const lock = acquireAgentLock(options.dir);
|
|
198
230
|
if (!lock)
|
|
199
231
|
throw new Error("a Scopebond Agent is already running for this computer");
|
|
232
|
+
rmSync(join(options.dir, STOPPED_FILE), { force: true });
|
|
233
|
+
// What the install helper of a signed update recorded before this start (installAfterExit).
|
|
234
|
+
const installed = takeInstallResult(options.dir);
|
|
235
|
+
if (installed)
|
|
236
|
+
log(installed.installed ? `the Scopebond Agent ${installed.version} was installed` : `the update to ${installed.version} was not installed: ${installed.reason ?? "unknown"}`);
|
|
200
237
|
const interval = options.intervalMs ?? INTERVAL_MS;
|
|
238
|
+
const nativeTray = process.platform === "win32" ? nativeTrayPath() : null;
|
|
201
239
|
let last = null;
|
|
202
240
|
let lastMaintenance = null;
|
|
203
241
|
let failures = 0;
|
|
@@ -209,8 +247,17 @@ export async function startService(options) {
|
|
|
209
247
|
let stopped = false;
|
|
210
248
|
let lastSelfCheckAt = 0;
|
|
211
249
|
// SB388: time asleep never counts as records waiting. A gap between cycles longer than the schedule allows means the
|
|
212
|
-
// computer slept (or the agent was stopped); waiting is counted again from the next cycle.
|
|
213
|
-
|
|
250
|
+
// computer slept (or the agent was stopped); waiting is counted again from the next cycle. The wake time is kept across
|
|
251
|
+
// restarts of the agent alone (awake.ts), so a restart never hides a backlog; after a long gap the waiting records decide.
|
|
252
|
+
const allowedGapMs = Math.max(3 * interval, 5 * 60_000);
|
|
253
|
+
const startedAt = Date.now();
|
|
254
|
+
let oldestPendingAt = null;
|
|
255
|
+
try {
|
|
256
|
+
const age = computerStatus(options.dir, startedAt).delivery.oldest_pending_age_s;
|
|
257
|
+
oldestPendingAt = age === null ? null : startedAt - age * 1000;
|
|
258
|
+
}
|
|
259
|
+
catch { /* no store yet: nothing waiting */ }
|
|
260
|
+
let awakeSince = awakeSinceAtStart({ saved: readAwake(options.dir), now: startedAt, uptimeMs: uptime() * 1000, allowedGapMs, oldestPendingAt });
|
|
214
261
|
let lastCycleAt = 0;
|
|
215
262
|
// A long step the person should see as "working" (an update), or null.
|
|
216
263
|
let working = null;
|
|
@@ -251,17 +298,18 @@ export async function startService(options) {
|
|
|
251
298
|
if (running)
|
|
252
299
|
return running;
|
|
253
300
|
const started = Date.now();
|
|
254
|
-
const allowedGap = failures ? MAX_BACKOFF_MS + 5 * 60_000 :
|
|
301
|
+
const allowedGap = failures ? MAX_BACKOFF_MS + 5 * 60_000 : allowedGapMs;
|
|
255
302
|
if (lastCycleAt && started - lastCycleAt > allowedGap)
|
|
256
303
|
awakeSince = started;
|
|
257
304
|
lastCycleAt = started;
|
|
305
|
+
writeAwake(options.dir, { awake_since: awakeSince, last_cycle_at: started });
|
|
258
306
|
cycleStartedAt = started;
|
|
259
307
|
// A cycle that does not finish within its limit is left behind (its requests time out on their own), counted as
|
|
260
308
|
// a failure, and the loop goes on. Nothing — the next cycle, Send now, stop — waits on it.
|
|
261
309
|
let limitTimer;
|
|
262
310
|
const overrun = new Promise((resolve) => {
|
|
263
311
|
limitTimer = setTimeout(() => resolve({ at: started, connected: true, delivered: 0, pending: computerStatus(options.dir).delivery.pending,
|
|
264
|
-
deliveryError: `a delivery cycle did not finish within ${Math.round(cycleLimit / 1000)} s`, rules: "skipped", missingHookEntries: [], requested: null }), cycleLimit);
|
|
312
|
+
deliveryError: `a delivery cycle did not finish within ${Math.round(cycleLimit / 1000)} s`, rules: "skipped", missingHookEntries: [], requested: null, more: false }), cycleLimit);
|
|
265
313
|
limitTimer.unref?.();
|
|
266
314
|
});
|
|
267
315
|
const work = runCycle({ dir: options.dir, fetchImpl: options.fetchImpl, deliveryTimeoutMs: options.deliveryTimeoutMs });
|
|
@@ -287,6 +335,9 @@ export async function startService(options) {
|
|
|
287
335
|
log("the workspace asked this computer to send now");
|
|
288
336
|
setTimeout(() => { void cycle(); }, 0);
|
|
289
337
|
}
|
|
338
|
+
// A long queue: this cycle's sending time ran out, so the next one starts right away instead of at the next interval.
|
|
339
|
+
else if (result.more && !stopped)
|
|
340
|
+
setTimeout(() => { void cycle(); }, 1_000);
|
|
290
341
|
if (result.requested === "self_check") {
|
|
291
342
|
log("the workspace asked this computer to check now");
|
|
292
343
|
setTimeout(() => { void maintain(true); }, 0);
|
|
@@ -306,59 +357,73 @@ export async function startService(options) {
|
|
|
306
357
|
if (target)
|
|
307
358
|
result.policy = target.policy;
|
|
308
359
|
const current = agentVersion();
|
|
309
|
-
// Hook entries first:
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
360
|
+
// Hook entries first: always the hook this agent carries, by its path (never a version the workspace names, and never
|
|
361
|
+
// an npx form resolved at run time). Under "hold" only broken ones are repaired. After an update the new agent pins
|
|
362
|
+
// them to the hook it carries at its own first upkeep.
|
|
363
|
+
// Codex and Cursor send nothing between actions, so a hook entry of theirs that cannot start is kept as a gap the
|
|
364
|
+
// workspace hears of, before the repair below (the actions in between ran unchecked either way).
|
|
365
|
+
const outage = noteUnresolvableHooks(options.dir, harnesses.filter((h) => h === "codex" || h === "cursor"));
|
|
366
|
+
if (outage.recorded.length)
|
|
367
|
+
log(`hook entry cannot start for ${outage.recorded.join(", ")}: kept as a gap for the workspace`);
|
|
368
|
+
result.hookEntries = maintainHookEntries(harnesses, target?.policy !== "hold");
|
|
314
369
|
for (const change of result.hookEntries)
|
|
315
370
|
log(`${change.reason}: ${change.file}`);
|
|
316
371
|
const kind = installKind();
|
|
317
|
-
|
|
372
|
+
const newer = target?.policy === "recommended" && target.agent && compareVersions(target.agent, current) > 0 ? target.agent : null;
|
|
373
|
+
if (newer && kind === "per-machine") {
|
|
318
374
|
// Installed for every user: the organisation that deployed it updates it.
|
|
319
|
-
log(`the workspace recommends the Scopebond Agent ${
|
|
375
|
+
log(`the workspace recommends the Scopebond Agent ${newer}; this install is updated by your organization`);
|
|
320
376
|
}
|
|
321
|
-
else if (
|
|
322
|
-
log(`updating the Scopebond Agent ${current} -> ${
|
|
323
|
-
const verified = await fetchVerifiedInstaller(
|
|
324
|
-
if (verified.ok) {
|
|
325
|
-
result.updatedTo =
|
|
377
|
+
else if (newer && kind === "per-user") {
|
|
378
|
+
log(`updating the Scopebond Agent ${current} -> ${newer} (signed installer)`);
|
|
379
|
+
const verified = await fetchVerifiedInstaller(newer, { dir: options.dir, fetchImpl: options.fetchImpl });
|
|
380
|
+
if (verified.ok && installerUnchanged(verified)) {
|
|
381
|
+
result.updatedTo = newer;
|
|
326
382
|
lastMaintenance = result;
|
|
327
383
|
if (options.onUpdated)
|
|
328
|
-
options.onUpdated(
|
|
384
|
+
options.onUpdated(newer);
|
|
329
385
|
else {
|
|
330
|
-
// The installer replaces this file, so it runs after this agent has exited, and starts the
|
|
331
|
-
|
|
386
|
+
// The installer replaces this file, so it runs after this agent has exited, checks it again, and starts the tray
|
|
387
|
+
// (which starts the agent) or the agent again.
|
|
388
|
+
installAfterExit(verified, process.pid, existsSync(launcherPath(options.dir)) ? launcherPath(options.dir) : null, nativeTray);
|
|
332
389
|
setTimeout(() => { setTimeout(() => process.exit(0), 5_000).unref(); void stop().finally(() => process.exit(0)); }, 500);
|
|
333
390
|
}
|
|
334
391
|
return result;
|
|
335
392
|
}
|
|
336
|
-
result.error = `update to ${
|
|
393
|
+
result.error = `update to ${newer} could not be verified: ${verified.ok ? "the installer changed after it was checked" : verified.reason}; nothing was installed`;
|
|
337
394
|
log(result.error);
|
|
338
395
|
}
|
|
339
|
-
else if (
|
|
340
|
-
log(`
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
396
|
+
else if (newer && recentlyFailedUpdate(options.dir, newer)) {
|
|
397
|
+
log(`the Scopebond Agent ${newer} did not start here earlier; staying on ${current} for now`);
|
|
398
|
+
}
|
|
399
|
+
else if (newer) {
|
|
400
|
+
log(`updating the Scopebond Agent ${current} -> ${newer}`);
|
|
401
|
+
const installed = await installAgent(newer, { fetchImpl: options.fetchImpl });
|
|
402
|
+
const started = installed.ok ? await (options.startCheck ?? startCheck)(newer) : null;
|
|
403
|
+
if (installed.ok && started?.ok) {
|
|
404
|
+
result.updatedTo = newer;
|
|
344
405
|
lastMaintenance = result;
|
|
345
406
|
if (options.onUpdated)
|
|
346
|
-
options.onUpdated(
|
|
347
|
-
else
|
|
348
|
-
|
|
349
|
-
// Hand over: stop, then exit. A stop that never finishes (a tray or window child that will not close) must not
|
|
350
|
-
// keep the old agent alive with the replacement waiting on it, so the exit has a hard deadline.
|
|
351
|
-
setTimeout(() => { setTimeout(() => process.exit(code), 5_000).unref(); void stop().finally(() => process.exit(code)); }, 500);
|
|
352
|
-
}
|
|
407
|
+
options.onUpdated(newer);
|
|
408
|
+
else
|
|
409
|
+
handOver(newer, current);
|
|
353
410
|
return result;
|
|
354
411
|
}
|
|
355
|
-
|
|
412
|
+
if (installed.ok) {
|
|
413
|
+
// Installed, but the new program does not start: put the running version back.
|
|
414
|
+
recordFailedUpdate(options.dir, newer);
|
|
415
|
+
const back = await installAgent(current, { provenance: false });
|
|
416
|
+
result.error = `the Scopebond Agent ${newer} did not start (${(started?.output ?? "").slice(-200)}); ${back.ok ? `went back to ${current}` : `going back to ${current} failed: ${back.output.slice(-200)}`}`;
|
|
417
|
+
}
|
|
418
|
+
else
|
|
419
|
+
result.error = `update to ${newer} failed: ${installed.output.slice(-300)}`;
|
|
356
420
|
log(result.error);
|
|
357
421
|
}
|
|
358
422
|
// The local store: older rows rewritten, acknowledged receipts past the retention window removed, space returned.
|
|
359
423
|
// The agent may rewrite the whole file once (the hook never does that during a tool call).
|
|
360
424
|
try {
|
|
361
|
-
|
|
425
|
+
// In a process of its own: a pass is synchronous SQLite work for up to 30 s, and the agent keeps answering meanwhile.
|
|
426
|
+
result.store = await (options.upkeep ?? runUpkeepApart)(options.dir);
|
|
362
427
|
const kept = result.store;
|
|
363
428
|
if (kept && (kept.migrated || kept.receiptsRemoved || kept.stateRemoved || kept.pagesFreed)) {
|
|
364
429
|
log(`local store: ${kept.migrated} row(s) rewritten, ${kept.receiptsRemoved} receipt(s) past retention removed, ${kept.stateRemoved} finished check record(s) removed, ${kept.pagesFreed} page(s) returned`);
|
|
@@ -494,6 +559,10 @@ export async function startService(options) {
|
|
|
494
559
|
// `scopebond-agent stop` and `autostart off`: answer first, then stop, so the caller hears back.
|
|
495
560
|
"POST /stop": () => {
|
|
496
561
|
log("stopping: asked to by this computer's user");
|
|
562
|
+
try {
|
|
563
|
+
writeFileSync(join(options.dir, STOPPED_FILE), `${JSON.stringify({ stopped_at: new Date().toISOString(), pid: process.pid })}\n`);
|
|
564
|
+
}
|
|
565
|
+
catch { /* the tray may start it again */ }
|
|
497
566
|
setTimeout(() => { void stop().finally(() => options.onStopped?.()); }, 50);
|
|
498
567
|
return { stopping: true };
|
|
499
568
|
},
|
|
@@ -514,8 +583,11 @@ export async function startService(options) {
|
|
|
514
583
|
setTimeout(() => { void sendReasons(); }, 2_000);
|
|
515
584
|
return answer;
|
|
516
585
|
},
|
|
517
|
-
});
|
|
518
|
-
|
|
586
|
+
}, nativeTray ? { loopback: false } : {}); // beside the native tray nothing uses loopback: its tray and its hook use the pipe
|
|
587
|
+
// Beside the native tray (the signed install), the tray is that program: it started this agent, or starts at sign-in.
|
|
588
|
+
const tray = (options.tray ?? true) && process.env.SCOPEBOND_AGENT_TRAY !== "off" && !nativeTray ? startTray(options.dir) : null;
|
|
589
|
+
if (nativeTray && retireLauncherRunValue())
|
|
590
|
+
log("removed the ScopebondAgent sign-in entry: the Scopebond tray starts the agent");
|
|
519
591
|
const stop = async () => {
|
|
520
592
|
stopped = true;
|
|
521
593
|
try {
|
|
@@ -532,7 +604,32 @@ export async function startService(options) {
|
|
|
532
604
|
await control.close();
|
|
533
605
|
releaseAgentLock(lock);
|
|
534
606
|
};
|
|
535
|
-
|
|
607
|
+
/** Hand over to the agent just installed: start it, stop serving, and wait for it to answer with the new version. If it
|
|
608
|
+
* never does, put `previous` back, start that, and exit. A service manager (systemd) restarts the agent itself instead. */
|
|
609
|
+
const handOver = (version, previous) => {
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
void (async () => {
|
|
612
|
+
// Whatever happens below, this process ends.
|
|
613
|
+
setTimeout(() => process.exit(0), HANDOVER_CONFIRM_MS + 10 * 60_000).unref();
|
|
614
|
+
const spawned = spawnReplacement(options.dir);
|
|
615
|
+
// A stop that never finishes (a tray or window child that will not close) must not keep this agent serving.
|
|
616
|
+
await Promise.race([stop().catch(() => undefined), new Promise((r) => setTimeout(r, 5_000))]);
|
|
617
|
+
if (!spawned)
|
|
618
|
+
process.exit(RESTART_EXIT_CODE);
|
|
619
|
+
if (await waitForAgentVersion(options.dir, version, HANDOVER_CONFIRM_MS))
|
|
620
|
+
process.exit(0);
|
|
621
|
+
log(`the Scopebond Agent ${version} did not answer within ${Math.round(HANDOVER_CONFIRM_MS / 1000)} s; going back to ${previous}`);
|
|
622
|
+
recordFailedUpdate(options.dir, version);
|
|
623
|
+
const back = await installAgent(previous, { provenance: false });
|
|
624
|
+
log(back.ok ? `went back to the Scopebond Agent ${previous}` : `going back to ${previous} failed: ${back.output.slice(-300)}`);
|
|
625
|
+
// The replacement's launcher may restart it on the files just put back; otherwise start the previous version now.
|
|
626
|
+
if (!await waitForAgentVersion(options.dir, previous, 45_000))
|
|
627
|
+
spawnReplacement(options.dir);
|
|
628
|
+
process.exit(0);
|
|
629
|
+
})();
|
|
630
|
+
}, 500);
|
|
631
|
+
};
|
|
632
|
+
log(`Scopebond Agent ${agentVersion()} running for ${options.dir} (control on ${[control.endpoint.socket, control.endpoint.port ? `127.0.0.1:${control.endpoint.port}` : null].filter(Boolean).join(" and ")})`);
|
|
536
633
|
// Maintenance is armed before the first cycle, so a first cycle that hangs cannot keep it from ever starting.
|
|
537
634
|
if (options.maintenance !== false) {
|
|
538
635
|
// Shortly after start (so a fresh sign-in is not slowed), then every six hours.
|