@mono-agent/agent-app 0.11.2 → 0.11.4
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/LICENSE +674 -0
- package/README.md +222 -26
- package/dist/adapter-send-tools-main.js +7 -1
- package/dist/adapter-send-tools-main.js.map +1 -1
- package/dist/adapter-send-tools.d.ts +19 -1
- package/dist/adapter-send-tools.d.ts.map +1 -1
- package/dist/adapter-send-tools.js +127 -13
- package/dist/adapter-send-tools.js.map +1 -1
- package/dist/app-config.d.ts.map +1 -1
- package/dist/app-config.js +15 -2
- package/dist/app-config.js.map +1 -1
- package/dist/app-controller-channels.d.ts +8 -0
- package/dist/app-controller-channels.d.ts.map +1 -0
- package/dist/app-controller-channels.js +207 -0
- package/dist/app-controller-channels.js.map +1 -0
- package/dist/app-controller-continuation.d.ts +26 -0
- package/dist/app-controller-continuation.d.ts.map +1 -0
- package/dist/app-controller-continuation.js +198 -0
- package/dist/app-controller-continuation.js.map +1 -0
- package/dist/app-controller-lifecycle.d.ts +6 -0
- package/dist/app-controller-lifecycle.d.ts.map +1 -0
- package/dist/app-controller-lifecycle.js +81 -0
- package/dist/app-controller-lifecycle.js.map +1 -0
- package/dist/app-controller-maintenance.d.ts +13 -0
- package/dist/app-controller-maintenance.d.ts.map +1 -0
- package/dist/app-controller-maintenance.js +180 -0
- package/dist/app-controller-maintenance.js.map +1 -0
- package/dist/app-controller-memory-health.d.ts +12 -0
- package/dist/app-controller-memory-health.d.ts.map +1 -0
- package/dist/app-controller-memory-health.js +160 -0
- package/dist/app-controller-memory-health.js.map +1 -0
- package/dist/app-controller-memory.d.ts +9 -0
- package/dist/app-controller-memory.d.ts.map +1 -0
- package/dist/app-controller-memory.js +83 -0
- package/dist/app-controller-memory.js.map +1 -0
- package/dist/app-controller-responder.d.ts +20 -0
- package/dist/app-controller-responder.d.ts.map +1 -0
- package/dist/app-controller-responder.js +246 -0
- package/dist/app-controller-responder.js.map +1 -0
- package/dist/app-controller-traceability.d.ts +25 -0
- package/dist/app-controller-traceability.d.ts.map +1 -0
- package/dist/app-controller-traceability.js +418 -0
- package/dist/app-controller-traceability.js.map +1 -0
- package/dist/app-controller-utils.d.ts +38 -0
- package/dist/app-controller-utils.d.ts.map +1 -0
- package/dist/app-controller-utils.js +185 -0
- package/dist/app-controller-utils.js.map +1 -0
- package/dist/app-controller.d.ts +400 -0
- package/dist/app-controller.d.ts.map +1 -0
- package/dist/app-controller.js +336 -0
- package/dist/app-controller.js.map +1 -0
- package/dist/app.d.ts +7 -115
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -1912
- package/dist/app.js.map +1 -1
- package/dist/artifact-retention.d.ts +4 -0
- package/dist/artifact-retention.d.ts.map +1 -1
- package/dist/artifact-retention.js +38 -0
- package/dist/artifact-retention.js.map +1 -1
- package/dist/backfill.d.ts +3 -1
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +5 -1
- package/dist/backfill.js.map +1 -1
- package/dist/background-runtime.d.ts +27 -0
- package/dist/background-runtime.d.ts.map +1 -1
- package/dist/background-runtime.js +383 -205
- package/dist/background-runtime.js.map +1 -1
- package/dist/background-worker-lease.d.ts.map +1 -1
- package/dist/background-worker-lease.js +41 -241
- package/dist/background-worker-lease.js.map +1 -1
- package/dist/background.d.ts +31 -9
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +543 -272
- package/dist/background.js.map +1 -1
- package/dist/channel-drivers/cron.d.ts +11 -0
- package/dist/channel-drivers/cron.d.ts.map +1 -0
- package/dist/channel-drivers/cron.js +168 -0
- package/dist/channel-drivers/cron.js.map +1 -0
- package/dist/channel-drivers/live.d.ts +11 -0
- package/dist/channel-drivers/live.d.ts.map +1 -0
- package/dist/channel-drivers/live.js +47 -0
- package/dist/channel-drivers/live.js.map +1 -0
- package/dist/channel-drivers/native-notify.d.ts +27 -0
- package/dist/channel-drivers/native-notify.d.ts.map +1 -0
- package/dist/channel-drivers/native-notify.js +94 -0
- package/dist/channel-drivers/native-notify.js.map +1 -0
- package/dist/channel-drivers/openai-api.d.ts +7 -0
- package/dist/channel-drivers/openai-api.d.ts.map +1 -0
- package/dist/channel-drivers/openai-api.js +63 -0
- package/dist/channel-drivers/openai-api.js.map +1 -0
- package/dist/channel-drivers/shared.d.ts +4 -0
- package/dist/channel-drivers/shared.d.ts.map +1 -0
- package/dist/channel-drivers/shared.js +5 -0
- package/dist/channel-drivers/shared.js.map +1 -0
- package/dist/channel-drivers/slack.d.ts +14 -0
- package/dist/channel-drivers/slack.d.ts.map +1 -0
- package/dist/channel-drivers/slack.js +227 -0
- package/dist/channel-drivers/slack.js.map +1 -0
- package/dist/channel-drivers/telegram.d.ts +11 -0
- package/dist/channel-drivers/telegram.d.ts.map +1 -0
- package/dist/channel-drivers/telegram.js +244 -0
- package/dist/channel-drivers/telegram.js.map +1 -0
- package/dist/channel-drivers/tui.d.ts +15 -0
- package/dist/channel-drivers/tui.d.ts.map +1 -0
- package/dist/channel-drivers/tui.js +108 -0
- package/dist/channel-drivers/tui.js.map +1 -0
- package/dist/channel-drivers/webhook.d.ts +7 -0
- package/dist/channel-drivers/webhook.d.ts.map +1 -0
- package/dist/channel-drivers/webhook.js +115 -0
- package/dist/channel-drivers/webhook.js.map +1 -0
- package/dist/channels.d.ts +21 -81
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +11 -1174
- package/dist/channels.js.map +1 -1
- package/dist/cli-args.d.ts +121 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +532 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-background-command.d.ts +59 -0
- package/dist/cli-background-command.d.ts.map +1 -0
- package/dist/cli-background-command.js +475 -0
- package/dist/cli-background-command.js.map +1 -0
- package/dist/cli-cancellation.d.ts +6 -0
- package/dist/cli-cancellation.d.ts.map +1 -0
- package/dist/cli-cancellation.js +23 -0
- package/dist/cli-cancellation.js.map +1 -0
- package/dist/cli-commands.d.ts +12 -0
- package/dist/cli-commands.d.ts.map +1 -0
- package/dist/cli-commands.js +306 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli-help.d.ts +11 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +262 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-init-command.d.ts +60 -0
- package/dist/cli-init-command.d.ts.map +1 -0
- package/dist/cli-init-command.js +1910 -0
- package/dist/cli-init-command.js.map +1 -0
- package/dist/cli-install-skill-command.d.ts +3 -0
- package/dist/cli-install-skill-command.d.ts.map +1 -0
- package/dist/cli-install-skill-command.js +51 -0
- package/dist/cli-install-skill-command.js.map +1 -0
- package/dist/cli-sandbox-command.d.ts +10 -0
- package/dist/cli-sandbox-command.d.ts.map +1 -0
- package/dist/cli-sandbox-command.js +100 -0
- package/dist/cli-sandbox-command.js.map +1 -0
- package/dist/cli-validate-config-command.d.ts +36 -0
- package/dist/cli-validate-config-command.d.ts.map +1 -0
- package/dist/cli-validate-config-command.js +317 -0
- package/dist/cli-validate-config-command.js.map +1 -0
- package/dist/cli.d.ts +6 -248
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -3792
- package/dist/cli.js.map +1 -1
- package/dist/config-reference.d.ts +2 -1
- package/dist/config-reference.d.ts.map +1 -1
- package/dist/config-reference.js +34 -8
- package/dist/config-reference.js.map +1 -1
- package/dist/configured-agent.d.ts +23 -0
- package/dist/configured-agent.d.ts.map +1 -1
- package/dist/configured-agent.js +92 -13
- package/dist/configured-agent.js.map +1 -1
- package/dist/continuation-dispatch-worker.d.ts +21 -0
- package/dist/continuation-dispatch-worker.d.ts.map +1 -0
- package/dist/continuation-dispatch-worker.js +425 -0
- package/dist/continuation-dispatch-worker.js.map +1 -0
- package/dist/continuation-http-routes.d.ts +15 -0
- package/dist/continuation-http-routes.d.ts.map +1 -0
- package/dist/continuation-http-routes.js +265 -0
- package/dist/continuation-http-routes.js.map +1 -0
- package/dist/continuation-operator-api.d.ts +18 -0
- package/dist/continuation-operator-api.d.ts.map +1 -0
- package/dist/continuation-operator-api.js +164 -0
- package/dist/continuation-operator-api.js.map +1 -0
- package/dist/continuation-origin-store.d.ts +19 -0
- package/dist/continuation-origin-store.d.ts.map +1 -0
- package/dist/continuation-origin-store.js +273 -0
- package/dist/continuation-origin-store.js.map +1 -0
- package/dist/continuation-service-errors.d.ts +18 -0
- package/dist/continuation-service-errors.d.ts.map +1 -0
- package/dist/continuation-service-errors.js +35 -0
- package/dist/continuation-service-errors.js.map +1 -0
- package/dist/continuation-service-helpers.d.ts +39 -0
- package/dist/continuation-service-helpers.d.ts.map +1 -0
- package/dist/continuation-service-helpers.js +308 -0
- package/dist/continuation-service-helpers.js.map +1 -0
- package/dist/continuation-service-origin.d.ts +55 -0
- package/dist/continuation-service-origin.d.ts.map +1 -0
- package/dist/continuation-service-origin.js +325 -0
- package/dist/continuation-service-origin.js.map +1 -0
- package/dist/continuation-service-types.d.ts +110 -0
- package/dist/continuation-service-types.d.ts.map +1 -0
- package/dist/continuation-service-types.js +11 -0
- package/dist/continuation-service-types.js.map +1 -0
- package/dist/continuation-service.d.ts +4 -97
- package/dist/continuation-service.d.ts.map +1 -1
- package/dist/continuation-service.js +9 -1525
- package/dist/continuation-service.js.map +1 -1
- package/dist/continuation-store-fs.d.ts +15 -0
- package/dist/continuation-store-fs.d.ts.map +1 -0
- package/dist/continuation-store-fs.js +247 -0
- package/dist/continuation-store-fs.js.map +1 -0
- package/dist/continuation-store-policy.d.ts +21 -0
- package/dist/continuation-store-policy.d.ts.map +1 -0
- package/dist/continuation-store-policy.js +278 -0
- package/dist/continuation-store-policy.js.map +1 -0
- package/dist/continuation-store-records.d.ts +9 -0
- package/dist/continuation-store-records.d.ts.map +1 -0
- package/dist/continuation-store-records.js +221 -0
- package/dist/continuation-store-records.js.map +1 -0
- package/dist/continuation-store-types.d.ts +173 -0
- package/dist/continuation-store-types.d.ts.map +1 -0
- package/dist/continuation-store-types.js +26 -0
- package/dist/continuation-store-types.js.map +1 -0
- package/dist/continuation-store.d.ts +4 -138
- package/dist/continuation-store.d.ts.map +1 -1
- package/dist/continuation-store.js +145 -1049
- package/dist/continuation-store.js.map +1 -1
- package/dist/doctor.d.ts +10 -5
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +629 -58
- package/dist/doctor.js.map +1 -1
- package/dist/init.d.ts +2 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +92 -239
- package/dist/init.js.map +1 -1
- package/dist/interaction-bridge.d.ts +19 -0
- package/dist/interaction-bridge.d.ts.map +1 -1
- package/dist/interaction-bridge.js +130 -0
- package/dist/interaction-bridge.js.map +1 -1
- package/dist/launchd-logs.d.ts +91 -0
- package/dist/launchd-logs.d.ts.map +1 -0
- package/dist/launchd-logs.js +1499 -0
- package/dist/launchd-logs.js.map +1 -0
- package/dist/launchd.d.ts +27 -0
- package/dist/launchd.d.ts.map +1 -1
- package/dist/launchd.js +89 -10
- package/dist/launchd.js.map +1 -1
- package/dist/memory-command.d.ts +2 -0
- package/dist/memory-command.d.ts.map +1 -1
- package/dist/memory-command.js +161 -48
- package/dist/memory-command.js.map +1 -1
- package/dist/memory-recall-settings.d.ts +64 -0
- package/dist/memory-recall-settings.d.ts.map +1 -0
- package/dist/memory-recall-settings.js +51 -0
- package/dist/memory-recall-settings.js.map +1 -0
- package/dist/memory-recall.d.ts +7 -95
- package/dist/memory-recall.d.ts.map +1 -1
- package/dist/memory-recall.js +8 -143
- package/dist/memory-recall.js.map +1 -1
- package/dist/memory-rituals.d.ts +3 -3
- package/dist/memory-rituals.d.ts.map +1 -1
- package/dist/memory-rituals.js +16 -111
- package/dist/memory-rituals.js.map +1 -1
- package/dist/modules/catalog.js +2 -2
- package/dist/modules/catalog.js.map +1 -1
- package/dist/modules/known-tools.d.ts +8 -6
- package/dist/modules/known-tools.d.ts.map +1 -1
- package/dist/modules/known-tools.js +8 -6
- package/dist/modules/known-tools.js.map +1 -1
- package/dist/notify-destinations.d.ts +5 -0
- package/dist/notify-destinations.d.ts.map +1 -1
- package/dist/notify-destinations.js +7 -1
- package/dist/notify-destinations.js.map +1 -1
- package/dist/owner-private-lock.d.ts +95 -0
- package/dist/owner-private-lock.d.ts.map +1 -0
- package/dist/owner-private-lock.js +566 -0
- package/dist/owner-private-lock.js.map +1 -0
- package/dist/pi-oauth-login.d.ts +2 -0
- package/dist/pi-oauth-login.d.ts.map +1 -1
- package/dist/pi-oauth-login.js +39 -5
- package/dist/pi-oauth-login.js.map +1 -1
- package/dist/posted-message-index.d.ts +35 -16
- package/dist/posted-message-index.d.ts.map +1 -1
- package/dist/posted-message-index.js +1068 -52
- package/dist/posted-message-index.js.map +1 -1
- package/dist/posted-reply-history.d.ts +17 -0
- package/dist/posted-reply-history.d.ts.map +1 -0
- package/dist/posted-reply-history.js +169 -0
- package/dist/posted-reply-history.js.map +1 -0
- package/dist/proactive-notify.d.ts.map +1 -1
- package/dist/proactive-notify.js +6 -4
- package/dist/proactive-notify.js.map +1 -1
- package/dist/project-skills.d.ts +4 -0
- package/dist/project-skills.d.ts.map +1 -1
- package/dist/project-skills.js +99 -130
- package/dist/project-skills.js.map +1 -1
- package/dist/provider-setup.d.ts +0 -1
- package/dist/provider-setup.d.ts.map +1 -1
- package/dist/provider-setup.js +0 -3
- package/dist/provider-setup.js.map +1 -1
- package/dist/readiness-probe-worker.d.ts +61 -1
- package/dist/readiness-probe-worker.d.ts.map +1 -1
- package/dist/readiness-probe-worker.js +54 -58
- package/dist/readiness-probe-worker.js.map +1 -1
- package/dist/readiness-probe.d.ts +6 -9
- package/dist/readiness-probe.d.ts.map +1 -1
- package/dist/readiness-probe.js +30 -56
- package/dist/readiness-probe.js.map +1 -1
- package/dist/redact-secrets.d.ts +15 -0
- package/dist/redact-secrets.d.ts.map +1 -0
- package/dist/redact-secrets.js +65 -0
- package/dist/redact-secrets.js.map +1 -0
- package/dist/run-history.js +34 -24
- package/dist/run-history.js.map +1 -1
- package/dist/runtime-provenance.d.ts +8 -0
- package/dist/runtime-provenance.d.ts.map +1 -0
- package/dist/runtime-provenance.js +225 -0
- package/dist/runtime-provenance.js.map +1 -0
- package/dist/sandbox-manager.d.ts +4 -1
- package/dist/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox-manager.js +114 -273
- package/dist/sandbox-manager.js.map +1 -1
- package/dist/secure-file-replace.d.ts +49 -0
- package/dist/secure-file-replace.d.ts.map +1 -0
- package/dist/secure-file-replace.js +270 -0
- package/dist/secure-file-replace.js.map +1 -0
- package/dist/seen-conversations.d.ts +32 -5
- package/dist/seen-conversations.d.ts.map +1 -1
- package/dist/seen-conversations.js +145 -15
- package/dist/seen-conversations.js.map +1 -1
- package/dist/sessions.d.ts +20 -0
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +41 -6
- package/dist/sessions.js.map +1 -1
- package/dist/wizard/index.d.ts +2 -2
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +1 -1
- package/dist/wizard/index.js.map +1 -1
- package/dist/wizard/presets.d.ts +2 -1
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +2 -1
- package/dist/wizard/presets.js.map +1 -1
- package/dist/wizard/run.d.ts +0 -26
- package/dist/wizard/run.d.ts.map +1 -1
- package/dist/wizard/run.js +0 -53
- package/dist/wizard/run.js.map +1 -1
- package/package.json +22 -17
- package/schema/mono-agent.config.schema.json +45 -10
- package/skills/mono-agent-composer/SKILL.md +3 -3
- package/skills/mono-agent-composer/references/config-blueprint.md +123 -29
- package/skills/mono-agent-composer/references/discovery-questions.md +10 -8
- package/skills/mono-agent-composer/references/feature-coverage.md +90 -76
- package/skills/mono-agent-composer/references/package-map.md +6 -4
- package/skills/mono-agent-composer/references/playbooks.md +48 -27
- package/skills/mono-agent-composer/references/validation.md +16 -3
- package/dist/notify-runtime.d.ts +0 -26
- package/dist/notify-runtime.d.ts.map +0 -1
- package/dist/notify-runtime.js +0 -27
- package/dist/notify-runtime.js.map +0 -1
- package/dist/notify-tool.d.ts +0 -51
- package/dist/notify-tool.d.ts.map +0 -1
- package/dist/notify-tool.js +0 -182
- package/dist/notify-tool.js.map +0 -1
- package/dist/recipes/base.d.ts +0 -15
- package/dist/recipes/base.d.ts.map +0 -1
- package/dist/recipes/base.js +0 -51
- package/dist/recipes/base.js.map +0 -1
- package/dist/recipes/catalog.d.ts +0 -4
- package/dist/recipes/catalog.d.ts.map +0 -1
- package/dist/recipes/catalog.js +0 -525
- package/dist/recipes/catalog.js.map +0 -1
- package/dist/recipes/index.d.ts +0 -11
- package/dist/recipes/index.d.ts.map +0 -1
- package/dist/recipes/index.js +0 -14
- package/dist/recipes/index.js.map +0 -1
- package/dist/recipes/types.d.ts +0 -70
- package/dist/recipes/types.d.ts.map +0 -1
- package/dist/recipes/types.js +0 -15
- package/dist/recipes/types.js.map +0 -1
- package/dist/setup.d.ts +0 -29
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js +0 -97
- package/dist/setup.js.map +0 -1
package/dist/background.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { constants as fsConstants } from "node:fs";
|
|
4
|
-
import { lstat, mkdir, open,
|
|
4
|
+
import { lstat, mkdir, open, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { listRecordedRuns, listTraceSources } from "@mono-agent/observability";
|
|
8
8
|
import { describeSensitiveDataExportWarning, phoenixAppBaseUrl, resolveAppTraceRegistryDir, resolveAppTraceStaleAfterMs, } from "./app-config.js";
|
|
9
9
|
import { formatChannelFactValue } from "./channel-fact-format.js";
|
|
10
|
-
import { bootout, bootstrap, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, isLoaded, launchdServiceInfo, launchdPathsFor, makeLaunchctlRunner, } from "./launchd.js";
|
|
10
|
+
import { bootout, bootstrap, buildLaunchdMaintenancePlistXml, buildPlistXml, defaultPathEnv, deriveLaunchdLabel, deriveLaunchdMaintenanceLabel, isLoaded, launchdServiceInfo, launchdPathsFor, MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV, makeLaunchctlRunner, } from "./launchd.js";
|
|
11
11
|
import { selectBackgroundOperationalEnvironment } from "./background-environment.js";
|
|
12
|
+
import { beginLaunchdLogMaintenanceIntent, clearLaunchdLogMaintenanceIntent, inspectLaunchdLogs, LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS, markLaunchdLogMaintenanceRestoring, markLaunchdLogMaintenanceStopped, markLaunchdLogMaintenanceStopping, readLaunchdLogMaintenanceIntent, rotateStoppedLaunchdLogs, } from "./launchd-logs.js";
|
|
12
13
|
import { ensureManagedBackgroundRuntime, MANAGED_BACKGROUND_WORKER_ENV, } from "./background-runtime.js";
|
|
13
14
|
import { backgroundSnapshotFromMetadata, captureBackgroundSnapshot, encodeBackgroundSnapshot, sameBackgroundSnapshot, } from "./background-snapshot.js";
|
|
14
15
|
import { resolveConfiguredManagedRuntimePackages } from "./managed-runtime-packages.js";
|
|
15
|
-
import {
|
|
16
|
+
import { processIncarnationFromJson, } from "./process-incarnation.js";
|
|
17
|
+
import { acquireOwnerPrivateLock, validateOwnerPrivateLockInputs } from "./owner-private-lock.js";
|
|
16
18
|
import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
|
|
17
19
|
import * as ui from "./ui.js";
|
|
18
20
|
export { acquireBackgroundWorkerLease, backgroundWorkerLeasePath, } from "./background-worker-lease.js";
|
|
21
|
+
export { LAUNCHD_LOG_MAX_BYTES, LAUNCHD_LOG_ROTATION_COUNT, } from "./launchd-logs.js";
|
|
19
22
|
/** Exact non-secret environment materialised into a managed LaunchAgent. */
|
|
20
23
|
export function managedBackgroundEnvironment(env) {
|
|
21
24
|
return {
|
|
@@ -26,6 +29,16 @@ export function managedBackgroundEnvironment(env) {
|
|
|
26
29
|
[MANAGED_BACKGROUND_WORKER_ENV]: "1",
|
|
27
30
|
};
|
|
28
31
|
}
|
|
32
|
+
/** Exact non-secret environment for the scheduled one-shot log maintainer. */
|
|
33
|
+
export function managedLaunchdLogMaintenanceEnvironment(env) {
|
|
34
|
+
return {
|
|
35
|
+
...selectBackgroundOperationalEnvironment(env),
|
|
36
|
+
// This unattended helper needs only pinned system tools. Do not persist a
|
|
37
|
+
// caller-controlled PATH that could shadow launchctl on a later run.
|
|
38
|
+
PATH: "/usr/bin:/bin",
|
|
39
|
+
[MANAGED_LAUNCHD_LOG_MAINTENANCE_ENV]: "1",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
29
42
|
/**
|
|
30
43
|
* Resolve everything the control commands need from CLI args. The registry dir
|
|
31
44
|
* and config path are resolved exactly as the worker will resolve them, so the
|
|
@@ -101,8 +114,21 @@ export function defaultBackgroundDeps() {
|
|
|
101
114
|
writeFile: writeOwnerPrivateLaunchdFile,
|
|
102
115
|
mkdir: ensureOwnerPrivateLaunchdDirectory,
|
|
103
116
|
rm: (path) => rm(path, { force: true }),
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
inspectLaunchdLogs: async (paths) => await inspectLaunchdLogs(paths),
|
|
118
|
+
rotateStoppedLaunchdLogs: async (paths) => {
|
|
119
|
+
await rotateStoppedLaunchdLogs(paths);
|
|
120
|
+
},
|
|
121
|
+
readLaunchdLogMaintenanceIntent: async (paths) => await readLaunchdLogMaintenanceIntent(paths),
|
|
122
|
+
beginLaunchdLogMaintenanceIntent: async (paths, intent) => {
|
|
123
|
+
await beginLaunchdLogMaintenanceIntent(paths, intent);
|
|
124
|
+
},
|
|
125
|
+
markLaunchdLogMaintenanceStopped: async (paths, expected) => await markLaunchdLogMaintenanceStopped(paths, expected),
|
|
126
|
+
markLaunchdLogMaintenanceRestoring: async (paths, expected) => await markLaunchdLogMaintenanceRestoring(paths, expected),
|
|
127
|
+
markLaunchdLogMaintenanceStopping: async (paths, expected) => await markLaunchdLogMaintenanceStopping(paths, expected),
|
|
128
|
+
clearLaunchdLogMaintenanceIntent: async (paths, expected) => {
|
|
129
|
+
await clearLaunchdLogMaintenanceIntent(paths, expected);
|
|
130
|
+
},
|
|
131
|
+
verifyLaunchdPlist: inspectOwnerPrivateLaunchdPlist,
|
|
106
132
|
isAlive: (pid) => {
|
|
107
133
|
try {
|
|
108
134
|
process.kill(pid, 0);
|
|
@@ -131,8 +157,6 @@ export function defaultBackgroundDeps() {
|
|
|
131
157
|
};
|
|
132
158
|
}
|
|
133
159
|
const DEFAULT_POLL = { timeoutMs: 18_000, intervalMs: 400 };
|
|
134
|
-
export const LAUNCHD_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
135
|
-
export const LAUNCHD_LOG_ROTATION_COUNT = 3;
|
|
136
160
|
export async function startBackground(target, deps, poll = DEFAULT_POLL) {
|
|
137
161
|
return (await ensureBackgroundReady(target, deps, poll)).ok ? 0 : 1;
|
|
138
162
|
}
|
|
@@ -165,6 +189,213 @@ export async function forceRestartBackground(target, deps, whileStopped, poll =
|
|
|
165
189
|
});
|
|
166
190
|
}
|
|
167
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Private one-shot invoked only by the scheduled maintenance LaunchAgent. It
|
|
194
|
+
* never creates or rewrites a service definition. It does not resurrect an
|
|
195
|
+
* intentionally stopped service; the sole exception is recovery authorized by
|
|
196
|
+
* its own durable lifecycle intent after a prior maintainer died post-bootout.
|
|
197
|
+
*/
|
|
198
|
+
export async function maintainLaunchdLogs(target, deps, poll = DEFAULT_POLL) {
|
|
199
|
+
const release = await deps.acquireLifecycleLock(target);
|
|
200
|
+
if (release === undefined) {
|
|
201
|
+
// Controller start/restart/stop owns the same lock. Skipping this periodic
|
|
202
|
+
// pass is expected and avoids making launchd record a false maintenance
|
|
203
|
+
// failure; the next interval retries from a fresh inventory.
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const uid = deps.getuid();
|
|
208
|
+
const service = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
209
|
+
let inspection;
|
|
210
|
+
try {
|
|
211
|
+
inspection = await deps.inspectLaunchdLogs(target.paths);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
reportMaintenanceFailure(target, deps, "inspect launchd logs", error);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
let maintenanceIntent;
|
|
218
|
+
try {
|
|
219
|
+
maintenanceIntent = await deps.readLaunchdLogMaintenanceIntent(target.paths);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", error);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
if (inspection.pendingMaintenance && maintenanceIntent === undefined) {
|
|
226
|
+
reportMaintenanceFailure(target, deps, "read durable launchd-log maintenance intent", new Error("The maintenance marker disappeared after inventory."));
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
if (!service.loaded && maintenanceIntent === undefined)
|
|
230
|
+
return 0;
|
|
231
|
+
let originalPlistIdentity;
|
|
232
|
+
try {
|
|
233
|
+
originalPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
reportMaintenanceFailure(target, deps, "verify the existing main LaunchAgent", error);
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
if (maintenanceIntent !== undefined
|
|
240
|
+
&& (maintenanceIntent.label !== target.label
|
|
241
|
+
|| maintenanceIntent.plistFingerprint !== originalPlistIdentity)) {
|
|
242
|
+
reportMaintenanceFailure(target, deps, "authenticate durable launchd-log maintenance intent", new Error("The pending intent does not match the exact main LaunchAgent definition."));
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
if (maintenanceIntent?.phase === "stopping" && !service.loaded) {
|
|
246
|
+
reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("The prior maintainer did not durably prove every old writer PID dead; refusing rotation."));
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
if (maintenanceIntent?.phase === "restoring") {
|
|
250
|
+
const replacement = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
251
|
+
if (!replacement.loaded || replacement.pid === undefined || !deps.isAlive(replacement.pid)) {
|
|
252
|
+
reportMaintenanceFailure(target, deps, "recover interrupted launchd-log restoration", new Error("The replacement writer identity was lost before live-worker proof; refusing stale rotation authority."));
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
reportMaintenanceFailure(target, deps, "clear recovered launchd-log restoration intent", error);
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
if (maintenanceIntent?.phase === "stopped" && service.loaded) {
|
|
265
|
+
reportMaintenanceFailure(target, deps, "recover interrupted launchd-log maintenance", new Error("launchd reports a writer loaded after durable stopped-writer proof; refusing rotation."));
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
if (!inspection.canMaintain) {
|
|
269
|
+
deps.stderr(ui.errorLine(`Scheduled log maintenance refused unsafe paths for ${target.label}.`));
|
|
270
|
+
for (const issue of inspection.issues)
|
|
271
|
+
deps.stderr(ui.style.dim(issue) + "\n");
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
if (!inspection.needsMaintenance && maintenanceIntent === undefined)
|
|
275
|
+
return 0;
|
|
276
|
+
if (maintenanceIntent === undefined) {
|
|
277
|
+
maintenanceIntent = {
|
|
278
|
+
version: 1,
|
|
279
|
+
phase: "stopping",
|
|
280
|
+
label: target.label,
|
|
281
|
+
plistFingerprint: originalPlistIdentity,
|
|
282
|
+
};
|
|
283
|
+
try {
|
|
284
|
+
await deps.beginLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
reportMaintenanceFailure(target, deps, "publish durable launchd-log maintenance intent", error);
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (maintenanceIntent.phase === "stopping") {
|
|
292
|
+
const stopped = await unloadLaunchdService(target.label, service, uniquePids([service.pid]), deps, uid, poll);
|
|
293
|
+
if (!stopped.ok) {
|
|
294
|
+
reportMaintenanceFailure(target, deps, "prove the launchd log writer stopped", stopped.failure);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
maintenanceIntent = await deps.markLaunchdLogMaintenanceStopped(target.paths, maintenanceIntent);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
reportMaintenanceFailure(target, deps, "record durable stopped-writer proof", error);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const current = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
307
|
+
if (current.loaded || (current.pid !== undefined && deps.isAlive(current.pid))) {
|
|
308
|
+
reportMaintenanceFailure(target, deps, "recheck durable stopped-writer proof", new Error("launchd exposed a live writer before recovered rotation."));
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
await deps.rotateStoppedLaunchdLogs(target.paths);
|
|
314
|
+
// Close the pathname-replacement race before handing the definition to
|
|
315
|
+
// launchctl. A changed/symlinked plist leaves the worker stopped.
|
|
316
|
+
const currentPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
|
|
317
|
+
if (currentPlistIdentity !== originalPlistIdentity) {
|
|
318
|
+
throw new Error("The main LaunchAgent plist changed during stopped-writer maintenance.");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
reportMaintenanceFailure(target, deps, "commit bounded stopped-writer logs", error);
|
|
323
|
+
return 1;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
maintenanceIntent = await deps.markLaunchdLogMaintenanceRestoring(target.paths, maintenanceIntent);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
reportMaintenanceFailure(target, deps, "invalidate stopped-writer proof before restoration", error);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
|
|
333
|
+
const observedRestorePids = new Set();
|
|
334
|
+
const running = await pollUntil(deps, poll, async () => {
|
|
335
|
+
const current = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
336
|
+
if (current.pid !== undefined)
|
|
337
|
+
observedRestorePids.add(current.pid);
|
|
338
|
+
return current.loaded && current.pid !== undefined && deps.isAlive(current.pid);
|
|
339
|
+
});
|
|
340
|
+
if (!running) {
|
|
341
|
+
const current = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
342
|
+
if (current.pid !== undefined)
|
|
343
|
+
observedRestorePids.add(current.pid);
|
|
344
|
+
const cleanedUp = await unloadLaunchdService(target.label, current, [...observedRestorePids], deps, uid, poll);
|
|
345
|
+
reportMaintenanceFailure(target, deps, "restore the exact main LaunchAgent after rotation", lifecycleFailure(booted, "launchd did not expose a live replacement worker"));
|
|
346
|
+
if (!cleanedUp.ok) {
|
|
347
|
+
reportMaintenanceFailure(target, deps, "remove the failed replacement worker", cleanedUp.failure);
|
|
348
|
+
}
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const restoredPlistIdentity = await deps.verifyLaunchdPlist(target.paths.plistPath);
|
|
353
|
+
if (restoredPlistIdentity !== originalPlistIdentity) {
|
|
354
|
+
throw new Error("The main LaunchAgent plist changed while launchd restored the worker.");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
const restoredService = await launchdServiceInfo(deps.runner, target.label, uid);
|
|
359
|
+
const stoppedAgain = await unloadLaunchdService(target.label, restoredService, uniquePids([restoredService.pid, ...observedRestorePids]), deps, uid, poll);
|
|
360
|
+
reportMaintenanceFailure(target, deps, "prove the restored main LaunchAgent definition", error);
|
|
361
|
+
if (!stoppedAgain.ok) {
|
|
362
|
+
reportMaintenanceFailure(target, deps, "stop the worker after its definition changed", stoppedAgain.failure);
|
|
363
|
+
}
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
reportMaintenanceFailure(target, deps, "clear durable launchd-log maintenance intent", error);
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
finally {
|
|
376
|
+
await release().catch((error) => {
|
|
377
|
+
deps.stderr(ui.errorLine(`Could not release lifecycle lock for ${target.label}: ${error instanceof Error ? error.message : String(error)}`));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function reportMaintenanceFailure(target, deps, action, error) {
|
|
382
|
+
deps.stderr(ui.errorLine(`Scheduled log maintenance could not ${action} for ${target.label}: ${maintenanceErrorMessage(error)}`));
|
|
383
|
+
}
|
|
384
|
+
function maintenanceErrorMessage(error) {
|
|
385
|
+
if (error instanceof Error)
|
|
386
|
+
return error.message;
|
|
387
|
+
if (typeof error === "object" && error !== null) {
|
|
388
|
+
const result = error;
|
|
389
|
+
const detail = [result.stderr, result.stdout]
|
|
390
|
+
.find((value) => typeof value === "string" && value.trim().length > 0)
|
|
391
|
+
?.trim();
|
|
392
|
+
if (detail !== undefined)
|
|
393
|
+
return detail;
|
|
394
|
+
if (typeof result.code === "number")
|
|
395
|
+
return `launchctl exited ${result.code}`;
|
|
396
|
+
}
|
|
397
|
+
return String(error);
|
|
398
|
+
}
|
|
168
399
|
/**
|
|
169
400
|
* Ensure the canonical per-config LaunchAgent is running and return the fresh
|
|
170
401
|
* trace source that proved it reached `startup-complete`. This is the shared
|
|
@@ -234,24 +465,50 @@ async function ensureBackgroundReadyUnlocked(target, deps, poll) {
|
|
|
234
465
|
reportLifecycleException(launchTarget, deps, "verify the existing background worker ownership", error);
|
|
235
466
|
return { ok: false, action: "start", reason: "ownership" };
|
|
236
467
|
}
|
|
237
|
-
let sinceMs;
|
|
468
|
+
let sinceMs = deps.now();
|
|
238
469
|
try {
|
|
239
470
|
await prepareLaunchdDirectories(launchTarget, deps);
|
|
240
|
-
await rotateLaunchdLogs(launchTarget.paths, deps);
|
|
241
|
-
await writePlist(launchTarget, deps);
|
|
242
|
-
sinceMs = deps.now();
|
|
243
471
|
}
|
|
244
472
|
catch (error) {
|
|
245
|
-
reportLifecycleException(launchTarget, deps, "prepare the LaunchAgent", error);
|
|
473
|
+
reportLifecycleException(launchTarget, deps, "prepare the LaunchAgent directories", error);
|
|
246
474
|
return { ok: false, action: "start", reason: "preparation" };
|
|
247
475
|
}
|
|
248
476
|
let outcome;
|
|
477
|
+
let prepared = false;
|
|
249
478
|
try {
|
|
250
|
-
|
|
479
|
+
let interruptedMaintenance = await deps.readLaunchdLogMaintenanceIntent(launchTarget.paths);
|
|
480
|
+
if (interruptedMaintenance?.phase === "stopping" || interruptedMaintenance?.phase === "restoring") {
|
|
481
|
+
const current = await launchdServiceInfo(deps.runner, launchTarget.label, uid);
|
|
482
|
+
if (!current.loaded) {
|
|
483
|
+
throw new Error("Interrupted launchd-log maintenance lost its writer PID before durable stopped-writer proof; refusing restart.");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
outcome = await bootstrapOrRestart(launchTarget, deps, uid, poll, async () => {
|
|
487
|
+
const stoppedIntent = interruptedMaintenance;
|
|
488
|
+
if (stoppedIntent?.phase === "stopped") {
|
|
489
|
+
interruptedMaintenance = await deps.markLaunchdLogMaintenanceStopping(launchTarget.paths, stoppedIntent);
|
|
490
|
+
}
|
|
491
|
+
}, async (mainStopProven) => {
|
|
492
|
+
const stoppingIntent = interruptedMaintenance;
|
|
493
|
+
if (stoppingIntent?.phase === "stopping" || stoppingIntent?.phase === "restoring") {
|
|
494
|
+
if (!mainStopProven) {
|
|
495
|
+
throw new Error("The controller could not renew stopped-writer proof for interrupted log maintenance.");
|
|
496
|
+
}
|
|
497
|
+
interruptedMaintenance = await deps.markLaunchdLogMaintenanceStopped(launchTarget.paths, stoppingIntent);
|
|
498
|
+
}
|
|
499
|
+
await deps.rotateStoppedLaunchdLogs(launchTarget.paths);
|
|
500
|
+
// A controller may be completing a maintainer that died after bootout.
|
|
501
|
+
// Once journal recovery finishes under the stopped-writer proof, cancel
|
|
502
|
+
// its old restore authority before replacing either plist.
|
|
503
|
+
await deps.clearLaunchdLogMaintenanceIntent(launchTarget.paths, interruptedMaintenance);
|
|
504
|
+
await writePlists(launchTarget, deps);
|
|
505
|
+
sinceMs = deps.now();
|
|
506
|
+
prepared = true;
|
|
507
|
+
});
|
|
251
508
|
}
|
|
252
509
|
catch (error) {
|
|
253
|
-
reportLifecycleException(launchTarget, deps, "start the LaunchAgent", error);
|
|
254
|
-
return { ok: false, action: "start", reason: "launchctl" };
|
|
510
|
+
reportLifecycleException(launchTarget, deps, prepared ? "start the LaunchAgent" : "prepare the stopped LaunchAgent", error);
|
|
511
|
+
return { ok: false, action: "start", reason: prepared ? "launchctl" : "preparation" };
|
|
255
512
|
}
|
|
256
513
|
const action = outcome.restarted ? "restart" : "start";
|
|
257
514
|
if (!outcome.ok) {
|
|
@@ -297,11 +554,12 @@ async function prepareLaunchdDirectories(target, deps) {
|
|
|
297
554
|
await deps.mkdir(target.paths.logDir);
|
|
298
555
|
await deps.mkdir(target.paths.launchAgentsDir);
|
|
299
556
|
}
|
|
300
|
-
async function
|
|
557
|
+
async function writePlists(target, deps) {
|
|
301
558
|
if (target.expectedSnapshot === undefined) {
|
|
302
559
|
throw new Error("A managed LaunchAgent requires an approved background snapshot.");
|
|
303
560
|
}
|
|
304
|
-
const
|
|
561
|
+
const maintenance = maintenancePathsForTarget(target);
|
|
562
|
+
const mainXml = buildPlistXml({
|
|
305
563
|
label: target.label,
|
|
306
564
|
nodePath: target.nodePath,
|
|
307
565
|
cliPath: target.cliPath,
|
|
@@ -313,38 +571,20 @@ async function writePlist(target, deps) {
|
|
|
313
571
|
stderrPath: target.paths.stderrPath,
|
|
314
572
|
environment: target.environment,
|
|
315
573
|
});
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (isErrno(error, "ENOENT"))
|
|
331
|
-
return;
|
|
332
|
-
throw error;
|
|
333
|
-
}
|
|
334
|
-
if (size <= LAUNCHD_LOG_MAX_BYTES) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
await deps.rm(`${path}.${LAUNCHD_LOG_ROTATION_COUNT}`);
|
|
338
|
-
for (let index = LAUNCHD_LOG_ROTATION_COUNT; index >= 1; index -= 1) {
|
|
339
|
-
const source = index === 1 ? path : `${path}.${index - 1}`;
|
|
340
|
-
const destination = `${path}.${index}`;
|
|
341
|
-
try {
|
|
342
|
-
await deps.rename(source, destination);
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
// Missing or locked rotation segment: leave it for the next launch cycle.
|
|
346
|
-
}
|
|
347
|
-
}
|
|
574
|
+
const maintenanceXml = buildLaunchdMaintenancePlistXml({
|
|
575
|
+
label: maintenance.label,
|
|
576
|
+
nodePath: target.nodePath,
|
|
577
|
+
cliPath: target.cliPath,
|
|
578
|
+
configPath: target.configPath,
|
|
579
|
+
// The agent folder may be renamed or removed while its already-running
|
|
580
|
+
// worker still owns log descriptors. Keep the scheduler's cwd in the
|
|
581
|
+
// durable account state tree; its config argument is already absolute.
|
|
582
|
+
cwd: dirname(target.paths.logDir),
|
|
583
|
+
environment: managedLaunchdLogMaintenanceEnvironment(target.environment),
|
|
584
|
+
intervalSeconds: LAUNCHD_LOG_MAINTENANCE_INTERVAL_SECONDS,
|
|
585
|
+
});
|
|
586
|
+
await deps.writeFile(target.paths.plistPath, mainXml);
|
|
587
|
+
await deps.writeFile(maintenance.plistPath, maintenanceXml);
|
|
348
588
|
}
|
|
349
589
|
async function ensureOwnerPrivateLaunchdDirectory(path) {
|
|
350
590
|
const parent = dirname(path);
|
|
@@ -431,27 +671,54 @@ async function writeOwnerPrivateLaunchdFile(path, data) {
|
|
|
431
671
|
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
432
672
|
}
|
|
433
673
|
}
|
|
434
|
-
async function
|
|
435
|
-
const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
674
|
+
async function inspectOwnerPrivateLaunchdPlist(path) {
|
|
675
|
+
const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK);
|
|
436
676
|
try {
|
|
437
677
|
const before = await handle.stat();
|
|
438
|
-
assertOwnerRegularFile(before, path, "LaunchAgent
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
678
|
+
assertOwnerRegularFile(before, path, "LaunchAgent plist");
|
|
679
|
+
if ((before.mode & 0o777) !== 0o600) {
|
|
680
|
+
throw new Error(`LaunchAgent plist ${path} must be owner-only.`);
|
|
681
|
+
}
|
|
682
|
+
if (!Number.isSafeInteger(before.size) || before.size < 1 || before.size > 1024 * 1024) {
|
|
683
|
+
throw new Error(`LaunchAgent plist ${path} has an unsafe size.`);
|
|
684
|
+
}
|
|
685
|
+
const contents = await readExactFileHandle(handle, before.size, "LaunchAgent plist");
|
|
686
|
+
const after = await handle.stat();
|
|
687
|
+
assertOwnerRegularFile(after, path, "LaunchAgent plist");
|
|
688
|
+
if ((after.mode & 0o777) !== 0o600
|
|
689
|
+
|| !sameFileSnapshot(before, after)
|
|
690
|
+
|| contents.length !== before.size) {
|
|
691
|
+
throw new Error(`LaunchAgent plist ${path} changed while it was read.`);
|
|
443
692
|
}
|
|
444
|
-
assertOwnerRegularFile(secured, path, "LaunchAgent log");
|
|
445
693
|
const current = await lstat(path);
|
|
446
|
-
|
|
447
|
-
|
|
694
|
+
assertOwnerRegularFile(current, path, "LaunchAgent plist");
|
|
695
|
+
if ((current.mode & 0o777) !== 0o600 || !sameFileSnapshot(after, current)) {
|
|
696
|
+
throw new Error(`LaunchAgent plist ${path} changed while it was inspected.`);
|
|
448
697
|
}
|
|
449
|
-
return
|
|
698
|
+
return [
|
|
699
|
+
String(after.dev),
|
|
700
|
+
String(after.ino),
|
|
701
|
+
String(after.size),
|
|
702
|
+
createHash("sha256").update(contents).digest("hex"),
|
|
703
|
+
].join(":");
|
|
450
704
|
}
|
|
451
705
|
finally {
|
|
452
706
|
await handle.close();
|
|
453
707
|
}
|
|
454
708
|
}
|
|
709
|
+
async function readExactFileHandle(handle, size, description) {
|
|
710
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
711
|
+
throw new Error(`${description} has an unsafe size.`);
|
|
712
|
+
const contents = Buffer.alloc(size);
|
|
713
|
+
let offset = 0;
|
|
714
|
+
while (offset < size) {
|
|
715
|
+
const result = await handle.read(contents, offset, size - offset, offset);
|
|
716
|
+
if (result.bytesRead === 0)
|
|
717
|
+
throw new Error(`${description} ended while its bounded contents were read.`);
|
|
718
|
+
offset += result.bytesRead;
|
|
719
|
+
}
|
|
720
|
+
return contents;
|
|
721
|
+
}
|
|
455
722
|
function assertOwnerDirectory(details, path, description) {
|
|
456
723
|
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
457
724
|
throw new Error(`${description} ${path} must be a real directory.`);
|
|
@@ -475,52 +742,139 @@ function assertCurrentUserOwns(details, path, description) {
|
|
|
475
742
|
function sameFilesystemIdentity(left, right) {
|
|
476
743
|
return left.dev === right.dev && left.ino === right.ino;
|
|
477
744
|
}
|
|
745
|
+
function sameFileSnapshot(left, right) {
|
|
746
|
+
return sameFilesystemIdentity(left, right)
|
|
747
|
+
&& left.uid === right.uid
|
|
748
|
+
&& left.nlink === right.nlink
|
|
749
|
+
&& left.mode === right.mode
|
|
750
|
+
&& left.size === right.size
|
|
751
|
+
&& left.mtimeMs === right.mtimeMs
|
|
752
|
+
&& left.ctimeMs === right.ctimeMs;
|
|
753
|
+
}
|
|
478
754
|
/**
|
|
479
755
|
* Idempotent: bootstrap when not loaded. A loaded job is always fully removed
|
|
480
756
|
* before bootstrap so launchd cannot retain stale ProgramArguments or env from
|
|
481
757
|
* the previous plist.
|
|
482
758
|
*/
|
|
483
|
-
async function bootstrapOrRestart(target, deps, uid, poll) {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return { ok: true, restarted: false };
|
|
489
|
-
}
|
|
490
|
-
return { ok: false, restarted: false, failure: booted };
|
|
491
|
-
}
|
|
492
|
-
const oldSources = await findInstances(target, deps);
|
|
493
|
-
const oldPids = uniquePids([
|
|
494
|
-
service.pid,
|
|
495
|
-
// A cleanly stopped manifest can outlive its process long enough for the OS
|
|
496
|
-
// to reuse that pid for unrelated work. It is historical evidence, not an
|
|
497
|
-
// ownership claim, so never wait for its recycled pid during restart.
|
|
498
|
-
...oldSources
|
|
499
|
-
.filter((source) => source.health !== "stopped")
|
|
500
|
-
.map((source) => source.pid),
|
|
759
|
+
async function bootstrapOrRestart(target, deps, uid, poll, beforeMainBootout, whileStopped) {
|
|
760
|
+
const maintenance = maintenancePathsForTarget(target);
|
|
761
|
+
const [service, maintenanceService] = await Promise.all([
|
|
762
|
+
launchdServiceInfo(deps.runner, target.label, uid),
|
|
763
|
+
launchdServiceInfo(deps.runner, maintenance.label, uid),
|
|
501
764
|
]);
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
765
|
+
const restarted = service.loaded;
|
|
766
|
+
// Stop the scheduler first. A helper that is already running either holds
|
|
767
|
+
// this lifecycle lock (so the controller could not enter) or is waiting to
|
|
768
|
+
// acquire it; bootout plus PID death prevents a post-stop resurrection.
|
|
769
|
+
const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
|
|
770
|
+
if (!maintenanceStopped.ok) {
|
|
771
|
+
return { ok: false, restarted, failure: maintenanceStopped.failure };
|
|
772
|
+
}
|
|
773
|
+
let mainStopProven = false;
|
|
774
|
+
if (service.loaded) {
|
|
775
|
+
const oldSources = await findInstances(target, deps);
|
|
776
|
+
const oldPids = uniquePids([
|
|
777
|
+
service.pid,
|
|
778
|
+
// A cleanly stopped manifest can outlive its process long enough for the OS
|
|
779
|
+
// to reuse that pid for unrelated work. It is historical evidence, not an
|
|
780
|
+
// ownership claim, so never wait for its recycled pid during restart.
|
|
781
|
+
...oldSources
|
|
782
|
+
.filter((source) => source.health !== "stopped")
|
|
783
|
+
.map((source) => source.pid),
|
|
784
|
+
]);
|
|
785
|
+
const mainStopped = await unloadLaunchdService(target.label, service, oldPids, deps, uid, poll, beforeMainBootout);
|
|
786
|
+
if (!mainStopped.ok) {
|
|
787
|
+
const restoreFailure = await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
|
|
788
|
+
return {
|
|
789
|
+
ok: false,
|
|
790
|
+
restarted: true,
|
|
791
|
+
failure: restoreFailure === undefined
|
|
792
|
+
? mainStopped.failure
|
|
793
|
+
: lifecycleFailure(mainStopped.failure, `scheduled maintenance restoration failed: ${maintenanceErrorMessage(restoreFailure)}`),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
mainStopProven = true;
|
|
797
|
+
}
|
|
798
|
+
await whileStopped(mainStopProven);
|
|
799
|
+
const maintenanceBooted = await bootstrap(deps.runner, maintenance.plistPath, uid);
|
|
800
|
+
const maintenanceLoaded = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
|
|
801
|
+
if (!maintenanceLoaded) {
|
|
505
802
|
return {
|
|
506
803
|
ok: false,
|
|
507
|
-
restarted
|
|
508
|
-
failure: lifecycleFailure(
|
|
804
|
+
restarted,
|
|
805
|
+
failure: lifecycleFailure(maintenanceBooted, "scheduled log maintenance did not report loaded"),
|
|
509
806
|
};
|
|
510
807
|
}
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
808
|
+
const booted = await bootstrap(deps.runner, target.paths.plistPath, uid);
|
|
809
|
+
if (booted.code === 0 || (await isLoaded(deps.runner, target.label, uid))) {
|
|
810
|
+
return { ok: true, restarted };
|
|
811
|
+
}
|
|
812
|
+
const maintenanceServiceAfterFailure = await launchdServiceInfo(deps.runner, maintenance.label, uid);
|
|
813
|
+
const maintenanceRemoval = await unloadLaunchdService(maintenance.label, maintenanceServiceAfterFailure, uniquePids([maintenanceServiceAfterFailure.pid]), deps, uid, poll);
|
|
814
|
+
return {
|
|
815
|
+
ok: false,
|
|
816
|
+
restarted,
|
|
817
|
+
failure: lifecycleFailure(booted, maintenanceRemoval.ok
|
|
818
|
+
? "main service did not load; scheduled maintenance was unloaded"
|
|
819
|
+
: `main service did not load; scheduled maintenance cleanup failed: ${maintenanceErrorMessage(maintenanceRemoval.failure)}`),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
async function unloadLaunchdService(label, service, ownedPids, deps, uid, poll, beforeLoadedBootout) {
|
|
823
|
+
const observedPids = new Set(uniquePids([service.pid, ...ownedPids]));
|
|
824
|
+
try {
|
|
825
|
+
await beforeLoadedBootout?.();
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
513
828
|
return {
|
|
514
829
|
ok: false,
|
|
515
|
-
|
|
516
|
-
failure: lifecycleFailure(removed, `previous worker pid(s) ${oldPids.join(", ")} remained alive after bootout`),
|
|
830
|
+
failure: { code: 1, stdout: "", stderr: maintenanceErrorMessage(error) },
|
|
517
831
|
};
|
|
518
832
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
833
|
+
// Refresh immediately before bootout. The caller may have spent time on
|
|
834
|
+
// inventory or plist verification while KeepAlive replaced its entry PID.
|
|
835
|
+
let current = await launchdServiceInfo(deps.runner, label, uid);
|
|
836
|
+
if (current.pid !== undefined)
|
|
837
|
+
observedPids.add(current.pid);
|
|
838
|
+
let removed;
|
|
839
|
+
if (current.loaded) {
|
|
840
|
+
removed = await bootout(deps.runner, label, uid);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
removed = { code: 0, stdout: "", stderr: "" };
|
|
844
|
+
}
|
|
845
|
+
// Keep launchd state and process liveness in one predicate. In particular,
|
|
846
|
+
// retain every KeepAlive replacement pid exposed while bootout converges;
|
|
847
|
+
// proving only the entry snapshot dead is not a stopped-writer proof.
|
|
848
|
+
const stopped = await pollUntil(deps, poll, async () => {
|
|
849
|
+
current = await launchdServiceInfo(deps.runner, label, uid);
|
|
850
|
+
if (current.pid !== undefined)
|
|
851
|
+
observedPids.add(current.pid);
|
|
852
|
+
return !current.loaded && [...observedPids].every((pid) => !deps.isAlive(pid));
|
|
853
|
+
});
|
|
854
|
+
if (!stopped) {
|
|
855
|
+
const livePids = [...observedPids].filter((pid) => deps.isAlive(pid));
|
|
856
|
+
const detail = current.loaded
|
|
857
|
+
? `launchd still reports ${label} loaded after bootout`
|
|
858
|
+
: `${label} pid(s) ${livePids.join(", ")} remained alive after bootout`;
|
|
859
|
+
return {
|
|
860
|
+
ok: false,
|
|
861
|
+
failure: lifecycleFailure(removed, detail),
|
|
862
|
+
};
|
|
522
863
|
}
|
|
523
|
-
return { ok:
|
|
864
|
+
return { ok: true };
|
|
865
|
+
}
|
|
866
|
+
function maintenancePathsForTarget(target) {
|
|
867
|
+
const label = deriveLaunchdMaintenanceLabel(target.label);
|
|
868
|
+
return { label, plistPath: resolve(target.paths.launchAgentsDir, `${label}.plist`) };
|
|
869
|
+
}
|
|
870
|
+
async function restoreMaintenanceDefinition(wasLoaded, maintenance, deps, uid, poll) {
|
|
871
|
+
if (!wasLoaded)
|
|
872
|
+
return undefined;
|
|
873
|
+
const restored = await bootstrap(deps.runner, maintenance.plistPath, uid);
|
|
874
|
+
const returned = await pollUntil(deps, poll, async () => await isLoaded(deps.runner, maintenance.label, uid));
|
|
875
|
+
if (returned)
|
|
876
|
+
return undefined;
|
|
877
|
+
return lifecycleFailure(restored, "scheduled maintenance did not return after the main stop failed");
|
|
524
878
|
}
|
|
525
879
|
async function findOwnershipConflict(target, deps, uid) {
|
|
526
880
|
const [service, sources] = await Promise.all([
|
|
@@ -577,8 +931,26 @@ export async function stopBackground(target, deps, poll = DEFAULT_POLL) {
|
|
|
577
931
|
}
|
|
578
932
|
async function stopBackgroundUnlocked(target, deps, poll) {
|
|
579
933
|
const uid = deps.getuid();
|
|
934
|
+
const maintenance = maintenancePathsForTarget(target);
|
|
580
935
|
const existing = await findInstances(target, deps);
|
|
581
|
-
const service = await
|
|
936
|
+
const [service, maintenanceService] = await Promise.all([
|
|
937
|
+
launchdServiceInfo(deps.runner, target.label, uid),
|
|
938
|
+
launchdServiceInfo(deps.runner, maintenance.label, uid),
|
|
939
|
+
]);
|
|
940
|
+
let maintenanceIntent;
|
|
941
|
+
try {
|
|
942
|
+
maintenanceIntent = await deps.readLaunchdLogMaintenanceIntent(target.paths);
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
deps.stderr(ui.errorLine(`Failed to inspect pending launchd-log maintenance for ${target.label}.`));
|
|
946
|
+
deps.stderr(ui.style.dim(error instanceof Error ? error.message : String(error)) + "\n");
|
|
947
|
+
return 1;
|
|
948
|
+
}
|
|
949
|
+
if ((maintenanceIntent?.phase === "stopping" || maintenanceIntent?.phase === "restoring") && !service.loaded) {
|
|
950
|
+
deps.stderr(ui.errorLine(`Refusing to erase interrupted launchd-log maintenance for ${target.label} without stopped-writer proof.`));
|
|
951
|
+
deps.stderr(ui.hint("Confirm the prior worker is gone, then restore or remove the owner-private maintenance marker explicitly."));
|
|
952
|
+
return 1;
|
|
953
|
+
}
|
|
582
954
|
const liveTracePids = uniquePids(existing
|
|
583
955
|
.filter((source) => source.health !== "stopped" && source.pid !== undefined && deps.isAlive(source.pid))
|
|
584
956
|
.map((source) => source.pid));
|
|
@@ -587,25 +959,53 @@ async function stopBackgroundUnlocked(target, deps, poll) {
|
|
|
587
959
|
deps.stderr(ui.hint("The LaunchAgent plist was preserved. Stop the unmanaged process explicitly, then retry."));
|
|
588
960
|
return 1;
|
|
589
961
|
}
|
|
590
|
-
const
|
|
591
|
-
|
|
962
|
+
const maintenanceStopped = await unloadLaunchdService(maintenance.label, maintenanceService, uniquePids([maintenanceService.pid]), deps, uid, poll);
|
|
963
|
+
if (!maintenanceStopped.ok) {
|
|
964
|
+
deps.stderr(ui.errorLine(`Failed to stop scheduled log maintenance for ${target.label}.`));
|
|
965
|
+
const detail = (maintenanceStopped.failure.stderr || maintenanceStopped.failure.stdout).trim();
|
|
966
|
+
if (detail.length > 0)
|
|
967
|
+
deps.stderr(ui.style.dim(detail) + "\n");
|
|
968
|
+
deps.stderr(ui.hint("Both LaunchAgent plists were preserved. Retry stop after the maintenance helper exits."));
|
|
969
|
+
return 1;
|
|
970
|
+
}
|
|
592
971
|
const ownedPids = uniquePids([service.pid, ...liveTracePids]);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
972
|
+
const mainStopped = await unloadLaunchdService(target.label, service, ownedPids, deps, uid, poll, async () => {
|
|
973
|
+
const stoppedIntent = maintenanceIntent;
|
|
974
|
+
if (stoppedIntent?.phase === "stopped") {
|
|
975
|
+
maintenanceIntent = await deps.markLaunchdLogMaintenanceStopping(target.paths, stoppedIntent);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
if (!mainStopped.ok) {
|
|
979
|
+
const result = mainStopped.failure;
|
|
980
|
+
const maintenanceRestoreFailure = await restoreMaintenanceDefinition(maintenanceService.loaded, maintenance, deps, uid, poll);
|
|
981
|
+
deps.stderr(ui.errorLine(`Failed to prove ${target.label} stopped${result?.code === undefined || result.code === 0 ? "" : ` (launchctl bootout exited ${result.code})`}.`));
|
|
982
|
+
const detail = (result?.stderr || result?.stdout || "").trim();
|
|
597
983
|
if (detail.length > 0) {
|
|
598
984
|
deps.stderr(ui.style.dim(detail) + "\n");
|
|
599
985
|
}
|
|
600
|
-
if (
|
|
601
|
-
deps.stderr(ui.style.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
986
|
+
if (maintenanceRestoreFailure !== undefined) {
|
|
987
|
+
deps.stderr(ui.style.yellow(`Scheduled log maintenance also could not be restored: ${maintenanceErrorMessage(maintenanceRestoreFailure)}`) + "\n");
|
|
988
|
+
}
|
|
989
|
+
deps.stderr(ui.hint("Both LaunchAgent plists were preserved so the service remains recoverable. Inspect status/logs and retry."));
|
|
990
|
+
return 1;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
// Explicit stop cancels any interrupted maintainer's restore authority
|
|
994
|
+
// only after both jobs and every observed pid are proven gone.
|
|
995
|
+
if (maintenanceIntent?.phase === "stopping" || maintenanceIntent?.phase === "restoring") {
|
|
996
|
+
maintenanceIntent = await deps.markLaunchdLogMaintenanceStopped(target.paths, maintenanceIntent);
|
|
997
|
+
}
|
|
998
|
+
await deps.clearLaunchdLogMaintenanceIntent(target.paths, maintenanceIntent);
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
deps.stderr(ui.errorLine(`Failed to clear pending launchd-log maintenance for ${target.label}.`));
|
|
1002
|
+
deps.stderr(ui.style.dim(error instanceof Error ? error.message : String(error)) + "\n");
|
|
1003
|
+
deps.stderr(ui.hint("Both LaunchAgent plists were preserved; retry stop after the owner-private marker is inspectable."));
|
|
605
1004
|
return 1;
|
|
606
1005
|
}
|
|
607
|
-
// Remove
|
|
608
|
-
//
|
|
1006
|
+
// Remove both definitions only after the helper and worker are unloaded and
|
|
1007
|
+
// every observed PID is dead. The helper can never resurrect a stopped job.
|
|
1008
|
+
await deps.rm(maintenance.plistPath);
|
|
609
1009
|
await deps.rm(target.paths.plistPath);
|
|
610
1010
|
for (const source of existing)
|
|
611
1011
|
await maybeUnlinkDeadManifest(target, deps, source);
|
|
@@ -719,9 +1119,6 @@ async function findInstances(target, deps) {
|
|
|
719
1119
|
})));
|
|
720
1120
|
return matches.filter((entry) => entry.matches).map((entry) => entry.source);
|
|
721
1121
|
}
|
|
722
|
-
async function findInstance(target, deps) {
|
|
723
|
-
return (await findInstances(target, deps))[0];
|
|
724
|
-
}
|
|
725
1122
|
async function maybeUnlinkDeadManifest(target, deps, existing) {
|
|
726
1123
|
// Only clean up a manifest whose process is already gone; a worker that is
|
|
727
1124
|
// still shutting down will mark its own manifest stopped.
|
|
@@ -1036,179 +1433,53 @@ export async function acquireFilesystemLifecycleLock(target, options = {}) {
|
|
|
1036
1433
|
const pid = options.pid ?? process.pid;
|
|
1037
1434
|
const now = options.now ?? (() => Date.now());
|
|
1038
1435
|
const isProcessAlive = options.isProcessAlive ?? processIsAlive;
|
|
1039
|
-
const incarnation = options.processIncarnation ?? await currentProcessIncarnation();
|
|
1040
|
-
const isSameProcess = options.isSameProcessIncarnation ?? matchesProcessIncarnation;
|
|
1041
1436
|
const ownerlessGraceMs = options.ownerlessGraceMs ?? 5 * 60_000;
|
|
1042
1437
|
const randomToken = options.randomToken ?? randomUUID;
|
|
1043
|
-
|
|
1044
|
-
throw new Error(`Lifecycle lock pid must be a positive safe integer; received ${String(pid)}.`);
|
|
1045
|
-
}
|
|
1046
|
-
if (!Number.isFinite(ownerlessGraceMs) || ownerlessGraceMs < 0) {
|
|
1047
|
-
throw new Error("Lifecycle lock ownerless grace must be a non-negative finite duration.");
|
|
1048
|
-
}
|
|
1438
|
+
validateOwnerPrivateLockInputs("Lifecycle lock", pid, ownerlessGraceMs);
|
|
1049
1439
|
const managedRoot = dirname(target.paths.logDir);
|
|
1050
1440
|
const locksDir = resolve(managedRoot, "locks");
|
|
1051
1441
|
for (const path of [managedRoot, locksDir]) {
|
|
1052
1442
|
await ensureOwnerPrivateLaunchdDirectory(path);
|
|
1053
1443
|
}
|
|
1054
1444
|
const lockDir = resolve(locksDir, `${target.label}.lock`);
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
flag: "wx",
|
|
1071
|
-
mode: 0o600,
|
|
1072
|
-
});
|
|
1073
|
-
if (!(await sameLifecycleLockDirectory(lockDir, createdIdentity))) {
|
|
1074
|
-
throw new Error(`Lifecycle lock ${lockDir} changed while its owner record was written.`);
|
|
1075
|
-
}
|
|
1076
|
-
const ownedIdentity = createdIdentity;
|
|
1077
|
-
return async () => {
|
|
1078
|
-
try {
|
|
1079
|
-
if (!(await sameLifecycleLockDirectory(lockDir, ownedIdentity))) {
|
|
1080
|
-
throw new Error(`Lifecycle lock ${lockDir} is no longer owned by this command.`);
|
|
1081
|
-
}
|
|
1082
|
-
const current = JSON.parse(await readFile(ownerPath, "utf8"));
|
|
1083
|
-
if (current.token !== token) {
|
|
1084
|
-
throw new Error(`Lifecycle lock ${lockDir} is no longer owned by this command.`);
|
|
1085
|
-
}
|
|
1086
|
-
const releasedPath = resolve(locksDir, `${target.label}.released-${now()}-${pid}-${randomToken()}`);
|
|
1087
|
-
if (!(await sameLifecycleLockDirectory(lockDir, ownedIdentity))) {
|
|
1088
|
-
throw new Error(`Lifecycle lock ${lockDir} changed during release.`);
|
|
1089
|
-
}
|
|
1090
|
-
await rename(lockDir, releasedPath);
|
|
1091
|
-
const moved = await lstat(releasedPath);
|
|
1092
|
-
if (!sameLifecycleLockIdentity(moved, ownedIdentity)) {
|
|
1093
|
-
await rename(releasedPath, lockDir).catch(() => undefined);
|
|
1094
|
-
throw new Error(`Lifecycle lock ${lockDir} changed during release.`);
|
|
1095
|
-
}
|
|
1096
|
-
await rm(releasedPath, { recursive: true, force: true });
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
if (!isErrno(error, "ENOENT"))
|
|
1100
|
-
throw error;
|
|
1101
|
-
}
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
catch (error) {
|
|
1105
|
-
if (createdIdentity !== undefined) {
|
|
1106
|
-
await removeSameLifecycleLockDirectory(lockDir, createdIdentity).catch(() => undefined);
|
|
1107
|
-
}
|
|
1108
|
-
if (!isErrno(error, "EEXIST"))
|
|
1109
|
-
throw error;
|
|
1110
|
-
}
|
|
1111
|
-
let observed;
|
|
1112
|
-
try {
|
|
1113
|
-
observed = await assertLifecycleLockDirectory(lockDir);
|
|
1114
|
-
}
|
|
1115
|
-
catch (error) {
|
|
1116
|
-
if (isErrno(error, "ENOENT"))
|
|
1117
|
-
continue;
|
|
1118
|
-
return undefined;
|
|
1119
|
-
}
|
|
1120
|
-
const observedIdentity = lifecycleLockIdentity(observed);
|
|
1121
|
-
const owner = await readLifecycleLockOwner(ownerPath);
|
|
1122
|
-
if (!(await sameLifecycleLockDirectory(lockDir, observedIdentity)))
|
|
1123
|
-
continue;
|
|
1124
|
-
if (owner !== undefined) {
|
|
1125
|
-
let sameOwner = true;
|
|
1126
|
-
try {
|
|
1127
|
-
sameOwner = owner.incarnation === undefined
|
|
1128
|
-
? isProcessAlive(owner.pid)
|
|
1129
|
-
: await isSameProcess(owner.pid, owner.incarnation);
|
|
1130
|
-
}
|
|
1131
|
-
catch {
|
|
1132
|
-
// A failed identity probe cannot authorize stealing another command's lock.
|
|
1133
|
-
sameOwner = true;
|
|
1134
|
-
}
|
|
1135
|
-
if (sameOwner)
|
|
1445
|
+
const held = await acquireOwnerPrivateLock({
|
|
1446
|
+
path: lockDir,
|
|
1447
|
+
label: "Lifecycle lock",
|
|
1448
|
+
schemaTag: "mono-agent.filesystem-lifecycle-lock.v1",
|
|
1449
|
+
ownerlessGraceMs,
|
|
1450
|
+
maxAcquireAttempts: 4,
|
|
1451
|
+
pid,
|
|
1452
|
+
now,
|
|
1453
|
+
randomToken,
|
|
1454
|
+
...(options.processIncarnation === undefined ? {} : { processIncarnation: options.processIncarnation }),
|
|
1455
|
+
...(options.isSameProcessIncarnation === undefined
|
|
1456
|
+
? {}
|
|
1457
|
+
: { isSameProcessIncarnation: options.isSameProcessIncarnation }),
|
|
1458
|
+
parseLegacyOwner: (record) => {
|
|
1459
|
+
if (typeof record.pid !== "number" || !Number.isSafeInteger(record.pid) || record.pid <= 0)
|
|
1136
1460
|
return undefined;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
// Another process may be between its atomic mkdir and owner.json write.
|
|
1141
|
-
// Never steal that fresh ownerless lock; only recover abandoned debris.
|
|
1142
|
-
if (lockAgeMs < ownerlessGraceMs)
|
|
1143
|
-
return undefined;
|
|
1144
|
-
}
|
|
1145
|
-
if (!(await sameLifecycleLockDirectory(lockDir, observedIdentity)))
|
|
1146
|
-
continue;
|
|
1147
|
-
await options.beforeStaleLockRename?.();
|
|
1148
|
-
const stalePath = resolve(locksDir, `${target.label}.stale-${now()}-${pid}-${randomToken()}`);
|
|
1149
|
-
try {
|
|
1150
|
-
await rename(lockDir, stalePath);
|
|
1151
|
-
}
|
|
1152
|
-
catch (error) {
|
|
1153
|
-
if (isErrno(error, "ENOENT") || isErrno(error, "EEXIST"))
|
|
1154
|
-
continue;
|
|
1155
|
-
return undefined;
|
|
1156
|
-
}
|
|
1157
|
-
const moved = await lstat(stalePath);
|
|
1158
|
-
if (!sameLifecycleLockIdentity(moved, observedIdentity)) {
|
|
1159
|
-
// A contender replaced the path after our final identity check. Restore
|
|
1160
|
-
// the directory we moved and fail closed; never delete its live lock.
|
|
1161
|
-
await rename(stalePath, lockDir).catch(() => undefined);
|
|
1162
|
-
return undefined;
|
|
1163
|
-
}
|
|
1164
|
-
await rm(stalePath, { recursive: true, force: true });
|
|
1165
|
-
}
|
|
1166
|
-
return undefined;
|
|
1167
|
-
}
|
|
1168
|
-
async function assertLifecycleLockDirectory(path) {
|
|
1169
|
-
const details = await lstat(path);
|
|
1170
|
-
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
1171
|
-
throw new Error(`Lifecycle lock ${path} must be a real directory.`);
|
|
1172
|
-
}
|
|
1173
|
-
if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
|
|
1174
|
-
throw new Error(`Lifecycle lock ${path} is not owned by the current user.`);
|
|
1175
|
-
}
|
|
1176
|
-
return details;
|
|
1177
|
-
}
|
|
1178
|
-
function lifecycleLockIdentity(details) {
|
|
1179
|
-
return { dev: details.dev, ino: details.ino };
|
|
1180
|
-
}
|
|
1181
|
-
function sameLifecycleLockIdentity(details, identity) {
|
|
1182
|
-
return details.dev === identity.dev && details.ino === identity.ino;
|
|
1183
|
-
}
|
|
1184
|
-
async function sameLifecycleLockDirectory(path, identity) {
|
|
1185
|
-
try {
|
|
1186
|
-
const details = await assertLifecycleLockDirectory(path);
|
|
1187
|
-
return sameLifecycleLockIdentity(details, identity);
|
|
1188
|
-
}
|
|
1189
|
-
catch {
|
|
1190
|
-
return false;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
async function removeSameLifecycleLockDirectory(path, identity) {
|
|
1194
|
-
if (await sameLifecycleLockDirectory(path, identity)) {
|
|
1195
|
-
await rm(path, { recursive: true, force: true });
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
async function readLifecycleLockOwner(path) {
|
|
1199
|
-
try {
|
|
1200
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1201
|
-
const incarnation = processIncarnationFromJson(parsed.incarnation);
|
|
1202
|
-
return typeof parsed.pid === "number" && Number.isSafeInteger(parsed.pid) && parsed.pid > 0
|
|
1203
|
-
? {
|
|
1204
|
-
pid: parsed.pid,
|
|
1461
|
+
const incarnation = processIncarnationFromJson(record.incarnation);
|
|
1462
|
+
return {
|
|
1463
|
+
pid: record.pid,
|
|
1205
1464
|
...(incarnation === undefined ? {} : { incarnation }),
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1465
|
+
};
|
|
1466
|
+
},
|
|
1467
|
+
// Permanent pre-v0.9.0 compatibility: a skipped-version upgrade can
|
|
1468
|
+
// encounter crash debris without incarnation identity indefinitely.
|
|
1469
|
+
// All owner records written since v0.9.0 use the stronger shared schema.
|
|
1470
|
+
allowCurrentUserLegacyOwnerMode: true,
|
|
1471
|
+
isLegacyProcessAlive: isProcessAlive,
|
|
1472
|
+
invalidOwner: "ownerless",
|
|
1473
|
+
livenessError: () => "assume-live",
|
|
1474
|
+
...(options.beforeStaleLockRename === undefined
|
|
1475
|
+
? {}
|
|
1476
|
+
: { beforeStaleRename: options.beforeStaleLockRename }),
|
|
1477
|
+
staleRace: "return",
|
|
1478
|
+
stalePath: ({ now: staleAt, pid: stalePid, token }) => resolve(locksDir, `${target.label}.stale-${staleAt}-${stalePid}-${token}`),
|
|
1479
|
+
releasedPath: ({ now: releasedAt, pid: ownerPid, token }) => resolve(locksDir, `${target.label}.released-${releasedAt}-${ownerPid}-${token}`),
|
|
1480
|
+
abandonedPath: ({ now: abandonedAt, pid: ownerPid, token }) => resolve(locksDir, `${target.label}.abandoned-${abandonedAt}-${ownerPid}-${token}`),
|
|
1481
|
+
});
|
|
1482
|
+
return held === undefined ? undefined : () => held.release();
|
|
1212
1483
|
}
|
|
1213
1484
|
function processIsAlive(pid) {
|
|
1214
1485
|
try {
|