@moondeck/station 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/meta/_journal.json +223 -0
- package/db/schema.ts +850 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1453 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +891 -0
- package/lib/env.ts +195 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +115 -8
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2583 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2899 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6349 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5545 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
- package/bin/moondeck-station.mjs +0 -3
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS service install via launchd (ADR-028).
|
|
3
|
+
*
|
|
4
|
+
* Writes ~/Library/LaunchAgents/io.moondeck.station.plist (KeepAlive +
|
|
5
|
+
* RunAtLoad), logs to ~/Library/Logs/Moondeck/, loads with `launchctl bootstrap gui/<uid>`.
|
|
6
|
+
* The service's PATH is the operator's PATH at install time (so node/pnpm/
|
|
7
|
+
* docker/git resolve); .env.local is loaded by the host process itself.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { lstatSync, mkdirSync, readlinkSync, rmSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { basename, dirname, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { STATION } from "../../src/station-identity";
|
|
15
|
+
import type {
|
|
16
|
+
InstallContext,
|
|
17
|
+
Installer,
|
|
18
|
+
ServiceDefinitionState,
|
|
19
|
+
ServiceManagerState,
|
|
20
|
+
} from "./types";
|
|
21
|
+
import {
|
|
22
|
+
escapeXml,
|
|
23
|
+
executeStep,
|
|
24
|
+
printFile,
|
|
25
|
+
readPrivateJournal,
|
|
26
|
+
removeFileDurable,
|
|
27
|
+
restoreDefinitionDurable,
|
|
28
|
+
snapshotDefinition,
|
|
29
|
+
step,
|
|
30
|
+
stepFailure,
|
|
31
|
+
writeDefinitionDurable,
|
|
32
|
+
type DirectorySync,
|
|
33
|
+
type StepExecutor,
|
|
34
|
+
type StepResult,
|
|
35
|
+
} from "./util";
|
|
36
|
+
|
|
37
|
+
export const LABEL = STATION.launchdLabel;
|
|
38
|
+
|
|
39
|
+
/** The per-user launchd domain the LaunchAgent is bootstrapped into. */
|
|
40
|
+
export function launchdDomain(uid: number = process.getuid?.() ?? 0): string {
|
|
41
|
+
return `gui/${uid}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The job as `launchctl` addresses it — `gui/<uid>/io.moondeck.station`. The
|
|
45
|
+
* service's own activation respawn (`src/activation-respawn.ts`) targets the
|
|
46
|
+
* same string, so the label and domain cannot drift between the CLI and the
|
|
47
|
+
* running host. */
|
|
48
|
+
export function launchdJobTarget(uid: number = process.getuid?.() ?? 0): string {
|
|
49
|
+
return `${launchdDomain(uid)}/${LABEL}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const domain = launchdDomain();
|
|
53
|
+
const target = launchdJobTarget();
|
|
54
|
+
const logDir = resolve(homedir(), "Library", "Logs", STATION.logDirName);
|
|
55
|
+
|
|
56
|
+
export function plistPath(): string {
|
|
57
|
+
return resolve(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function darwinInstallJournalPath(definition = plistPath()): string {
|
|
61
|
+
return resolve(dirname(definition), `.${basename(definition)}.install-journal.json`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PlistInputs {
|
|
65
|
+
execPath: string;
|
|
66
|
+
args: string[];
|
|
67
|
+
cwd: string;
|
|
68
|
+
path: string;
|
|
69
|
+
home: string;
|
|
70
|
+
outLog: string;
|
|
71
|
+
errLog: string;
|
|
72
|
+
runAtLoad?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderPlist(p: PlistInputs): string {
|
|
76
|
+
const progArgs = [p.execPath, ...p.args]
|
|
77
|
+
.map((a) => ` <string>${escapeXml(a)}</string>`)
|
|
78
|
+
.join("\n");
|
|
79
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
80
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
81
|
+
<plist version="1.0">
|
|
82
|
+
<dict>
|
|
83
|
+
<key>Label</key>
|
|
84
|
+
<string>${LABEL}</string>
|
|
85
|
+
<key>ProgramArguments</key>
|
|
86
|
+
<array>
|
|
87
|
+
${progArgs}
|
|
88
|
+
</array>
|
|
89
|
+
<key>WorkingDirectory</key>
|
|
90
|
+
<string>${escapeXml(p.cwd)}</string>
|
|
91
|
+
<key>EnvironmentVariables</key>
|
|
92
|
+
<dict>
|
|
93
|
+
<key>PATH</key>
|
|
94
|
+
<string>${escapeXml(p.path)}</string>
|
|
95
|
+
<key>HOME</key>
|
|
96
|
+
<string>${escapeXml(p.home)}</string>
|
|
97
|
+
</dict>
|
|
98
|
+
<key>RunAtLoad</key>
|
|
99
|
+
<${p.runAtLoad === false ? "false" : "true"}/>
|
|
100
|
+
<key>KeepAlive</key>
|
|
101
|
+
<true/>
|
|
102
|
+
<key>ThrottleInterval</key>
|
|
103
|
+
<integer>10</integer>
|
|
104
|
+
<key>StandardOutPath</key>
|
|
105
|
+
<string>${escapeXml(p.outLog)}</string>
|
|
106
|
+
<key>StandardErrorPath</key>
|
|
107
|
+
<string>${escapeXml(p.errLog)}</string>
|
|
108
|
+
</dict>
|
|
109
|
+
</plist>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function inputs(ctx: InstallContext, logs = logDir): PlistInputs {
|
|
114
|
+
return {
|
|
115
|
+
execPath: ctx.execPath,
|
|
116
|
+
args: ctx.args,
|
|
117
|
+
cwd: ctx.cwd,
|
|
118
|
+
path: ctx.envPath ?? process.env.PATH ?? "",
|
|
119
|
+
home: homedir(),
|
|
120
|
+
outLog: resolve(logs, STATION.logFileName.replace(/\.log$/, ".out.log")),
|
|
121
|
+
errLog: resolve(logs, STATION.logFileName),
|
|
122
|
+
runAtLoad: ctx.startOnInstall !== false,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DarwinInstallDeps extends DarwinServiceDeps {
|
|
127
|
+
logDirectory?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function installDarwinService(
|
|
131
|
+
ctx: InstallContext,
|
|
132
|
+
deps: DarwinInstallDeps = {},
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
135
|
+
const logs = deps.logDirectory ?? logDir;
|
|
136
|
+
const execute = deps.execute ?? executeStep;
|
|
137
|
+
const delay = deps.delay ?? defaultDelay;
|
|
138
|
+
const plist = renderPlist(inputs(ctx, logs));
|
|
139
|
+
if (ctx.dryRun) {
|
|
140
|
+
printFile(definition, plist);
|
|
141
|
+
step(true, "mkdir", ["-p", logs]);
|
|
142
|
+
step(true, "launchctl", ["bootout", target]);
|
|
143
|
+
if (ctx.startOnInstall !== false) {
|
|
144
|
+
step(true, "launchctl", ["bootstrap", domain, definition]);
|
|
145
|
+
await enableDarwinService(true);
|
|
146
|
+
} else {
|
|
147
|
+
step(true, "launchctl", ["disable", target]);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
152
|
+
const installIntent = ctx.definitionInstallIntent;
|
|
153
|
+
assertDefinitionInstallIntent(installIntent);
|
|
154
|
+
await reconcileDarwinDefinitionInstall(
|
|
155
|
+
{ ...deps, definitionPath: definition, journalPath },
|
|
156
|
+
installIntent?.nonce,
|
|
157
|
+
);
|
|
158
|
+
mkdirSync(logs, { recursive: true });
|
|
159
|
+
mkdirSync(dirname(definition), { recursive: true });
|
|
160
|
+
const previous = snapshotDefinition(definition);
|
|
161
|
+
const prior = inspectLaunchdJob(execute);
|
|
162
|
+
if (!previous.exists && prior.loaded) {
|
|
163
|
+
throw new Error("launchd reports the host job loaded without a recoverable plist");
|
|
164
|
+
}
|
|
165
|
+
const journal: DarwinInstallJournal = {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
phase: "installed",
|
|
168
|
+
installIntent: installIntent ?? null,
|
|
169
|
+
prior: serializeDefinition(previous),
|
|
170
|
+
manager: { ...prior, enabled: inspectLaunchdEnabled(execute) },
|
|
171
|
+
};
|
|
172
|
+
writeDefinitionDurable(journalPath, `${JSON.stringify(journal)}\n`, 0o600);
|
|
173
|
+
try {
|
|
174
|
+
writeDefinitionDurable(
|
|
175
|
+
definition,
|
|
176
|
+
plist,
|
|
177
|
+
0o644,
|
|
178
|
+
deps.definitionDirectorySync,
|
|
179
|
+
);
|
|
180
|
+
await assertLaunchdUnloaded(false, execute, delay);
|
|
181
|
+
if (ctx.startOnInstall === false) {
|
|
182
|
+
await setDarwinServiceEnabled(false, false, { execute });
|
|
183
|
+
if (!installIntent) removeFileDurable(journalPath);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await enableDarwinService(false, { execute });
|
|
187
|
+
await bootstrapWithRetry(execute, delay, definition);
|
|
188
|
+
await kickstartDarwinAndVerify(execute, delay);
|
|
189
|
+
if (!installIntent) removeFileDurable(journalPath);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
try {
|
|
192
|
+
const rollbackDeps = {
|
|
193
|
+
...deps,
|
|
194
|
+
definitionPath: definition,
|
|
195
|
+
journalPath,
|
|
196
|
+
};
|
|
197
|
+
if (installIntent) {
|
|
198
|
+
await prepareDarwinDefinitionInstallRollback(
|
|
199
|
+
rollbackDeps,
|
|
200
|
+
installIntent.nonce,
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
await reconcileDarwinDefinitionInstall(rollbackDeps);
|
|
204
|
+
}
|
|
205
|
+
} catch (rollbackError) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`${errorMessage(error)}; service-definition rollback failed: ${errorMessage(rollbackError)}`,
|
|
208
|
+
{ cause: error },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `bootout` unloads the old job asynchronously, so an immediate `bootstrap`
|
|
217
|
+
* can race it ("Bootstrap failed: 5: Input/output error"). Retry a few times
|
|
218
|
+
* so re-running `install` is idempotent instead of leaving nothing loaded.
|
|
219
|
+
*/
|
|
220
|
+
async function bootstrapWithRetry(
|
|
221
|
+
execute: StepExecutor = executeStep,
|
|
222
|
+
delay: (milliseconds: number) => Promise<void> = defaultDelay,
|
|
223
|
+
definition = plistPath(),
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
let last: StepResult | null = null;
|
|
226
|
+
const args = ["bootstrap", domain, definition];
|
|
227
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
228
|
+
last = execute(false, "launchctl", args);
|
|
229
|
+
if (last.status === 0) return;
|
|
230
|
+
if (attempt < 5) await delay(500);
|
|
231
|
+
}
|
|
232
|
+
throw stepFailure("launchctl", args, last!);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface DarwinServiceDeps {
|
|
236
|
+
execute?: StepExecutor;
|
|
237
|
+
removeFile?: (path: string) => void;
|
|
238
|
+
delay?: (milliseconds: number) => Promise<void>;
|
|
239
|
+
definitionPath?: string;
|
|
240
|
+
journalPath?: string;
|
|
241
|
+
definitionDirectorySync?: DirectorySync;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const LAUNCHD_ABSENT =
|
|
245
|
+
/could not find (?:specified )?service|service(?: .+)? not found|no such process/i;
|
|
246
|
+
const defaultDelay = (milliseconds: number): Promise<void> =>
|
|
247
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
248
|
+
|
|
249
|
+
interface DarwinInstallJournal {
|
|
250
|
+
readonly schemaVersion: 1;
|
|
251
|
+
readonly phase: "installed" | "definition-restored";
|
|
252
|
+
readonly installIntent: {
|
|
253
|
+
readonly nonce: string;
|
|
254
|
+
readonly ownerPid: number;
|
|
255
|
+
} | null;
|
|
256
|
+
readonly prior:
|
|
257
|
+
| { readonly exists: false }
|
|
258
|
+
| {
|
|
259
|
+
readonly exists: true;
|
|
260
|
+
readonly bytesBase64: string;
|
|
261
|
+
readonly mode: number;
|
|
262
|
+
};
|
|
263
|
+
readonly manager: {
|
|
264
|
+
readonly loaded: boolean;
|
|
265
|
+
readonly running: boolean;
|
|
266
|
+
readonly enabled: boolean;
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function assertDefinitionInstallIntent(
|
|
271
|
+
intent: InstallContext["definitionInstallIntent"],
|
|
272
|
+
): void {
|
|
273
|
+
if (
|
|
274
|
+
intent &&
|
|
275
|
+
(!/^[a-f0-9]{64}$/.test(intent.nonce) ||
|
|
276
|
+
!Number.isSafeInteger(intent.ownerPid) ||
|
|
277
|
+
intent.ownerPid < 1)
|
|
278
|
+
) {
|
|
279
|
+
throw new Error("service definition install intent is invalid");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function serializeDefinition(
|
|
284
|
+
snapshot: ReturnType<typeof snapshotDefinition>,
|
|
285
|
+
): DarwinInstallJournal["prior"] {
|
|
286
|
+
return snapshot.exists
|
|
287
|
+
? {
|
|
288
|
+
exists: true,
|
|
289
|
+
bytesBase64: snapshot.bytes.toString("base64"),
|
|
290
|
+
mode: snapshot.mode,
|
|
291
|
+
}
|
|
292
|
+
: { exists: false };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseDarwinInstallJournal(raw: string): DarwinInstallJournal {
|
|
296
|
+
let value: unknown;
|
|
297
|
+
try {
|
|
298
|
+
value = JSON.parse(raw);
|
|
299
|
+
} catch {
|
|
300
|
+
throw new Error("launchd service install journal is invalid");
|
|
301
|
+
}
|
|
302
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
303
|
+
throw new Error("launchd service install journal is invalid");
|
|
304
|
+
}
|
|
305
|
+
const record = value as Record<string, unknown>;
|
|
306
|
+
if (
|
|
307
|
+
Object.keys(record).sort().join("\0") !==
|
|
308
|
+
["installIntent", "manager", "phase", "prior", "schemaVersion"].join(
|
|
309
|
+
"\0",
|
|
310
|
+
) ||
|
|
311
|
+
record.schemaVersion !== 1 ||
|
|
312
|
+
(record.phase !== "installed" && record.phase !== "definition-restored") ||
|
|
313
|
+
!record.manager ||
|
|
314
|
+
typeof record.manager !== "object" ||
|
|
315
|
+
Array.isArray(record.manager) ||
|
|
316
|
+
!record.prior ||
|
|
317
|
+
typeof record.prior !== "object" ||
|
|
318
|
+
Array.isArray(record.prior) ||
|
|
319
|
+
(record.installIntent !== null &&
|
|
320
|
+
(typeof record.installIntent !== "object" || Array.isArray(record.installIntent)))
|
|
321
|
+
) {
|
|
322
|
+
throw new Error("launchd service install journal is invalid");
|
|
323
|
+
}
|
|
324
|
+
const manager = record.manager as Record<string, unknown>;
|
|
325
|
+
const prior = record.prior as Record<string, unknown>;
|
|
326
|
+
const installIntent = record.installIntent as Record<string, unknown> | null;
|
|
327
|
+
if (
|
|
328
|
+
Object.keys(manager).sort().join("\0") !==
|
|
329
|
+
["enabled", "loaded", "running"].join("\0") ||
|
|
330
|
+
typeof manager.enabled !== "boolean" ||
|
|
331
|
+
typeof manager.loaded !== "boolean" ||
|
|
332
|
+
typeof manager.running !== "boolean" ||
|
|
333
|
+
(manager.running && !manager.loaded) ||
|
|
334
|
+
typeof prior.exists !== "boolean" ||
|
|
335
|
+
(installIntent !== null &&
|
|
336
|
+
(Object.keys(installIntent).sort().join("\0") !== ["nonce", "ownerPid"].join("\0") ||
|
|
337
|
+
typeof installIntent.nonce !== "string" ||
|
|
338
|
+
!/^[a-f0-9]{64}$/.test(installIntent.nonce) ||
|
|
339
|
+
typeof installIntent.ownerPid !== "number" ||
|
|
340
|
+
!Number.isSafeInteger(installIntent.ownerPid) ||
|
|
341
|
+
installIntent.ownerPid < 1))
|
|
342
|
+
) {
|
|
343
|
+
throw new Error("launchd service install journal is invalid");
|
|
344
|
+
}
|
|
345
|
+
if (prior.exists === false) {
|
|
346
|
+
if (Object.keys(prior).join("\0") !== "exists" || manager.loaded) {
|
|
347
|
+
throw new Error("launchd service install journal is invalid");
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
schemaVersion: 1,
|
|
351
|
+
phase: record.phase,
|
|
352
|
+
installIntent: installIntent as DarwinInstallJournal["installIntent"],
|
|
353
|
+
prior: { exists: false },
|
|
354
|
+
manager: manager as DarwinInstallJournal["manager"],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
Object.keys(prior).sort().join("\0") !==
|
|
359
|
+
["bytesBase64", "exists", "mode"].join("\0") ||
|
|
360
|
+
typeof prior.bytesBase64 !== "string" ||
|
|
361
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
362
|
+
prior.bytesBase64,
|
|
363
|
+
) ||
|
|
364
|
+
typeof prior.mode !== "number" ||
|
|
365
|
+
!Number.isInteger(prior.mode) ||
|
|
366
|
+
prior.mode < 0 ||
|
|
367
|
+
prior.mode > 0o7777
|
|
368
|
+
) {
|
|
369
|
+
throw new Error("launchd service install journal is invalid");
|
|
370
|
+
}
|
|
371
|
+
const bytes = Buffer.from(prior.bytesBase64, "base64");
|
|
372
|
+
if (bytes.toString("base64") !== prior.bytesBase64) {
|
|
373
|
+
throw new Error("launchd service install journal is invalid");
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
phase: record.phase,
|
|
378
|
+
installIntent: installIntent as DarwinInstallJournal["installIntent"],
|
|
379
|
+
prior: {
|
|
380
|
+
exists: true,
|
|
381
|
+
bytesBase64: prior.bytesBase64,
|
|
382
|
+
mode: prior.mode,
|
|
383
|
+
},
|
|
384
|
+
manager: manager as DarwinInstallJournal["manager"],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function inspectLaunchdJob(execute: StepExecutor): {
|
|
389
|
+
loaded: boolean;
|
|
390
|
+
running: boolean;
|
|
391
|
+
} {
|
|
392
|
+
const args = ["print", target];
|
|
393
|
+
const result = execute(false, "launchctl", args);
|
|
394
|
+
if (result.status === 0) {
|
|
395
|
+
return { loaded: true, running: launchdReportsRunning(result) };
|
|
396
|
+
}
|
|
397
|
+
if (probeConfirmsUnloaded(result)) return { loaded: false, running: false };
|
|
398
|
+
throw stepFailure("launchctl", args, result, "could not determine prior job state");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function inspectLaunchdEnabled(execute: StepExecutor): boolean {
|
|
402
|
+
const args = ["print-disabled", domain];
|
|
403
|
+
const result = execute(false, "launchctl", args);
|
|
404
|
+
if (result.status !== 0) throw stepFailure("launchctl", args, result);
|
|
405
|
+
const escaped = LABEL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
406
|
+
const match = new RegExp(`"${escaped}"\\s*=>\\s*(true|false)`).exec(result.stdout);
|
|
407
|
+
return match?.[1] !== "true";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function errorMessage(error: unknown): string {
|
|
411
|
+
return error instanceof Error ? error.message : String(error);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function probeConfirmsUnloaded(result: StepResult): boolean {
|
|
415
|
+
return (
|
|
416
|
+
typeof result.status === "number" &&
|
|
417
|
+
result.status !== 0 &&
|
|
418
|
+
LAUNCHD_ABSENT.test(`${result.stdout}\n${result.stderr}`)
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function resultDetail(result: StepResult): string {
|
|
423
|
+
return (
|
|
424
|
+
result.stderr ||
|
|
425
|
+
result.stdout ||
|
|
426
|
+
result.error?.message ||
|
|
427
|
+
`exit status ${String(result.status)}`
|
|
428
|
+
).trim();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function assertLaunchdUnloaded(
|
|
432
|
+
dryRun: boolean,
|
|
433
|
+
execute: StepExecutor,
|
|
434
|
+
delay: (milliseconds: number) => Promise<void>,
|
|
435
|
+
): Promise<void> {
|
|
436
|
+
const bootoutArgs = ["bootout", target];
|
|
437
|
+
const result = execute(dryRun, "launchctl", bootoutArgs);
|
|
438
|
+
if (dryRun) return;
|
|
439
|
+
|
|
440
|
+
const printArgs = ["print", target];
|
|
441
|
+
let probe = execute(false, "launchctl", printArgs);
|
|
442
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
443
|
+
if (probeConfirmsUnloaded(probe)) return;
|
|
444
|
+
if (result.status !== 0 || probe.status !== 0 || attempt === 5) break;
|
|
445
|
+
await delay(500);
|
|
446
|
+
probe = execute(false, "launchctl", printArgs);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const context = `could not verify that the service unloaded: ${resultDetail(probe)}`;
|
|
450
|
+
if (result.status === 0) {
|
|
451
|
+
throw new Error(`\`launchctl ${bootoutArgs.join(" ")}\` ${context}`);
|
|
452
|
+
}
|
|
453
|
+
throw stepFailure("launchctl", bootoutArgs, result, context);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function reconcileDarwinDefinitionInstall(
|
|
457
|
+
deps: DarwinInstallDeps = {},
|
|
458
|
+
installIntent?: string,
|
|
459
|
+
): Promise<void> {
|
|
460
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
461
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
462
|
+
const raw = readPrivateJournal(journalPath);
|
|
463
|
+
if (raw === null) return;
|
|
464
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
465
|
+
if (journal.installIntent !== null) {
|
|
466
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
467
|
+
throw new Error(
|
|
468
|
+
"public service definition rollback requires runtime command restoration between prepare and resume",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
await prepareDarwinDefinitionInstallRollback(deps);
|
|
472
|
+
await resumeDarwinDefinitionInstallRollback(deps);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export async function prepareDarwinDefinitionInstallRollback(
|
|
476
|
+
deps: DarwinInstallDeps = {},
|
|
477
|
+
installIntent?: string,
|
|
478
|
+
): Promise<void> {
|
|
479
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
480
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
481
|
+
const raw = readPrivateJournal(journalPath);
|
|
482
|
+
if (raw === null) return;
|
|
483
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
484
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
485
|
+
const execute = deps.execute ?? executeStep;
|
|
486
|
+
const delay = deps.delay ?? defaultDelay;
|
|
487
|
+
|
|
488
|
+
await assertLaunchdUnloaded(false, execute, delay);
|
|
489
|
+
// Disable while the target definition/command graph is still intact. A
|
|
490
|
+
// reboot after the following rename must not launch the restored definition
|
|
491
|
+
// through a not-yet-restored shared shim.
|
|
492
|
+
await setDarwinServiceEnabled(false, false, { execute });
|
|
493
|
+
const prior = journal.prior.exists
|
|
494
|
+
? {
|
|
495
|
+
exists: true as const,
|
|
496
|
+
bytes: Buffer.from(journal.prior.bytesBase64, "base64"),
|
|
497
|
+
mode: journal.prior.mode,
|
|
498
|
+
}
|
|
499
|
+
: ({ exists: false } as const);
|
|
500
|
+
restoreDefinitionDurable(definition, prior, deps.definitionDirectorySync);
|
|
501
|
+
|
|
502
|
+
if (journal.phase !== "definition-restored") {
|
|
503
|
+
writeDefinitionDurable(
|
|
504
|
+
journalPath,
|
|
505
|
+
`${JSON.stringify({ ...journal, phase: "definition-restored" })}\n`,
|
|
506
|
+
0o600,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export async function resumeDarwinDefinitionInstallRollback(
|
|
512
|
+
deps: DarwinInstallDeps = {},
|
|
513
|
+
installIntent?: string,
|
|
514
|
+
): Promise<void> {
|
|
515
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
516
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
517
|
+
const raw = readPrivateJournal(journalPath);
|
|
518
|
+
if (raw === null) return;
|
|
519
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
520
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
521
|
+
if (journal.phase !== "definition-restored") {
|
|
522
|
+
throw new Error("launchd service definition rollback has not been prepared");
|
|
523
|
+
}
|
|
524
|
+
const execute = deps.execute ?? executeStep;
|
|
525
|
+
const delay = deps.delay ?? defaultDelay;
|
|
526
|
+
|
|
527
|
+
// Reassert the no-daemon boundary in case a supervisor raced the command
|
|
528
|
+
// restoration between prepare and resume.
|
|
529
|
+
await assertLaunchdUnloaded(false, execute, delay);
|
|
530
|
+
const prior = journal.prior.exists
|
|
531
|
+
? {
|
|
532
|
+
exists: true as const,
|
|
533
|
+
bytes: Buffer.from(journal.prior.bytesBase64, "base64"),
|
|
534
|
+
mode: journal.prior.mode,
|
|
535
|
+
}
|
|
536
|
+
: ({ exists: false } as const);
|
|
537
|
+
restoreDefinitionDurable(definition, prior, deps.definitionDirectorySync);
|
|
538
|
+
|
|
539
|
+
if (prior.exists) {
|
|
540
|
+
const enabledArgs = [journal.manager.enabled ? "enable" : "disable", target];
|
|
541
|
+
const enabled = execute(false, "launchctl", enabledArgs);
|
|
542
|
+
if (enabled.status !== 0) throw stepFailure("launchctl", enabledArgs, enabled);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (journal.manager.loaded) {
|
|
546
|
+
await bootstrapWithRetry(execute, delay, definition);
|
|
547
|
+
if (journal.manager.running) {
|
|
548
|
+
const kickstartArgs = ["kickstart", target];
|
|
549
|
+
const kickstart = execute(false, "launchctl", kickstartArgs);
|
|
550
|
+
if (kickstart.status !== 0) {
|
|
551
|
+
throw stepFailure("launchctl", kickstartArgs, kickstart);
|
|
552
|
+
}
|
|
553
|
+
const printArgs = ["print", target];
|
|
554
|
+
let probe = execute(false, "launchctl", printArgs);
|
|
555
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
556
|
+
if (launchdReportsRunning(probe)) break;
|
|
557
|
+
if (attempt === 5) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`\`launchctl ${kickstartArgs.join(" ")}\` did not restore the prior service: ${resultDetail(probe)}`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
await delay(500);
|
|
563
|
+
probe = execute(false, "launchctl", printArgs);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
removeFileDurable(journalPath);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export async function completeDarwinDefinitionInstall(
|
|
571
|
+
deps: DarwinInstallDeps = {},
|
|
572
|
+
installIntent?: string,
|
|
573
|
+
): Promise<void> {
|
|
574
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
575
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
576
|
+
const raw = readPrivateJournal(journalPath);
|
|
577
|
+
if (raw === null) return;
|
|
578
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
579
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
580
|
+
if (journal.phase !== "installed") {
|
|
581
|
+
throw new Error("cannot commit a service definition whose rollback was prepared");
|
|
582
|
+
}
|
|
583
|
+
removeFileDurable(journalPath);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function assertJournalAccess(
|
|
587
|
+
owner: DarwinInstallJournal["installIntent"],
|
|
588
|
+
installIntent?: string,
|
|
589
|
+
): void {
|
|
590
|
+
if (owner === null) {
|
|
591
|
+
if (installIntent) {
|
|
592
|
+
throw new Error("service definition journal does not belong to this install intent");
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (owner.nonce !== installIntent) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`service definition update is owned by managed installer PID ${owner.ownerPid}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function darwinDefinitionState(
|
|
604
|
+
managedCommand: string,
|
|
605
|
+
definition = plistPath(),
|
|
606
|
+
expectedManagedTarget = resolve(
|
|
607
|
+
homedir(),
|
|
608
|
+
STATION.runtimeDirName,
|
|
609
|
+
"runtime",
|
|
610
|
+
"current",
|
|
611
|
+
"bin",
|
|
612
|
+
"uai-host",
|
|
613
|
+
),
|
|
614
|
+
execute: StepExecutor = executeStep,
|
|
615
|
+
): Promise<ServiceDefinitionState> {
|
|
616
|
+
const snapshot = snapshotDefinition(definition);
|
|
617
|
+
const loadedCommand = inspectLaunchdLoadedCommand(execute, managedCommand);
|
|
618
|
+
if (!snapshot.exists && loadedCommand.loaded) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
"launchd reports the host job loaded without a recoverable plist",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (!snapshot.exists) {
|
|
624
|
+
// launchd's print-disabled map treats an absent label as enabled by
|
|
625
|
+
// default. There is no definition to enable, so normalize a truly missing
|
|
626
|
+
// service to the all-false lifecycle state expected by install admission.
|
|
627
|
+
return {
|
|
628
|
+
ownership: "missing",
|
|
629
|
+
manager: { loaded: false, active: false, enabled: false },
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
const manager: ServiceManagerState = {
|
|
633
|
+
loaded: loadedCommand.loaded,
|
|
634
|
+
// This LaunchAgent is KeepAlive+RunAtLoad. A loaded-but-currently-waiting
|
|
635
|
+
// job is launchd restart/throttle pending and must be quiesced/preserved as
|
|
636
|
+
// active even when no daemon answers at the instant of inspection.
|
|
637
|
+
active: loadedCommand.loaded,
|
|
638
|
+
enabled: inspectLaunchdEnabled(execute),
|
|
639
|
+
};
|
|
640
|
+
const args = parsePlistProgramArguments(snapshot.bytes.toString("utf8"));
|
|
641
|
+
if (
|
|
642
|
+
args?.length === 2 &&
|
|
643
|
+
args[0] === managedCommand &&
|
|
644
|
+
args[1] === "run" &&
|
|
645
|
+
managedCommandIsOwned(managedCommand, expectedManagedTarget) &&
|
|
646
|
+
loadedCommand.matches !== false
|
|
647
|
+
) {
|
|
648
|
+
return { ownership: "managed", manager };
|
|
649
|
+
}
|
|
650
|
+
return { ownership: "legacy", manager };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export async function darwinDefinitionOwnership(
|
|
654
|
+
managedCommand: string,
|
|
655
|
+
definition = plistPath(),
|
|
656
|
+
expectedManagedTarget = resolve(
|
|
657
|
+
homedir(),
|
|
658
|
+
STATION.runtimeDirName,
|
|
659
|
+
"runtime",
|
|
660
|
+
"current",
|
|
661
|
+
"bin",
|
|
662
|
+
"uai-host",
|
|
663
|
+
),
|
|
664
|
+
execute: StepExecutor = executeStep,
|
|
665
|
+
): Promise<"missing" | "managed" | "legacy"> {
|
|
666
|
+
return (
|
|
667
|
+
await darwinDefinitionState(
|
|
668
|
+
managedCommand,
|
|
669
|
+
definition,
|
|
670
|
+
expectedManagedTarget,
|
|
671
|
+
execute,
|
|
672
|
+
)
|
|
673
|
+
).ownership;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function inspectLaunchdLoadedCommand(
|
|
677
|
+
execute: StepExecutor,
|
|
678
|
+
managedCommand: string,
|
|
679
|
+
): { loaded: boolean; matches: boolean | null } {
|
|
680
|
+
const args = ["print", target];
|
|
681
|
+
const result = execute(false, "launchctl", args);
|
|
682
|
+
if (probeConfirmsUnloaded(result)) {
|
|
683
|
+
return { loaded: false, matches: null };
|
|
684
|
+
}
|
|
685
|
+
if (result.status !== 0) {
|
|
686
|
+
throw stepFailure(
|
|
687
|
+
"launchctl",
|
|
688
|
+
args,
|
|
689
|
+
result,
|
|
690
|
+
"could not determine the loaded host command",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
const programLines = result.stdout
|
|
694
|
+
.split(/\r?\n/)
|
|
695
|
+
.map((line) => /^\s*program = (.*)$/.exec(line)?.[1])
|
|
696
|
+
.filter((value): value is string => value !== undefined);
|
|
697
|
+
const argumentBlocks = [
|
|
698
|
+
...result.stdout.matchAll(/^\s*arguments = \{\s*$([\s\S]*?)^\s*\}\s*$/gm),
|
|
699
|
+
];
|
|
700
|
+
if (programLines.length !== 1 || argumentBlocks.length !== 1) {
|
|
701
|
+
return {
|
|
702
|
+
loaded: true,
|
|
703
|
+
matches: false,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const loadedArgs = (argumentBlocks[0]?.[1] ?? "")
|
|
707
|
+
.split(/\r?\n/)
|
|
708
|
+
.map((line) => line.trim())
|
|
709
|
+
.filter(Boolean);
|
|
710
|
+
return {
|
|
711
|
+
loaded: true,
|
|
712
|
+
matches:
|
|
713
|
+
programLines[0] === managedCommand &&
|
|
714
|
+
loadedArgs.length === 2 &&
|
|
715
|
+
loadedArgs[0] === managedCommand &&
|
|
716
|
+
loadedArgs[1] === "run",
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function parsePlistProgramArguments(contents: string): string[] | null {
|
|
721
|
+
const matches = [
|
|
722
|
+
...contents.matchAll(
|
|
723
|
+
/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/g,
|
|
724
|
+
),
|
|
725
|
+
];
|
|
726
|
+
if (matches.length !== 1) return null;
|
|
727
|
+
const body = matches[0]?.[1] ?? "";
|
|
728
|
+
const args: string[] = [];
|
|
729
|
+
const strings = /<string>([\s\S]*?)<\/string>/g;
|
|
730
|
+
let cursor = 0;
|
|
731
|
+
for (const match of body.matchAll(strings)) {
|
|
732
|
+
const index = match.index ?? 0;
|
|
733
|
+
if (body.slice(cursor, index).trim() !== "") return null;
|
|
734
|
+
const decoded = decodeXmlString(match[1] ?? "");
|
|
735
|
+
if (decoded === null) return null;
|
|
736
|
+
args.push(decoded);
|
|
737
|
+
cursor = index + match[0].length;
|
|
738
|
+
}
|
|
739
|
+
return body.slice(cursor).trim() === "" ? args : null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function decodeXmlString(value: string): string | null {
|
|
743
|
+
const decoded = value.replace(
|
|
744
|
+
/&(amp|lt|gt|quot|apos);/g,
|
|
745
|
+
(_entity, name: string) =>
|
|
746
|
+
({ amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" })[name] ?? "",
|
|
747
|
+
);
|
|
748
|
+
return /&(?:#\d+|#x[0-9a-f]+|[a-z][a-z0-9]*);/i.test(decoded)
|
|
749
|
+
? null
|
|
750
|
+
: decoded;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function managedCommandIsOwned(command: string, expectedTarget: string): boolean {
|
|
754
|
+
try {
|
|
755
|
+
const stat = lstatSync(command);
|
|
756
|
+
if (!stat.isSymbolicLink()) return false;
|
|
757
|
+
const actual = resolve(dirname(command), readlinkSync(command));
|
|
758
|
+
return actual === expectedTarget;
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export async function uninstallDarwinService(
|
|
766
|
+
dryRun = false,
|
|
767
|
+
deps: DarwinServiceDeps = {},
|
|
768
|
+
): Promise<void> {
|
|
769
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
770
|
+
await assertLaunchdUnloaded(
|
|
771
|
+
dryRun,
|
|
772
|
+
deps.execute ?? executeStep,
|
|
773
|
+
deps.delay ?? defaultDelay,
|
|
774
|
+
);
|
|
775
|
+
if (dryRun) {
|
|
776
|
+
console.log(`$ rm -f ${plistPath()}`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
(deps.removeFile ?? ((path) => rmSync(path, { force: true })))(
|
|
780
|
+
deps.definitionPath ?? plistPath(),
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function enableDarwinService(
|
|
785
|
+
dryRun = false,
|
|
786
|
+
deps: DarwinServiceDeps = {},
|
|
787
|
+
): Promise<void> {
|
|
788
|
+
await setDarwinServiceEnabled(true, dryRun, deps);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function setDarwinServiceEnabled(
|
|
792
|
+
enabled: boolean,
|
|
793
|
+
dryRun = false,
|
|
794
|
+
deps: DarwinServiceDeps = {},
|
|
795
|
+
): Promise<void> {
|
|
796
|
+
const args = [enabled ? "enable" : "disable", target];
|
|
797
|
+
const result = (deps.execute ?? executeStep)(dryRun, "launchctl", args);
|
|
798
|
+
if (result.status !== 0) {
|
|
799
|
+
throw stepFailure("launchctl", args, result);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function launchdReportsRunning(result: StepResult): boolean {
|
|
804
|
+
return (
|
|
805
|
+
result.status === 0 &&
|
|
806
|
+
/(?:^|\n)\s*state = running(?:\s|$)/.test(result.stdout)
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function kickstartDarwinAndVerify(
|
|
811
|
+
execute: StepExecutor,
|
|
812
|
+
delay: (milliseconds: number) => Promise<void>,
|
|
813
|
+
): Promise<void> {
|
|
814
|
+
const kickstartArgs = ["kickstart", target];
|
|
815
|
+
const kickstart = execute(false, "launchctl", kickstartArgs);
|
|
816
|
+
if (kickstart.status !== 0) {
|
|
817
|
+
throw stepFailure("launchctl", kickstartArgs, kickstart);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const printArgs = ["print", target];
|
|
821
|
+
let probe = execute(false, "launchctl", printArgs);
|
|
822
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
823
|
+
if (launchdReportsRunning(probe)) return;
|
|
824
|
+
if (attempt === 5) break;
|
|
825
|
+
await delay(500);
|
|
826
|
+
probe = execute(false, "launchctl", printArgs);
|
|
827
|
+
}
|
|
828
|
+
throw new Error(
|
|
829
|
+
`\`launchctl ${kickstartArgs.join(" ")}\` did not leave the service running: ${resultDetail(probe)}`,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export async function startDarwinService(
|
|
834
|
+
dryRun = false,
|
|
835
|
+
deps: DarwinServiceDeps = {},
|
|
836
|
+
): Promise<void> {
|
|
837
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
838
|
+
const execute = deps.execute ?? executeStep;
|
|
839
|
+
await enableDarwinService(dryRun, deps);
|
|
840
|
+
|
|
841
|
+
const bootstrapArgs = ["bootstrap", domain, deps.definitionPath ?? plistPath()];
|
|
842
|
+
const bootstrap = execute(dryRun, "launchctl", bootstrapArgs);
|
|
843
|
+
if (dryRun) {
|
|
844
|
+
execute(true, "launchctl", ["kickstart", target]);
|
|
845
|
+
execute(true, "launchctl", ["print", target]);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (bootstrap.status !== 0) {
|
|
849
|
+
const loaded = execute(false, "launchctl", ["print", target]);
|
|
850
|
+
if (loaded.status !== 0) {
|
|
851
|
+
throw stepFailure(
|
|
852
|
+
"launchctl",
|
|
853
|
+
bootstrapArgs,
|
|
854
|
+
bootstrap,
|
|
855
|
+
`service is not already loaded: ${resultDetail(loaded)}`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
await kickstartDarwinAndVerify(execute, deps.delay ?? defaultDelay);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export async function stopDarwinService(
|
|
864
|
+
dryRun = false,
|
|
865
|
+
deps: DarwinServiceDeps = {},
|
|
866
|
+
): Promise<void> {
|
|
867
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
868
|
+
await assertLaunchdUnloaded(
|
|
869
|
+
dryRun,
|
|
870
|
+
deps.execute ?? executeStep,
|
|
871
|
+
deps.delay ?? defaultDelay,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export async function stopDarwinServiceForInstall(
|
|
876
|
+
installIntent: string,
|
|
877
|
+
priorManager: ServiceManagerState,
|
|
878
|
+
deps: DarwinServiceDeps = {},
|
|
879
|
+
): Promise<void> {
|
|
880
|
+
assertServiceManagerState(priorManager);
|
|
881
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
882
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
883
|
+
const raw = readPrivateJournal(journalPath);
|
|
884
|
+
if (raw !== null) {
|
|
885
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
886
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
887
|
+
}
|
|
888
|
+
// Disable first so launchd cannot relaunch a waiting/KeepAlive predecessor
|
|
889
|
+
// between a successful unload and the runtime's durable stopped marker.
|
|
890
|
+
await setDarwinServiceEnabled(false, false, deps);
|
|
891
|
+
await assertLaunchdUnloaded(
|
|
892
|
+
false,
|
|
893
|
+
deps.execute ?? executeStep,
|
|
894
|
+
deps.delay ?? defaultDelay,
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
export async function resumeDarwinServiceForInstall(
|
|
899
|
+
installIntent: string,
|
|
900
|
+
priorManager: ServiceManagerState,
|
|
901
|
+
deps: DarwinServiceDeps = {},
|
|
902
|
+
): Promise<void> {
|
|
903
|
+
assertServiceManagerState(priorManager);
|
|
904
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
905
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
906
|
+
const raw = readPrivateJournal(journalPath);
|
|
907
|
+
if (raw !== null) {
|
|
908
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
909
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
910
|
+
if (journal.phase === "installed") {
|
|
911
|
+
throw new Error("cannot resume the prior service before definition rollback");
|
|
912
|
+
}
|
|
913
|
+
await resumeDarwinDefinitionInstallRollback(deps, installIntent);
|
|
914
|
+
}
|
|
915
|
+
const execute = deps.execute ?? executeStep;
|
|
916
|
+
const delay = deps.delay ?? defaultDelay;
|
|
917
|
+
const definitionPath = deps.definitionPath ?? plistPath();
|
|
918
|
+
await setDarwinServiceEnabled(priorManager.enabled, false, { execute });
|
|
919
|
+
if (priorManager.loaded) {
|
|
920
|
+
await bootstrapWithRetry(execute, delay, definitionPath);
|
|
921
|
+
}
|
|
922
|
+
if (priorManager.active) {
|
|
923
|
+
await kickstartDarwinAndVerify(execute, delay);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function assertServiceManagerState(state: ServiceManagerState): void {
|
|
928
|
+
if (
|
|
929
|
+
typeof state.loaded !== "boolean" ||
|
|
930
|
+
typeof state.active !== "boolean" ||
|
|
931
|
+
typeof state.enabled !== "boolean" ||
|
|
932
|
+
(state.active && !state.loaded)
|
|
933
|
+
) {
|
|
934
|
+
throw new Error("managed service lifecycle snapshot is invalid");
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export async function restartDarwinService(
|
|
939
|
+
dryRun = false,
|
|
940
|
+
deps: DarwinServiceDeps = {},
|
|
941
|
+
): Promise<void> {
|
|
942
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
943
|
+
const args = ["kickstart", "-k", target];
|
|
944
|
+
const result = (deps.execute ?? executeStep)(dryRun, "launchctl", args);
|
|
945
|
+
if (result.status !== 0) {
|
|
946
|
+
throw stepFailure("launchctl", args, result);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** What `launchctl print gui/<uid>/com.runuai.host` says about the job. */
|
|
951
|
+
export interface LaunchdJobState {
|
|
952
|
+
readonly loaded: boolean;
|
|
953
|
+
/** `state = …` verbatim: `running`, `not running`, `waiting`, … */
|
|
954
|
+
readonly state: string | null;
|
|
955
|
+
readonly pid: number | null;
|
|
956
|
+
/** `last exit code = …` verbatim, e.g. `75: EX_TEMPFAIL` or `(never exited)`. */
|
|
957
|
+
readonly lastExitCode: string | null;
|
|
958
|
+
/** `pended nondemand spawn = …` — launchd holds a spawn it has not executed
|
|
959
|
+
* (live 2026-09-06: `inefficient`, with the job `not running` after an
|
|
960
|
+
* exit-75 activation restart that KeepAlive never honoured). */
|
|
961
|
+
readonly pendedNondemandSpawn: string | null;
|
|
962
|
+
/** Why the probe failed, when it did not confirm a loaded job. */
|
|
963
|
+
readonly detail: string | null;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function launchdPrintField(stdout: string, name: string): string | null {
|
|
967
|
+
// Top-level keys are indented by exactly one tab; nested dicts (endpoints,
|
|
968
|
+
// event triggers) reuse names like `state = active` two tabs deep, so the
|
|
969
|
+
// job's own key is preferred and the loose match is only a fallback. The
|
|
970
|
+
// whitespace class is horizontal only, so `state` never matches the tail
|
|
971
|
+
// of `spawn type` or another longer key.
|
|
972
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
973
|
+
const value = `(.+?)[ \\t]*(?:\\r?\\n|$)`;
|
|
974
|
+
return (
|
|
975
|
+
new RegExp(`(?:^|\\n)\\t${escaped} = ${value}`).exec(stdout)?.[1] ??
|
|
976
|
+
new RegExp(`(?:^|\\n)[ \\t]*${escaped} = ${value}`).exec(stdout)?.[1] ??
|
|
977
|
+
null
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export function parseLaunchdJobPrint(stdout: string): LaunchdJobState {
|
|
982
|
+
const pid = launchdPrintField(stdout, "pid");
|
|
983
|
+
return {
|
|
984
|
+
loaded: true,
|
|
985
|
+
state: launchdPrintField(stdout, "state"),
|
|
986
|
+
pid: pid !== null && /^\d+$/.test(pid) ? Number(pid) : null,
|
|
987
|
+
lastExitCode: launchdPrintField(stdout, "last exit code"),
|
|
988
|
+
pendedNondemandSpawn: launchdPrintField(stdout, "pended nondemand spawn"),
|
|
989
|
+
detail: null,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** One `launchctl print` of the host job, parsed. Read-only: safe to call
|
|
994
|
+
* from the running service (a boot after an activation logs it so a pended
|
|
995
|
+
* respawn is visible in the service log rather than only in `launchctl`). */
|
|
996
|
+
export function describeLaunchdJob(execute: StepExecutor = executeStep): LaunchdJobState {
|
|
997
|
+
const result = execute(false, "launchctl", ["print", target]);
|
|
998
|
+
if (result.status !== 0) {
|
|
999
|
+
return {
|
|
1000
|
+
loaded: false,
|
|
1001
|
+
state: null,
|
|
1002
|
+
pid: null,
|
|
1003
|
+
lastExitCode: null,
|
|
1004
|
+
pendedNondemandSpawn: null,
|
|
1005
|
+
detail: resultDetail(result) || null,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return parseLaunchdJobPrint(result.stdout);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
export function formatLaunchdJobState(job: LaunchdJobState): string {
|
|
1012
|
+
if (!job.loaded) return `not loaded${job.detail ? ` (${job.detail})` : ""}`;
|
|
1013
|
+
const parts = [`state = ${job.state ?? "unknown"}`];
|
|
1014
|
+
if (job.pid !== null) parts.push(`pid = ${job.pid}`);
|
|
1015
|
+
if (job.lastExitCode !== null) parts.push(`last exit code = ${job.lastExitCode}`);
|
|
1016
|
+
if (job.pendedNondemandSpawn !== null) {
|
|
1017
|
+
parts.push(`pended nondemand spawn = ${job.pendedNondemandSpawn}`);
|
|
1018
|
+
}
|
|
1019
|
+
return parts.join(", ");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function status(): Promise<string> {
|
|
1023
|
+
const job = describeLaunchdJob();
|
|
1024
|
+
if (!job.loaded) return "not installed / not loaded";
|
|
1025
|
+
return `${job.state ?? "unknown"}${job.pid !== null ? ` (pid ${job.pid})` : ""}`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
export const installer: Installer = {
|
|
1029
|
+
install: installDarwinService,
|
|
1030
|
+
uninstall: uninstallDarwinService,
|
|
1031
|
+
start: startDarwinService,
|
|
1032
|
+
stop: stopDarwinService,
|
|
1033
|
+
stopForInstall: (installIntent, priorManager) =>
|
|
1034
|
+
stopDarwinServiceForInstall(installIntent, priorManager),
|
|
1035
|
+
resumeServiceForInstall: (installIntent, priorManager) =>
|
|
1036
|
+
resumeDarwinServiceForInstall(installIntent, priorManager),
|
|
1037
|
+
restart: restartDarwinService,
|
|
1038
|
+
status,
|
|
1039
|
+
reconcileDefinitionInstall: (installIntent) =>
|
|
1040
|
+
reconcileDarwinDefinitionInstall({}, installIntent),
|
|
1041
|
+
prepareDefinitionInstallRollback: (installIntent) =>
|
|
1042
|
+
prepareDarwinDefinitionInstallRollback({}, installIntent),
|
|
1043
|
+
resumeDefinitionInstallRollback: (installIntent) =>
|
|
1044
|
+
resumeDarwinDefinitionInstallRollback({}, installIntent),
|
|
1045
|
+
completeDefinitionInstall: (installIntent) =>
|
|
1046
|
+
completeDarwinDefinitionInstall({}, installIntent),
|
|
1047
|
+
definitionState: darwinDefinitionState,
|
|
1048
|
+
};
|