@runuai/host 0.9.14 → 0.9.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- 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/meta/_journal.json +21 -0
- package/db/schema.ts +80 -30
- package/images/standard/Dockerfile +36 -10
- package/images/standard/README.md +63 -18
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +308 -38
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/lib/agent-cli.ts +33 -2
- package/lib/agent.ts +46 -7
- package/lib/agents/claude.ts +13 -8
- package/lib/agents/codex.ts +11 -6
- package/lib/agents/cursor.ts +39 -29
- package/lib/agents/durable-proc.ts +20 -27
- package/lib/agents/factory.ts +9 -25
- package/lib/agents/grok.ts +43 -30
- package/lib/agents/kimi.ts +44 -29
- package/lib/agents/opencode.ts +43 -31
- package/lib/agents/proc.ts +149 -114
- package/lib/agents/transport.ts +62 -50
- package/lib/agents/types.ts +6 -4
- package/lib/apple-runtime-recycle.ts +236 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/browser-testing.ts +233 -93
- package/lib/codex-auth.ts +40 -6
- package/lib/command-db.ts +20 -0
- package/lib/container-runtime.ts +1338 -0
- package/lib/db.ts +1 -0
- package/lib/docker-exec.ts +87 -5
- package/lib/engine-accounts.ts +68 -5
- package/lib/engine-login.ts +1952 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +155 -0
- package/lib/env.ts +4 -0
- package/lib/git-diff.ts +98 -32
- package/lib/git-identity.ts +199 -87
- package/lib/github-tokens.ts +202 -91
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-logs.ts +962 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +49 -0
- package/lib/managed-runtime.ts +3644 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-gateway.ts +450 -23
- package/lib/orchestrator.ts +3051 -200
- package/lib/preview-sidecar.ts +68 -14
- package/lib/release-manifest.ts +708 -0
- package/lib/release-trust.ts +28 -0
- package/lib/runtime-activation-tail.ts +232 -0
- package/lib/runtime-archive.ts +1086 -0
- package/lib/runtime-authority.ts +79 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-state.ts +232 -12
- package/lib/skills.ts +24 -3
- package/lib/ssh.ts +18 -0
- package/lib/standard-image.ts +1104 -141
- package/lib/stopped-task-status-queue.ts +44 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-diff.ts +66 -46
- package/lib/task-environment/apple-container.ts +757 -0
- package/lib/task-environment/docker.ts +956 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +459 -0
- package/lib/task-environment/registry.ts +58 -0
- package/lib/task-environment/types.ts +408 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +585 -0
- package/lib/tunnel-registry.ts +135 -19
- package/lib/tunnel-runtime.ts +235 -0
- package/package.json +1 -1
- package/scripts/agent/_common.sh +123 -3
- package/scripts/agent/task-down.sh +146 -38
- package/scripts/agent/task-status.sh +19 -3
- package/scripts/agent/task-up.sh +1405 -107
- package/scripts/install/darwin.ts +848 -50
- package/scripts/install/linux.ts +838 -35
- package/scripts/install/types.ts +43 -0
- package/scripts/install/util.ts +215 -8
- package/scripts/install/win.ts +12 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/cli.ts +1464 -72
- package/src/event-outbox.ts +83 -4
- package/src/index.ts +766 -42
- package/src/main.ts +1398 -255
- package/src/paths.ts +17 -1
- package/src/protocol.ts +695 -1
- package/src/runtime-bootstrap.ts +165 -0
- package/src/ui/server.ts +46 -10
- package/src/ui/types.ts +37 -0
|
@@ -7,12 +7,31 @@
|
|
|
7
7
|
* docker/git resolve); .env.local is loaded by the host process itself.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { mkdirSync,
|
|
10
|
+
import { lstatSync, mkdirSync, readlinkSync, rmSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, resolve } from "node:path";
|
|
13
13
|
|
|
14
|
-
import type {
|
|
15
|
-
|
|
14
|
+
import type {
|
|
15
|
+
InstallContext,
|
|
16
|
+
Installer,
|
|
17
|
+
ServiceDefinitionState,
|
|
18
|
+
ServiceManagerState,
|
|
19
|
+
} from "./types";
|
|
20
|
+
import {
|
|
21
|
+
escapeXml,
|
|
22
|
+
executeStep,
|
|
23
|
+
printFile,
|
|
24
|
+
readPrivateJournal,
|
|
25
|
+
removeFileDurable,
|
|
26
|
+
restoreDefinitionDurable,
|
|
27
|
+
snapshotDefinition,
|
|
28
|
+
step,
|
|
29
|
+
stepFailure,
|
|
30
|
+
writeDefinitionDurable,
|
|
31
|
+
type DirectorySync,
|
|
32
|
+
type StepExecutor,
|
|
33
|
+
type StepResult,
|
|
34
|
+
} from "./util";
|
|
16
35
|
|
|
17
36
|
export const LABEL = "com.runuai.host";
|
|
18
37
|
|
|
@@ -25,6 +44,10 @@ export function plistPath(): string {
|
|
|
25
44
|
return resolve(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
26
45
|
}
|
|
27
46
|
|
|
47
|
+
export function darwinInstallJournalPath(definition = plistPath()): string {
|
|
48
|
+
return resolve(dirname(definition), `.${basename(definition)}.install-journal.json`);
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
export interface PlistInputs {
|
|
29
52
|
execPath: string;
|
|
30
53
|
args: string[];
|
|
@@ -33,6 +56,7 @@ export interface PlistInputs {
|
|
|
33
56
|
home: string;
|
|
34
57
|
outLog: string;
|
|
35
58
|
errLog: string;
|
|
59
|
+
runAtLoad?: boolean;
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
export function renderPlist(p: PlistInputs): string {
|
|
@@ -59,7 +83,7 @@ ${progArgs}
|
|
|
59
83
|
<string>${escapeXml(p.home)}</string>
|
|
60
84
|
</dict>
|
|
61
85
|
<key>RunAtLoad</key>
|
|
62
|
-
|
|
86
|
+
<${p.runAtLoad === false ? "false" : "true"}/>
|
|
63
87
|
<key>KeepAlive</key>
|
|
64
88
|
<true/>
|
|
65
89
|
<key>ThrottleInterval</key>
|
|
@@ -73,35 +97,106 @@ ${progArgs}
|
|
|
73
97
|
`;
|
|
74
98
|
}
|
|
75
99
|
|
|
76
|
-
function inputs(ctx: InstallContext): PlistInputs {
|
|
100
|
+
function inputs(ctx: InstallContext, logs = logDir): PlistInputs {
|
|
77
101
|
return {
|
|
78
102
|
execPath: ctx.execPath,
|
|
79
103
|
args: ctx.args,
|
|
80
104
|
cwd: ctx.cwd,
|
|
81
|
-
path: process.env.PATH ?? "",
|
|
105
|
+
path: ctx.envPath ?? process.env.PATH ?? "",
|
|
82
106
|
home: homedir(),
|
|
83
|
-
outLog: resolve(
|
|
84
|
-
errLog: resolve(
|
|
107
|
+
outLog: resolve(logs, "host.out.log"),
|
|
108
|
+
errLog: resolve(logs, "host.log"),
|
|
109
|
+
runAtLoad: ctx.startOnInstall !== false,
|
|
85
110
|
};
|
|
86
111
|
}
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
export interface DarwinInstallDeps extends DarwinServiceDeps {
|
|
114
|
+
logDirectory?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function installDarwinService(
|
|
118
|
+
ctx: InstallContext,
|
|
119
|
+
deps: DarwinInstallDeps = {},
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
122
|
+
const logs = deps.logDirectory ?? logDir;
|
|
123
|
+
const execute = deps.execute ?? executeStep;
|
|
124
|
+
const delay = deps.delay ?? defaultDelay;
|
|
125
|
+
const plist = renderPlist(inputs(ctx, logs));
|
|
90
126
|
if (ctx.dryRun) {
|
|
91
|
-
printFile(
|
|
92
|
-
step(true, "mkdir", ["-p",
|
|
127
|
+
printFile(definition, plist);
|
|
128
|
+
step(true, "mkdir", ["-p", logs]);
|
|
93
129
|
step(true, "launchctl", ["bootout", target]);
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
if (ctx.startOnInstall !== false) {
|
|
131
|
+
step(true, "launchctl", ["bootstrap", domain, definition]);
|
|
132
|
+
await enableDarwinService(true);
|
|
133
|
+
} else {
|
|
134
|
+
step(true, "launchctl", ["disable", target]);
|
|
135
|
+
}
|
|
96
136
|
return;
|
|
97
137
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
139
|
+
const installIntent = ctx.definitionInstallIntent;
|
|
140
|
+
assertDefinitionInstallIntent(installIntent);
|
|
141
|
+
await reconcileDarwinDefinitionInstall(
|
|
142
|
+
{ ...deps, definitionPath: definition, journalPath },
|
|
143
|
+
installIntent?.nonce,
|
|
144
|
+
);
|
|
145
|
+
mkdirSync(logs, { recursive: true });
|
|
146
|
+
mkdirSync(dirname(definition), { recursive: true });
|
|
147
|
+
const previous = snapshotDefinition(definition);
|
|
148
|
+
const prior = inspectLaunchdJob(execute);
|
|
149
|
+
if (!previous.exists && prior.loaded) {
|
|
150
|
+
throw new Error("launchd reports the host job loaded without a recoverable plist");
|
|
151
|
+
}
|
|
152
|
+
const journal: DarwinInstallJournal = {
|
|
153
|
+
schemaVersion: 1,
|
|
154
|
+
phase: "installed",
|
|
155
|
+
installIntent: installIntent ?? null,
|
|
156
|
+
prior: serializeDefinition(previous),
|
|
157
|
+
manager: { ...prior, enabled: inspectLaunchdEnabled(execute) },
|
|
158
|
+
};
|
|
159
|
+
writeDefinitionDurable(journalPath, `${JSON.stringify(journal)}\n`, 0o600);
|
|
160
|
+
try {
|
|
161
|
+
writeDefinitionDurable(
|
|
162
|
+
definition,
|
|
163
|
+
plist,
|
|
164
|
+
0o644,
|
|
165
|
+
deps.definitionDirectorySync,
|
|
166
|
+
);
|
|
167
|
+
await assertLaunchdUnloaded(false, execute, delay);
|
|
168
|
+
if (ctx.startOnInstall === false) {
|
|
169
|
+
await setDarwinServiceEnabled(false, false, { execute });
|
|
170
|
+
if (!installIntent) removeFileDurable(journalPath);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await enableDarwinService(false, { execute });
|
|
174
|
+
await bootstrapWithRetry(execute, delay, definition);
|
|
175
|
+
await kickstartDarwinAndVerify(execute, delay);
|
|
176
|
+
if (!installIntent) removeFileDurable(journalPath);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
try {
|
|
179
|
+
const rollbackDeps = {
|
|
180
|
+
...deps,
|
|
181
|
+
definitionPath: definition,
|
|
182
|
+
journalPath,
|
|
183
|
+
};
|
|
184
|
+
if (installIntent) {
|
|
185
|
+
await prepareDarwinDefinitionInstallRollback(
|
|
186
|
+
rollbackDeps,
|
|
187
|
+
installIntent.nonce,
|
|
188
|
+
);
|
|
189
|
+
} else {
|
|
190
|
+
await reconcileDarwinDefinitionInstall(rollbackDeps);
|
|
191
|
+
}
|
|
192
|
+
} catch (rollbackError) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`${errorMessage(error)}; service-definition rollback failed: ${errorMessage(rollbackError)}`,
|
|
195
|
+
{ cause: error },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
105
200
|
}
|
|
106
201
|
|
|
107
202
|
/**
|
|
@@ -109,44 +204,734 @@ async function install(ctx: InstallContext): Promise<void> {
|
|
|
109
204
|
* can race it ("Bootstrap failed: 5: Input/output error"). Retry a few times
|
|
110
205
|
* so re-running `install` is idempotent instead of leaving nothing loaded.
|
|
111
206
|
*/
|
|
112
|
-
async function bootstrapWithRetry(
|
|
113
|
-
|
|
207
|
+
async function bootstrapWithRetry(
|
|
208
|
+
execute: StepExecutor = executeStep,
|
|
209
|
+
delay: (milliseconds: number) => Promise<void> = defaultDelay,
|
|
210
|
+
definition = plistPath(),
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
let last: StepResult | null = null;
|
|
213
|
+
const args = ["bootstrap", domain, definition];
|
|
114
214
|
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
215
|
+
last = execute(false, "launchctl", args);
|
|
216
|
+
if (last.status === 0) return;
|
|
217
|
+
if (attempt < 5) await delay(500);
|
|
218
|
+
}
|
|
219
|
+
throw stepFailure("launchctl", args, last!);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface DarwinServiceDeps {
|
|
223
|
+
execute?: StepExecutor;
|
|
224
|
+
removeFile?: (path: string) => void;
|
|
225
|
+
delay?: (milliseconds: number) => Promise<void>;
|
|
226
|
+
definitionPath?: string;
|
|
227
|
+
journalPath?: string;
|
|
228
|
+
definitionDirectorySync?: DirectorySync;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const LAUNCHD_ABSENT =
|
|
232
|
+
/could not find (?:specified )?service|service(?: .+)? not found|no such process/i;
|
|
233
|
+
const defaultDelay = (milliseconds: number): Promise<void> =>
|
|
234
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
235
|
+
|
|
236
|
+
interface DarwinInstallJournal {
|
|
237
|
+
readonly schemaVersion: 1;
|
|
238
|
+
readonly phase: "installed" | "definition-restored";
|
|
239
|
+
readonly installIntent: {
|
|
240
|
+
readonly nonce: string;
|
|
241
|
+
readonly ownerPid: number;
|
|
242
|
+
} | null;
|
|
243
|
+
readonly prior:
|
|
244
|
+
| { readonly exists: false }
|
|
245
|
+
| {
|
|
246
|
+
readonly exists: true;
|
|
247
|
+
readonly bytesBase64: string;
|
|
248
|
+
readonly mode: number;
|
|
249
|
+
};
|
|
250
|
+
readonly manager: {
|
|
251
|
+
readonly loaded: boolean;
|
|
252
|
+
readonly running: boolean;
|
|
253
|
+
readonly enabled: boolean;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function assertDefinitionInstallIntent(
|
|
258
|
+
intent: InstallContext["definitionInstallIntent"],
|
|
259
|
+
): void {
|
|
260
|
+
if (
|
|
261
|
+
intent &&
|
|
262
|
+
(!/^[a-f0-9]{64}$/.test(intent.nonce) ||
|
|
263
|
+
!Number.isSafeInteger(intent.ownerPid) ||
|
|
264
|
+
intent.ownerPid < 1)
|
|
265
|
+
) {
|
|
266
|
+
throw new Error("service definition install intent is invalid");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function serializeDefinition(
|
|
271
|
+
snapshot: ReturnType<typeof snapshotDefinition>,
|
|
272
|
+
): DarwinInstallJournal["prior"] {
|
|
273
|
+
return snapshot.exists
|
|
274
|
+
? {
|
|
275
|
+
exists: true,
|
|
276
|
+
bytesBase64: snapshot.bytes.toString("base64"),
|
|
277
|
+
mode: snapshot.mode,
|
|
278
|
+
}
|
|
279
|
+
: { exists: false };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseDarwinInstallJournal(raw: string): DarwinInstallJournal {
|
|
283
|
+
let value: unknown;
|
|
284
|
+
try {
|
|
285
|
+
value = JSON.parse(raw);
|
|
286
|
+
} catch {
|
|
287
|
+
throw new Error("launchd service install journal is invalid");
|
|
288
|
+
}
|
|
289
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
290
|
+
throw new Error("launchd service install journal is invalid");
|
|
291
|
+
}
|
|
292
|
+
const record = value as Record<string, unknown>;
|
|
293
|
+
if (
|
|
294
|
+
Object.keys(record).sort().join("\0") !==
|
|
295
|
+
["installIntent", "manager", "phase", "prior", "schemaVersion"].join(
|
|
296
|
+
"\0",
|
|
297
|
+
) ||
|
|
298
|
+
record.schemaVersion !== 1 ||
|
|
299
|
+
(record.phase !== "installed" && record.phase !== "definition-restored") ||
|
|
300
|
+
!record.manager ||
|
|
301
|
+
typeof record.manager !== "object" ||
|
|
302
|
+
Array.isArray(record.manager) ||
|
|
303
|
+
!record.prior ||
|
|
304
|
+
typeof record.prior !== "object" ||
|
|
305
|
+
Array.isArray(record.prior) ||
|
|
306
|
+
(record.installIntent !== null &&
|
|
307
|
+
(typeof record.installIntent !== "object" || Array.isArray(record.installIntent)))
|
|
308
|
+
) {
|
|
309
|
+
throw new Error("launchd service install journal is invalid");
|
|
310
|
+
}
|
|
311
|
+
const manager = record.manager as Record<string, unknown>;
|
|
312
|
+
const prior = record.prior as Record<string, unknown>;
|
|
313
|
+
const installIntent = record.installIntent as Record<string, unknown> | null;
|
|
314
|
+
if (
|
|
315
|
+
Object.keys(manager).sort().join("\0") !==
|
|
316
|
+
["enabled", "loaded", "running"].join("\0") ||
|
|
317
|
+
typeof manager.enabled !== "boolean" ||
|
|
318
|
+
typeof manager.loaded !== "boolean" ||
|
|
319
|
+
typeof manager.running !== "boolean" ||
|
|
320
|
+
(manager.running && !manager.loaded) ||
|
|
321
|
+
typeof prior.exists !== "boolean" ||
|
|
322
|
+
(installIntent !== null &&
|
|
323
|
+
(Object.keys(installIntent).sort().join("\0") !== ["nonce", "ownerPid"].join("\0") ||
|
|
324
|
+
typeof installIntent.nonce !== "string" ||
|
|
325
|
+
!/^[a-f0-9]{64}$/.test(installIntent.nonce) ||
|
|
326
|
+
typeof installIntent.ownerPid !== "number" ||
|
|
327
|
+
!Number.isSafeInteger(installIntent.ownerPid) ||
|
|
328
|
+
installIntent.ownerPid < 1))
|
|
329
|
+
) {
|
|
330
|
+
throw new Error("launchd service install journal is invalid");
|
|
331
|
+
}
|
|
332
|
+
if (prior.exists === false) {
|
|
333
|
+
if (Object.keys(prior).join("\0") !== "exists" || manager.loaded) {
|
|
334
|
+
throw new Error("launchd service install journal is invalid");
|
|
121
335
|
}
|
|
336
|
+
return {
|
|
337
|
+
schemaVersion: 1,
|
|
338
|
+
phase: record.phase,
|
|
339
|
+
installIntent: installIntent as DarwinInstallJournal["installIntent"],
|
|
340
|
+
prior: { exists: false },
|
|
341
|
+
manager: manager as DarwinInstallJournal["manager"],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (
|
|
345
|
+
Object.keys(prior).sort().join("\0") !==
|
|
346
|
+
["bytesBase64", "exists", "mode"].join("\0") ||
|
|
347
|
+
typeof prior.bytesBase64 !== "string" ||
|
|
348
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
349
|
+
prior.bytesBase64,
|
|
350
|
+
) ||
|
|
351
|
+
typeof prior.mode !== "number" ||
|
|
352
|
+
!Number.isInteger(prior.mode) ||
|
|
353
|
+
prior.mode < 0 ||
|
|
354
|
+
prior.mode > 0o7777
|
|
355
|
+
) {
|
|
356
|
+
throw new Error("launchd service install journal is invalid");
|
|
357
|
+
}
|
|
358
|
+
const bytes = Buffer.from(prior.bytesBase64, "base64");
|
|
359
|
+
if (bytes.toString("base64") !== prior.bytesBase64) {
|
|
360
|
+
throw new Error("launchd service install journal is invalid");
|
|
122
361
|
}
|
|
123
|
-
|
|
362
|
+
return {
|
|
363
|
+
schemaVersion: 1,
|
|
364
|
+
phase: record.phase,
|
|
365
|
+
installIntent: installIntent as DarwinInstallJournal["installIntent"],
|
|
366
|
+
prior: {
|
|
367
|
+
exists: true,
|
|
368
|
+
bytesBase64: prior.bytesBase64,
|
|
369
|
+
mode: prior.mode,
|
|
370
|
+
},
|
|
371
|
+
manager: manager as DarwinInstallJournal["manager"],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function inspectLaunchdJob(execute: StepExecutor): {
|
|
376
|
+
loaded: boolean;
|
|
377
|
+
running: boolean;
|
|
378
|
+
} {
|
|
379
|
+
const args = ["print", target];
|
|
380
|
+
const result = execute(false, "launchctl", args);
|
|
381
|
+
if (result.status === 0) {
|
|
382
|
+
return { loaded: true, running: launchdReportsRunning(result) };
|
|
383
|
+
}
|
|
384
|
+
if (probeConfirmsUnloaded(result)) return { loaded: false, running: false };
|
|
385
|
+
throw stepFailure("launchctl", args, result, "could not determine prior job state");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function inspectLaunchdEnabled(execute: StepExecutor): boolean {
|
|
389
|
+
const args = ["print-disabled", domain];
|
|
390
|
+
const result = execute(false, "launchctl", args);
|
|
391
|
+
if (result.status !== 0) throw stepFailure("launchctl", args, result);
|
|
392
|
+
const escaped = LABEL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
393
|
+
const match = new RegExp(`"${escaped}"\\s*=>\\s*(true|false)`).exec(result.stdout);
|
|
394
|
+
return match?.[1] !== "true";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function errorMessage(error: unknown): string {
|
|
398
|
+
return error instanceof Error ? error.message : String(error);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function probeConfirmsUnloaded(result: StepResult): boolean {
|
|
402
|
+
return (
|
|
403
|
+
typeof result.status === "number" &&
|
|
404
|
+
result.status !== 0 &&
|
|
405
|
+
LAUNCHD_ABSENT.test(`${result.stdout}\n${result.stderr}`)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resultDetail(result: StepResult): string {
|
|
410
|
+
return (
|
|
411
|
+
result.stderr ||
|
|
412
|
+
result.stdout ||
|
|
413
|
+
result.error?.message ||
|
|
414
|
+
`exit status ${String(result.status)}`
|
|
415
|
+
).trim();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function assertLaunchdUnloaded(
|
|
419
|
+
dryRun: boolean,
|
|
420
|
+
execute: StepExecutor,
|
|
421
|
+
delay: (milliseconds: number) => Promise<void>,
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
const bootoutArgs = ["bootout", target];
|
|
424
|
+
const result = execute(dryRun, "launchctl", bootoutArgs);
|
|
425
|
+
if (dryRun) return;
|
|
426
|
+
|
|
427
|
+
const printArgs = ["print", target];
|
|
428
|
+
let probe = execute(false, "launchctl", printArgs);
|
|
429
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
430
|
+
if (probeConfirmsUnloaded(probe)) return;
|
|
431
|
+
if (result.status !== 0 || probe.status !== 0 || attempt === 5) break;
|
|
432
|
+
await delay(500);
|
|
433
|
+
probe = execute(false, "launchctl", printArgs);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const context = `could not verify that the service unloaded: ${resultDetail(probe)}`;
|
|
437
|
+
if (result.status === 0) {
|
|
438
|
+
throw new Error(`\`launchctl ${bootoutArgs.join(" ")}\` ${context}`);
|
|
439
|
+
}
|
|
440
|
+
throw stepFailure("launchctl", bootoutArgs, result, context);
|
|
124
441
|
}
|
|
125
442
|
|
|
126
|
-
async function
|
|
127
|
-
|
|
443
|
+
export async function reconcileDarwinDefinitionInstall(
|
|
444
|
+
deps: DarwinInstallDeps = {},
|
|
445
|
+
installIntent?: string,
|
|
446
|
+
): Promise<void> {
|
|
447
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
448
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
449
|
+
const raw = readPrivateJournal(journalPath);
|
|
450
|
+
if (raw === null) return;
|
|
451
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
452
|
+
if (journal.installIntent !== null) {
|
|
453
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
454
|
+
throw new Error(
|
|
455
|
+
"public service definition rollback requires runtime command restoration between prepare and resume",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
await prepareDarwinDefinitionInstallRollback(deps);
|
|
459
|
+
await resumeDarwinDefinitionInstallRollback(deps);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function prepareDarwinDefinitionInstallRollback(
|
|
463
|
+
deps: DarwinInstallDeps = {},
|
|
464
|
+
installIntent?: string,
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
467
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
468
|
+
const raw = readPrivateJournal(journalPath);
|
|
469
|
+
if (raw === null) return;
|
|
470
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
471
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
472
|
+
const execute = deps.execute ?? executeStep;
|
|
473
|
+
const delay = deps.delay ?? defaultDelay;
|
|
474
|
+
|
|
475
|
+
await assertLaunchdUnloaded(false, execute, delay);
|
|
476
|
+
// Disable while the target definition/command graph is still intact. A
|
|
477
|
+
// reboot after the following rename must not launch the restored definition
|
|
478
|
+
// through a not-yet-restored shared shim.
|
|
479
|
+
await setDarwinServiceEnabled(false, false, { execute });
|
|
480
|
+
const prior = journal.prior.exists
|
|
481
|
+
? {
|
|
482
|
+
exists: true as const,
|
|
483
|
+
bytes: Buffer.from(journal.prior.bytesBase64, "base64"),
|
|
484
|
+
mode: journal.prior.mode,
|
|
485
|
+
}
|
|
486
|
+
: ({ exists: false } as const);
|
|
487
|
+
restoreDefinitionDurable(definition, prior, deps.definitionDirectorySync);
|
|
488
|
+
|
|
489
|
+
if (journal.phase !== "definition-restored") {
|
|
490
|
+
writeDefinitionDurable(
|
|
491
|
+
journalPath,
|
|
492
|
+
`${JSON.stringify({ ...journal, phase: "definition-restored" })}\n`,
|
|
493
|
+
0o600,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export async function resumeDarwinDefinitionInstallRollback(
|
|
499
|
+
deps: DarwinInstallDeps = {},
|
|
500
|
+
installIntent?: string,
|
|
501
|
+
): Promise<void> {
|
|
502
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
503
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
504
|
+
const raw = readPrivateJournal(journalPath);
|
|
505
|
+
if (raw === null) return;
|
|
506
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
507
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
508
|
+
if (journal.phase !== "definition-restored") {
|
|
509
|
+
throw new Error("launchd service definition rollback has not been prepared");
|
|
510
|
+
}
|
|
511
|
+
const execute = deps.execute ?? executeStep;
|
|
512
|
+
const delay = deps.delay ?? defaultDelay;
|
|
513
|
+
|
|
514
|
+
// Reassert the no-daemon boundary in case a supervisor raced the command
|
|
515
|
+
// restoration between prepare and resume.
|
|
516
|
+
await assertLaunchdUnloaded(false, execute, delay);
|
|
517
|
+
const prior = journal.prior.exists
|
|
518
|
+
? {
|
|
519
|
+
exists: true as const,
|
|
520
|
+
bytes: Buffer.from(journal.prior.bytesBase64, "base64"),
|
|
521
|
+
mode: journal.prior.mode,
|
|
522
|
+
}
|
|
523
|
+
: ({ exists: false } as const);
|
|
524
|
+
restoreDefinitionDurable(definition, prior, deps.definitionDirectorySync);
|
|
525
|
+
|
|
526
|
+
if (prior.exists) {
|
|
527
|
+
const enabledArgs = [journal.manager.enabled ? "enable" : "disable", target];
|
|
528
|
+
const enabled = execute(false, "launchctl", enabledArgs);
|
|
529
|
+
if (enabled.status !== 0) throw stepFailure("launchctl", enabledArgs, enabled);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (journal.manager.loaded) {
|
|
533
|
+
await bootstrapWithRetry(execute, delay, definition);
|
|
534
|
+
if (journal.manager.running) {
|
|
535
|
+
const kickstartArgs = ["kickstart", target];
|
|
536
|
+
const kickstart = execute(false, "launchctl", kickstartArgs);
|
|
537
|
+
if (kickstart.status !== 0) {
|
|
538
|
+
throw stepFailure("launchctl", kickstartArgs, kickstart);
|
|
539
|
+
}
|
|
540
|
+
const printArgs = ["print", target];
|
|
541
|
+
let probe = execute(false, "launchctl", printArgs);
|
|
542
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
543
|
+
if (launchdReportsRunning(probe)) break;
|
|
544
|
+
if (attempt === 5) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`\`launchctl ${kickstartArgs.join(" ")}\` did not restore the prior service: ${resultDetail(probe)}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
await delay(500);
|
|
550
|
+
probe = execute(false, "launchctl", printArgs);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
removeFileDurable(journalPath);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export async function completeDarwinDefinitionInstall(
|
|
558
|
+
deps: DarwinInstallDeps = {},
|
|
559
|
+
installIntent?: string,
|
|
560
|
+
): Promise<void> {
|
|
561
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
562
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
563
|
+
const raw = readPrivateJournal(journalPath);
|
|
564
|
+
if (raw === null) return;
|
|
565
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
566
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
567
|
+
if (journal.phase !== "installed") {
|
|
568
|
+
throw new Error("cannot commit a service definition whose rollback was prepared");
|
|
569
|
+
}
|
|
570
|
+
removeFileDurable(journalPath);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function assertJournalAccess(
|
|
574
|
+
owner: DarwinInstallJournal["installIntent"],
|
|
575
|
+
installIntent?: string,
|
|
576
|
+
): void {
|
|
577
|
+
if (owner === null) {
|
|
578
|
+
if (installIntent) {
|
|
579
|
+
throw new Error("service definition journal does not belong to this install intent");
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (owner.nonce !== installIntent) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`service definition update is owned by managed installer PID ${owner.ownerPid}`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export async function darwinDefinitionState(
|
|
591
|
+
managedCommand: string,
|
|
592
|
+
definition = plistPath(),
|
|
593
|
+
expectedManagedTarget = resolve(
|
|
594
|
+
homedir(),
|
|
595
|
+
".uai-host",
|
|
596
|
+
"runtime",
|
|
597
|
+
"current",
|
|
598
|
+
"bin",
|
|
599
|
+
"uai-host",
|
|
600
|
+
),
|
|
601
|
+
execute: StepExecutor = executeStep,
|
|
602
|
+
): Promise<ServiceDefinitionState> {
|
|
603
|
+
const snapshot = snapshotDefinition(definition);
|
|
604
|
+
const loadedCommand = inspectLaunchdLoadedCommand(execute, managedCommand);
|
|
605
|
+
if (!snapshot.exists && loadedCommand.loaded) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
"launchd reports the host job loaded without a recoverable plist",
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (!snapshot.exists) {
|
|
611
|
+
// launchd's print-disabled map treats an absent label as enabled by
|
|
612
|
+
// default. There is no definition to enable, so normalize a truly missing
|
|
613
|
+
// service to the all-false lifecycle state expected by install admission.
|
|
614
|
+
return {
|
|
615
|
+
ownership: "missing",
|
|
616
|
+
manager: { loaded: false, active: false, enabled: false },
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const manager: ServiceManagerState = {
|
|
620
|
+
loaded: loadedCommand.loaded,
|
|
621
|
+
// This LaunchAgent is KeepAlive+RunAtLoad. A loaded-but-currently-waiting
|
|
622
|
+
// job is launchd restart/throttle pending and must be quiesced/preserved as
|
|
623
|
+
// active even when no daemon answers at the instant of inspection.
|
|
624
|
+
active: loadedCommand.loaded,
|
|
625
|
+
enabled: inspectLaunchdEnabled(execute),
|
|
626
|
+
};
|
|
627
|
+
const args = parsePlistProgramArguments(snapshot.bytes.toString("utf8"));
|
|
628
|
+
if (
|
|
629
|
+
args?.length === 2 &&
|
|
630
|
+
args[0] === managedCommand &&
|
|
631
|
+
args[1] === "run" &&
|
|
632
|
+
managedCommandIsOwned(managedCommand, expectedManagedTarget) &&
|
|
633
|
+
loadedCommand.matches !== false
|
|
634
|
+
) {
|
|
635
|
+
return { ownership: "managed", manager };
|
|
636
|
+
}
|
|
637
|
+
return { ownership: "legacy", manager };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export async function darwinDefinitionOwnership(
|
|
641
|
+
managedCommand: string,
|
|
642
|
+
definition = plistPath(),
|
|
643
|
+
expectedManagedTarget = resolve(
|
|
644
|
+
homedir(),
|
|
645
|
+
".uai-host",
|
|
646
|
+
"runtime",
|
|
647
|
+
"current",
|
|
648
|
+
"bin",
|
|
649
|
+
"uai-host",
|
|
650
|
+
),
|
|
651
|
+
execute: StepExecutor = executeStep,
|
|
652
|
+
): Promise<"missing" | "managed" | "legacy"> {
|
|
653
|
+
return (
|
|
654
|
+
await darwinDefinitionState(
|
|
655
|
+
managedCommand,
|
|
656
|
+
definition,
|
|
657
|
+
expectedManagedTarget,
|
|
658
|
+
execute,
|
|
659
|
+
)
|
|
660
|
+
).ownership;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function inspectLaunchdLoadedCommand(
|
|
664
|
+
execute: StepExecutor,
|
|
665
|
+
managedCommand: string,
|
|
666
|
+
): { loaded: boolean; matches: boolean | null } {
|
|
667
|
+
const args = ["print", target];
|
|
668
|
+
const result = execute(false, "launchctl", args);
|
|
669
|
+
if (probeConfirmsUnloaded(result)) {
|
|
670
|
+
return { loaded: false, matches: null };
|
|
671
|
+
}
|
|
672
|
+
if (result.status !== 0) {
|
|
673
|
+
throw stepFailure(
|
|
674
|
+
"launchctl",
|
|
675
|
+
args,
|
|
676
|
+
result,
|
|
677
|
+
"could not determine the loaded host command",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const programLines = result.stdout
|
|
681
|
+
.split(/\r?\n/)
|
|
682
|
+
.map((line) => /^\s*program = (.*)$/.exec(line)?.[1])
|
|
683
|
+
.filter((value): value is string => value !== undefined);
|
|
684
|
+
const argumentBlocks = [
|
|
685
|
+
...result.stdout.matchAll(/^\s*arguments = \{\s*$([\s\S]*?)^\s*\}\s*$/gm),
|
|
686
|
+
];
|
|
687
|
+
if (programLines.length !== 1 || argumentBlocks.length !== 1) {
|
|
688
|
+
return {
|
|
689
|
+
loaded: true,
|
|
690
|
+
matches: false,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const loadedArgs = (argumentBlocks[0]?.[1] ?? "")
|
|
694
|
+
.split(/\r?\n/)
|
|
695
|
+
.map((line) => line.trim())
|
|
696
|
+
.filter(Boolean);
|
|
697
|
+
return {
|
|
698
|
+
loaded: true,
|
|
699
|
+
matches:
|
|
700
|
+
programLines[0] === managedCommand &&
|
|
701
|
+
loadedArgs.length === 2 &&
|
|
702
|
+
loadedArgs[0] === managedCommand &&
|
|
703
|
+
loadedArgs[1] === "run",
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function parsePlistProgramArguments(contents: string): string[] | null {
|
|
708
|
+
const matches = [
|
|
709
|
+
...contents.matchAll(
|
|
710
|
+
/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/g,
|
|
711
|
+
),
|
|
712
|
+
];
|
|
713
|
+
if (matches.length !== 1) return null;
|
|
714
|
+
const body = matches[0]?.[1] ?? "";
|
|
715
|
+
const args: string[] = [];
|
|
716
|
+
const strings = /<string>([\s\S]*?)<\/string>/g;
|
|
717
|
+
let cursor = 0;
|
|
718
|
+
for (const match of body.matchAll(strings)) {
|
|
719
|
+
const index = match.index ?? 0;
|
|
720
|
+
if (body.slice(cursor, index).trim() !== "") return null;
|
|
721
|
+
const decoded = decodeXmlString(match[1] ?? "");
|
|
722
|
+
if (decoded === null) return null;
|
|
723
|
+
args.push(decoded);
|
|
724
|
+
cursor = index + match[0].length;
|
|
725
|
+
}
|
|
726
|
+
return body.slice(cursor).trim() === "" ? args : null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function decodeXmlString(value: string): string | null {
|
|
730
|
+
const decoded = value.replace(
|
|
731
|
+
/&(amp|lt|gt|quot|apos);/g,
|
|
732
|
+
(_entity, name: string) =>
|
|
733
|
+
({ amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" })[name] ?? "",
|
|
734
|
+
);
|
|
735
|
+
return /&(?:#\d+|#x[0-9a-f]+|[a-z][a-z0-9]*);/i.test(decoded)
|
|
736
|
+
? null
|
|
737
|
+
: decoded;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function managedCommandIsOwned(command: string, expectedTarget: string): boolean {
|
|
741
|
+
try {
|
|
742
|
+
const stat = lstatSync(command);
|
|
743
|
+
if (!stat.isSymbolicLink()) return false;
|
|
744
|
+
const actual = resolve(dirname(command), readlinkSync(command));
|
|
745
|
+
return actual === expectedTarget;
|
|
746
|
+
} catch (error) {
|
|
747
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export async function uninstallDarwinService(
|
|
753
|
+
dryRun = false,
|
|
754
|
+
deps: DarwinServiceDeps = {},
|
|
755
|
+
): Promise<void> {
|
|
756
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
757
|
+
await assertLaunchdUnloaded(
|
|
758
|
+
dryRun,
|
|
759
|
+
deps.execute ?? executeStep,
|
|
760
|
+
deps.delay ?? defaultDelay,
|
|
761
|
+
);
|
|
128
762
|
if (dryRun) {
|
|
129
763
|
console.log(`$ rm -f ${plistPath()}`);
|
|
130
764
|
return;
|
|
131
765
|
}
|
|
132
|
-
|
|
766
|
+
(deps.removeFile ?? ((path) => rmSync(path, { force: true })))(
|
|
767
|
+
deps.definitionPath ?? plistPath(),
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export async function enableDarwinService(
|
|
772
|
+
dryRun = false,
|
|
773
|
+
deps: DarwinServiceDeps = {},
|
|
774
|
+
): Promise<void> {
|
|
775
|
+
await setDarwinServiceEnabled(true, dryRun, deps);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function setDarwinServiceEnabled(
|
|
779
|
+
enabled: boolean,
|
|
780
|
+
dryRun = false,
|
|
781
|
+
deps: DarwinServiceDeps = {},
|
|
782
|
+
): Promise<void> {
|
|
783
|
+
const args = [enabled ? "enable" : "disable", target];
|
|
784
|
+
const result = (deps.execute ?? executeStep)(dryRun, "launchctl", args);
|
|
785
|
+
if (result.status !== 0) {
|
|
786
|
+
throw stepFailure("launchctl", args, result);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function launchdReportsRunning(result: StepResult): boolean {
|
|
791
|
+
return (
|
|
792
|
+
result.status === 0 &&
|
|
793
|
+
/(?:^|\n)\s*state = running(?:\s|$)/.test(result.stdout)
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function kickstartDarwinAndVerify(
|
|
798
|
+
execute: StepExecutor,
|
|
799
|
+
delay: (milliseconds: number) => Promise<void>,
|
|
800
|
+
): Promise<void> {
|
|
801
|
+
const kickstartArgs = ["kickstart", target];
|
|
802
|
+
const kickstart = execute(false, "launchctl", kickstartArgs);
|
|
803
|
+
if (kickstart.status !== 0) {
|
|
804
|
+
throw stepFailure("launchctl", kickstartArgs, kickstart);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const printArgs = ["print", target];
|
|
808
|
+
let probe = execute(false, "launchctl", printArgs);
|
|
809
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
810
|
+
if (launchdReportsRunning(probe)) return;
|
|
811
|
+
if (attempt === 5) break;
|
|
812
|
+
await delay(500);
|
|
813
|
+
probe = execute(false, "launchctl", printArgs);
|
|
814
|
+
}
|
|
815
|
+
throw new Error(
|
|
816
|
+
`\`launchctl ${kickstartArgs.join(" ")}\` did not leave the service running: ${resultDetail(probe)}`,
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export async function startDarwinService(
|
|
821
|
+
dryRun = false,
|
|
822
|
+
deps: DarwinServiceDeps = {},
|
|
823
|
+
): Promise<void> {
|
|
824
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
825
|
+
const execute = deps.execute ?? executeStep;
|
|
826
|
+
await enableDarwinService(dryRun, deps);
|
|
827
|
+
|
|
828
|
+
const bootstrapArgs = ["bootstrap", domain, deps.definitionPath ?? plistPath()];
|
|
829
|
+
const bootstrap = execute(dryRun, "launchctl", bootstrapArgs);
|
|
830
|
+
if (dryRun) {
|
|
831
|
+
execute(true, "launchctl", ["kickstart", target]);
|
|
832
|
+
execute(true, "launchctl", ["print", target]);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (bootstrap.status !== 0) {
|
|
836
|
+
const loaded = execute(false, "launchctl", ["print", target]);
|
|
837
|
+
if (loaded.status !== 0) {
|
|
838
|
+
throw stepFailure(
|
|
839
|
+
"launchctl",
|
|
840
|
+
bootstrapArgs,
|
|
841
|
+
bootstrap,
|
|
842
|
+
`service is not already loaded: ${resultDetail(loaded)}`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
await kickstartDarwinAndVerify(execute, deps.delay ?? defaultDelay);
|
|
133
848
|
}
|
|
134
849
|
|
|
135
|
-
async function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
850
|
+
export async function stopDarwinService(
|
|
851
|
+
dryRun = false,
|
|
852
|
+
deps: DarwinServiceDeps = {},
|
|
853
|
+
): Promise<void> {
|
|
854
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
855
|
+
await assertLaunchdUnloaded(
|
|
856
|
+
dryRun,
|
|
857
|
+
deps.execute ?? executeStep,
|
|
858
|
+
deps.delay ?? defaultDelay,
|
|
859
|
+
);
|
|
142
860
|
}
|
|
143
861
|
|
|
144
|
-
async function
|
|
145
|
-
|
|
862
|
+
export async function stopDarwinServiceForInstall(
|
|
863
|
+
installIntent: string,
|
|
864
|
+
priorManager: ServiceManagerState,
|
|
865
|
+
deps: DarwinServiceDeps = {},
|
|
866
|
+
): Promise<void> {
|
|
867
|
+
assertServiceManagerState(priorManager);
|
|
868
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
869
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
870
|
+
const raw = readPrivateJournal(journalPath);
|
|
871
|
+
if (raw !== null) {
|
|
872
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
873
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
874
|
+
}
|
|
875
|
+
// Disable first so launchd cannot relaunch a waiting/KeepAlive predecessor
|
|
876
|
+
// between a successful unload and the runtime's durable stopped marker.
|
|
877
|
+
await setDarwinServiceEnabled(false, false, deps);
|
|
878
|
+
await assertLaunchdUnloaded(
|
|
879
|
+
false,
|
|
880
|
+
deps.execute ?? executeStep,
|
|
881
|
+
deps.delay ?? defaultDelay,
|
|
882
|
+
);
|
|
146
883
|
}
|
|
147
884
|
|
|
148
|
-
async function
|
|
149
|
-
|
|
885
|
+
export async function resumeDarwinServiceForInstall(
|
|
886
|
+
installIntent: string,
|
|
887
|
+
priorManager: ServiceManagerState,
|
|
888
|
+
deps: DarwinServiceDeps = {},
|
|
889
|
+
): Promise<void> {
|
|
890
|
+
assertServiceManagerState(priorManager);
|
|
891
|
+
const definition = deps.definitionPath ?? plistPath();
|
|
892
|
+
const journalPath = deps.journalPath ?? darwinInstallJournalPath(definition);
|
|
893
|
+
const raw = readPrivateJournal(journalPath);
|
|
894
|
+
if (raw !== null) {
|
|
895
|
+
const journal = parseDarwinInstallJournal(raw);
|
|
896
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
897
|
+
if (journal.phase === "installed") {
|
|
898
|
+
throw new Error("cannot resume the prior service before definition rollback");
|
|
899
|
+
}
|
|
900
|
+
await resumeDarwinDefinitionInstallRollback(deps, installIntent);
|
|
901
|
+
}
|
|
902
|
+
const execute = deps.execute ?? executeStep;
|
|
903
|
+
const delay = deps.delay ?? defaultDelay;
|
|
904
|
+
const definitionPath = deps.definitionPath ?? plistPath();
|
|
905
|
+
await setDarwinServiceEnabled(priorManager.enabled, false, { execute });
|
|
906
|
+
if (priorManager.loaded) {
|
|
907
|
+
await bootstrapWithRetry(execute, delay, definitionPath);
|
|
908
|
+
}
|
|
909
|
+
if (priorManager.active) {
|
|
910
|
+
await kickstartDarwinAndVerify(execute, delay);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function assertServiceManagerState(state: ServiceManagerState): void {
|
|
915
|
+
if (
|
|
916
|
+
typeof state.loaded !== "boolean" ||
|
|
917
|
+
typeof state.active !== "boolean" ||
|
|
918
|
+
typeof state.enabled !== "boolean" ||
|
|
919
|
+
(state.active && !state.loaded)
|
|
920
|
+
) {
|
|
921
|
+
throw new Error("managed service lifecycle snapshot is invalid");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
export async function restartDarwinService(
|
|
926
|
+
dryRun = false,
|
|
927
|
+
deps: DarwinServiceDeps = {},
|
|
928
|
+
): Promise<void> {
|
|
929
|
+
if (!dryRun) await reconcileDarwinDefinitionInstall(deps);
|
|
930
|
+
const args = ["kickstart", "-k", target];
|
|
931
|
+
const result = (deps.execute ?? executeStep)(dryRun, "launchctl", args);
|
|
932
|
+
if (result.status !== 0) {
|
|
933
|
+
throw stepFailure("launchctl", args, result);
|
|
934
|
+
}
|
|
150
935
|
}
|
|
151
936
|
|
|
152
937
|
async function status(): Promise<string> {
|
|
@@ -158,10 +943,23 @@ async function status(): Promise<string> {
|
|
|
158
943
|
}
|
|
159
944
|
|
|
160
945
|
export const installer: Installer = {
|
|
161
|
-
install,
|
|
162
|
-
uninstall,
|
|
163
|
-
start,
|
|
164
|
-
stop,
|
|
165
|
-
|
|
946
|
+
install: installDarwinService,
|
|
947
|
+
uninstall: uninstallDarwinService,
|
|
948
|
+
start: startDarwinService,
|
|
949
|
+
stop: stopDarwinService,
|
|
950
|
+
stopForInstall: (installIntent, priorManager) =>
|
|
951
|
+
stopDarwinServiceForInstall(installIntent, priorManager),
|
|
952
|
+
resumeServiceForInstall: (installIntent, priorManager) =>
|
|
953
|
+
resumeDarwinServiceForInstall(installIntent, priorManager),
|
|
954
|
+
restart: restartDarwinService,
|
|
166
955
|
status,
|
|
956
|
+
reconcileDefinitionInstall: (installIntent) =>
|
|
957
|
+
reconcileDarwinDefinitionInstall({}, installIntent),
|
|
958
|
+
prepareDefinitionInstallRollback: (installIntent) =>
|
|
959
|
+
prepareDarwinDefinitionInstallRollback({}, installIntent),
|
|
960
|
+
resumeDefinitionInstallRollback: (installIntent) =>
|
|
961
|
+
resumeDarwinDefinitionInstallRollback({}, installIntent),
|
|
962
|
+
completeDefinitionInstall: (installIntent) =>
|
|
963
|
+
completeDarwinDefinitionInstall({}, installIntent),
|
|
964
|
+
definitionState: darwinDefinitionState,
|
|
167
965
|
};
|