@mono-agent/agent-app 0.13.0 → 0.15.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 +164 -37
- 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-config.d.ts.map +1 -1
- package/dist/app-config.js +3 -0
- package/dist/app-config.js.map +1 -1
- package/dist/app-controller-channels.d.ts +41 -7
- package/dist/app-controller-channels.d.ts.map +1 -1
- package/dist/app-controller-channels.js +4 -4
- package/dist/app-controller-channels.js.map +1 -1
- package/dist/app-controller-continuation.d.ts +43 -11
- 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-lifecycle.d.ts +34 -5
- package/dist/app-controller-lifecycle.d.ts.map +1 -1
- package/dist/app-controller-lifecycle.js.map +1 -1
- package/dist/app-controller-maintenance.d.ts +46 -8
- 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-memory-health.d.ts +39 -10
- package/dist/app-controller-memory-health.d.ts.map +1 -1
- package/dist/app-controller-memory-health.js.map +1 -1
- package/dist/app-controller-memory.d.ts +27 -5
- package/dist/app-controller-memory.d.ts.map +1 -1
- package/dist/app-controller-memory.js +0 -1
- package/dist/app-controller-memory.js.map +1 -1
- package/dist/app-controller-responder.d.ts +53 -7
- package/dist/app-controller-responder.d.ts.map +1 -1
- package/dist/app-controller-responder.js +5 -5
- package/dist/app-controller-responder.js.map +1 -1
- package/dist/app-controller-traceability.d.ts +63 -14
- package/dist/app-controller-traceability.d.ts.map +1 -1
- package/dist/app-controller-traceability.js.map +1 -1
- package/dist/app-controller-types.d.ts +63 -0
- package/dist/app-controller-types.d.ts.map +1 -0
- package/dist/app-controller-types.js +2 -0
- package/dist/app-controller-types.js.map +1 -0
- package/dist/app-controller-utils.d.ts +1 -1
- package/dist/app-controller-utils.d.ts.map +1 -1
- package/dist/app-controller-utils.js +1 -1
- package/dist/app-controller-utils.js.map +1 -1
- package/dist/app-controller.d.ts +5 -62
- package/dist/app-controller.d.ts.map +1 -1
- package/dist/app-controller.js +0 -6
- package/dist/app-controller.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-lifecycle-utils.d.ts +27 -0
- package/dist/background-lifecycle-utils.d.ts.map +1 -0
- package/dist/background-lifecycle-utils.js +69 -0
- package/dist/background-lifecycle-utils.js.map +1 -0
- package/dist/background-log-maintenance.d.ts +6 -0
- package/dist/background-log-maintenance.d.ts.map +1 -0
- package/dist/background-log-maintenance.js +185 -0
- package/dist/background-log-maintenance.js.map +1 -0
- 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 +28 -5
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +229 -297
- 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/channel-plugins.d.ts +2 -11
- package/dist/channel-plugins.d.ts.map +1 -1
- package/dist/channel-plugins.js +63 -60
- package/dist/channel-plugins.js.map +1 -1
- package/dist/channel-status-display.js +2 -6
- package/dist/channel-status-display.js.map +1 -1
- package/dist/channels.d.ts +5 -6
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +7 -5
- package/dist/channels.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 +98 -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 +90 -15
- 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/cli-validate-config-command.d.ts.map +1 -1
- package/dist/cli-validate-config-command.js +12 -8
- package/dist/cli-validate-config-command.js.map +1 -1
- package/dist/config-reference.d.ts +4 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +245 -69
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +2 -8
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +3 -25
- package/dist/configured-agent.js.map +1 -1
- package/dist/consumer-contract.d.ts.map +1 -1
- package/dist/consumer-contract.js +2 -4
- package/dist/consumer-contract.js.map +1 -1
- package/dist/doctor-observability.d.ts +7 -0
- package/dist/doctor-observability.d.ts.map +1 -0
- package/dist/doctor-observability.js +80 -0
- package/dist/doctor-observability.js.map +1 -0
- package/dist/doctor-types.d.ts +17 -0
- package/dist/doctor-types.d.ts.map +1 -0
- package/dist/doctor-types.js +2 -0
- package/dist/doctor-types.js.map +1 -0
- package/dist/doctor.d.ts +2 -12
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +8 -108
- package/dist/doctor.js.map +1 -1
- package/dist/first-run-readiness.d.ts +1 -1
- package/dist/first-run-readiness.d.ts.map +1 -1
- package/dist/first-run-readiness.js +1 -1
- package/dist/first-run-readiness.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.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/managed-web-logs.d.ts +16 -0
- package/dist/managed-web-logs.d.ts.map +1 -0
- package/dist/managed-web-logs.js +153 -0
- package/dist/managed-web-logs.js.map +1 -0
- 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/posted-reply-history.d.ts.map +1 -1
- package/dist/posted-reply-history.js +3 -0
- package/dist/posted-reply-history.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/web-command.d.ts +4 -1
- package/dist/web-command.d.ts.map +1 -1
- package/dist/web-command.js +31 -1
- package/dist/web-command.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 +16 -16
- package/schema/mono-agent.config.schema.json +282 -164
- package/skills/mono-agent-composer/SKILL.md +5 -5
- package/skills/mono-agent-composer/references/config-blueprint.md +6 -13
- package/skills/mono-agent-composer/references/discovery-questions.md +1 -1
- package/skills/mono-agent-composer/references/feature-coverage.md +7 -8
- package/skills/mono-agent-composer/references/package-map.md +5 -6
- package/skills/mono-agent-composer/references/playbooks.md +3 -3
- package/skills/mono-agent-composer/references/validation.md +21 -1
package/dist/background.js
CHANGED
|
@@ -9,16 +9,18 @@ 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";
|
|
19
19
|
import { processIncarnationFromJson, } from "./process-incarnation.js";
|
|
20
20
|
import { acquireOwnerPrivateLock, validateOwnerPrivateLockInputs } from "./owner-private-lock.js";
|
|
21
21
|
import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
|
|
22
|
+
import { lifecycleFailure, maintenanceErrorMessage, pollUntil, uniquePids, unloadLaunchdService, } from "./background-lifecycle-utils.js";
|
|
23
|
+
import { maintainLaunchdLogsOperation, reportMaintenanceFailure, } from "./background-log-maintenance.js";
|
|
22
24
|
import * as ui from "./ui.js";
|
|
23
25
|
export { acquireBackgroundWorkerLease, backgroundWorkerLeasePath, } from "./background-worker-lease.js";
|
|
24
26
|
export { LAUNCHD_LOG_MAX_BYTES, LAUNCHD_LOG_ROTATION_COUNT, } from "./launchd-logs.js";
|
|
@@ -68,6 +70,7 @@ export async function resolveInstanceTarget(input) {
|
|
|
68
70
|
paths: launchdPathsFor(label),
|
|
69
71
|
nodePath: process.execPath,
|
|
70
72
|
cliPath: input.cliPath,
|
|
73
|
+
controllerCliPath: input.cliPath,
|
|
71
74
|
// Bake an explicit --env-file (resolved absolute) into the plist so the
|
|
72
75
|
// launchd worker loads the same env file the launcher did.
|
|
73
76
|
...(input.args.envFile === undefined ? {} : { envFile: resolve(cwd, input.args.envFile) }),
|
|
@@ -110,6 +113,7 @@ export function defaultBackgroundDeps() {
|
|
|
110
113
|
return {
|
|
111
114
|
runner: makeLaunchctlRunner(),
|
|
112
115
|
getuid: () => process.getuid?.() ?? 0,
|
|
116
|
+
currentPid: () => process.pid,
|
|
113
117
|
now: () => Date.now(),
|
|
114
118
|
sleep: (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
|
|
115
119
|
listRecordedRuns,
|
|
@@ -143,6 +147,8 @@ export function defaultBackgroundDeps() {
|
|
|
143
147
|
}
|
|
144
148
|
},
|
|
145
149
|
ensureManagedRuntime: (input) => ensureManagedBackgroundRuntime(input),
|
|
150
|
+
inspectManagedRuntimeSourceIdentity,
|
|
151
|
+
verifyManagedRuntimeLaunch,
|
|
146
152
|
resolveManagedRuntimePackages: (target) => resolveConfiguredManagedRuntimePackages({
|
|
147
153
|
cwd: target.cwd,
|
|
148
154
|
configPath: target.configPath,
|
|
@@ -172,6 +178,118 @@ export async function startBackground(target, deps, poll) {
|
|
|
172
178
|
export async function restartBackground(target, deps, poll) {
|
|
173
179
|
return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
|
|
174
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Private one-shot recovery controller invoked by the maintenance LaunchAgent.
|
|
183
|
+
* It authenticates the exact helper PID launchd owns, compares the inert source
|
|
184
|
+
* CLI identity with the strictly parsed loaded worker definition, and repairs
|
|
185
|
+
* only when the worker, snapshot, definition, or available source closure has
|
|
186
|
+
* drifted. A healthy worker remains serving while a replacement runtime is
|
|
187
|
+
* materialized.
|
|
188
|
+
*/
|
|
189
|
+
export async function maintainLaunchdController(target, deps, options) {
|
|
190
|
+
const helper = maintenancePathsForTarget(target);
|
|
191
|
+
const uid = deps.getuid();
|
|
192
|
+
const helperService = await launchdServiceInfo(deps.runner, helper.label, uid);
|
|
193
|
+
const helperPid = deps.currentPid();
|
|
194
|
+
if (!helperService.loaded || helperService.pid !== helperPid || !deps.isAlive(helperPid)) {
|
|
195
|
+
reportMaintenanceFailure(target, deps, "authenticate the launchd-owned recovery controller", new Error(`launchd does not own this helper pid ${helperPid}`));
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const release = await deps.acquireLifecycleLock(target);
|
|
199
|
+
if (release === undefined) {
|
|
200
|
+
// A manual lifecycle command owns the label. The helper exits cleanly so
|
|
201
|
+
// that command can boot it out; StartInterval supplies the next retry.
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
let maintainLogsOnly = false;
|
|
205
|
+
let resultCode = 1;
|
|
206
|
+
try {
|
|
207
|
+
const [worker, sources, desiredIdentity] = await Promise.all([
|
|
208
|
+
launchdManagedWorkerInfo(deps.runner, target.label, uid),
|
|
209
|
+
findInstances(target, deps),
|
|
210
|
+
deps.inspectManagedRuntimeSourceIdentity(target.cliPath),
|
|
211
|
+
]);
|
|
212
|
+
let loadedIdentity;
|
|
213
|
+
if (worker.definition !== undefined) {
|
|
214
|
+
try {
|
|
215
|
+
loadedIdentity = await deps.verifyManagedRuntimeLaunch({
|
|
216
|
+
currentCliPath: worker.definition.cliPath,
|
|
217
|
+
launchProof: worker.definition.expectedManagedRuntimeLaunch,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// A malformed/unverified loaded closure is a recovery condition. Do not
|
|
222
|
+
// trust any identity or execute its advertised CLI.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const source = worker.pid === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: sources.find((candidate) => candidate.pid === worker.pid);
|
|
228
|
+
const durableSnapshotStillMatches = await snapshotStillMatches(target, deps);
|
|
229
|
+
const workerHealthy = worker.loaded
|
|
230
|
+
&& worker.pid !== undefined
|
|
231
|
+
&& deps.isAlive(worker.pid)
|
|
232
|
+
&& source !== undefined
|
|
233
|
+
&& isReady(source, false)
|
|
234
|
+
&& snapshotMetadataMatches(source, target.expectedSnapshot)
|
|
235
|
+
&& durableSnapshotStillMatches;
|
|
236
|
+
const definitionMatches = worker.definition !== undefined
|
|
237
|
+
&& managedWorkerDefinitionMatchesTarget(worker.definition, target);
|
|
238
|
+
const runtimeMatches = loadedIdentity !== undefined
|
|
239
|
+
&& sameManagedRuntimeIdentity(loadedIdentity, desiredIdentity);
|
|
240
|
+
const needsRecovery = !workerHealthy
|
|
241
|
+
|| !definitionMatches
|
|
242
|
+
|| loadedIdentity === undefined
|
|
243
|
+
|| (options.sourceAvailable && !runtimeMatches);
|
|
244
|
+
if (!needsRecovery) {
|
|
245
|
+
maintainLogsOnly = true;
|
|
246
|
+
resultCode = 0;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (!options.sourceAvailable) {
|
|
250
|
+
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");
|
|
251
|
+
}
|
|
252
|
+
const recovered = await ensureBackgroundReadyUnlocked(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL, options.readinessPoll ?? DEFAULT_READINESS_POLL, { preserveMaintenanceService: true, preserveDefinitionsOnFailure: true });
|
|
253
|
+
resultCode = recovered.ok ? 0 : 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
reportMaintenanceFailure(target, deps, "reconcile the managed worker", error);
|
|
258
|
+
resultCode = 1;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await release().catch((error) => {
|
|
262
|
+
deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return maintainLogsOnly
|
|
266
|
+
? await maintainLaunchdLogs(target, deps, options.controlPoll ?? DEFAULT_CONTROL_POLL)
|
|
267
|
+
: resultCode;
|
|
268
|
+
}
|
|
269
|
+
function managedWorkerDefinitionMatchesTarget(definition, target) {
|
|
270
|
+
return target.expectedSnapshot !== undefined
|
|
271
|
+
&& definition.plistPath === target.paths.plistPath
|
|
272
|
+
&& definition.nodePath === target.nodePath
|
|
273
|
+
&& definition.configPath === target.configPath
|
|
274
|
+
&& definition.cwd === target.cwd
|
|
275
|
+
&& definition.envFile === target.envFile
|
|
276
|
+
&& definition.stdoutPath === target.paths.stdoutPath
|
|
277
|
+
&& definition.stderrPath === target.paths.stderrPath
|
|
278
|
+
&& definition.expectedBackgroundSnapshot === encodeBackgroundSnapshot(target.expectedSnapshot)
|
|
279
|
+
&& sameStringRecord(definition.environment, target.environment);
|
|
280
|
+
}
|
|
281
|
+
function sameManagedRuntimeIdentity(loaded, desired) {
|
|
282
|
+
return loaded.packageVersion === desired.packageVersion && loaded.cliSha256 === desired.cliSha256;
|
|
283
|
+
}
|
|
284
|
+
function sameStringRecord(left, right) {
|
|
285
|
+
const leftEntries = Object.entries(left).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
286
|
+
const rightEntries = Object.entries(right).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
287
|
+
return leftEntries.length === rightEntries.length
|
|
288
|
+
&& leftEntries.every(([name, value], index) => {
|
|
289
|
+
const other = rightEntries[index];
|
|
290
|
+
return other?.[0] === name && other[1] === value;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
175
293
|
/**
|
|
176
294
|
* Stop, perform the caller's stopped-worker mutation, and start again while
|
|
177
295
|
* retaining one lifecycle lock. This closes the force-restart gap in which a
|
|
@@ -205,205 +323,7 @@ export async function forceRestartBackground(target, deps, whileStopped, poll) {
|
|
|
205
323
|
* its own durable lifecycle intent after a prior maintainer died post-bootout.
|
|
206
324
|
*/
|
|
207
325
|
export async function maintainLaunchdLogs(target, deps, poll = DEFAULT_CONTROL_POLL) {
|
|
208
|
-
|
|
209
|
-
if (release === undefined) {
|
|
210
|
-
// Controller start/restart/stop owns the same lock. Skipping this periodic
|
|
211
|
-
// pass is expected and avoids making launchd record a false maintenance
|
|
212
|
-
// failure; the next interval retries from a fresh inventory.
|
|
213
|
-
return 0;
|
|
214
|
-
}
|
|
215
|
-
try {
|
|
216
|
-
const uid = deps.getuid();
|
|
217
|
-
const service = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
218
|
-
let inspection;
|
|
219
|
-
try {
|
|
220
|
-
inspection = await deps.inspectLaunchdLogs(target.paths);
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
reportMaintenanceFailure(target, deps, "inspect launchd logs", error);
|
|
224
|
-
return 1;
|
|
225
|
-
}
|
|
226
|
-
let maintenanceIntent;
|
|
227
|
-
try {
|
|
228
|
-
maintenanceIntent = await deps.readLaunchdLogMaintenanceIntent(target.paths);
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", error);
|
|
232
|
-
return 1;
|
|
233
|
-
}
|
|
234
|
-
if (inspection.pendingMaintenance && maintenanceIntent === undefined) {
|
|
235
|
-
reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", new Error("The maintenance marker disappeared after inventory."));
|
|
236
|
-
return 1;
|
|
237
|
-
}
|
|
238
|
-
if (!service.loaded && maintenanceIntent === undefined)
|
|
239
|
-
return 0;
|
|
240
|
-
let originalPlistIdentity;
|
|
241
|
-
try {
|
|
242
|
-
originalPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
reportMaintenanceFailure(target, deps, "verify the existing main LaunchAgent", error);
|
|
246
|
-
return 1;
|
|
247
|
-
}
|
|
248
|
-
if (maintenanceIntent !== undefined
|
|
249
|
-
&& (maintenanceIntent.label !== target.label
|
|
250
|
-
|| maintenanceIntent.plistFingerprint !== originalPlistIdentity)) {
|
|
251
|
-
reportMaintenanceFailure(target, deps, "authenticate durable launchd-log maintenance intent", new Error("The pending intent does not match the exact main LaunchAgent definition."));
|
|
252
|
-
return 1;
|
|
253
|
-
}
|
|
254
|
-
if (maintenanceIntent?.phase === "stopping" && !service.loaded) {
|
|
255
|
-
reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("The prior maintainer did not durably prove every old writer PID dead; refusing rotation."));
|
|
256
|
-
return 1;
|
|
257
|
-
}
|
|
258
|
-
if (maintenanceIntent?.phase === "restoring") {
|
|
259
|
-
const replacement = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
260
|
-
if (!replacement.loaded || replacement.pid === undefined || !deps.isAlive(replacement.pid)) {
|
|
261
|
-
reportMaintenanceFailure(target, deps, "recover interrupted launchd-log restoration", new Error("The replacement writer identity was lost before live-worker proof; refusing stale rotation authority."));
|
|
262
|
-
return 1;
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
reportMaintenanceFailure(target, deps, "clear recovered launchd-log restoration intent", error);
|
|
269
|
-
return 1;
|
|
270
|
-
}
|
|
271
|
-
return 0;
|
|
272
|
-
}
|
|
273
|
-
if (maintenanceIntent?.phase === "stopped" && service.loaded) {
|
|
274
|
-
reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("launchd reports a writer loaded after durable stopped-writer proof; refusing rotation."));
|
|
275
|
-
return 1;
|
|
276
|
-
}
|
|
277
|
-
if (!inspection.canMaintain) {
|
|
278
|
-
deps.stderr(ui.errorLine(`Scheduled log maintenance refused unsafe paths for ${target.label}.`));
|
|
279
|
-
for (const issue of inspection.issues)
|
|
280
|
-
deps.stderr(ui.style.dim(issue) + "\n");
|
|
281
|
-
return 1;
|
|
282
|
-
}
|
|
283
|
-
if (!inspection.needsMaintenance && maintenanceIntent === undefined)
|
|
284
|
-
return 0;
|
|
285
|
-
if (maintenanceIntent === undefined) {
|
|
286
|
-
maintenanceIntent = {
|
|
287
|
-
version: 1,
|
|
288
|
-
phase: "stopping",
|
|
289
|
-
label: target.label,
|
|
290
|
-
plistFingerprint: originalPlistIdentity,
|
|
291
|
-
};
|
|
292
|
-
try {
|
|
293
|
-
await deps.beginLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
reportMaintenanceFailure(target, deps, "publish durable launchd-log maintenance intent", error);
|
|
297
|
-
return 1;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (maintenanceIntent.phase === "stopping") {
|
|
301
|
-
const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid]), deps, uid, poll);
|
|
302
|
-
if (!stopped.ok) {
|
|
303
|
-
reportMaintenanceFailure(target, deps, "prove the launchd log writer stopped", stopped.failure);
|
|
304
|
-
return 1;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
maintenanceIntent = await deps.markLaunchdLogMaintenanceStopped(target.paths, maintenanceIntent);
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
reportMaintenanceFailure(target, deps, "record durable stopped-writer proof", error);
|
|
311
|
-
return 1;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
const current = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
316
|
-
if (current.loaded || (current.pid !== undefined && deps.isAlive(current.pid))) {
|
|
317
|
-
reportMaintenanceFailure(target, deps, "recheck durable stopped-writer proof", new Error("launchd exposed a live writer before recovered rotation."));
|
|
318
|
-
return 1;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
await deps.rotateStoppedLaunchdLogs(target.paths);
|
|
323
|
-
// Close the pathname-replacement race before handing the definition to
|
|
324
|
-
// launchctl. A changed/symlinked plist leaves the worker stopped.
|
|
325
|
-
const currentPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
|
|
326
|
-
if (currentPlistIdentity !== originalPlistIdentity) {
|
|
327
|
-
throw new Error("The main LaunchAgent plist changed during stopped-writer maintenance.");
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
catch (error) {
|
|
331
|
-
reportMaintenanceFailure(target, deps, "commit bounded stopped-writer logs", error);
|
|
332
|
-
return 1;
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
maintenanceIntent = await deps.markLaunchdLogMaintenanceRestoring(target.paths, maintenanceIntent);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
reportMaintenanceFailure(target, deps, "invalidate stopped-writer proof before restoration", error);
|
|
339
|
-
return 1;
|
|
340
|
-
}
|
|
341
|
-
const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
|
|
342
|
-
const observedRestorePids = new Set();
|
|
343
|
-
const running = await pollUntil(deps, poll, async () => {
|
|
344
|
-
const current = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
345
|
-
if (current.pid !== undefined)
|
|
346
|
-
observedRestorePids.add(current.pid);
|
|
347
|
-
return current.loaded && current.pid !== undefined && deps.isAlive(current.pid);
|
|
348
|
-
});
|
|
349
|
-
if (!running) {
|
|
350
|
-
const current = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
351
|
-
if (current.pid !== undefined)
|
|
352
|
-
observedRestorePids.add(current.pid);
|
|
353
|
-
const cleanedUp = await unloadLaunchdService(target.label, current, [...observedRestorePids], deps, uid, poll);
|
|
354
|
-
reportMaintenanceFailure(target, deps, "restore the exact main LaunchAgent after rotation", lifecycleFailure(booted, "launchd did not expose a live replacement worker"));
|
|
355
|
-
if (!cleanedUp.ok) {
|
|
356
|
-
reportMaintenanceFailure(target, deps, "remove the failed replacement worker", cleanedUp.failure);
|
|
357
|
-
}
|
|
358
|
-
return 1;
|
|
359
|
-
}
|
|
360
|
-
try {
|
|
361
|
-
const restoredPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
|
|
362
|
-
if (restoredPlistIdentity !== originalPlistIdentity) {
|
|
363
|
-
throw new Error("The main LaunchAgent plist changed while launchd restored the worker.");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
const restoredService = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
368
|
-
const stoppedAgain = await unloadLaunchdService(target.label, restoredService, uniquePids([restoredService.pid, ...observedRestorePids]), deps, uid, poll);
|
|
369
|
-
reportMaintenanceFailure(target, deps, "prove the restored main LaunchAgent definition", error);
|
|
370
|
-
if (!stoppedAgain.ok) {
|
|
371
|
-
reportMaintenanceFailure(target, deps, "stop the worker after its definition changed", stoppedAgain.failure);
|
|
372
|
-
}
|
|
373
|
-
return 1;
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
reportMaintenanceFailure(target, deps, "clear durable launchd-log maintenance intent", error);
|
|
380
|
-
return 1;
|
|
381
|
-
}
|
|
382
|
-
return 0;
|
|
383
|
-
}
|
|
384
|
-
finally {
|
|
385
|
-
await release().catch((error) => {
|
|
386
|
-
deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
function reportMaintenanceFailure(target, deps, action, error) {
|
|
391
|
-
deps.stderr(ui.errorLine(`Scheduled log maintenance could not ${action} for ${target.label}: ${maintenanceErrorMessage(error)}`));
|
|
392
|
-
}
|
|
393
|
-
function maintenanceErrorMessage(error) {
|
|
394
|
-
if (error instanceof Error)
|
|
395
|
-
return error.message;
|
|
396
|
-
if (typeof error === "object" && error !== null) {
|
|
397
|
-
const result = error;
|
|
398
|
-
const detail = [result.stderr, result.stdout]
|
|
399
|
-
.find((value) => typeof value === "string" && value.trim().length > 0)
|
|
400
|
-
?.trim();
|
|
401
|
-
if (detail !== undefined)
|
|
402
|
-
return detail;
|
|
403
|
-
if (typeof result.code === "number")
|
|
404
|
-
return `launchctl exited ${result.code}`;
|
|
405
|
-
}
|
|
406
|
-
return String(error);
|
|
326
|
+
return await maintainLaunchdLogsOperation(target, deps, poll);
|
|
407
327
|
}
|
|
408
328
|
/**
|
|
409
329
|
* Ensure the canonical per-config LaunchAgent is running and return the fresh
|
|
@@ -427,7 +347,7 @@ export async function ensureBackgroundReady(target, deps, poll) {
|
|
|
427
347
|
});
|
|
428
348
|
}
|
|
429
349
|
}
|
|
430
|
-
async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll) {
|
|
350
|
+
async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readinessPoll, options = {}) {
|
|
431
351
|
if (target.expectedSnapshot === undefined) {
|
|
432
352
|
deps.stderr(ui.errorLine("Refusing to launch a managed worker without an approved background snapshot."));
|
|
433
353
|
deps.stderr(ui.hint("No LaunchAgent changes were made. Retry from the wizard or `mono-agent start`."));
|
|
@@ -453,9 +373,9 @@ async function ensureBackgroundReadyUnlocked(target, deps, controlPoll, readines
|
|
|
453
373
|
deps.stderr(ui.errorLine(`Another runtime publication is already active for ${target.label}.`));
|
|
454
374
|
return { ok: false, action: "start", reason: "ownership" };
|
|
455
375
|
}
|
|
456
|
-
return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier);
|
|
376
|
+
return await ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options);
|
|
457
377
|
}
|
|
458
|
-
async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier) {
|
|
378
|
+
async function ensureBackgroundReadyWithPublicationBarrier(target, deps, controlPoll, readinessPoll, publicationBarrier, options) {
|
|
459
379
|
let barrierReleased = false;
|
|
460
380
|
const releaseBarrier = async () => {
|
|
461
381
|
if (barrierReleased)
|
|
@@ -464,7 +384,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
|
|
|
464
384
|
barrierReleased = true;
|
|
465
385
|
};
|
|
466
386
|
try {
|
|
467
|
-
return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier);
|
|
387
|
+
return await ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options);
|
|
468
388
|
}
|
|
469
389
|
finally {
|
|
470
390
|
if (!barrierReleased) {
|
|
@@ -474,7 +394,7 @@ async function ensureBackgroundReadyWithPublicationBarrier(target, deps, control
|
|
|
474
394
|
}
|
|
475
395
|
}
|
|
476
396
|
}
|
|
477
|
-
async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier) {
|
|
397
|
+
async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, controlPoll, readinessPoll, releaseBarrier, options) {
|
|
478
398
|
const uid = deps.getuid();
|
|
479
399
|
let launchTarget;
|
|
480
400
|
const runtimeStartedAt = deps.now();
|
|
@@ -560,7 +480,7 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
|
|
|
560
480
|
// The replacement plist now contains the finalized-runtime proof. Let
|
|
561
481
|
// launchd respawn only after the stopped-window commit is complete.
|
|
562
482
|
await releaseBarrier();
|
|
563
|
-
});
|
|
483
|
+
}, { preserveMaintenanceService: options.preserveMaintenanceService === true });
|
|
564
484
|
}
|
|
565
485
|
catch (error) {
|
|
566
486
|
reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
|
|
@@ -582,13 +502,13 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
|
|
|
582
502
|
}
|
|
583
503
|
catch (error) {
|
|
584
504
|
reportReadinessException(deps, error);
|
|
585
|
-
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
|
|
586
|
-
reportReadinessCleanup(launchTarget, deps, stopped);
|
|
505
|
+
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
|
|
506
|
+
reportReadinessCleanup(launchTarget, deps, stopped, options);
|
|
587
507
|
return { ok: false, action, reason: "readiness" };
|
|
588
508
|
}
|
|
589
509
|
if (ready === undefined) {
|
|
590
510
|
if (!(await snapshotStillMatches(launchTarget, deps))) {
|
|
591
|
-
const stopped = await
|
|
511
|
+
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options) ? 0 : 1;
|
|
592
512
|
reportSnapshotDrift(launchTarget, deps);
|
|
593
513
|
if (stopped === 0) {
|
|
594
514
|
deps.stderr(ui.style.dim("The drifted LaunchAgent was stopped before returning control.") + "\n");
|
|
@@ -599,8 +519,8 @@ async function ensureBackgroundReadyAfterPublicationBarrier(target, deps, contro
|
|
|
599
519
|
return { ok: false, action, reason: "snapshot" };
|
|
600
520
|
}
|
|
601
521
|
reportTimeout(deps);
|
|
602
|
-
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll);
|
|
603
|
-
reportReadinessCleanup(launchTarget, deps, stopped);
|
|
522
|
+
const stopped = await cleanUpUnreadyBackground(launchTarget, deps, controlPoll, options);
|
|
523
|
+
reportReadinessCleanup(launchTarget, deps, stopped, options);
|
|
604
524
|
return { ok: false, action, reason: "timeout" };
|
|
605
525
|
}
|
|
606
526
|
const completedAction = outcome.restarted ? "restarted" : "started";
|
|
@@ -633,6 +553,10 @@ async function writePlists(target, deps) {
|
|
|
633
553
|
if (target.managedRuntimeLaunchProof === undefined) {
|
|
634
554
|
throw new Error("A managed LaunchAgent requires a finalized runtime launch proof.");
|
|
635
555
|
}
|
|
556
|
+
const agentPath = target.environment.PATH;
|
|
557
|
+
if (agentPath === undefined || agentPath.length === 0) {
|
|
558
|
+
throw new Error("A managed LaunchAgent recovery controller requires the durable worker PATH.");
|
|
559
|
+
}
|
|
636
560
|
const maintenance = maintenancePathsForTarget(target);
|
|
637
561
|
const mainXml = buildPlistXml({
|
|
638
562
|
label: target.label,
|
|
@@ -652,6 +576,10 @@ async function writePlists(target, deps) {
|
|
|
652
576
|
nodePath: target.nodePath,
|
|
653
577
|
cliPath: target.cliPath,
|
|
654
578
|
configPath: target.configPath,
|
|
579
|
+
controllerCliPath: target.controllerCliPath ?? target.cliPath,
|
|
580
|
+
agentCwd: target.cwd,
|
|
581
|
+
agentPath,
|
|
582
|
+
...(target.envFile === undefined ? {} : { envFile: target.envFile }),
|
|
655
583
|
// The agent folder may be renamed or removed while its already-running
|
|
656
584
|
// worker still owns log descriptors. Keep the scheduler's cwd in the
|
|
657
585
|
// durable account state tree; its config argument is already absolute.
|
|
@@ -832,7 +760,7 @@ function sameFileSnapshot(left, right) {
|
|
|
832
760
|
* before bootstrap so launchd cannot retain stale ProgramArguments or env from
|
|
833
761
|
* the previous plist.
|
|
834
762
|
*/
|
|
835
|
-
async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
|
|
763
|
+
async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped, options = {}) {
|
|
836
764
|
const maintenance = maintenancePathsForTarget(target);
|
|
837
765
|
const [service, maintenanceService] = await Promise.all([
|
|
838
766
|
launchdServiceInfo(deps.runner, target.label, uid),
|
|
@@ -842,9 +770,11 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
842
770
|
// Stop the scheduler first. A helper that is already running either holds
|
|
843
771
|
// this lifecycle lock (so the controller could not enter) or is waiting to
|
|
844
772
|
// acquire it; bootout plus PID death prevents a post-stop resurrection.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
773
|
+
if (options.preserveMaintenanceService !== true) {
|
|
774
|
+
const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
|
|
775
|
+
if (!maintenanceStopped.ok) {
|
|
776
|
+
return { ok: false, restarted, failure: maintenanceStopped.failure };
|
|
777
|
+
}
|
|
848
778
|
}
|
|
849
779
|
let mainStopProven = false;
|
|
850
780
|
if (service.loaded) {
|
|
@@ -860,7 +790,9 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
860
790
|
]);
|
|
861
791
|
const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
|
|
862
792
|
if (!mainStopped.ok) {
|
|
863
|
-
const restoreFailure =
|
|
793
|
+
const restoreFailure = options.preserveMaintenanceService === true
|
|
794
|
+
? undefined
|
|
795
|
+
: await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
|
|
864
796
|
return {
|
|
865
797
|
ok: false,
|
|
866
798
|
restarted: true,
|
|
@@ -872,19 +804,28 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
872
804
|
mainStopProven = true;
|
|
873
805
|
}
|
|
874
806
|
await whileStopped(mainStopProven);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
807
|
+
if (options.preserveMaintenanceService !== true) {
|
|
808
|
+
const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
|
|
809
|
+
const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
|
|
810
|
+
if (!maintenanceLoaded) {
|
|
811
|
+
return {
|
|
812
|
+
ok: false,
|
|
813
|
+
restarted,
|
|
814
|
+
failure: lifecycleFailure(maintenanceBooted, "scheduled recovery controller did not report loaded"),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
883
817
|
}
|
|
884
818
|
const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
|
|
885
819
|
if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
|
|
886
820
|
return { ok: true, restarted };
|
|
887
821
|
}
|
|
822
|
+
if (options.preserveMaintenanceService === true) {
|
|
823
|
+
return {
|
|
824
|
+
ok: false,
|
|
825
|
+
restarted,
|
|
826
|
+
failure: lifecycleFailure(booted, "main service did not load; scheduled recovery remains loaded for retry"),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
888
829
|
const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
|
|
889
830
|
const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
|
|
890
831
|
return {
|
|
@@ -895,50 +836,6 @@ async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, wh
|
|
|
895
836
|
: `main service did not load; scheduled maintenance cleanup failed: ${maintenanceErrorMessage(maintenanceRemoval.failure)}`),
|
|
896
837
|
};
|
|
897
838
|
}
|
|
898
|
-
async function unloadLaunchdService(label, service, ownedPids, deps, uid, poll, beforeLoadedBootout) {
|
|
899
|
-
const observedPids = new Set(uniquePids([service.pid, ...ownedPids]));
|
|
900
|
-
try {
|
|
901
|
-
await beforeLoadedBootout?.();
|
|
902
|
-
}
|
|
903
|
-
catch (error) {
|
|
904
|
-
return {
|
|
905
|
-
ok: false,
|
|
906
|
-
failure: { code: 1, stdout: "", stderr: maintenanceErrorMessage(error) },
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
// Refresh immediately before bootout. The caller may have spent time on
|
|
910
|
-
// inventory or plist verification while KeepAlive replaced its entry PID.
|
|
911
|
-
let current = await launchdServiceInfo(deps.runner, label, uid);
|
|
912
|
-
if (current.pid !== undefined)
|
|
913
|
-
observedPids.add(current.pid);
|
|
914
|
-
let removed;
|
|
915
|
-
if (current.loaded) {
|
|
916
|
-
removed = await bootout(deps.runner, label, uid);
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
removed = { code: 0, stdout: "", stderr: "" };
|
|
920
|
-
}
|
|
921
|
-
// Keep launchd state and process liveness in one predicate. In particular,
|
|
922
|
-
// retain every KeepAlive replacement pid exposed while bootout converges;
|
|
923
|
-
// proving only the entry snapshot dead is not a stopped-writer proof.
|
|
924
|
-
const stopped = await pollUntil(deps, poll, async () => {
|
|
925
|
-
current = await launchdServiceInfo(deps.runner, label, uid);
|
|
926
|
-
if (current.pid !== undefined)
|
|
927
|
-
observedPids.add(current.pid);
|
|
928
|
-
return !current.loaded && [...observedPids].every((pid) => !deps.isAlive(pid));
|
|
929
|
-
});
|
|
930
|
-
if (!stopped) {
|
|
931
|
-
const livePids = [...observedPids].filter((pid) => deps.isAlive(pid));
|
|
932
|
-
const detail = current.loaded
|
|
933
|
-
? `launchd still reports ${label} loaded after bootout`
|
|
934
|
-
: `${label} pid(s) ${livePids.join(", ")} remained alive after bootout`;
|
|
935
|
-
return {
|
|
936
|
-
ok: false,
|
|
937
|
-
failure: lifecycleFailure(removed, detail),
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
return { ok: true };
|
|
941
|
-
}
|
|
942
839
|
function maintenancePathsForTarget(target) {
|
|
943
840
|
const label = deriveLaunchdMaintenanceLabel(target.label);
|
|
944
841
|
return { label, plistPath: resolve(target.paths.launchAgentsDir, `${label}.plist`) };
|
|
@@ -969,26 +866,6 @@ async function findOwnershipConflict(target, deps, uid) {
|
|
|
969
866
|
}
|
|
970
867
|
return undefined;
|
|
971
868
|
}
|
|
972
|
-
async function pollUntil(deps, options, condition) {
|
|
973
|
-
const deadline = deps.now() + options.timeoutMs;
|
|
974
|
-
for (;;) {
|
|
975
|
-
if (await condition())
|
|
976
|
-
return true;
|
|
977
|
-
if (deps.now() >= deadline)
|
|
978
|
-
return false;
|
|
979
|
-
await deps.sleep(options.intervalMs);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
function lifecycleFailure(result, detail) {
|
|
983
|
-
return {
|
|
984
|
-
code: result.code === 0 ? 1 : result.code,
|
|
985
|
-
stdout: result.stdout,
|
|
986
|
-
stderr: [result.stderr.trim(), detail].filter((value) => value.length > 0).join("\n"),
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
function uniquePids(values) {
|
|
990
|
-
return [...new Set(values.filter((value) => value !== undefined && value > 0))];
|
|
991
|
-
}
|
|
992
869
|
export async function stopBackground(target, deps, poll = DEFAULT_CONTROL_POLL) {
|
|
993
870
|
const release = await deps.acquireLifecycleLock(target);
|
|
994
871
|
if (release === undefined) {
|
|
@@ -1092,12 +969,24 @@ async function stopBackgroundUnlocked(target, deps, poll) {
|
|
|
1092
969
|
return 0;
|
|
1093
970
|
}
|
|
1094
971
|
export async function statusBackground(target, deps, options = {}) {
|
|
1095
|
-
const result = await
|
|
972
|
+
const [result, service] = await Promise.all([
|
|
973
|
+
deps.listTraceSources({ registryDir: target.registryDir, staleAfterMs: target.staleAfterMs }),
|
|
974
|
+
launchdServiceInfo(deps.runner, target.label, deps.getuid()),
|
|
975
|
+
]);
|
|
1096
976
|
const classified = await Promise.all(result.sources.map(async (source) => ({
|
|
1097
977
|
source,
|
|
1098
978
|
matches: await matchesConfig(source, target.configPath),
|
|
1099
979
|
})));
|
|
1100
|
-
const
|
|
980
|
+
const matchingSources = classified.filter((entry) => entry.matches).map((entry) => entry.source);
|
|
981
|
+
const recorded = service.pid === undefined
|
|
982
|
+
? matchingSources[0]
|
|
983
|
+
: matchingSources.find((source) => source.pid === service.pid) ?? matchingSources[0];
|
|
984
|
+
const active = service.loaded
|
|
985
|
+
&& service.pid !== undefined
|
|
986
|
+
&& deps.isAlive(service.pid)
|
|
987
|
+
&& recorded?.pid === service.pid
|
|
988
|
+
&& recorded.health === "running";
|
|
989
|
+
const current = recorded === undefined || active ? recorded : inactiveStatusSource(recorded);
|
|
1101
990
|
// Only surface other instances that are live or crashed — cleanly stopped
|
|
1102
991
|
// manifests linger in the registry and would just be noise.
|
|
1103
992
|
const others = classified
|
|
@@ -1106,11 +995,11 @@ export async function statusBackground(target, deps, options = {}) {
|
|
|
1106
995
|
if (options.json === true) {
|
|
1107
996
|
const instance = current === undefined ? null : await assembleInstanceStatus(current, target, deps);
|
|
1108
997
|
deps.stdout(`${JSON.stringify({
|
|
1109
|
-
ok:
|
|
998
|
+
ok: active,
|
|
1110
999
|
instance,
|
|
1111
1000
|
others: others.map(assembleOtherInstanceStatus),
|
|
1112
1001
|
})}\n`);
|
|
1113
|
-
return
|
|
1002
|
+
return active ? 0 : 1;
|
|
1114
1003
|
}
|
|
1115
1004
|
if (current === undefined) {
|
|
1116
1005
|
deps.stdout(ui.style.dim(`No running mono-agent instance for ${target.configPath}.`) + "\n");
|
|
@@ -1126,7 +1015,32 @@ export async function statusBackground(target, deps, options = {}) {
|
|
|
1126
1015
|
deps.stdout(`${formatOtherInstance(source)}\n`);
|
|
1127
1016
|
}
|
|
1128
1017
|
}
|
|
1129
|
-
return
|
|
1018
|
+
return active ? 0 : 1;
|
|
1019
|
+
}
|
|
1020
|
+
function inactiveStatusSource(source) {
|
|
1021
|
+
const { transports: _staleTransports, pid: _stalePid, ...withoutLiveProcessFacts } = source;
|
|
1022
|
+
const metadata = source.metadata;
|
|
1023
|
+
const channels = metadata?.channels;
|
|
1024
|
+
const stoppedChannels = isPlainRecord(channels)
|
|
1025
|
+
? Object.fromEntries(Object.entries(channels).map(([id, value]) => {
|
|
1026
|
+
if (!isPlainRecord(value) || value.kind !== "running")
|
|
1027
|
+
return [id, value];
|
|
1028
|
+
return [id, { kind: "stopped", reason: "instance is not running" }];
|
|
1029
|
+
}))
|
|
1030
|
+
: undefined;
|
|
1031
|
+
return {
|
|
1032
|
+
...withoutLiveProcessFacts,
|
|
1033
|
+
health: "stopped",
|
|
1034
|
+
status: "stopped",
|
|
1035
|
+
...(metadata === undefined
|
|
1036
|
+
? {}
|
|
1037
|
+
: {
|
|
1038
|
+
metadata: {
|
|
1039
|
+
...metadata,
|
|
1040
|
+
...(stoppedChannels === undefined ? {} : { channels: stoppedChannels }),
|
|
1041
|
+
},
|
|
1042
|
+
}),
|
|
1043
|
+
};
|
|
1130
1044
|
}
|
|
1131
1045
|
/**
|
|
1132
1046
|
* The machine-readable instance record for `status --json`. It mirrors the
|
|
@@ -1295,8 +1209,24 @@ function reportTimeout(deps) {
|
|
|
1295
1209
|
deps.stderr(ui.errorLine("mono-agent did not report ready within the timeout."));
|
|
1296
1210
|
deps.stderr(ui.style.dim("The committed agent files were preserved.") + "\n");
|
|
1297
1211
|
}
|
|
1298
|
-
async function cleanUpUnreadyBackground(target, deps, poll) {
|
|
1212
|
+
async function cleanUpUnreadyBackground(target, deps, poll, options = {}) {
|
|
1299
1213
|
try {
|
|
1214
|
+
if (options.preserveDefinitionsOnFailure === true) {
|
|
1215
|
+
const uid = deps.getuid();
|
|
1216
|
+
const [service, sources] = await Promise.all([
|
|
1217
|
+
launchdServiceInfo(deps.runner, target.label, uid),
|
|
1218
|
+
findInstances(target, deps),
|
|
1219
|
+
]);
|
|
1220
|
+
const liveTracePids = uniquePids(sources
|
|
1221
|
+
.filter((source) => source.health !== "stopped"
|
|
1222
|
+
&& source.pid !== undefined
|
|
1223
|
+
&& deps.isAlive(source.pid))
|
|
1224
|
+
.map((source) => source.pid));
|
|
1225
|
+
if (liveTracePids.some((pid) => pid !== service.pid))
|
|
1226
|
+
return false;
|
|
1227
|
+
const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid, ...liveTracePids]), deps, uid, poll);
|
|
1228
|
+
return stopped.ok;
|
|
1229
|
+
}
|
|
1300
1230
|
return await stopBackgroundUnlocked(target, { ...deps, stdout: () => undefined }, poll) === 0;
|
|
1301
1231
|
}
|
|
1302
1232
|
catch (error) {
|
|
@@ -1304,9 +1234,11 @@ async function cleanUpUnreadyBackground(target, deps, poll) {
|
|
|
1304
1234
|
return false;
|
|
1305
1235
|
}
|
|
1306
1236
|
}
|
|
1307
|
-
function reportReadinessCleanup(target, deps, stopped) {
|
|
1237
|
+
function reportReadinessCleanup(target, deps, stopped, options = {}) {
|
|
1308
1238
|
deps.stderr(stopped
|
|
1309
|
-
?
|
|
1239
|
+
? options.preserveDefinitionsOnFailure === true
|
|
1240
|
+
? ui.style.dim("The unready main LaunchAgent was stopped; both definitions and the scheduled recovery controller remain for retry.") + "\n"
|
|
1241
|
+
: ui.style.dim("The unready LaunchAgent and scheduled maintenance were stopped and their definitions removed before returning control.") + "\n"
|
|
1310
1242
|
: ui.style.yellow("The unready LaunchAgent could not be proven stopped; a worker or maintenance helper may still be running.") + "\n");
|
|
1311
1243
|
reportLaunchRecovery(target, deps);
|
|
1312
1244
|
}
|