akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.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/CHANGELOG.md +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Platform notes:
|
|
13
13
|
* • The `bootstrap` / `bootout` / `enable` / `disable` subcommands require
|
|
14
|
-
* macOS 10.10 (Yosemite) or newer.
|
|
15
|
-
* are `launchctl load -w` / `unload -w`. We only target modern macOS.
|
|
14
|
+
* macOS 10.10 (Yosemite) or newer. We only target modern macOS.
|
|
16
15
|
* • `gui/<uid>` is the per-user GUI launchd domain — agents in this
|
|
17
|
-
* domain only run while the user is logged in
|
|
18
|
-
* the
|
|
19
|
-
*
|
|
16
|
+
* domain only run while the user is logged in. Tasks that need to run
|
|
17
|
+
* when the user is logged out should be installed as system Daemons,
|
|
18
|
+
* which is out of scope.
|
|
19
|
+
* • launchd strips the environment; the syncing shell's PATH goes into the
|
|
20
|
+
* plist's `EnvironmentVariables` so task bodies find the same binaries.
|
|
20
21
|
*
|
|
21
22
|
* Tests inject a fake exec + filesystem so the backend can be unit-tested
|
|
22
23
|
* without touching the host launchctl.
|
|
@@ -31,14 +32,11 @@ import { getTaskLogDir } from "../../core/paths.js";
|
|
|
31
32
|
import { warn } from "../../core/warn.js";
|
|
32
33
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
33
34
|
import { parseSchedule, translateToLaunchd } from "../schedule.js";
|
|
34
|
-
import {
|
|
35
|
+
import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
|
|
35
36
|
import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
36
37
|
import { escapeXml, nodeExec, nodeFs, runOrThrow } from "./exec-utils.js";
|
|
37
38
|
export const LAUNCHD_LABEL_PREFIX = "com.akm.task.";
|
|
38
|
-
const MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
39
|
-
const MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES = 4096;
|
|
40
39
|
const LAUNCHD_AKM_LABEL_RE = /^com\.akm\.task\.[A-Za-z0-9._-]{1,1024}$/u;
|
|
41
|
-
const LAUNCHD_SNAPSHOT = Symbol("akm-launchd-binding-snapshot");
|
|
42
40
|
export function LAUNCHD_BACKEND(options = {}) {
|
|
43
41
|
const exec = options.exec ?? defaultLaunchdExec();
|
|
44
42
|
const fsLike = options.fs ?? defaultLaunchdFs();
|
|
@@ -46,691 +44,106 @@ export function LAUNCHD_BACKEND(options = {}) {
|
|
|
46
44
|
const logDir = options.logDir ?? getTaskLogDir();
|
|
47
45
|
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
48
46
|
const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
|
|
47
|
+
const envPath = options.envPath === false ? undefined : typeof options.envPath === "string" ? options.envPath : process.env.PATH;
|
|
48
|
+
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext));
|
|
49
49
|
const plistPath = (nativeId) => path.join(agentsDir, `${LAUNCHD_LABEL_PREFIX}${nativeId}.plist`);
|
|
50
50
|
const label = (nativeId) => `${LAUNCHD_LABEL_PREFIX}${nativeId}`;
|
|
51
51
|
const target = (nativeId) => `gui/${exec.uid()}/${label(nativeId)}`;
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
const setEnableState = (nativeId, enabled) => {
|
|
53
|
+
const verb = enabled ? "enable" : "disable";
|
|
54
|
+
runOrThrow(exec, ["launchctl", verb, target(nativeId)], {
|
|
55
|
+
message: (result) => `launchctl ${verb} failed: ${result.stderr || result.stdout || "no output"}.`,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const bootout = (nativeId) => runOrThrow(exec, ["launchctl", "bootout", target(nativeId)], {
|
|
59
|
+
isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
|
|
60
|
+
message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
61
|
+
});
|
|
62
|
+
const xmlFor = (task, opts) => buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, envPath);
|
|
54
63
|
return {
|
|
55
64
|
name: "launchd",
|
|
56
|
-
install(task, opts
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
target,
|
|
67
|
-
setEnableState,
|
|
68
|
-
});
|
|
69
|
-
},
|
|
70
|
-
uninstall(nativeId, expected) {
|
|
71
|
-
const inspectionContext = { exec, fsLike, agentsDir, plistPath, target, label };
|
|
72
|
-
const initialNamespace = inspectStableLaunchdNamespace([nativeId], inspectionContext);
|
|
73
|
-
let initialArtifact;
|
|
74
|
-
if (expected) {
|
|
75
|
-
assertSchedulerExpectationIdentity({ ...expected, state: "present" });
|
|
76
|
-
initialArtifact = assertSchedulerNativeArtifactCardinality(initialNamespace.inspection.artifacts, nativeId, 1);
|
|
77
|
-
if (!initialArtifact) {
|
|
78
|
-
throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} disappeared during inspection.`, "INVALID_CONFIG_FILE");
|
|
79
|
-
}
|
|
80
|
-
assertSchedulerRemovalArtifact(initialArtifact.nativeId, expected, initialArtifact.invocation, initialArtifact.fingerprint);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
initialArtifact = assertLaunchdDirectRemovalOwner(initialNamespace.inspection.artifacts, nativeId);
|
|
84
|
-
}
|
|
85
|
-
let resolvedNativeId = initialArtifact?.nativeId ?? nativeId;
|
|
86
|
-
const file = plistPath(resolvedNativeId);
|
|
87
|
-
const snapshot = snapshotLaunchdBindings([resolvedNativeId], {
|
|
88
|
-
exec,
|
|
89
|
-
fsLike,
|
|
90
|
-
agentsDir,
|
|
91
|
-
plistPath,
|
|
92
|
-
target,
|
|
93
|
-
label,
|
|
94
|
-
});
|
|
95
|
-
const finalArtifact = expected
|
|
96
|
-
? assertSchedulerNativeArtifactCardinality(snapshot.artifacts, nativeId, 1)
|
|
97
|
-
: assertLaunchdDirectRemovalOwner(snapshot.artifacts, nativeId);
|
|
98
|
-
if (expected) {
|
|
99
|
-
if (!finalArtifact) {
|
|
100
|
-
throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} disappeared during snapshot.`, "INVALID_CONFIG_FILE");
|
|
101
|
-
}
|
|
102
|
-
assertSchedulerRemovalArtifact(finalArtifact.nativeId, expected, finalArtifact.invocation, finalArtifact.fingerprint);
|
|
103
|
-
}
|
|
104
|
-
if (!finalArtifact)
|
|
105
|
-
return;
|
|
106
|
-
resolvedNativeId = finalArtifact.nativeId;
|
|
65
|
+
install(task, opts) {
|
|
66
|
+
const xml = xmlFor(task, opts);
|
|
67
|
+
const nativeId = schedulerBindingNativeId(task);
|
|
68
|
+
const file = plistPath(nativeId);
|
|
69
|
+
fsLike.ensureDir(agentsDir);
|
|
70
|
+
// launchd refuses to start a job whose StandardOutPath directory is
|
|
71
|
+
// missing; create it before touching launchd state.
|
|
72
|
+
fsLike.ensureDir(logDir);
|
|
73
|
+
const tempFile = path.join(agentsDir, `.${nativeId}.${Date.now()}.tmp`);
|
|
74
|
+
fsLike.writeFile(tempFile, xml);
|
|
107
75
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
76
|
+
bootout(nativeId);
|
|
77
|
+
fsLike.replaceFile(tempFile, file);
|
|
78
|
+
// A disable override survives bootout and plist replacement. Clear it
|
|
79
|
+
// before bootstrap, then apply the desired state after registration.
|
|
80
|
+
setEnableState(nativeId, true);
|
|
81
|
+
runOrThrow(exec, ["launchctl", "bootstrap", `gui/${exec.uid()}`, file], {
|
|
82
|
+
message: (r) => `launchctl bootstrap failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
83
|
+
hint: "Ensure `launchctl` is available; on macOS it is part of the base system.",
|
|
111
84
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (fsLike.exists(file))
|
|
115
|
-
fsLike.removeFile(file);
|
|
85
|
+
if (!task.enabled)
|
|
86
|
+
setEnableState(nativeId, false);
|
|
116
87
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
launchdRollbackExpectationForCurrent(resolvedNativeId, inspectionContext, initialArtifact),
|
|
121
|
-
];
|
|
122
|
-
restoreLaunchdBindings(snapshot, { exec, fsLike, agentsDir, plistPath, target, label, setEnableState }, rollbackExpected);
|
|
123
|
-
}
|
|
124
|
-
catch (rollbackError) {
|
|
125
|
-
throw new AggregateError([primaryError, rollbackError], `${errorMessage(primaryError)}; rollback for launchd removal ${JSON.stringify(nativeId)} was incomplete.`);
|
|
126
|
-
}
|
|
127
|
-
throw primaryError;
|
|
88
|
+
finally {
|
|
89
|
+
if (fsLike.exists(tempFile))
|
|
90
|
+
fsLike.removeFile(tempFile);
|
|
128
91
|
}
|
|
129
92
|
},
|
|
93
|
+
uninstall(nativeId) {
|
|
94
|
+
bootout(nativeId);
|
|
95
|
+
// launchctl disable overrides persist after the plist is removed.
|
|
96
|
+
setEnableState(nativeId, true);
|
|
97
|
+
const file = plistPath(nativeId);
|
|
98
|
+
if (fsLike.exists(file))
|
|
99
|
+
fsLike.removeFile(file);
|
|
100
|
+
},
|
|
130
101
|
setEnabled(nativeId, enabled) {
|
|
131
102
|
setEnableState(nativeId, enabled);
|
|
132
103
|
},
|
|
133
104
|
list() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
105
|
+
const domain = exec.run(["launchctl", "print", `gui/${exec.uid()}`]);
|
|
106
|
+
if (domain.status !== 0) {
|
|
107
|
+
throw new ConfigError(`launchctl failed to enumerate the loaded user domain: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
108
|
+
}
|
|
109
|
+
const loadedLabels = parseLaunchdLoadedLabels(domain.stdout);
|
|
110
|
+
const disabledLabels = readDisabledLabels(exec);
|
|
111
|
+
const rows = [];
|
|
112
|
+
if (!fsLike.exists(agentsDir))
|
|
113
|
+
return rows;
|
|
114
|
+
for (const file of fsLike.list(agentsDir).sort()) {
|
|
115
|
+
if (!file.startsWith(LAUNCHD_LABEL_PREFIX) || !file.endsWith(".plist"))
|
|
116
|
+
continue;
|
|
117
|
+
const nativeId = file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length);
|
|
118
|
+
const raw = fsLike.readFile(plistPath(nativeId));
|
|
119
|
+
const parsed = extractPlistInvocation(raw);
|
|
120
|
+
if (!parsed)
|
|
143
121
|
continue;
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
id:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
122
|
+
const enabled = !disabledLabels.has(label(nativeId));
|
|
123
|
+
const loaded = loadedLabels.has(label(nativeId));
|
|
124
|
+
rows.push({
|
|
125
|
+
id: schedulerLogicalBindingId(nativeId, parsed.invocation),
|
|
126
|
+
nativeId,
|
|
127
|
+
enabled: enabled && loaded,
|
|
128
|
+
// An unloaded service or a disable override is drift: the signature
|
|
129
|
+
// carries both, so sync bootstraps/enables it again.
|
|
130
|
+
signature: launchdFingerprint(raw, enabled, loaded),
|
|
131
|
+
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
132
|
+
binding: parsed.binding,
|
|
133
|
+
contextPath: parsed.contextPath,
|
|
134
|
+
invocation: parsed.invocation,
|
|
135
|
+
});
|
|
154
136
|
}
|
|
155
|
-
return
|
|
156
|
-
},
|
|
157
|
-
listNativeArtifacts() {
|
|
158
|
-
return [...inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label }).artifacts];
|
|
159
|
-
},
|
|
160
|
-
inspectBindings() {
|
|
161
|
-
return inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label });
|
|
162
|
-
},
|
|
163
|
-
snapshotBindings(ids) {
|
|
164
|
-
return snapshotLaunchdBindings(ids, { exec, fsLike, agentsDir, plistPath, target, label });
|
|
165
|
-
},
|
|
166
|
-
restoreBindings(snapshot, expectedCurrent) {
|
|
167
|
-
restoreLaunchdBindings(snapshot, { exec, fsLike, agentsDir, plistPath, target, label, setEnableState }, expectedCurrent);
|
|
137
|
+
return rows;
|
|
168
138
|
},
|
|
169
139
|
expectedSignature(task, opts) {
|
|
170
|
-
return launchdFingerprint(
|
|
140
|
+
return launchdFingerprint(xmlFor(task, opts), task.enabled, true);
|
|
171
141
|
},
|
|
172
142
|
};
|
|
173
143
|
}
|
|
174
|
-
function installLaunchdBinding(task, opts, expected, context) {
|
|
175
|
-
const { exec, fsLike, agentsDir, logDir, akmArgv, defaultContextPath, plistPath, label, target, setEnableState } = context;
|
|
176
|
-
if (expected)
|
|
177
|
-
assertSchedulerExpectationIdentity(expected, task);
|
|
178
|
-
// Capture PATH at install time so launchd (which strips the environment
|
|
179
|
-
// aggressively) can find the same binaries the user sees interactively.
|
|
180
|
-
const xml = buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
|
|
181
|
-
const nativeId = schedulerBindingNativeId(task);
|
|
182
|
-
const file = plistPath(nativeId);
|
|
183
|
-
fsLike.ensureDir(agentsDir);
|
|
184
|
-
// launchd refuses to start a job when StandardOutPath/StandardErrorPath
|
|
185
|
-
// points at a non-existent directory; create it before reading or mutating
|
|
186
|
-
// launchd state so a local preparation failure has no scheduler side effect.
|
|
187
|
-
fsLike.ensureDir(logDir);
|
|
188
|
-
const inspectionContext = { exec, fsLike, agentsDir, plistPath, target, label };
|
|
189
|
-
const initialNamespace = inspectStableLaunchdNamespace([nativeId], inspectionContext);
|
|
190
|
-
const initialArtifact = expected
|
|
191
|
-
? assertSchedulerNativeArtifactCardinality(initialNamespace.inspection.artifacts, nativeId, expected.state === "absent" ? 0 : 1)
|
|
192
|
-
: assertLaunchdDirectMutationOwner(initialNamespace.inspection.artifacts, nativeId, task);
|
|
193
|
-
if (expected)
|
|
194
|
-
assertSchedulerMutationArtifact(initialArtifact, expected);
|
|
195
|
-
const priorEntry = initialArtifact
|
|
196
|
-
? initialNamespace.entries.find((entry) => entry.nativeId === initialArtifact.nativeId)
|
|
197
|
-
: undefined;
|
|
198
|
-
const previousPlist = priorEntry?.plist;
|
|
199
|
-
const previousEnabled = priorEntry?.enabled ?? true;
|
|
200
|
-
const tempFile = path.join(agentsDir, `.${nativeId}.${Date.now()}.tmp`);
|
|
201
|
-
fsLike.writeFile(tempFile, xml);
|
|
202
|
-
let bootoutCompleted = false;
|
|
203
|
-
let previousWasLoaded = false;
|
|
204
|
-
let fileReplaced = false;
|
|
205
|
-
let enableStateTouched = false;
|
|
206
|
-
try {
|
|
207
|
-
if (expected) {
|
|
208
|
-
const finalInspection = inspectStableLaunchdNamespace([nativeId], inspectionContext).inspection;
|
|
209
|
-
const finalArtifact = assertSchedulerNativeArtifactCardinality(finalInspection.artifacts, nativeId, expected.state === "absent" ? 0 : 1);
|
|
210
|
-
assertSchedulerMutationArtifact(finalArtifact, expected);
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
const finalArtifact = assertLaunchdDirectMutationOwner(inspectStableLaunchdNamespace([nativeId], inspectionContext).inspection.artifacts, nativeId, task);
|
|
214
|
-
if (finalArtifact?.nativeId !== initialArtifact?.nativeId ||
|
|
215
|
-
finalArtifact?.fingerprint !== initialArtifact?.fingerprint) {
|
|
216
|
-
throw new ConfigError(`launchd task ${JSON.stringify(nativeId)} changed while it was being prepared; refusing to replace an unverified owner.`, "INVALID_CONFIG_FILE");
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const bootout = runOrThrow(exec, ["launchctl", "bootout", target(nativeId)], {
|
|
220
|
-
isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
|
|
221
|
-
message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
222
|
-
});
|
|
223
|
-
bootoutCompleted = true;
|
|
224
|
-
previousWasLoaded = previousPlist !== undefined && bootout.status === 0;
|
|
225
|
-
fsLike.replaceFile(tempFile, file);
|
|
226
|
-
fileReplaced = true;
|
|
227
|
-
// A disable override survives bootout and plist replacement. Clear it
|
|
228
|
-
// before bootstrap, then apply the desired state after registration.
|
|
229
|
-
enableStateTouched = true;
|
|
230
|
-
setEnableState(nativeId, true);
|
|
231
|
-
runOrThrow(exec, ["launchctl", "bootstrap", `gui/${exec.uid()}`, file], {
|
|
232
|
-
message: (r) => `launchctl bootstrap failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
233
|
-
hint: "Ensure `launchctl` is available; on macOS it is part of the base system.",
|
|
234
|
-
});
|
|
235
|
-
if (!task.enabled)
|
|
236
|
-
setEnableState(nativeId, false);
|
|
237
|
-
}
|
|
238
|
-
catch (err) {
|
|
239
|
-
if (!bootoutCompleted)
|
|
240
|
-
throw err;
|
|
241
|
-
const rollbackErrors = [];
|
|
242
|
-
let priorFileRestored = !fileReplaced;
|
|
243
|
-
if (fileReplaced) {
|
|
244
|
-
let replacementUnloaded = false;
|
|
245
|
-
try {
|
|
246
|
-
const rollbackBootout = exec.run(["launchctl", "bootout", target(nativeId)]);
|
|
247
|
-
replacementUnloaded = rollbackBootout.status === 0 || isServiceNotFoundResult(rollbackBootout);
|
|
248
|
-
if (!replacementUnloaded) {
|
|
249
|
-
rollbackErrors.push(new ConfigError(`launchctl bootout during rollback failed: ${rollbackBootout.stderr || rollbackBootout.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch (rollbackError) {
|
|
253
|
-
rollbackErrors.push(rollbackError);
|
|
254
|
-
}
|
|
255
|
-
if (replacementUnloaded) {
|
|
256
|
-
try {
|
|
257
|
-
if (previousPlist === undefined) {
|
|
258
|
-
if (fsLike.exists(file))
|
|
259
|
-
fsLike.removeFile(file);
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
fsLike.writeFile(tempFile, previousPlist);
|
|
263
|
-
fsLike.replaceFile(tempFile, file);
|
|
264
|
-
}
|
|
265
|
-
priorFileRestored = true;
|
|
266
|
-
}
|
|
267
|
-
catch (rollbackError) {
|
|
268
|
-
rollbackErrors.push(rollbackError);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
restoreLaunchdInstallState({
|
|
273
|
-
exec,
|
|
274
|
-
nativeId,
|
|
275
|
-
file,
|
|
276
|
-
previousPlist,
|
|
277
|
-
previousEnabled,
|
|
278
|
-
previousWasLoaded,
|
|
279
|
-
priorFileRestored,
|
|
280
|
-
enableStateTouched,
|
|
281
|
-
setEnableState,
|
|
282
|
-
rollbackErrors,
|
|
283
|
-
});
|
|
284
|
-
if (rollbackErrors.length > 0) {
|
|
285
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
286
|
-
throw new AggregateError([err, ...rollbackErrors], `${message}; rollback for launchd task "${task.id}" was incomplete.`);
|
|
287
|
-
}
|
|
288
|
-
throw err;
|
|
289
|
-
}
|
|
290
|
-
finally {
|
|
291
|
-
if (fsLike.exists(tempFile))
|
|
292
|
-
fsLike.removeFile(tempFile);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
function restoreLaunchdInstallState(input) {
|
|
296
|
-
const { exec, nativeId, file, previousPlist, previousEnabled, previousWasLoaded, priorFileRestored, enableStateTouched, setEnableState, rollbackErrors, } = input;
|
|
297
|
-
if (previousPlist !== undefined && previousWasLoaded && priorFileRestored) {
|
|
298
|
-
try {
|
|
299
|
-
setEnableState(nativeId, true);
|
|
300
|
-
const restore = exec.run(["launchctl", "bootstrap", `gui/${exec.uid()}`, file]);
|
|
301
|
-
if (restore.status !== 0) {
|
|
302
|
-
rollbackErrors.push(new ConfigError(`launchctl bootstrap during rollback failed: ${restore.stderr || restore.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
catch (rollbackError) {
|
|
306
|
-
rollbackErrors.push(rollbackError);
|
|
307
|
-
}
|
|
308
|
-
if (!previousEnabled) {
|
|
309
|
-
try {
|
|
310
|
-
setEnableState(nativeId, false);
|
|
311
|
-
}
|
|
312
|
-
catch (rollbackError) {
|
|
313
|
-
rollbackErrors.push(rollbackError);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else if (enableStateTouched) {
|
|
318
|
-
try {
|
|
319
|
-
setEnableState(nativeId, previousPlist === undefined || previousEnabled);
|
|
320
|
-
}
|
|
321
|
-
catch (rollbackError) {
|
|
322
|
-
rollbackErrors.push(rollbackError);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function snapshotLaunchdBindings(ids, context) {
|
|
327
|
-
const requestedKeys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
328
|
-
const namespace = inspectStableLaunchdNamespace(ids, context);
|
|
329
|
-
const matchingEntries = namespace.entries.filter((entry) => requestedKeys.has(schedulerNativeArtifactKey(entry.nativeId)));
|
|
330
|
-
const matchedKeys = new Set(matchingEntries.map((entry) => schedulerNativeArtifactKey(entry.nativeId)));
|
|
331
|
-
const entries = [
|
|
332
|
-
...matchingEntries.map((entry) => Object.freeze({
|
|
333
|
-
id: entry.nativeId,
|
|
334
|
-
...(entry.plist !== undefined ? { plist: entry.plist } : {}),
|
|
335
|
-
loaded: entry.loaded,
|
|
336
|
-
enabled: entry.enabled,
|
|
337
|
-
})),
|
|
338
|
-
...ids
|
|
339
|
-
.filter((id) => !matchedKeys.has(schedulerNativeArtifactKey(id)))
|
|
340
|
-
.map((id) => Object.freeze({ id, loaded: false, enabled: true })),
|
|
341
|
-
];
|
|
342
|
-
const artifacts = matchingEntries.map((entry) => entry.artifact);
|
|
343
|
-
return Object.freeze({
|
|
344
|
-
kind: LAUNCHD_SNAPSHOT,
|
|
345
|
-
nativeIds: Object.freeze([...ids]),
|
|
346
|
-
artifacts: Object.freeze(artifacts),
|
|
347
|
-
entries: Object.freeze(entries),
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
function restoreLaunchdBindings(snapshot, context, expectedCurrent) {
|
|
351
|
-
if (!isLaunchdBindingSnapshot(snapshot)) {
|
|
352
|
-
throw new ConfigError("Invalid launchd scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
353
|
-
}
|
|
354
|
-
const preflightErrors = [];
|
|
355
|
-
const duplicateSnapshotKeys = new Set();
|
|
356
|
-
const requestedIdsByKey = new Map();
|
|
357
|
-
for (const nativeId of snapshot.nativeIds) {
|
|
358
|
-
const key = schedulerNativeArtifactKey(nativeId);
|
|
359
|
-
const priorId = requestedIdsByKey.get(key);
|
|
360
|
-
if (priorId !== undefined) {
|
|
361
|
-
duplicateSnapshotKeys.add(key);
|
|
362
|
-
preflightErrors.push(new ConfigError(`Launchd scheduler snapshot requests normalized duplicate artifacts ${JSON.stringify(priorId)} and ${JSON.stringify(nativeId)}; refusing restore.`, "INVALID_CONFIG_FILE"));
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
requestedIdsByKey.set(key, nativeId);
|
|
366
|
-
}
|
|
367
|
-
const snapshotEntryIdsByKey = new Map();
|
|
368
|
-
for (const entry of snapshot.entries) {
|
|
369
|
-
const key = schedulerNativeArtifactKey(entry.id);
|
|
370
|
-
const priorId = snapshotEntryIdsByKey.get(key);
|
|
371
|
-
if (priorId !== undefined) {
|
|
372
|
-
if (duplicateSnapshotKeys.has(key))
|
|
373
|
-
continue;
|
|
374
|
-
duplicateSnapshotKeys.add(key);
|
|
375
|
-
preflightErrors.push(new ConfigError(`Launchd scheduler snapshot contains normalized duplicate artifacts ${JSON.stringify(priorId)} and ${JSON.stringify(entry.id)}; refusing restore.`, "INVALID_CONFIG_FILE"));
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
snapshotEntryIdsByKey.set(key, entry.id);
|
|
379
|
-
}
|
|
380
|
-
let rollbackInventory;
|
|
381
|
-
try {
|
|
382
|
-
rollbackInventory = inspectStableLaunchdNamespace(snapshot.nativeIds, context).inspection;
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
preflightErrors.push(error);
|
|
386
|
-
}
|
|
387
|
-
if (preflightErrors.length > 0) {
|
|
388
|
-
throw new AggregateError(preflightErrors, `Failed to preflight ${preflightErrors.length} launchd scheduler restore operation(s).`);
|
|
389
|
-
}
|
|
390
|
-
const expectationsById = new Map();
|
|
391
|
-
const expectationIdsByKey = new Map();
|
|
392
|
-
if (expectedCurrent) {
|
|
393
|
-
for (const expected of expectedCurrent) {
|
|
394
|
-
const key = schedulerNativeArtifactKey(expected.nativeId);
|
|
395
|
-
const priorExact = expectationsById.get(expected.nativeId);
|
|
396
|
-
const priorKeyId = expectationIdsByKey.get(key);
|
|
397
|
-
if (priorExact !== undefined || priorKeyId !== undefined) {
|
|
398
|
-
preflightErrors.push(new ConfigError(`Launchd rollback expectations contain duplicate normalized artifact ${JSON.stringify(expected.nativeId)}.`, "INVALID_CONFIG_FILE"));
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
expectationsById.set(expected.nativeId, expected);
|
|
402
|
-
expectationIdsByKey.set(key, expected.nativeId);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
const plannedEntries = [];
|
|
406
|
-
const coveredExpectationIds = new Set();
|
|
407
|
-
if (rollbackInventory) {
|
|
408
|
-
for (const entry of snapshot.entries) {
|
|
409
|
-
if (duplicateSnapshotKeys.has(schedulerNativeArtifactKey(entry.id)))
|
|
410
|
-
continue;
|
|
411
|
-
try {
|
|
412
|
-
let current;
|
|
413
|
-
if (expectedCurrent) {
|
|
414
|
-
const expected = expectationsById.get(entry.id);
|
|
415
|
-
if (!expected) {
|
|
416
|
-
throw new ConfigError(`Missing launchd rollback expectation for ${JSON.stringify(entry.id)}.`, "INVALID_CONFIG_FILE");
|
|
417
|
-
}
|
|
418
|
-
coveredExpectationIds.add(entry.id);
|
|
419
|
-
current = assertSchedulerRollbackArtifactCardinality(rollbackInventory.artifacts, expected);
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
const currentCount = rollbackInventory.artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === schedulerNativeArtifactKey(entry.id)).length;
|
|
423
|
-
current = assertSchedulerNativeArtifactCardinality(rollbackInventory.artifacts, entry.id, currentCount === 0 ? 0 : 1);
|
|
424
|
-
if (current !== undefined && current.nativeId !== entry.id) {
|
|
425
|
-
throw new ConfigError(`Launchd scheduler artifact ${JSON.stringify(entry.id)} changed to case- or suffix-equivalent spelling ${JSON.stringify(current.nativeId)} before restore.`, "INVALID_CONFIG_FILE");
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
if (entry.loaded && entry.plist === undefined) {
|
|
429
|
-
assertUnrestorableLaunchdSnapshotUnchanged(entry, rollbackInventory.artifacts);
|
|
430
|
-
}
|
|
431
|
-
plannedEntries.push(Object.freeze({
|
|
432
|
-
entry,
|
|
433
|
-
immediateExpectation: launchdRollbackExpectationFromArtifact(entry.id, current),
|
|
434
|
-
noOp: entry.loaded && entry.plist === undefined,
|
|
435
|
-
}));
|
|
436
|
-
}
|
|
437
|
-
catch (error) {
|
|
438
|
-
preflightErrors.push(error);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (expectedCurrent) {
|
|
443
|
-
for (const expected of expectedCurrent) {
|
|
444
|
-
if (!coveredExpectationIds.has(expected.nativeId)) {
|
|
445
|
-
preflightErrors.push(new ConfigError(`Unexpected launchd rollback expectation for ${JSON.stringify(expected.nativeId)} is not covered by the snapshot.`, "INVALID_CONFIG_FILE"));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
if (preflightErrors.length > 0) {
|
|
450
|
-
throw new AggregateError(preflightErrors, `Failed to preflight ${preflightErrors.length} launchd scheduler restore operation(s).`);
|
|
451
|
-
}
|
|
452
|
-
const errors = [];
|
|
453
|
-
for (const planned of plannedEntries) {
|
|
454
|
-
if (planned.noOp)
|
|
455
|
-
continue;
|
|
456
|
-
try {
|
|
457
|
-
const immediateInventory = inspectStableLaunchdNamespace(snapshot.nativeIds, context).inspection;
|
|
458
|
-
assertSchedulerRollbackArtifactCardinality(immediateInventory.artifacts, planned.immediateExpectation);
|
|
459
|
-
}
|
|
460
|
-
catch (error) {
|
|
461
|
-
errors.push(error);
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
restoreLaunchdBindingEntry(planned.entry, context, errors);
|
|
465
|
-
}
|
|
466
|
-
if (errors.length > 0) {
|
|
467
|
-
throw new AggregateError(errors, `Failed to completely restore ${errors.length} launchd scheduler operation(s).`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
function launchdRollbackExpectationFromArtifact(nativeId, current) {
|
|
471
|
-
if (current === undefined) {
|
|
472
|
-
return Object.freeze({ nativeId, allowed: Object.freeze([{ state: "absent" }]) });
|
|
473
|
-
}
|
|
474
|
-
if (current.nativeId !== nativeId || current.fingerprint === undefined) {
|
|
475
|
-
throw new ConfigError(`Launchd scheduler artifact ${JSON.stringify(nativeId)} has no exact restorable fingerprint.`, "INVALID_CONFIG_FILE");
|
|
476
|
-
}
|
|
477
|
-
return Object.freeze({
|
|
478
|
-
nativeId,
|
|
479
|
-
allowed: Object.freeze([
|
|
480
|
-
Object.freeze({
|
|
481
|
-
state: "present",
|
|
482
|
-
...(current.bindingId !== undefined ? { bindingId: current.bindingId } : {}),
|
|
483
|
-
...(current.invocation !== undefined ? { invocation: current.invocation } : {}),
|
|
484
|
-
fingerprint: current.fingerprint,
|
|
485
|
-
}),
|
|
486
|
-
]),
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
function assertUnrestorableLaunchdSnapshotUnchanged(entry, currentArtifacts) {
|
|
490
|
-
const current = assertSchedulerNativeArtifactCardinality(currentArtifacts, entry.id, 1);
|
|
491
|
-
const expectedFingerprint = launchdMissingPlistArtifact(entry.id, entry.enabled, entry.loaded).fingerprint;
|
|
492
|
-
if (current?.nativeId !== entry.id ||
|
|
493
|
-
current.invocation !== undefined ||
|
|
494
|
-
current.bindingId !== undefined ||
|
|
495
|
-
current.fingerprint !== expectedFingerprint) {
|
|
496
|
-
throw new ConfigError(`Loaded launchd artifact ${JSON.stringify(entry.id)} has no restorable plist and changed after its snapshot; refusing mutation.`, "INVALID_CONFIG_FILE");
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Enumerate the current launchd AKM service namespace (loaded domain members,
|
|
501
|
-
* print-disabled labels, and on-disk plists) in one pass.
|
|
502
|
-
*
|
|
503
|
-
* This used to run the enumeration twice and fail closed if the two passes
|
|
504
|
-
* disagreed, guarding against launchd state mutating between the two
|
|
505
|
-
* `launchctl` shell-outs (milliseconds apart, in-process). The only actor
|
|
506
|
-
* that could cause that is another concurrent akm process or the user
|
|
507
|
-
* running launchctl by hand at that exact instant; a subsequent `task sync`
|
|
508
|
-
* (idempotent by design) reconciles any such drift on its own, so the
|
|
509
|
-
* two-pass fencing added a TOCTOU-shaped guard around a race with a working
|
|
510
|
-
* self-heal already in place, not a way to avoid one.
|
|
511
|
-
*/
|
|
512
|
-
function inspectStableLaunchdNamespace(seedIds, context) {
|
|
513
|
-
const domain = context.exec.run(["launchctl", "print", `gui/${context.exec.uid()}`]);
|
|
514
|
-
if (domain.status !== 0) {
|
|
515
|
-
throw new ConfigError(`launchctl failed to enumerate the loaded user domain during scheduler state inspection: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
516
|
-
}
|
|
517
|
-
const loadedLabels = parseLaunchdLoadedLabels(domain.stdout);
|
|
518
|
-
const disabledLabels = readDisabledLabels(context.exec);
|
|
519
|
-
const akmDisabledLabels = [...disabledLabels].filter((label) => label.startsWith(LAUNCHD_LABEL_PREFIX)).sort();
|
|
520
|
-
const plistEntries = [];
|
|
521
|
-
if (context.fsLike.exists(context.agentsDir)) {
|
|
522
|
-
for (const file of context.fsLike.list(context.agentsDir).sort()) {
|
|
523
|
-
if (!file.startsWith(LAUNCHD_LABEL_PREFIX) || !file.endsWith(".plist"))
|
|
524
|
-
continue;
|
|
525
|
-
const nativeId = file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length);
|
|
526
|
-
plistEntries.push(Object.freeze([nativeId, context.fsLike.readFile(context.plistPath(nativeId))]));
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
const ids = new Set(seedIds);
|
|
530
|
-
for (const [nativeId] of plistEntries)
|
|
531
|
-
ids.add(nativeId);
|
|
532
|
-
for (const serviceLabel of loadedLabels)
|
|
533
|
-
ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
|
|
534
|
-
for (const serviceLabel of akmDisabledLabels)
|
|
535
|
-
ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
|
|
536
|
-
const plistByNativeId = new Map(plistEntries);
|
|
537
|
-
const entries = [];
|
|
538
|
-
for (const nativeId of [...ids].sort()) {
|
|
539
|
-
const serviceLabel = context.label(nativeId);
|
|
540
|
-
const plist = plistByNativeId.get(nativeId);
|
|
541
|
-
const enabled = !disabledLabels.has(serviceLabel);
|
|
542
|
-
const loaded = loadedLabels.has(serviceLabel);
|
|
543
|
-
if (plist === undefined && enabled && !loaded)
|
|
544
|
-
continue;
|
|
545
|
-
const artifact = plist === undefined
|
|
546
|
-
? launchdMissingPlistArtifact(nativeId, enabled, loaded)
|
|
547
|
-
: launchdArtifact(nativeId, plist, enabled, loaded);
|
|
548
|
-
entries.push(Object.freeze({ nativeId, ...(plist !== undefined ? { plist } : {}), enabled, loaded, artifact }));
|
|
549
|
-
}
|
|
550
|
-
const artifacts = entries.map((entry) => entry.artifact);
|
|
551
|
-
const installed = [];
|
|
552
|
-
for (const entry of entries) {
|
|
553
|
-
if (entry.plist === undefined)
|
|
554
|
-
continue;
|
|
555
|
-
const parsed = extractPlistInvocation(entry.plist);
|
|
556
|
-
if (!parsed)
|
|
557
|
-
continue;
|
|
558
|
-
installed.push(withInstalledInvocation({
|
|
559
|
-
id: schedulerLogicalBindingId(entry.nativeId, parsed.invocation),
|
|
560
|
-
enabled: entry.enabled && entry.loaded,
|
|
561
|
-
...(entry.loaded ? { signature: entry.artifact.fingerprint } : {}),
|
|
562
|
-
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
563
|
-
binding: parsed.binding,
|
|
564
|
-
contextPath: parsed.contextPath,
|
|
565
|
-
}, parsed.invocation, entry.nativeId));
|
|
566
|
-
}
|
|
567
|
-
return Object.freeze({
|
|
568
|
-
inspection: Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) }),
|
|
569
|
-
entries: Object.freeze(entries),
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
function restoreLaunchdBindingEntry(entry, context, errors) {
|
|
573
|
-
try {
|
|
574
|
-
runOrThrow(context.exec, ["launchctl", "bootout", context.target(entry.id)], {
|
|
575
|
-
isOk: (result) => result.status === 0 || isServiceNotFoundResult(result),
|
|
576
|
-
message: (result) => `launchctl bootout failed while restoring task "${entry.id}": ${result.stderr || result.stdout || "no output"}.`,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
catch (error) {
|
|
580
|
-
errors.push(error);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const file = context.plistPath(entry.id);
|
|
584
|
-
let priorFileRestored = false;
|
|
585
|
-
try {
|
|
586
|
-
if (entry.plist === undefined) {
|
|
587
|
-
if (context.fsLike.exists(file))
|
|
588
|
-
context.fsLike.removeFile(file);
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
context.fsLike.ensureDir(context.agentsDir);
|
|
592
|
-
context.fsLike.writeFile(file, entry.plist);
|
|
593
|
-
}
|
|
594
|
-
priorFileRestored = true;
|
|
595
|
-
}
|
|
596
|
-
catch (error) {
|
|
597
|
-
errors.push(error);
|
|
598
|
-
}
|
|
599
|
-
if (entry.loaded && entry.plist !== undefined && priorFileRestored) {
|
|
600
|
-
let overrideCleared = false;
|
|
601
|
-
try {
|
|
602
|
-
// A disable override survives bootout. Clear it before bootstrap or
|
|
603
|
-
// launchd can refuse to register the exact prior loaded definition.
|
|
604
|
-
context.setEnableState(entry.id, true);
|
|
605
|
-
overrideCleared = true;
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
errors.push(error);
|
|
609
|
-
}
|
|
610
|
-
if (overrideCleared) {
|
|
611
|
-
try {
|
|
612
|
-
runOrThrow(context.exec, ["launchctl", "bootstrap", `gui/${context.exec.uid()}`, file], {
|
|
613
|
-
message: (result) => `launchctl bootstrap failed while restoring task "${entry.id}": ${result.stderr || result.stdout || "no output"}.`,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
catch (error) {
|
|
617
|
-
errors.push(error);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
try {
|
|
622
|
-
// Apply the prior override last even when an earlier step failed; this is
|
|
623
|
-
// both the exact state ordering and the best available partial recovery.
|
|
624
|
-
context.setEnableState(entry.id, entry.enabled);
|
|
625
|
-
}
|
|
626
|
-
catch (error) {
|
|
627
|
-
errors.push(error);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function isLaunchdBindingSnapshot(value) {
|
|
631
|
-
return (typeof value === "object" &&
|
|
632
|
-
value !== null &&
|
|
633
|
-
value.kind === LAUNCHD_SNAPSHOT &&
|
|
634
|
-
Array.isArray(value.entries));
|
|
635
|
-
}
|
|
636
|
-
function inspectLaunchdState(context) {
|
|
637
|
-
return inspectStableLaunchdNamespace([], context).inspection;
|
|
638
|
-
}
|
|
639
|
-
function assertLaunchdDirectMutationOwner(artifacts, nativeId, task) {
|
|
640
|
-
const key = schedulerNativeArtifactKey(nativeId);
|
|
641
|
-
const count = artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
|
|
642
|
-
const artifact = assertSchedulerNativeArtifactCardinality(artifacts, nativeId, count === 0 ? 0 : 1);
|
|
643
|
-
if (artifact)
|
|
644
|
-
assertSchedulerNativeArtifactOwner(artifact.nativeId, task, artifact.invocation);
|
|
645
|
-
return artifact;
|
|
646
|
-
}
|
|
647
|
-
function assertLaunchdDirectRemovalOwner(artifacts, nativeId) {
|
|
648
|
-
const key = schedulerNativeArtifactKey(nativeId);
|
|
649
|
-
const count = artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
|
|
650
|
-
const artifact = assertSchedulerNativeArtifactCardinality(artifacts, nativeId, count === 0 ? 0 : 1);
|
|
651
|
-
if (!artifact)
|
|
652
|
-
return undefined;
|
|
653
|
-
if (artifact.nativeId !== nativeId || artifact.invocation === undefined) {
|
|
654
|
-
throw new ConfigError(`launchd artifact ${JSON.stringify(artifact.nativeId)} is not the exact proven requested owner ${JSON.stringify(nativeId)}; refusing direct removal.`, "INVALID_CONFIG_FILE");
|
|
655
|
-
}
|
|
656
|
-
return artifact;
|
|
657
|
-
}
|
|
658
|
-
function launchdRollbackExpectationForCurrent(nativeId, context, priorArtifact) {
|
|
659
|
-
const namespace = inspectStableLaunchdNamespace([nativeId], context);
|
|
660
|
-
const key = schedulerNativeArtifactKey(nativeId);
|
|
661
|
-
const count = namespace.inspection.artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
|
|
662
|
-
const artifact = assertSchedulerNativeArtifactCardinality(namespace.inspection.artifacts, nativeId, count === 0 ? 0 : 1);
|
|
663
|
-
if (!artifact) {
|
|
664
|
-
return Object.freeze({ nativeId, allowed: Object.freeze([{ state: "absent" }]) });
|
|
665
|
-
}
|
|
666
|
-
if (artifact.fingerprint === undefined ||
|
|
667
|
-
artifact.invocation === undefined ||
|
|
668
|
-
priorArtifact?.invocation === undefined ||
|
|
669
|
-
!sameStringArray(artifact.invocation, priorArtifact.invocation)) {
|
|
670
|
-
throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} has no exact transaction-owned rollback fingerprint.`, "INVALID_CONFIG_FILE");
|
|
671
|
-
}
|
|
672
|
-
return Object.freeze({
|
|
673
|
-
nativeId,
|
|
674
|
-
allowed: Object.freeze([
|
|
675
|
-
Object.freeze({
|
|
676
|
-
state: "present",
|
|
677
|
-
...(artifact.bindingId !== undefined ? { bindingId: artifact.bindingId } : {}),
|
|
678
|
-
invocation: Object.freeze([...artifact.invocation]),
|
|
679
|
-
fingerprint: artifact.fingerprint,
|
|
680
|
-
}),
|
|
681
|
-
]),
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
function sameStringArray(left, right) {
|
|
685
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
686
|
-
}
|
|
687
|
-
function errorMessage(error) {
|
|
688
|
-
return error instanceof Error ? error.message : String(error);
|
|
689
|
-
}
|
|
690
|
-
function launchdMissingPlistArtifact(nativeId, enabled, loaded) {
|
|
691
|
-
const artifact = { nativeId };
|
|
692
|
-
Object.defineProperty(artifact, "fingerprint", {
|
|
693
|
-
value: `launchd:missing-plist:enabled=${enabled}:loaded=${loaded}`,
|
|
694
|
-
});
|
|
695
|
-
return artifact;
|
|
696
|
-
}
|
|
697
|
-
function launchdArtifact(nativeId, raw, enabled, loaded) {
|
|
698
|
-
const parsed = extractPlistInvocation(raw);
|
|
699
|
-
const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
|
|
700
|
-
const artifact = parsed
|
|
701
|
-
? {
|
|
702
|
-
nativeId,
|
|
703
|
-
...(owner !== undefined ? { bindingId: owner } : {}),
|
|
704
|
-
invocation: Object.freeze([...parsed.invocation]),
|
|
705
|
-
}
|
|
706
|
-
: { nativeId };
|
|
707
|
-
if (enabled !== undefined && loaded !== undefined) {
|
|
708
|
-
Object.defineProperty(artifact, "fingerprint", { value: launchdFingerprint(raw, enabled, loaded) });
|
|
709
|
-
}
|
|
710
|
-
return artifact;
|
|
711
|
-
}
|
|
712
144
|
function launchdFingerprint(raw, enabled, loaded) {
|
|
713
145
|
const signed = raw.replace(/<!-- akm-enabled:(?:true|false) -->/, `<!-- akm-enabled:${enabled} -->`);
|
|
714
|
-
return `${
|
|
715
|
-
}
|
|
716
|
-
function withInstalledInvocation(ref, invocation, nativeId) {
|
|
717
|
-
Object.defineProperty(ref, "nativeId", { value: nativeId });
|
|
718
|
-
Object.defineProperty(ref, "invocation", { value: Object.freeze([...invocation]) });
|
|
719
|
-
return ref;
|
|
720
|
-
}
|
|
721
|
-
function launchdDefaultContextPath(options, scheduledContext) {
|
|
722
|
-
const pathEnv = options.envPath === false
|
|
723
|
-
? undefined
|
|
724
|
-
: typeof options.envPath === "string"
|
|
725
|
-
? options.envPath
|
|
726
|
-
: (process.env.PATH ?? "");
|
|
727
|
-
return schedulerContextPath(schedulerContextDescriptor(scheduledContext, pathEnv ?? ""));
|
|
728
|
-
}
|
|
729
|
-
function setLaunchdEnableState(exec, nativeTarget, enabled) {
|
|
730
|
-
const verb = enabled ? "enable" : "disable";
|
|
731
|
-
runOrThrow(exec, ["launchctl", verb, nativeTarget], {
|
|
732
|
-
message: (result) => `launchctl ${verb} failed: ${result.stderr || result.stdout || "no output"}.`,
|
|
733
|
-
});
|
|
146
|
+
return `${signed.replace(/\r\n/g, "\n").trim()}:loaded=${loaded}`;
|
|
734
147
|
}
|
|
735
148
|
export function extractPlistInvocation(xml) {
|
|
736
149
|
const block = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
@@ -748,25 +161,31 @@ function decodeXmlEntities(value) {
|
|
|
748
161
|
.replaceAll("&", "&");
|
|
749
162
|
}
|
|
750
163
|
// ── XML builder (exported for tests) ────────────────────────────────────────
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
164
|
+
function renderPlistEnvironment(envPath) {
|
|
165
|
+
if (!envPath)
|
|
166
|
+
return "";
|
|
167
|
+
return [
|
|
168
|
+
" <key>EnvironmentVariables</key>",
|
|
169
|
+
" <dict>",
|
|
170
|
+
" <key>PATH</key>",
|
|
171
|
+
` <string>${escapeXml(envPath)}</string>`,
|
|
172
|
+
" </dict>",
|
|
173
|
+
"",
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
export function buildPlistXml(task, akmArgv, logDir, contextPath, envPath) {
|
|
177
|
+
const trigger = translateToLaunchd(parseSchedule(task.cron, "launchd"));
|
|
754
178
|
const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
|
|
755
|
-
const
|
|
756
|
-
const programArgs = argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
179
|
+
const programArgs = invocation.argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
757
180
|
const nativeId = schedulerBindingNativeId(task);
|
|
758
181
|
const logPath = path.join(logDir, `${nativeId}.log`);
|
|
759
|
-
const triggerXml = renderLaunchdTrigger(trigger);
|
|
760
|
-
const executionEvidence = task.executionEvidenceDigest === undefined
|
|
761
|
-
? ""
|
|
762
|
-
: ` <!-- akm-workflow-evidence:${assertSchedulerExecutionEvidenceDigest(task.executionEvidenceDigest)} -->\n`;
|
|
763
182
|
const xml = launchdTemplate
|
|
764
|
-
.replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n
|
|
183
|
+
.replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n`)
|
|
765
184
|
.replace("{{LABEL}}", LAUNCHD_LABEL_PREFIX + escapeXml(nativeId))
|
|
766
185
|
.replace("{{PROGRAM_ARGS}}", programArgs)
|
|
767
186
|
.replaceAll("{{LOG_PATH}}", escapeXml(logPath))
|
|
768
|
-
.replace("{{ENV_VARS}}",
|
|
769
|
-
.replace("{{TRIGGER_XML}}",
|
|
187
|
+
.replace("{{ENV_VARS}}", renderPlistEnvironment(envPath))
|
|
188
|
+
.replace("{{TRIGGER_XML}}", renderLaunchdTrigger(trigger));
|
|
770
189
|
for (const char of xml) {
|
|
771
190
|
const code = char.codePointAt(0) ?? 0;
|
|
772
191
|
if (code <= 0x08 || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f)) {
|
|
@@ -778,22 +197,18 @@ export function buildPlistXml(task, akmArgv, logDir, contextPath, _target) {
|
|
|
778
197
|
function renderLaunchdTrigger(trigger) {
|
|
779
198
|
if (trigger.calendars !== undefined) {
|
|
780
199
|
const lines = [" <key>StartCalendarInterval</key>", " <array>"];
|
|
781
|
-
for (const calendar of trigger.calendars)
|
|
200
|
+
for (const calendar of trigger.calendars)
|
|
782
201
|
lines.push(...renderCalendar(calendar, " "));
|
|
783
|
-
}
|
|
784
202
|
lines.push(" </array>");
|
|
785
203
|
return lines.join("\n");
|
|
786
204
|
}
|
|
787
|
-
|
|
788
|
-
const lines = [" <key>StartCalendarInterval</key>", ...renderCalendar(cal, " ")];
|
|
789
|
-
return lines.join("\n");
|
|
205
|
+
return [" <key>StartCalendarInterval</key>", ...renderCalendar(trigger.calendar ?? {}, " ")].join("\n");
|
|
790
206
|
}
|
|
791
207
|
function renderCalendar(calendar, indent) {
|
|
792
208
|
const valueIndent = `${indent} `;
|
|
793
209
|
const lines = [`${indent}<dict>`];
|
|
794
|
-
if (calendar.Minute !== undefined)
|
|
210
|
+
if (calendar.Minute !== undefined)
|
|
795
211
|
lines.push(`${valueIndent}<key>Minute</key><integer>${calendar.Minute}</integer>`);
|
|
796
|
-
}
|
|
797
212
|
if (calendar.Hour !== undefined)
|
|
798
213
|
lines.push(`${valueIndent}<key>Hour</key><integer>${calendar.Hour}</integer>`);
|
|
799
214
|
if (calendar.Day !== undefined)
|
|
@@ -806,49 +221,25 @@ function renderCalendar(calendar, indent) {
|
|
|
806
221
|
lines.push(`${indent}</dict>`);
|
|
807
222
|
return lines;
|
|
808
223
|
}
|
|
809
|
-
function normalizeSignature(xml) {
|
|
810
|
-
return xml.replace(/\r\n/g, "\n").trim();
|
|
811
|
-
}
|
|
812
224
|
/**
|
|
813
225
|
* Collect the akm-owned service labels present in `launchctl` output.
|
|
814
226
|
*
|
|
815
|
-
* This reads the CURRENT USER'S OWN scheduler inventory
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
* `com.akm.task.` namespace and ignore everything else.
|
|
819
|
-
*
|
|
820
|
-
* It previously enforced a rigid grammar over the whole document and returned
|
|
821
|
-
* `undefined` — surfacing as a hard INVALID_CONFIG_FILE that refused to inspect
|
|
822
|
-
* scheduler state at all — if ANY line failed to match. Real
|
|
823
|
-
* `launchctl print gui/<uid>` output on macOS is far richer than that grammar
|
|
824
|
-
* allowed, so on a real Mac this rejected the user's own inventory and broke
|
|
825
|
-
* every launchd operation. Caught by the gated native-scheduler suite, which
|
|
826
|
-
* had never been dispatched before.
|
|
827
|
-
*
|
|
828
|
-
* The two bounds that remain are real resource bounds, not structural ones: a
|
|
829
|
-
* cap on how much output we will read, and a cap on how many distinct akm
|
|
830
|
-
* labels we will track. Exceeding either still returns `undefined`.
|
|
227
|
+
* This reads the CURRENT USER'S OWN scheduler inventory, so it is parsed
|
|
228
|
+
* permissively: scan for labels in our own `com.akm.task.` namespace and
|
|
229
|
+
* ignore everything else.
|
|
831
230
|
*/
|
|
832
231
|
export function parseLaunchdLoadedLabels(output) {
|
|
833
232
|
const labels = new Set();
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const label = match[0];
|
|
841
|
-
if (!LAUNCHD_AKM_LABEL_RE.test(label))
|
|
842
|
-
continue;
|
|
843
|
-
labels.add(label);
|
|
844
|
-
if (labels.size >= MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
|
|
845
|
-
break;
|
|
233
|
+
// `[^\s"{}=,()]` stops the token at whatever punctuation the surrounding
|
|
234
|
+
// launchctl syntax uses, so a label works whether it appears as a bare
|
|
235
|
+
// table cell, a quoted string, or a dictionary key.
|
|
236
|
+
for (const match of output.matchAll(/com\.akm\.task\.[^\s"{}=,()]+/gu)) {
|
|
237
|
+
if (LAUNCHD_AKM_LABEL_RE.test(match[0]))
|
|
238
|
+
labels.add(match[0]);
|
|
846
239
|
}
|
|
847
240
|
return labels;
|
|
848
241
|
}
|
|
849
|
-
|
|
850
|
-
return output.length > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES ? output.slice(0, MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES) : output;
|
|
851
|
-
}
|
|
242
|
+
/** `print-disabled` failing or looking unfamiliar degrades to "nothing is disabled". */
|
|
852
243
|
function readDisabledLabels(exec) {
|
|
853
244
|
try {
|
|
854
245
|
const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
|
|
@@ -856,35 +247,25 @@ function readDisabledLabels(exec) {
|
|
|
856
247
|
warn("[akm] launchctl print-disabled exited %d; assuming no akm task is disabled.", result.status);
|
|
857
248
|
return new Set();
|
|
858
249
|
}
|
|
859
|
-
|
|
250
|
+
const disabled = new Set();
|
|
251
|
+
const entryPattern = /"(com\.akm\.task\.[^"\r\n]+)"\s*=>\s*(true|false|enabled|disabled)/gu;
|
|
252
|
+
for (const match of result.stdout.matchAll(entryPattern)) {
|
|
253
|
+
const label = match[1];
|
|
254
|
+
if (LAUNCHD_AKM_LABEL_RE.test(label) && (match[2] === "true" || match[2] === "disabled"))
|
|
255
|
+
disabled.add(label);
|
|
256
|
+
}
|
|
257
|
+
return disabled;
|
|
860
258
|
}
|
|
861
259
|
catch (error) {
|
|
862
260
|
warn("[akm] launchctl print-disabled could not be run; assuming no akm task is disabled: %s", error instanceof Error ? error.message : String(error));
|
|
863
261
|
return new Set();
|
|
864
262
|
}
|
|
865
263
|
}
|
|
866
|
-
function parseDisabledLabels(output) {
|
|
867
|
-
const disabled = new Set();
|
|
868
|
-
const entryPattern = /"(com\.akm\.task\.[^"\r\n]+)"\s*=>\s*(true|false|enabled|disabled)/gu;
|
|
869
|
-
for (const match of boundLaunchdOutput(output).matchAll(entryPattern)) {
|
|
870
|
-
const label = match[1];
|
|
871
|
-
if (!LAUNCHD_AKM_LABEL_RE.test(label))
|
|
872
|
-
continue;
|
|
873
|
-
if (match[2] === "true" || match[2] === "disabled")
|
|
874
|
-
disabled.add(label);
|
|
875
|
-
if (disabled.size >= MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
return disabled;
|
|
879
|
-
}
|
|
880
264
|
function isServiceNotFoundResult(result) {
|
|
881
265
|
const output = `${result.stderr ?? ""}\n${result.stdout ?? ""}`;
|
|
882
266
|
return /could not find service\b|service\b.*\bnot found\b|\bno such process\b/i.test(output);
|
|
883
267
|
}
|
|
884
268
|
function defaultAgentsDir() {
|
|
885
|
-
// launchd's per-user LaunchAgents live under the user's home directory.
|
|
886
|
-
// If we can't determine HOME, refuse rather than silently producing a
|
|
887
|
-
// relative path that would write somewhere unexpected.
|
|
888
269
|
const home = os.homedir();
|
|
889
270
|
if (!home) {
|
|
890
271
|
throw new ConfigError("Cannot determine user home directory; launchd backend requires HOME to locate ~/Library/LaunchAgents.", "INVALID_CONFIG_FILE", "Set $HOME (POSIX) or the equivalent before running `akm task` on macOS.");
|
|
@@ -917,9 +298,8 @@ function defaultLaunchdFs() {
|
|
|
917
298
|
return fs.readdirSync(dir);
|
|
918
299
|
}
|
|
919
300
|
catch (error) {
|
|
920
|
-
// Genuinely absent is an empty listing; anything else (e.g. EACCES)
|
|
921
|
-
// not "no plists"
|
|
922
|
-
// think there is nothing to reconcile.
|
|
301
|
+
// Genuinely absent is an empty listing; anything else (e.g. EACCES)
|
|
302
|
+
// is not "no plists".
|
|
923
303
|
if (hasErrnoCode(error, "ENOENT"))
|
|
924
304
|
return [];
|
|
925
305
|
throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");
|