@runuai/host 0.9.13 → 0.9.42
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 +69 -4
- 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 +3070 -223
- package/lib/preview-sidecar.ts +57 -13
- 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 +945 -0
- package/lib/task-environment/index.ts +364 -0
- package/lib/task-environment/legacy-adoption.ts +443 -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 +1463 -109
- 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 +871 -50
- 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
package/scripts/install/linux.ts
CHANGED
|
@@ -10,12 +10,30 @@
|
|
|
10
10
|
* dedicated headless server a system unit + service user would be the move.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { mkdirSync,
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
|
-
import { dirname, resolve } from "node:path";
|
|
13
|
+
import { lstatSync, mkdirSync, readlinkSync, rmSync } from "node:fs";
|
|
14
|
+
import { homedir, userInfo } from "node:os";
|
|
15
|
+
import { basename, dirname, resolve } from "node:path";
|
|
16
16
|
|
|
17
|
-
import type {
|
|
18
|
-
|
|
17
|
+
import type {
|
|
18
|
+
InstallContext,
|
|
19
|
+
Installer,
|
|
20
|
+
ServiceDefinitionState,
|
|
21
|
+
ServiceManagerState,
|
|
22
|
+
} from "./types";
|
|
23
|
+
import {
|
|
24
|
+
executeStep,
|
|
25
|
+
printFile,
|
|
26
|
+
readPrivateJournal,
|
|
27
|
+
removeFileDurable,
|
|
28
|
+
restoreDefinitionDurable,
|
|
29
|
+
snapshotDefinition,
|
|
30
|
+
step,
|
|
31
|
+
stepFailure,
|
|
32
|
+
writeDefinitionDurable,
|
|
33
|
+
type DirectorySync,
|
|
34
|
+
type StepExecutor,
|
|
35
|
+
type StepResult,
|
|
36
|
+
} from "./util";
|
|
19
37
|
|
|
20
38
|
export const UNIT = "uai-host.service";
|
|
21
39
|
|
|
@@ -23,6 +41,10 @@ export function unitPath(): string {
|
|
|
23
41
|
return resolve(homedir(), ".config", "systemd", "user", UNIT);
|
|
24
42
|
}
|
|
25
43
|
|
|
44
|
+
export function linuxInstallJournalPath(definition = unitPath()): string {
|
|
45
|
+
return resolve(dirname(definition), `.${basename(definition)}.install-journal.json`);
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
export interface UnitInputs {
|
|
27
49
|
execPath: string;
|
|
28
50
|
args: string[];
|
|
@@ -30,8 +52,18 @@ export interface UnitInputs {
|
|
|
30
52
|
path: string;
|
|
31
53
|
}
|
|
32
54
|
|
|
55
|
+
function systemdQuote(value: string): string {
|
|
56
|
+
if (/[\n\r\0]/.test(value)) {
|
|
57
|
+
throw new Error("systemd service values must not contain control characters");
|
|
58
|
+
}
|
|
59
|
+
return `"${value
|
|
60
|
+
.replace(/%/g, "%%")
|
|
61
|
+
.replace(/\\/g, "\\\\")
|
|
62
|
+
.replace(/"/g, '\\"')}"`;
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
export function renderUnit(p: UnitInputs): string {
|
|
34
|
-
const exec = [p.execPath, ...p.args].join(" ");
|
|
66
|
+
const exec = [p.execPath, ...p.args].map(systemdQuote).join(" ");
|
|
35
67
|
return `[Unit]
|
|
36
68
|
Description=Uai host service (ADR-028)
|
|
37
69
|
After=network-online.target
|
|
@@ -39,8 +71,8 @@ Wants=network-online.target
|
|
|
39
71
|
|
|
40
72
|
[Service]
|
|
41
73
|
Type=simple
|
|
42
|
-
WorkingDirectory=${p.cwd}
|
|
43
|
-
Environment
|
|
74
|
+
WorkingDirectory=${systemdQuote(p.cwd)}
|
|
75
|
+
Environment=${systemdQuote(`PATH=${p.path}`)}
|
|
44
76
|
ExecStart=${exec}
|
|
45
77
|
Restart=on-failure
|
|
46
78
|
RestartSec=10
|
|
@@ -55,47 +87,805 @@ function inputs(ctx: InstallContext): UnitInputs {
|
|
|
55
87
|
execPath: ctx.execPath,
|
|
56
88
|
args: ctx.args,
|
|
57
89
|
cwd: ctx.cwd,
|
|
58
|
-
path: process.env.PATH ?? "",
|
|
90
|
+
path: ctx.envPath ?? process.env.PATH ?? "",
|
|
59
91
|
};
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
|
|
94
|
+
export interface LinuxInstallDeps extends LinuxServiceDeps {
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function installLinuxService(
|
|
98
|
+
ctx: InstallContext,
|
|
99
|
+
deps: LinuxInstallDeps = {},
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const execute = deps.execute ?? executeStep;
|
|
63
102
|
const unit = renderUnit(inputs(ctx));
|
|
103
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
64
104
|
if (ctx.dryRun) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
105
|
+
await ensureLinuxLinger(true, deps);
|
|
106
|
+
printFile(definition, unit);
|
|
107
|
+
execute(true, "systemctl", ["--user", "daemon-reload"]);
|
|
108
|
+
execute(
|
|
109
|
+
true,
|
|
110
|
+
"systemctl",
|
|
111
|
+
ctx.startOnInstall === false
|
|
112
|
+
? ["--user", "disable", "--now", UNIT]
|
|
113
|
+
: ["--user", "enable", "--now", UNIT],
|
|
114
|
+
);
|
|
68
115
|
return;
|
|
69
116
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
117
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
118
|
+
const installIntent = ctx.definitionInstallIntent;
|
|
119
|
+
assertDefinitionInstallIntent(installIntent);
|
|
120
|
+
await reconcileLinuxDefinitionInstall(
|
|
121
|
+
{ ...deps, definitionPath: definition, journalPath },
|
|
122
|
+
installIntent?.nonce,
|
|
123
|
+
);
|
|
124
|
+
await ensureLinuxLinger(false, deps);
|
|
125
|
+
mkdirSync(dirname(definition), { recursive: true });
|
|
126
|
+
const previous = snapshotDefinition(definition);
|
|
127
|
+
const manager = inspectSystemdState(execute);
|
|
128
|
+
if (!previous.exists && (manager.active || manager.enabled)) {
|
|
129
|
+
throw new Error("systemd reports the host unit active/enabled without a recoverable unit file");
|
|
130
|
+
}
|
|
131
|
+
const journal: LinuxInstallJournal = {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
phase: "installed",
|
|
134
|
+
installIntent: installIntent ?? null,
|
|
135
|
+
prior: serializeDefinition(previous),
|
|
136
|
+
manager,
|
|
137
|
+
};
|
|
138
|
+
writeDefinitionDurable(journalPath, `${JSON.stringify(journal)}\n`, 0o600);
|
|
139
|
+
try {
|
|
140
|
+
writeDefinitionDurable(
|
|
141
|
+
definition,
|
|
142
|
+
unit,
|
|
143
|
+
0o644,
|
|
144
|
+
deps.definitionDirectorySync,
|
|
145
|
+
);
|
|
146
|
+
strictStep(execute, "systemctl", ["--user", "daemon-reload"]);
|
|
147
|
+
strictStep(
|
|
148
|
+
execute,
|
|
149
|
+
"systemctl",
|
|
150
|
+
ctx.startOnInstall === false
|
|
151
|
+
? ["--user", "disable", "--now", UNIT]
|
|
152
|
+
: ["--user", "enable", "--now", UNIT],
|
|
153
|
+
);
|
|
154
|
+
if (!installIntent) removeFileDurable(journalPath);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
try {
|
|
157
|
+
const rollbackDeps = {
|
|
158
|
+
...deps,
|
|
159
|
+
definitionPath: definition,
|
|
160
|
+
journalPath,
|
|
161
|
+
};
|
|
162
|
+
if (installIntent) {
|
|
163
|
+
await prepareLinuxDefinitionInstallRollback(
|
|
164
|
+
rollbackDeps,
|
|
165
|
+
installIntent.nonce,
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
await reconcileLinuxDefinitionInstall(rollbackDeps);
|
|
169
|
+
}
|
|
170
|
+
} catch (rollbackError) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`${errorMessage(error)}; service-definition rollback failed: ${errorMessage(rollbackError)}`,
|
|
173
|
+
{ cause: error },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
74
178
|
}
|
|
75
179
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
180
|
+
export interface LinuxServiceDeps {
|
|
181
|
+
execute?: StepExecutor;
|
|
182
|
+
removeFile?: (path: string) => void;
|
|
183
|
+
username?: string;
|
|
184
|
+
definitionPath?: string;
|
|
185
|
+
journalPath?: string;
|
|
186
|
+
definitionDirectorySync?: DirectorySync;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const STOPPED_SYSTEMD_STATES = new Set(["inactive", "failed", "unknown"]);
|
|
190
|
+
const ACTIVE_SYSTEMD_STATES = new Set([
|
|
191
|
+
"active",
|
|
192
|
+
"activating",
|
|
193
|
+
"reloading",
|
|
194
|
+
"deactivating",
|
|
195
|
+
]);
|
|
196
|
+
const DISABLED_SYSTEMD_STATES = new Set([
|
|
197
|
+
"disabled",
|
|
198
|
+
"static",
|
|
199
|
+
"indirect",
|
|
200
|
+
"generated",
|
|
201
|
+
"transient",
|
|
202
|
+
"not-found",
|
|
203
|
+
"masked",
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
interface LinuxInstallJournal {
|
|
207
|
+
readonly schemaVersion: 1;
|
|
208
|
+
readonly phase: "installed" | "definition-restored";
|
|
209
|
+
readonly installIntent: {
|
|
210
|
+
readonly nonce: string;
|
|
211
|
+
readonly ownerPid: number;
|
|
212
|
+
} | null;
|
|
213
|
+
readonly prior:
|
|
214
|
+
| { readonly exists: false }
|
|
215
|
+
| {
|
|
216
|
+
readonly exists: true;
|
|
217
|
+
readonly bytesBase64: string;
|
|
218
|
+
readonly mode: number;
|
|
219
|
+
};
|
|
220
|
+
readonly manager: { readonly active: boolean; readonly enabled: boolean };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function assertDefinitionInstallIntent(
|
|
224
|
+
intent: InstallContext["definitionInstallIntent"],
|
|
225
|
+
): void {
|
|
226
|
+
if (
|
|
227
|
+
intent &&
|
|
228
|
+
(!/^[a-f0-9]{64}$/.test(intent.nonce) ||
|
|
229
|
+
!Number.isSafeInteger(intent.ownerPid) ||
|
|
230
|
+
intent.ownerPid < 1)
|
|
231
|
+
) {
|
|
232
|
+
throw new Error("service definition install intent is invalid");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function serializeDefinition(
|
|
237
|
+
snapshot: ReturnType<typeof snapshotDefinition>,
|
|
238
|
+
): LinuxInstallJournal["prior"] {
|
|
239
|
+
return snapshot.exists
|
|
240
|
+
? {
|
|
241
|
+
exists: true,
|
|
242
|
+
bytesBase64: snapshot.bytes.toString("base64"),
|
|
243
|
+
mode: snapshot.mode,
|
|
244
|
+
}
|
|
245
|
+
: { exists: false };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseLinuxInstallJournal(raw: string): LinuxInstallJournal {
|
|
249
|
+
let value: unknown;
|
|
250
|
+
try {
|
|
251
|
+
value = JSON.parse(raw);
|
|
252
|
+
} catch {
|
|
253
|
+
throw new Error("systemd service install journal is invalid");
|
|
254
|
+
}
|
|
255
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
256
|
+
throw new Error("systemd service install journal is invalid");
|
|
257
|
+
}
|
|
258
|
+
const record = value as Record<string, unknown>;
|
|
259
|
+
if (
|
|
260
|
+
Object.keys(record).sort().join("\0") !==
|
|
261
|
+
["installIntent", "manager", "phase", "prior", "schemaVersion"].join(
|
|
262
|
+
"\0",
|
|
263
|
+
) ||
|
|
264
|
+
record.schemaVersion !== 1 ||
|
|
265
|
+
(record.phase !== "installed" && record.phase !== "definition-restored") ||
|
|
266
|
+
!record.manager ||
|
|
267
|
+
typeof record.manager !== "object" ||
|
|
268
|
+
Array.isArray(record.manager) ||
|
|
269
|
+
!record.prior ||
|
|
270
|
+
typeof record.prior !== "object" ||
|
|
271
|
+
Array.isArray(record.prior) ||
|
|
272
|
+
(record.installIntent !== null &&
|
|
273
|
+
(typeof record.installIntent !== "object" || Array.isArray(record.installIntent)))
|
|
274
|
+
) {
|
|
275
|
+
throw new Error("systemd service install journal is invalid");
|
|
276
|
+
}
|
|
277
|
+
const manager = record.manager as Record<string, unknown>;
|
|
278
|
+
const prior = record.prior as Record<string, unknown>;
|
|
279
|
+
const installIntent = record.installIntent as Record<string, unknown> | null;
|
|
280
|
+
if (
|
|
281
|
+
Object.keys(manager).sort().join("\0") !== ["active", "enabled"].join("\0") ||
|
|
282
|
+
typeof manager.active !== "boolean" ||
|
|
283
|
+
typeof manager.enabled !== "boolean" ||
|
|
284
|
+
typeof prior.exists !== "boolean" ||
|
|
285
|
+
(installIntent !== null &&
|
|
286
|
+
(Object.keys(installIntent).sort().join("\0") !== ["nonce", "ownerPid"].join("\0") ||
|
|
287
|
+
typeof installIntent.nonce !== "string" ||
|
|
288
|
+
!/^[a-f0-9]{64}$/.test(installIntent.nonce) ||
|
|
289
|
+
typeof installIntent.ownerPid !== "number" ||
|
|
290
|
+
!Number.isSafeInteger(installIntent.ownerPid) ||
|
|
291
|
+
installIntent.ownerPid < 1))
|
|
292
|
+
) {
|
|
293
|
+
throw new Error("systemd service install journal is invalid");
|
|
294
|
+
}
|
|
295
|
+
if (prior.exists === false) {
|
|
296
|
+
if (
|
|
297
|
+
Object.keys(prior).join("\0") !== "exists" ||
|
|
298
|
+
manager.active ||
|
|
299
|
+
manager.enabled
|
|
300
|
+
) {
|
|
301
|
+
throw new Error("systemd service install journal is invalid");
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
schemaVersion: 1,
|
|
305
|
+
phase: record.phase,
|
|
306
|
+
installIntent: installIntent as LinuxInstallJournal["installIntent"],
|
|
307
|
+
prior: { exists: false },
|
|
308
|
+
manager: manager as LinuxInstallJournal["manager"],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (
|
|
312
|
+
Object.keys(prior).sort().join("\0") !==
|
|
313
|
+
["bytesBase64", "exists", "mode"].join("\0") ||
|
|
314
|
+
typeof prior.bytesBase64 !== "string" ||
|
|
315
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
|
|
316
|
+
prior.bytesBase64,
|
|
317
|
+
) ||
|
|
318
|
+
typeof prior.mode !== "number" ||
|
|
319
|
+
!Number.isInteger(prior.mode) ||
|
|
320
|
+
prior.mode < 0 ||
|
|
321
|
+
prior.mode > 0o7777
|
|
322
|
+
) {
|
|
323
|
+
throw new Error("systemd service install journal is invalid");
|
|
324
|
+
}
|
|
325
|
+
const bytes = Buffer.from(prior.bytesBase64, "base64");
|
|
326
|
+
if (bytes.toString("base64") !== prior.bytesBase64) {
|
|
327
|
+
throw new Error("systemd service install journal is invalid");
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
phase: record.phase,
|
|
332
|
+
installIntent: installIntent as LinuxInstallJournal["installIntent"],
|
|
333
|
+
prior: {
|
|
334
|
+
exists: true,
|
|
335
|
+
bytesBase64: prior.bytesBase64,
|
|
336
|
+
mode: prior.mode,
|
|
337
|
+
},
|
|
338
|
+
manager: manager as LinuxInstallJournal["manager"],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function inspectSystemdState(execute: StepExecutor): {
|
|
343
|
+
active: boolean;
|
|
344
|
+
enabled: boolean;
|
|
345
|
+
} {
|
|
346
|
+
const activeArgs = ["--user", "is-active", UNIT];
|
|
347
|
+
const activeResult = execute(false, "systemctl", activeArgs);
|
|
348
|
+
const activeState = activeResult.stdout.trim();
|
|
349
|
+
if (typeof activeResult.status !== "number") {
|
|
350
|
+
throw stepFailure(
|
|
351
|
+
"systemctl",
|
|
352
|
+
activeArgs,
|
|
353
|
+
activeResult,
|
|
354
|
+
"could not determine prior unit activity",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const active = ACTIVE_SYSTEMD_STATES.has(activeState);
|
|
358
|
+
if (!active && !STOPPED_SYSTEMD_STATES.has(activeState)) {
|
|
359
|
+
throw stepFailure(
|
|
360
|
+
"systemctl",
|
|
361
|
+
activeArgs,
|
|
362
|
+
activeResult,
|
|
363
|
+
"could not determine prior unit activity",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const enabledArgs = ["--user", "is-enabled", UNIT];
|
|
368
|
+
const enabledResult = execute(false, "systemctl", enabledArgs);
|
|
369
|
+
const enabledState = enabledResult.stdout.trim();
|
|
370
|
+
if (typeof enabledResult.status !== "number") {
|
|
371
|
+
throw stepFailure(
|
|
372
|
+
"systemctl",
|
|
373
|
+
enabledArgs,
|
|
374
|
+
enabledResult,
|
|
375
|
+
"could not determine prior unit enablement",
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const enabled = enabledResult.status === 0 && enabledState === "enabled";
|
|
379
|
+
if (!enabled && !DISABLED_SYSTEMD_STATES.has(enabledState)) {
|
|
380
|
+
throw stepFailure(
|
|
381
|
+
"systemctl",
|
|
382
|
+
enabledArgs,
|
|
383
|
+
enabledResult,
|
|
384
|
+
"could not determine prior unit enablement",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return { active, enabled };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function strictStep(execute: StepExecutor, command: string, args: string[]): void {
|
|
391
|
+
const result = execute(false, command, args);
|
|
392
|
+
if (result.status !== 0) throw stepFailure(command, args, result);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function errorMessage(error: unknown): string {
|
|
396
|
+
return error instanceof Error ? error.message : String(error);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function lingerIsEnabled(result: StepResult): boolean {
|
|
400
|
+
return result.status === 0 && result.stdout.trim().toLowerCase() === "yes";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function ensureLinuxLinger(
|
|
404
|
+
dryRun = false,
|
|
405
|
+
deps: LinuxServiceDeps = {},
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
const execute = deps.execute ?? executeStep;
|
|
408
|
+
const username = deps.username ?? userInfo().username;
|
|
409
|
+
const showArgs = ["show-user", username, "-p", "Linger", "--value"];
|
|
410
|
+
const initial = execute(dryRun, "loginctl", showArgs);
|
|
411
|
+
if (dryRun) {
|
|
412
|
+
execute(true, "loginctl", ["enable-linger", username]);
|
|
413
|
+
execute(true, "loginctl", showArgs);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (lingerIsEnabled(initial)) return;
|
|
417
|
+
|
|
418
|
+
const enableArgs = ["enable-linger", username];
|
|
419
|
+
const enabled = execute(false, "loginctl", enableArgs);
|
|
420
|
+
const adminGuidance =
|
|
421
|
+
`ask an administrator to run: sudo loginctl enable-linger ${username}`;
|
|
422
|
+
if (enabled.status !== 0) {
|
|
423
|
+
throw stepFailure("loginctl", enableArgs, enabled, adminGuidance);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const verified = execute(false, "loginctl", showArgs);
|
|
427
|
+
if (!lingerIsEnabled(verified)) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`loginctl did not confirm reboot persistence for uai-host; ${adminGuidance}`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function probeConfirmsStopped(result: StepResult): boolean {
|
|
435
|
+
return (
|
|
436
|
+
typeof result.status === "number" &&
|
|
437
|
+
result.status !== 0 &&
|
|
438
|
+
STOPPED_SYSTEMD_STATES.has(result.stdout.trim())
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function assertSystemdStopped(
|
|
443
|
+
dryRun: boolean,
|
|
444
|
+
actionArgs: string[],
|
|
445
|
+
execute: StepExecutor,
|
|
446
|
+
): void {
|
|
447
|
+
const result = execute(dryRun, "systemctl", actionArgs);
|
|
448
|
+
if (result.status === 0 || dryRun) return;
|
|
449
|
+
|
|
450
|
+
const probeArgs = ["--user", "is-active", UNIT];
|
|
451
|
+
const probe = execute(false, "systemctl", probeArgs);
|
|
452
|
+
if (probeConfirmsStopped(probe)) return;
|
|
453
|
+
|
|
454
|
+
const probeDetail = (
|
|
455
|
+
probe.stderr ||
|
|
456
|
+
probe.stdout ||
|
|
457
|
+
probe.error?.message ||
|
|
458
|
+
`exit status ${String(probe.status)}`
|
|
459
|
+
).trim();
|
|
460
|
+
throw stepFailure(
|
|
461
|
+
"systemctl",
|
|
462
|
+
actionArgs,
|
|
463
|
+
result,
|
|
464
|
+
`could not verify that the service stopped: ${probeDetail}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function reconcileLinuxDefinitionInstall(
|
|
469
|
+
deps: LinuxInstallDeps = {},
|
|
470
|
+
installIntent?: string,
|
|
471
|
+
): Promise<void> {
|
|
472
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
473
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
474
|
+
const raw = readPrivateJournal(journalPath);
|
|
475
|
+
if (raw === null) return;
|
|
476
|
+
const journal = parseLinuxInstallJournal(raw);
|
|
477
|
+
if (journal.installIntent !== null) {
|
|
478
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
479
|
+
throw new Error(
|
|
480
|
+
"public service definition rollback requires runtime command restoration between prepare and resume",
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
await prepareLinuxDefinitionInstallRollback(deps);
|
|
484
|
+
await resumeLinuxDefinitionInstallRollback(deps);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export async function prepareLinuxDefinitionInstallRollback(
|
|
488
|
+
deps: LinuxInstallDeps = {},
|
|
489
|
+
installIntent?: string,
|
|
490
|
+
): Promise<void> {
|
|
491
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
492
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
493
|
+
const raw = readPrivateJournal(journalPath);
|
|
494
|
+
if (raw === null) return;
|
|
495
|
+
const journal = parseLinuxInstallJournal(raw);
|
|
496
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
497
|
+
const execute = deps.execute ?? executeStep;
|
|
498
|
+
|
|
499
|
+
assertSystemdStopped(false, ["--user", "stop", UNIT], execute);
|
|
500
|
+
const prior = journal.prior.exists
|
|
501
|
+
? {
|
|
502
|
+
exists: true as const,
|
|
503
|
+
bytes: Buffer.from(journal.prior.bytesBase64, "base64"),
|
|
504
|
+
mode: journal.prior.mode,
|
|
505
|
+
}
|
|
506
|
+
: ({ exists: false } as const);
|
|
507
|
+
// Persist the disabled state before restoring a definition which may still
|
|
508
|
+
// point through a shared shim. A reboot in the restore/fsync window cannot
|
|
509
|
+
// launch it until runtime-command rollback has completed.
|
|
510
|
+
assertSystemdDisabledForRollback(execute, !prior.exists);
|
|
511
|
+
restoreDefinitionDurable(definition, prior, deps.definitionDirectorySync);
|
|
512
|
+
strictStep(execute, "systemctl", ["--user", "daemon-reload"]);
|
|
513
|
+
|
|
514
|
+
if (journal.phase !== "definition-restored") {
|
|
515
|
+
writeDefinitionDurable(
|
|
516
|
+
journalPath,
|
|
517
|
+
`${JSON.stringify({ ...journal, phase: "definition-restored" })}\n`,
|
|
518
|
+
0o600,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function assertSystemdDisabledForRollback(
|
|
524
|
+
execute: StepExecutor,
|
|
525
|
+
allowMissing: boolean,
|
|
526
|
+
): void {
|
|
527
|
+
const disableArgs = ["--user", "disable", UNIT];
|
|
528
|
+
const disabled = execute(false, "systemctl", disableArgs);
|
|
529
|
+
if (disabled.status === 0) return;
|
|
530
|
+
if (allowMissing) {
|
|
531
|
+
const probeArgs = ["--user", "is-enabled", UNIT];
|
|
532
|
+
const probe = execute(false, "systemctl", probeArgs);
|
|
533
|
+
if (
|
|
534
|
+
probe.status !== 0 &&
|
|
535
|
+
DISABLED_SYSTEMD_STATES.has(probe.stdout.trim())
|
|
536
|
+
) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
throw stepFailure(
|
|
541
|
+
"systemctl",
|
|
542
|
+
disableArgs,
|
|
543
|
+
disabled,
|
|
544
|
+
"could not durably disable the replacement service before rollback",
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export async function resumeLinuxDefinitionInstallRollback(
|
|
549
|
+
deps: LinuxInstallDeps = {},
|
|
550
|
+
installIntent?: string,
|
|
551
|
+
): Promise<void> {
|
|
552
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
553
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
554
|
+
const raw = readPrivateJournal(journalPath);
|
|
555
|
+
if (raw === null) return;
|
|
556
|
+
const journal = parseLinuxInstallJournal(raw);
|
|
557
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
558
|
+
if (journal.phase !== "definition-restored") {
|
|
559
|
+
throw new Error("systemd service definition rollback has not been prepared");
|
|
560
|
+
}
|
|
561
|
+
const execute = deps.execute ?? executeStep;
|
|
562
|
+
|
|
563
|
+
// Nothing may launch between restoring the command graph and resuming the
|
|
564
|
+
// recorded predecessor, so reassert the stopped boundary first.
|
|
565
|
+
assertSystemdStopped(false, ["--user", "stop", UNIT], execute);
|
|
566
|
+
const prior = journal.prior.exists
|
|
567
|
+
? {
|
|
568
|
+
exists: true as const,
|
|
569
|
+
bytes: Buffer.from(journal.prior.bytesBase64, "base64"),
|
|
570
|
+
mode: journal.prior.mode,
|
|
571
|
+
}
|
|
572
|
+
: ({ exists: false } as const);
|
|
573
|
+
restoreDefinitionDurable(definition, prior, deps.definitionDirectorySync);
|
|
574
|
+
strictStep(execute, "systemctl", ["--user", "daemon-reload"]);
|
|
575
|
+
if (prior.exists) {
|
|
576
|
+
strictStep(execute, "systemctl", [
|
|
577
|
+
"--user",
|
|
578
|
+
journal.manager.enabled ? "enable" : "disable",
|
|
579
|
+
UNIT,
|
|
580
|
+
]);
|
|
581
|
+
}
|
|
582
|
+
if (journal.manager.active) {
|
|
583
|
+
strictStep(execute, "systemctl", ["--user", "start", UNIT]);
|
|
584
|
+
}
|
|
585
|
+
removeFileDurable(journalPath);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export async function completeLinuxDefinitionInstall(
|
|
589
|
+
deps: LinuxInstallDeps = {},
|
|
590
|
+
installIntent?: string,
|
|
591
|
+
): Promise<void> {
|
|
592
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
593
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
594
|
+
const raw = readPrivateJournal(journalPath);
|
|
595
|
+
if (raw === null) return;
|
|
596
|
+
const journal = parseLinuxInstallJournal(raw);
|
|
597
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
598
|
+
if (journal.phase !== "installed") {
|
|
599
|
+
throw new Error("cannot commit a service definition whose rollback was prepared");
|
|
600
|
+
}
|
|
601
|
+
removeFileDurable(journalPath);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function assertJournalAccess(
|
|
605
|
+
owner: LinuxInstallJournal["installIntent"],
|
|
606
|
+
installIntent?: string,
|
|
607
|
+
): void {
|
|
608
|
+
if (owner === null) {
|
|
609
|
+
if (installIntent) {
|
|
610
|
+
throw new Error("service definition journal does not belong to this install intent");
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (owner.nonce !== installIntent) {
|
|
615
|
+
throw new Error(
|
|
616
|
+
`service definition update is owned by managed installer PID ${owner.ownerPid}`,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export async function linuxDefinitionState(
|
|
622
|
+
managedCommand: string,
|
|
623
|
+
definition = unitPath(),
|
|
624
|
+
expectedManagedTarget = resolve(
|
|
625
|
+
homedir(),
|
|
626
|
+
".uai-host",
|
|
627
|
+
"runtime",
|
|
628
|
+
"current",
|
|
629
|
+
"bin",
|
|
630
|
+
"uai-host",
|
|
631
|
+
),
|
|
632
|
+
execute: StepExecutor = executeStep,
|
|
633
|
+
): Promise<ServiceDefinitionState> {
|
|
634
|
+
const snapshot = snapshotDefinition(definition);
|
|
635
|
+
const loadedCommand = inspectSystemdLoadedCommand(execute, managedCommand);
|
|
636
|
+
if (!snapshot.exists && loadedCommand.loaded) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
"systemd reports the host unit loaded without a recoverable unit file",
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
if (!snapshot.exists) {
|
|
642
|
+
return {
|
|
643
|
+
ownership: "missing",
|
|
644
|
+
manager: { loaded: false, active: false, enabled: false },
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const managerState = inspectSystemdState(execute);
|
|
648
|
+
const manager: ServiceManagerState = {
|
|
649
|
+
loaded: loadedCommand.loaded,
|
|
650
|
+
active: managerState.active,
|
|
651
|
+
enabled: managerState.enabled,
|
|
652
|
+
};
|
|
653
|
+
const execStarts = snapshot.bytes
|
|
654
|
+
.toString("utf8")
|
|
655
|
+
.split(/\r?\n/)
|
|
656
|
+
.filter((line) => line.startsWith("ExecStart="));
|
|
657
|
+
const expected = `ExecStart=${[managedCommand, "run"].map(systemdQuote).join(" ")}`;
|
|
658
|
+
if (
|
|
659
|
+
execStarts.length === 1 &&
|
|
660
|
+
execStarts[0] === expected &&
|
|
661
|
+
managedCommandIsOwned(managedCommand, expectedManagedTarget) &&
|
|
662
|
+
loadedCommand.matches !== false
|
|
663
|
+
) {
|
|
664
|
+
return { ownership: "managed", manager };
|
|
665
|
+
}
|
|
666
|
+
return { ownership: "legacy", manager };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export async function linuxDefinitionOwnership(
|
|
670
|
+
managedCommand: string,
|
|
671
|
+
definition = unitPath(),
|
|
672
|
+
expectedManagedTarget = resolve(
|
|
673
|
+
homedir(),
|
|
674
|
+
".uai-host",
|
|
675
|
+
"runtime",
|
|
676
|
+
"current",
|
|
677
|
+
"bin",
|
|
678
|
+
"uai-host",
|
|
679
|
+
),
|
|
680
|
+
execute: StepExecutor = executeStep,
|
|
681
|
+
): Promise<"missing" | "managed" | "legacy"> {
|
|
682
|
+
return (
|
|
683
|
+
await linuxDefinitionState(
|
|
684
|
+
managedCommand,
|
|
685
|
+
definition,
|
|
686
|
+
expectedManagedTarget,
|
|
687
|
+
execute,
|
|
688
|
+
)
|
|
689
|
+
).ownership;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function inspectSystemdLoadedCommand(
|
|
693
|
+
execute: StepExecutor,
|
|
694
|
+
managedCommand: string,
|
|
695
|
+
): { loaded: boolean; matches: boolean | null } {
|
|
696
|
+
const loadArgs = ["--user", "show", UNIT, "--property=LoadState", "--value"];
|
|
697
|
+
const load = execute(false, "systemctl", loadArgs);
|
|
698
|
+
const loadState = load.stdout.trim();
|
|
699
|
+
if (typeof load.status !== "number") {
|
|
700
|
+
throw stepFailure(
|
|
701
|
+
"systemctl",
|
|
702
|
+
loadArgs,
|
|
703
|
+
load,
|
|
704
|
+
"could not determine the loaded host command",
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (load.status !== 0) {
|
|
708
|
+
if (/not[- ]found|could not be found|no such (?:file|unit)/i.test(
|
|
709
|
+
`${load.stdout}\n${load.stderr}`,
|
|
710
|
+
)) {
|
|
711
|
+
return { loaded: false, matches: null };
|
|
712
|
+
}
|
|
713
|
+
throw stepFailure(
|
|
714
|
+
"systemctl",
|
|
715
|
+
loadArgs,
|
|
716
|
+
load,
|
|
717
|
+
"could not determine the loaded host command",
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
if (loadState === "not-found") return { loaded: false, matches: null };
|
|
721
|
+
if (loadState !== "loaded") return { loaded: false, matches: false };
|
|
722
|
+
|
|
723
|
+
const execArgs = ["--user", "show", UNIT, "--property=ExecStart", "--value"];
|
|
724
|
+
const result = execute(false, "systemctl", execArgs);
|
|
725
|
+
if (typeof result.status !== "number") {
|
|
726
|
+
throw stepFailure(
|
|
727
|
+
"systemctl",
|
|
728
|
+
execArgs,
|
|
729
|
+
result,
|
|
730
|
+
"could not determine the loaded host command",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
if (result.status !== 0) {
|
|
734
|
+
throw stepFailure(
|
|
735
|
+
"systemctl",
|
|
736
|
+
execArgs,
|
|
737
|
+
result,
|
|
738
|
+
"could not determine the loaded host command",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const records = [
|
|
742
|
+
...result.stdout.matchAll(
|
|
743
|
+
/\{\s*path=([^;]*?)\s*;\s*argv\[\]=([^;]*?)\s*;/g,
|
|
744
|
+
),
|
|
745
|
+
];
|
|
746
|
+
return {
|
|
747
|
+
loaded: true,
|
|
748
|
+
matches:
|
|
749
|
+
records.length === 1 &&
|
|
750
|
+
records[0]?.[1] === managedCommand &&
|
|
751
|
+
records[0]?.[2] === `${managedCommand} run`,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function managedCommandIsOwned(command: string, expectedTarget: string): boolean {
|
|
756
|
+
try {
|
|
757
|
+
const stat = lstatSync(command);
|
|
758
|
+
if (!stat.isSymbolicLink()) return false;
|
|
759
|
+
const actual = resolve(dirname(command), readlinkSync(command));
|
|
760
|
+
return actual === expectedTarget;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export async function uninstallLinuxService(
|
|
768
|
+
dryRun = false,
|
|
769
|
+
deps: LinuxServiceDeps = {},
|
|
770
|
+
): Promise<void> {
|
|
771
|
+
if (!dryRun) await reconcileLinuxDefinitionInstall(deps);
|
|
772
|
+
const execute = deps.execute ?? executeStep;
|
|
773
|
+
assertSystemdStopped(
|
|
774
|
+
dryRun,
|
|
775
|
+
["--user", "disable", "--now", UNIT],
|
|
776
|
+
execute,
|
|
777
|
+
);
|
|
80
778
|
if (dryRun) {
|
|
81
779
|
console.log(`$ rm -f ${unitPath()}`);
|
|
82
|
-
|
|
780
|
+
execute(true, "systemctl", ["--user", "daemon-reload"]);
|
|
83
781
|
return;
|
|
84
782
|
}
|
|
85
|
-
|
|
86
|
-
|
|
783
|
+
(deps.removeFile ?? ((path) => rmSync(path, { force: true })))(
|
|
784
|
+
deps.definitionPath ?? unitPath(),
|
|
785
|
+
);
|
|
786
|
+
const reloadArgs = ["--user", "daemon-reload"];
|
|
787
|
+
const reload = execute(false, "systemctl", reloadArgs);
|
|
788
|
+
if (reload.status !== 0) {
|
|
789
|
+
throw stepFailure("systemctl", reloadArgs, reload);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export async function startLinuxService(
|
|
794
|
+
dryRun = false,
|
|
795
|
+
deps: LinuxServiceDeps = {},
|
|
796
|
+
): Promise<void> {
|
|
797
|
+
if (!dryRun) await reconcileLinuxDefinitionInstall(deps);
|
|
798
|
+
const args = ["--user", "enable", "--now", UNIT];
|
|
799
|
+
const result = (deps.execute ?? executeStep)(dryRun, "systemctl", args);
|
|
800
|
+
if (result.status !== 0) throw stepFailure("systemctl", args, result);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export async function stopLinuxService(
|
|
804
|
+
dryRun = false,
|
|
805
|
+
deps: LinuxServiceDeps = {},
|
|
806
|
+
): Promise<void> {
|
|
807
|
+
if (!dryRun) await reconcileLinuxDefinitionInstall(deps);
|
|
808
|
+
assertSystemdStopped(
|
|
809
|
+
dryRun,
|
|
810
|
+
["--user", "stop", UNIT],
|
|
811
|
+
deps.execute ?? executeStep,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export async function stopLinuxServiceForInstall(
|
|
816
|
+
installIntent: string,
|
|
817
|
+
priorManager: ServiceManagerState,
|
|
818
|
+
deps: LinuxServiceDeps = {},
|
|
819
|
+
): Promise<void> {
|
|
820
|
+
assertServiceManagerState(priorManager);
|
|
821
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
822
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
823
|
+
const raw = readPrivateJournal(journalPath);
|
|
824
|
+
if (raw !== null) {
|
|
825
|
+
const journal = parseLinuxInstallJournal(raw);
|
|
826
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
827
|
+
}
|
|
828
|
+
// Disable before stopping so a pending automatic restart or reboot cannot
|
|
829
|
+
// relaunch the legacy definition after quiescence is durably acknowledged.
|
|
830
|
+
strictStep(deps.execute ?? executeStep, "systemctl", [
|
|
831
|
+
"--user",
|
|
832
|
+
"disable",
|
|
833
|
+
UNIT,
|
|
834
|
+
]);
|
|
835
|
+
assertSystemdStopped(
|
|
836
|
+
false,
|
|
837
|
+
["--user", "stop", UNIT],
|
|
838
|
+
deps.execute ?? executeStep,
|
|
839
|
+
);
|
|
87
840
|
}
|
|
88
841
|
|
|
89
|
-
async function
|
|
90
|
-
|
|
842
|
+
export async function resumeLinuxServiceForInstall(
|
|
843
|
+
installIntent: string,
|
|
844
|
+
priorManager: ServiceManagerState,
|
|
845
|
+
deps: LinuxServiceDeps = {},
|
|
846
|
+
): Promise<void> {
|
|
847
|
+
assertServiceManagerState(priorManager);
|
|
848
|
+
const definition = deps.definitionPath ?? unitPath();
|
|
849
|
+
const journalPath = deps.journalPath ?? linuxInstallJournalPath(definition);
|
|
850
|
+
const raw = readPrivateJournal(journalPath);
|
|
851
|
+
if (raw !== null) {
|
|
852
|
+
const journal = parseLinuxInstallJournal(raw);
|
|
853
|
+
assertJournalAccess(journal.installIntent, installIntent);
|
|
854
|
+
if (journal.phase === "installed") {
|
|
855
|
+
throw new Error("cannot resume the prior service before definition rollback");
|
|
856
|
+
}
|
|
857
|
+
await resumeLinuxDefinitionInstallRollback(deps, installIntent);
|
|
858
|
+
}
|
|
859
|
+
const execute = deps.execute ?? executeStep;
|
|
860
|
+
strictStep(execute, "systemctl", [
|
|
861
|
+
"--user",
|
|
862
|
+
priorManager.enabled ? "enable" : "disable",
|
|
863
|
+
UNIT,
|
|
864
|
+
]);
|
|
865
|
+
if (priorManager.active) {
|
|
866
|
+
strictStep(execute, "systemctl", ["--user", "start", UNIT]);
|
|
867
|
+
}
|
|
91
868
|
}
|
|
92
869
|
|
|
93
|
-
|
|
94
|
-
|
|
870
|
+
function assertServiceManagerState(state: ServiceManagerState): void {
|
|
871
|
+
if (
|
|
872
|
+
typeof state.loaded !== "boolean" ||
|
|
873
|
+
typeof state.active !== "boolean" ||
|
|
874
|
+
typeof state.enabled !== "boolean" ||
|
|
875
|
+
(state.active && !state.loaded)
|
|
876
|
+
) {
|
|
877
|
+
throw new Error("managed service lifecycle snapshot is invalid");
|
|
878
|
+
}
|
|
95
879
|
}
|
|
96
880
|
|
|
97
|
-
async function
|
|
98
|
-
|
|
881
|
+
export async function restartLinuxService(
|
|
882
|
+
dryRun = false,
|
|
883
|
+
deps: LinuxServiceDeps = {},
|
|
884
|
+
): Promise<void> {
|
|
885
|
+
if (!dryRun) await reconcileLinuxDefinitionInstall(deps);
|
|
886
|
+
const args = ["--user", "restart", UNIT];
|
|
887
|
+
const result = (deps.execute ?? executeStep)(dryRun, "systemctl", args);
|
|
888
|
+
if (result.status !== 0) throw stepFailure("systemctl", args, result);
|
|
99
889
|
}
|
|
100
890
|
|
|
101
891
|
async function status(): Promise<string> {
|
|
@@ -106,10 +896,23 @@ async function status(): Promise<string> {
|
|
|
106
896
|
}
|
|
107
897
|
|
|
108
898
|
export const installer: Installer = {
|
|
109
|
-
install,
|
|
110
|
-
uninstall,
|
|
111
|
-
start,
|
|
112
|
-
stop,
|
|
113
|
-
|
|
899
|
+
install: installLinuxService,
|
|
900
|
+
uninstall: uninstallLinuxService,
|
|
901
|
+
start: startLinuxService,
|
|
902
|
+
stop: stopLinuxService,
|
|
903
|
+
stopForInstall: (installIntent, priorManager) =>
|
|
904
|
+
stopLinuxServiceForInstall(installIntent, priorManager),
|
|
905
|
+
resumeServiceForInstall: (installIntent, priorManager) =>
|
|
906
|
+
resumeLinuxServiceForInstall(installIntent, priorManager),
|
|
907
|
+
restart: restartLinuxService,
|
|
114
908
|
status,
|
|
909
|
+
reconcileDefinitionInstall: (installIntent) =>
|
|
910
|
+
reconcileLinuxDefinitionInstall({}, installIntent),
|
|
911
|
+
prepareDefinitionInstallRollback: (installIntent) =>
|
|
912
|
+
prepareLinuxDefinitionInstallRollback({}, installIntent),
|
|
913
|
+
resumeDefinitionInstallRollback: (installIntent) =>
|
|
914
|
+
resumeLinuxDefinitionInstallRollback({}, installIntent),
|
|
915
|
+
completeDefinitionInstall: (installIntent) =>
|
|
916
|
+
completeLinuxDefinitionInstall({}, installIntent),
|
|
917
|
+
definitionState: linuxDefinitionState,
|
|
115
918
|
};
|