@mono-agent/agent-app 0.13.0 → 0.14.0
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/README.md +163 -32
- package/dist/adapter-send-tools-main.js +0 -1
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +8 -5
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +133 -287
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +2 -1
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts.map +1 -1
- package/dist/app-controller-continuation.js +1 -5
- package/dist/app-controller-continuation.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +2 -1
- package/dist/app-controller-maintenance.d.ts.map +1 -1
- package/dist/app-controller-maintenance.js +64 -1
- package/dist/app-controller-maintenance.js.map +1 -1
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +4 -1
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-utils.js +1 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/background-environment.d.ts +1 -0
- package/dist/background-environment.d.ts.map +1 -1
- package/dist/background-environment.js +1 -0
- package/dist/background-environment.js.map +1 -1
- package/dist/background-runtime.d.ts +13 -2
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +27 -3
- package/dist/background-runtime.js.map +1 -1
- package/dist/background.d.ts +26 -1
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +226 -34
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.js +8 -1
- package/dist/channel-drivers/cron.js.map +1 -1
- package/dist/channel-drivers/native-notify.d.ts +2 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -1
- package/dist/channel-drivers/native-notify.js +14 -2
- package/dist/channel-drivers/native-notify.js.map +1 -1
- package/dist/channel-drivers/slack.d.ts.map +1 -1
- package/dist/channel-drivers/slack.js +49 -0
- package/dist/channel-drivers/slack.js.map +1 -1
- package/dist/channel-drivers/telegram.d.ts.map +1 -1
- package/dist/channel-drivers/telegram.js +7 -18
- package/dist/channel-drivers/telegram.js.map +1 -1
- package/dist/channel-drivers/tui.d.ts.map +1 -1
- package/dist/channel-drivers/tui.js +8 -0
- package/dist/channel-drivers/tui.js.map +1 -1
- package/dist/cli-args.d.ts +8 -2
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +48 -27
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-background-command.d.ts.map +1 -1
- package/dist/cli-background-command.js +89 -14
- package/dist/cli-background-command.js.map +1 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +0 -14
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +1 -7
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-runs-command.d.ts +0 -3
- package/dist/cli-runs-command.d.ts.map +1 -1
- package/dist/cli-runs-command.js +1 -5
- package/dist/cli-runs-command.js.map +1 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +3 -4
- package/dist/config-reference.js.map +1 -1
- package/dist/doctor.js +1 -2
- package/dist/doctor.js.map +1 -1
- package/dist/interaction-bridge.d.ts +5 -22
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +251 -141
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/launchd.d.ts +33 -1
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +129 -2
- package/dist/launchd.js.map +1 -1
- package/dist/modules/catalog.js +1 -1
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/known-tools.d.ts +1 -1
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +0 -3
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/run-history.d.ts +6 -2
- package/dist/run-history.d.ts.map +1 -1
- package/dist/run-history.js +476 -96
- package/dist/run-history.js.map +1 -1
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/wizard/prompts.d.ts.map +1 -1
- package/dist/wizard/prompts.js +3 -4
- package/dist/wizard/prompts.js.map +1 -1
- package/dist/wizard/run.js +3 -3
- package/dist/wizard/run.js.map +1 -1
- package/package.json +15 -15
- package/schema/mono-agent.config.schema.json +2 -2
- package/skills/mono-agent-composer/SKILL.md +5 -5
- package/skills/mono-agent-composer/references/config-blueprint.md +2 -2
- package/skills/mono-agent-composer/references/feature-coverage.md +3 -3
- package/skills/mono-agent-composer/references/package-map.md +1 -1
- package/skills/mono-agent-composer/references/playbooks.md +1 -1
- package/skills/mono-agent-composer/references/validation.md +20 -0
package/dist/background.js
CHANGED
|
@@ -9,10 +9,10 @@ import { describeSensitiveDataExportWarning, phoenixAppBaseUrl, resolveAppTraceR
|
|
|
9
9
|
import { formatChannelFactValue } from "./channel-fact-format.js";
|
|
10
10
|
import { formatHumanChannelSections } from "./channel-status-display.js";
|
|
11
11
|
import { hasCompletedManagedStartup } from "./managed-startup.js";
|
|
12
|
-
import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
|
|
12
|
+
import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdManagedWorkerInfo, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
|
|
13
13
|
import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
|
|
14
14
|
import { beginLaunchdLogMaintenanceIntent, clearLaunchdLogMaintenanceIntent, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, markLaunchdLogMaintenanceRestoring, markLaunchdLogMaintenanceStopped, markLaunchdLogMaintenanceStopping, readLaunchdLogMaintenanceIntent, rotateStoppedLaunchdLogs, } from "./launchd-logs.js";
|
|
15
|
-
import { ensureManagedBackgroundRuntime, MANAGED_BACKGROUND_WORKER_ENV, } from "./background-runtime.js";
|
|
15
|
+
import { ensureManagedBackgroundRuntime, inspectManagedRuntimeSourceIdentity, MANAGED_BACKGROUND_WORKER_ENV, verifyManagedRuntimeLaunch, } from "./background-runtime.js";
|
|
16
16
|
import { backgroundSnapshotFromMetadata, captureBackgroundSnapshot, encodeBackgroundSnapshot, sameBackgroundSnapshot, } from "./background-snapshot.js";
|
|
17
17
|
import { resolveConfiguredManagedRuntimePackages } from "./managed-runtime-packages.js";
|
|
18
18
|
import { acquireManagedRuntimePublicationBarrier } from "./managed-runtime-publication.js";
|
|
@@ -68,6 +68,7 @@ export async function resolveInstanceTarget(input) {
|
|
|
68
68
|
paths: launchdPathsFor(label),
|
|
69
69
|
nodePath: process.execPath,
|
|
70
70
|
cliPath: input.cliPath,
|
|
71
|
+
controllerCliPath: input.cliPath,
|
|
71
72
|
// Bake an explicit --env-file (resolved absolute) into the plist so the
|
|
72
73
|
// launchd worker loads the same env file the launcher did.
|
|
73
74
|
...(input.args.envFile === undefined ? {} : { envFile: resolve(cwd, input.args.envFile) }),
|
|
@@ -110,6 +111,7 @@ export function defaultBackgroundDeps() {
|
|
|
110
111
|
return {
|
|
111
112
|
runner: makeLaunchctlRunner(),
|
|
112
113
|
getuid: () => process.getuid?.() ?? 0,
|
|
114
|
+
currentPid: () => process.pid,
|
|
113
115
|
now: () => Date.now(),
|
|
114
116
|
sleep: (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
|
|
115
117
|
listRecordedRuns,
|
|
@@ -143,6 +145,8 @@ export function defaultBackgroundDeps() {
|
|
|
143
145
|
}
|
|
144
146
|
},
|
|
145
147
|
ensureManagedRuntime: (input) => ensureManagedBackgroundRuntime(input),
|
|
148
|
+
inspectManagedRuntimeSourceIdentity,
|
|
149
|
+
verifyManagedRuntimeLaunch,
|
|
146
150
|
resolveManagedRuntimePackages: (target) => resolveConfiguredManagedRuntimePackages({
|
|
147
151
|
cwd: target.cwd,
|
|
148
152
|
configPath: target.configPath,
|
|
@@ -172,6 +176,118 @@ export async function startBackground(target, deps, poll) {
|
|
|
172
176
|
export async function restartBackground(target, deps, poll) {
|
|
173
177
|
return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
|
|
174
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Private one-shot recovery controller invoked by the maintenance LaunchAgent.
|
|
181
|
+
* It authenticates the exact helper PID launchd owns, compares the inert source
|
|
182
|
+
* CLI identity with the strictly parsed loaded worker definition, and repairs
|
|
183
|
+
* only when the worker, snapshot, definition, or available source closure has
|
|
184
|
+
* drifted. A healthy worker remains serving while a replacement runtime is
|
|
185
|
+
* materialized.
|
|
186
|
+
*/
|
|
187
|
+
export async function maintainLaunchdController(target, deps, options) {
|
|
188
|
+
const helper = maintenancePathsForTarget(target);
|
|
189
|
+
const uid = deps.getuid();
|
|
190
|
+
const helperService = await launchdServiceInfo(deps.runner, helper.label, uid);
|
|
191
|
+
const helperPid = deps.currentPid();
|
|
192
|
+
if (!helperService.loaded || helperService.pid !== helperPid || !deps.isAlive(helperPid)) {
|
|
193
|
+
reportMaintenanceFailure(target, deps, "authenticate the launchd-owned recovery controller", new Error(`launchd does not own this helper pid ${helperPid}`));
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
const release = await deps.acquireLifecycleLock(target);
|
|
197
|
+
if (release === undefined) {
|
|
198
|
+
// A manual lifecycle command owns the label. The helper exits cleanly so
|
|
199
|
+
// that command can boot it out; StartInterval supplies the next retry.
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
let maintainLogsOnly = false;
|
|
203
|
+
let resultCode = 1;
|
|
204
|
+
try {
|
|
205
|
+
const [worker, sources, desiredIdentity] = await Promise.all([
|
|
206
|
+
launchdManagedWorkerInfo(deps.runner, target.label, uid),
|
|
207
|
+
findInstances(target, deps),
|
|
208
|
+
deps.inspectManagedRuntimeSourceIdentity(target.cliPath),
|
|
209
|
+
]);
|
|
210
|
+
let loadedIdentity;
|
|
211
|
+
if (worker.definition !== undefined) {
|
|
212
|
+
try {
|
|
213
|
+
loadedIdentity = await deps.verifyManagedRuntimeLaunch({
|
|
214
|
+
currentCliPath: worker.definition.cliPath,
|
|
215
|
+
launchProof: worker.definition.expectedManagedRuntimeLaunch,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// A malformed/unverified loaded closure is a recovery condition. Do not
|
|
220
|
+
// trust any identity or execute its advertised CLI.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const source = worker.pid === undefined
|
|
224
|
+
? undefined
|
|
225
|
+
: sources.find((candidate) => candidate.pid === worker.pid);
|
|
226
|
+
const durableSnapshotStillMatches = await snapshotStillMatches(target, deps);
|
|
227
|
+
const workerHealthy = worker.loaded
|
|
228
|
+
&& worker.pid !== undefined
|
|
229
|
+
&& deps.isAlive(worker.pid)
|
|
230
|
+
&& source !== undefined
|
|
231
|
+
&& isReady(source, false)
|
|
232
|
+
&& snapshotMetadataMatches(source, target.expectedSnapshot)
|
|
233
|
+
&& durableSnapshotStillMatches;
|
|
234
|
+
const definitionMatches = worker.definition !== undefined
|
|
235
|
+
&& managedWorkerDefinitionMatchesTarget(worker.definition, target);
|
|
236
|
+
const runtimeMatches = loadedIdentity !== undefined
|
|
237
|
+
&& sameManagedRuntimeIdentity(loadedIdentity, desiredIdentity);
|
|
238
|
+
const needsRecovery = !workerHealthy
|
|
239
|
+
|| !definitionMatches
|
|
240
|
+
|| loadedIdentity === undefined
|
|
241
|
+
|| (options.sourceAvailable && !runtimeMatches);
|
|
242
|
+
if (!needsRecovery) {
|
|
243
|
+
maintainLogsOnly = true;
|
|
244
|
+
resultCode = 0;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
if (!options.sourceAvailable) {
|
|
248
|
+
deps.stderr(ui.style.dim(`The original controller CLI is unavailable; recovering ${target.label} from the helper's private closure without claiming an upgrade.`) + "\n");
|
|
249
|
+
}
|
|
250
|
+
const recovered = await ensureBackgroundReadyUnlocked(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL, options.readinessPoll ?? DEFAULT_READINESS_POLL, { preserveMaintenanceService: true, preserveDefinitionsOnFailure: true });
|
|
251
|
+
resultCode = recovered.ok ? 0 : 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
reportMaintenanceFailure(target, deps, "reconcile the managed worker", error);
|
|
256
|
+
resultCode = 1;
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
await release().catch((error) => {
|
|
260
|
+
deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return maintainLogsOnly
|
|
264
|
+
? await maintainLaunchdLogs(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL)
|
|
265
|
+
: resultCode;
|
|
266
|
+
}
|
|
267
|
+
function managedWorkerDefinitionMatchesTarget(definition, target) {
|
|
268
|
+
return target.expectedSnapshot !== undefined
|
|
269
|
+
&& definition.plistPath === target.paths.plistPath
|
|
270
|
+
&& definition.nodePath === target.nodePath
|
|
271
|
+
&& definition.configPath === target.configPath
|
|
272
|
+
&& definition.cwd === target.cwd
|
|
273
|
+
&& definition.envFile === target.envFile
|
|
274
|
+
&& definition.stdoutPath === target.paths.stdoutPath
|
|
275
|
+
&& definition.stderrPath === target.paths.stderrPath
|
|
276
|
+
&& definition.expectedBackgroundSnapshot === encodeBackgroundSnapshot(target.expectedSnapshot)
|
|
277
|
+
&& sameStringRecord(definition.environment, target.environment);
|
|
278
|
+
}
|
|
279
|
+
function sameManagedRuntimeIdentity(loaded, desired) {
|
|
280
|
+
return loaded.packageVersion === desired.packageVersion && loaded.cliSha256 === desired.cliSha256;
|
|
281
|
+
}
|
|
282
|
+
function sameStringRecord(left, right) {
|
|
283
|
+
const leftEntries = Object.entries(left).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
284
|
+
const rightEntries = Object.entries(right).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
285
|
+
return leftEntries.length === rightEntries.length
|
|
286
|
+
&& leftEntries.every(([name, value], index) => {
|
|
287
|
+
const other = rightEntries[index];
|
|
288
|
+
return other?.[0] === name && other[1] === value;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
175
291
|
/**
|
|
176
292
|
* Stop, perform the caller's stopped-worker mutation, and start again while
|
|
177
293
|
* retaining one lifecycle lock. This closes the force-restart gap in which a
|
|
@@ -427,7 +543,7 @@ export async function ensureBackgroundReady(target, deps, poll) {
|
|
|
427
543
|
});
|
|
428
544
|
}
|
|
429
545
|
}
|
|
430
|
-
async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll) {
|
|
546
|
+
async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll, options = {}) {
|
|
431
547
|
if (target.expectedSnapshot === undefined) {
|
|
432
548
|
deps.stderr(ui.errorLine("Refusing to launch a managed worker without an approved background snapshot."));
|
|
433
549
|
deps.stderr(ui.hint("No LaunchAgent changes were made. Retry from the wizard or `mono-agent start`."));
|
|
@@ -453,9 +569,9 @@ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readines
|
|
|
453
569
|
deps.stderr(ui.errorLine(`Another runtime publication is already active for ${target.label}.`));
|
|
454
570
|
return { ok: false, action: "start", reason: "ownership" };
|
|
455
571
|
}
|
|
456
|
-
return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier);
|
|
572
|
+
return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options);
|
|
457
573
|
}
|
|
458
|
-
async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier) {
|
|
574
|
+
async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options) {
|
|
459
575
|
let barrierReleased = false;
|
|
460
576
|
const releaseBarrier = async () => {
|
|
461
577
|
if (barrierReleased)
|
|
@@ -464,7 +580,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
|
|
|
464
580
|
barrierReleased = true;
|
|
465
581
|
};
|
|
466
582
|
try {
|
|
467
|
-
return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier);
|
|
583
|
+
return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options);
|
|
468
584
|
}
|
|
469
585
|
finally {
|
|
470
586
|
if (!barrierReleased) {
|
|
@@ -474,7 +590,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
|
|
|
474
590
|
}
|
|
475
591
|
}
|
|
476
592
|
}
|
|
477
|
-
async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier) {
|
|
593
|
+
async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options) {
|
|
478
594
|
const uid = deps.getuid();
|
|
479
595
|
let launchTarget;
|
|
480
596
|
const runtimeStartedAt = deps.now();
|
|
@@ -560,7 +676,7 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
|
|
|
560
676
|
// The replacement plist now contains the finalized-runtime proof. Let
|
|
561
677
|
// launchd respawn only after the stopped-window commit is complete.
|
|
562
678
|
await releaseBarrier();
|
|
563
|
-
});
|
|
679
|
+
}, { preserveMaintenanceService: options.preserveMaintenanceService === true });
|
|
564
680
|
}
|
|
565
681
|
catch (error) {
|
|
566
682
|
reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
|
|
@@ -582,13 +698,13 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
|
|
|
582
698
|
}
|
|
583
699
|
catch (error) {
|
|
584
700
|
reportReadinessException(deps, error);
|
|
585
|
-
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
|
|
586
|
-
reportReadinessCleanup(launchTarget, deps, stopped);
|
|
701
|
+
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
|
|
702
|
+
reportReadinessCleanup(launchTarget, deps, stopped, options);
|
|
587
703
|
return { ok: false, action, reason: "readiness" };
|
|
588
704
|
}
|
|
589
705
|
if (ready === undefined) {
|
|
590
706
|
if (!(await snapshotStillMatches(launchTarget, deps))) {
|
|
591
|
-
const stopped = await
|
|
707
|
+
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options) ? 0 : 1;
|
|
592
708
|
reportSnapshotDrift(launchTarget, deps);
|
|
593
709
|
if (stopped === 0) {
|
|
594
710
|
deps.stderr(ui.style.dim("The drifted LaunchAgent was stopped before returning control.") + "\n");
|
|
@@ -599,8 +715,8 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
|
|
|
599
715
|
return { ok: false, action, reason: "snapshot" };
|
|
600
716
|
}
|
|
601
717
|
reportTimeout(deps);
|
|
602
|
-
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
|
|
603
|
-
reportReadinessCleanup(launchTarget, deps, stopped);
|
|
718
|
+
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
|
|
719
|
+
reportReadinessCleanup(launchTarget, deps, stopped, options);
|
|
604
720
|
return { ok: false, action, reason: "timeout" };
|
|
605
721
|
}
|
|
606
722
|
const completedAction = outcome.restarted ? "restarted" : "started";
|
|
@@ -633,6 +749,10 @@ async function writePlists(target, deps) {
|
|
|
633
749
|
if (target.managedRuntimeLaunchProof === undefined) {
|
|
634
750
|
throw new Error("A managed LaunchAgent requires a finalized runtime launch proof.");
|
|
635
751
|
}
|
|
752
|
+
const agentPath = target.environment.PATH;
|
|
753
|
+
if (agentPath === undefined || agentPath.length === 0) {
|
|
754
|
+
throw new Error("A managed LaunchAgent recovery controller requires the durable worker PATH.");
|
|
755
|
+
}
|
|
636
756
|
const maintenance = maintenancePathsForTarget(target);
|
|
637
757
|
const mainXml = buildPlistXml({
|
|
638
758
|
label: target.label,
|
|
@@ -652,6 +772,10 @@ async function writePlists(target, deps) {
|
|
|
652
772
|
nodePath: target.nodePath,
|
|
653
773
|
cliPath: target.cliPath,
|
|
654
774
|
configPath: target.configPath,
|
|
775
|
+
controllerCliPath: target.controllerCliPath ?? target.cliPath,
|
|
776
|
+
agentCwd: target.cwd,
|
|
777
|
+
agentPath,
|
|
778
|
+
...(target.envFile === undefined ? {} : { envFile: target.envFile }),
|
|
655
779
|
// The agent folder may be renamed or removed while its already-running
|
|
656
780
|
// worker still owns log descriptors. Keep the scheduler's cwd in the
|
|
657
781
|
// durable account state tree; its config argument is already absolute.
|
|
@@ -832,7 +956,7 @@ function sameFileSnapshot(left, right) {
|
|
|
832
956
|
* before bootstrap so launchd cannot retain stale ProgramArguments or env from
|
|
833
957
|
* the previous plist.
|
|
834
958
|
*/
|
|
835
|
-
async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
|
|
959
|
+
async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped, options = {}) {
|
|
836
960
|
const maintenance = maintenancePathsForTarget(target);
|
|
837
961
|
const [service, maintenanceService] = await Promise.all([
|
|
838
962
|
launchdServiceInfo(deps.runner, target.label, uid),
|
|
@@ -842,9 +966,11 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
842
966
|
// Stop the scheduler first. A helper that is already running either holds
|
|
843
967
|
// this lifecycle lock (so the controller could not enter) or is waiting to
|
|
844
968
|
// acquire it; bootout plus PID death prevents a post-stop resurrection.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
969
|
+
if (options.preserveMaintenanceService !== true) {
|
|
970
|
+
const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
|
|
971
|
+
if (!maintenanceStopped.ok) {
|
|
972
|
+
return { ok: false, restarted, failure: maintenanceStopped.failure };
|
|
973
|
+
}
|
|
848
974
|
}
|
|
849
975
|
let mainStopProven = false;
|
|
850
976
|
if (service.loaded) {
|
|
@@ -860,7 +986,9 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
860
986
|
]);
|
|
861
987
|
const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
|
|
862
988
|
if (!mainStopped.ok) {
|
|
863
|
-
const restoreFailure =
|
|
989
|
+
const restoreFailure = options.preserveMaintenanceService === true
|
|
990
|
+
? undefined
|
|
991
|
+
: await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
|
|
864
992
|
return {
|
|
865
993
|
ok: false,
|
|
866
994
|
restarted: true,
|
|
@@ -872,19 +1000,28 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
872
1000
|
mainStopProven = true;
|
|
873
1001
|
}
|
|
874
1002
|
await whileStopped(mainStopProven);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1003
|
+
if (options.preserveMaintenanceService !== true) {
|
|
1004
|
+
const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
|
|
1005
|
+
const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
|
|
1006
|
+
if (!maintenanceLoaded) {
|
|
1007
|
+
return {
|
|
1008
|
+
ok: false,
|
|
1009
|
+
restarted,
|
|
1010
|
+
failure: lifecycleFailure(maintenanceBooted, "scheduled recovery controller did not report loaded"),
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
883
1013
|
}
|
|
884
1014
|
const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
|
|
885
1015
|
if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
|
|
886
1016
|
return { ok: true, restarted };
|
|
887
1017
|
}
|
|
1018
|
+
if (options.preserveMaintenanceService === true) {
|
|
1019
|
+
return {
|
|
1020
|
+
ok: false,
|
|
1021
|
+
restarted,
|
|
1022
|
+
failure: lifecycleFailure(booted, "main service did not load; scheduled recovery remains loaded for retry"),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
888
1025
|
const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
|
|
889
1026
|
const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
|
|
890
1027
|
return {
|
|
@@ -1092,12 +1229,24 @@ async function stopBackgroundUnlocked(target, deps, poll) {
|
|
|
1092
1229
|
return 0;
|
|
1093
1230
|
}
|
|
1094
1231
|
export async function statusBackground(target, deps, options = {}) {
|
|
1095
|
-
const result = await
|
|
1232
|
+
const [result, service] = await Promise.all([
|
|
1233
|
+
deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs }),
|
|
1234
|
+
launchdServiceInfo(deps.runner, target.label, deps.getuid()),
|
|
1235
|
+
]);
|
|
1096
1236
|
const classified = await Promise.all(result.sources.map(async (source) => ({
|
|
1097
1237
|
source,
|
|
1098
1238
|
matches: await matchesConfig(source, target.configPath),
|
|
1099
1239
|
})));
|
|
1100
|
-
const
|
|
1240
|
+
const matchingSources = classified.filter((entry) => entry.matches).map((entry) => entry.source);
|
|
1241
|
+
const recorded = service.pid === undefined
|
|
1242
|
+
? matchingSources[0]
|
|
1243
|
+
: matchingSources.find((source) => source.pid === service.pid) ?? matchingSources[0];
|
|
1244
|
+
const active = service.loaded
|
|
1245
|
+
&& service.pid !== undefined
|
|
1246
|
+
&& deps.isAlive(service.pid)
|
|
1247
|
+
&& recorded?.pid === service.pid
|
|
1248
|
+
&& recorded.health === "running";
|
|
1249
|
+
const current = recorded === undefined || active ? recorded : inactiveStatusSource(recorded);
|
|
1101
1250
|
// Only surface other instances that are live or crashed — cleanly stopped
|
|
1102
1251
|
// manifests linger in the registry and would just be noise.
|
|
1103
1252
|
const others = classified
|
|
@@ -1106,11 +1255,11 @@ export async function statusBackground(target, deps, options = {}) {
|
|
|
1106
1255
|
if (options.json === true) {
|
|
1107
1256
|
const instance = current === undefined ? null : await assembleInstanceStatus(current, target, deps);
|
|
1108
1257
|
deps.stdout(`${JSON.stringify({
|
|
1109
|
-
ok:
|
|
1258
|
+
ok: active,
|
|
1110
1259
|
instance,
|
|
1111
1260
|
others: others.map(assembleOtherInstanceStatus),
|
|
1112
1261
|
})}\n`);
|
|
1113
|
-
return
|
|
1262
|
+
return active ? 0 : 1;
|
|
1114
1263
|
}
|
|
1115
1264
|
if (current === undefined) {
|
|
1116
1265
|
deps.stdout(ui.style.dim(`No running mono-agent instance for ${target.configPath}.`) + "\n");
|
|
@@ -1126,7 +1275,32 @@ export async function statusBackground(target, deps, options = {}) {
|
|
|
1126
1275
|
deps.stdout(`${formatOtherInstance(source)}\n`);
|
|
1127
1276
|
}
|
|
1128
1277
|
}
|
|
1129
|
-
return
|
|
1278
|
+
return active ? 0 : 1;
|
|
1279
|
+
}
|
|
1280
|
+
function inactiveStatusSource(source) {
|
|
1281
|
+
const { transports: _staleTransports, pid: _stalePid, ...withoutLiveProcessFacts } = source;
|
|
1282
|
+
const metadata = source.metadata;
|
|
1283
|
+
const channels = metadata?.channels;
|
|
1284
|
+
const stoppedChannels = isPlainRecord(channels)
|
|
1285
|
+
? Object.fromEntries(Object.entries(channels).map(([id, value]) => {
|
|
1286
|
+
if (!isPlainRecord(value) || value.kind !== "running")
|
|
1287
|
+
return [id, value];
|
|
1288
|
+
return [id, { kind: "stopped", reason: "instance is not running" }];
|
|
1289
|
+
}))
|
|
1290
|
+
: undefined;
|
|
1291
|
+
return {
|
|
1292
|
+
...withoutLiveProcessFacts,
|
|
1293
|
+
health: "stopped",
|
|
1294
|
+
status: "stopped",
|
|
1295
|
+
...(metadata === undefined
|
|
1296
|
+
? {}
|
|
1297
|
+
: {
|
|
1298
|
+
metadata: {
|
|
1299
|
+
...metadata,
|
|
1300
|
+
...(stoppedChannels === undefined ? {} : { channels: stoppedChannels }),
|
|
1301
|
+
},
|
|
1302
|
+
}),
|
|
1303
|
+
};
|
|
1130
1304
|
}
|
|
1131
1305
|
/**
|
|
1132
1306
|
* The machine-readable instance record for `status --json`. It mirrors the
|
|
@@ -1295,8 +1469,24 @@ function reportTimeout(deps) {
|
|
|
1295
1469
|
deps.stderr(ui.errorLine("mono-agent did not report ready within the timeout."));
|
|
1296
1470
|
deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
|
|
1297
1471
|
}
|
|
1298
|
-
async function cleanUpUnreadyBackground(target, deps, poll) {
|
|
1472
|
+
async function cleanUpUnreadyBackground(target, deps, poll, options = {}) {
|
|
1299
1473
|
try {
|
|
1474
|
+
if (options.preserveDefinitionsOnFailure === true) {
|
|
1475
|
+
const uid = deps.getuid();
|
|
1476
|
+
const [service, sources] = await Promise.all([
|
|
1477
|
+
launchdServiceInfo(deps.runner, target.label, uid),
|
|
1478
|
+
findInstances(target, deps),
|
|
1479
|
+
]);
|
|
1480
|
+
const liveTracePids = uniquePids(sources
|
|
1481
|
+
.filter((source) => source.health !== "stopped"
|
|
1482
|
+
&& source.pid !== undefined
|
|
1483
|
+
&& deps.isAlive(source.pid))
|
|
1484
|
+
.map((source) => source.pid));
|
|
1485
|
+
if (liveTracePids.some((pid) => pid !== service.pid))
|
|
1486
|
+
return false;
|
|
1487
|
+
const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid, ...liveTracePids]), deps, uid, poll);
|
|
1488
|
+
return stopped.ok;
|
|
1489
|
+
}
|
|
1300
1490
|
return await stopBackgroundUnlocked(target, { ...deps, stdout: () => undefined }, poll) === 0;
|
|
1301
1491
|
}
|
|
1302
1492
|
catch (error) {
|
|
@@ -1304,9 +1494,11 @@ async function cleanUpUnreadyBackground(target, deps, poll) {
|
|
|
1304
1494
|
return false;
|
|
1305
1495
|
}
|
|
1306
1496
|
}
|
|
1307
|
-
function reportReadinessCleanup(target, deps, stopped) {
|
|
1497
|
+
function reportReadinessCleanup(target, deps, stopped, options = {}) {
|
|
1308
1498
|
deps.stderr(stopped
|
|
1309
|
-
?
|
|
1499
|
+
? options.preserveDefinitionsOnFailure === true
|
|
1500
|
+
? ui.style.dim("The unready main LaunchAgent was stopped; both definitions and the scheduled recovery controller remain for retry.") + "\n"
|
|
1501
|
+
: ui.style.dim("The unready LaunchAgent and scheduled maintenance were stopped and their definitions removed before returning control.") + "\n"
|
|
1310
1502
|
: ui.style.yellow("The unready LaunchAgent could not be proven stopped; a worker or maintenance helper may still be running.") + "\n");
|
|
1311
1503
|
reportLaunchRecovery(target, deps);
|
|
1312
1504
|
}
|