systemd-ts 0.3.0 → 0.3.1
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 +61 -0
- package/dist/index-DeNBLyGA.d.mts +1612 -0
- package/dist/index.d.mts +2 -1612
- package/dist/index.mjs +1 -1609
- package/dist/main-BXq7TQdg.mjs +1610 -0
- package/dist/test.d.mts +29 -1
- package/dist/test.mjs +93 -16
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,1610 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
//#region src/main/errors.ts
|
|
9
|
-
var SystemdTsError = class extends Error {
|
|
10
|
-
code;
|
|
11
|
-
constructor(code, message, options = {}) {
|
|
12
|
-
super(message, { cause: options.cause });
|
|
13
|
-
this.code = code;
|
|
14
|
-
this.name = new.target.name;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
var InvalidExecDirectiveError = class extends SystemdTsError {
|
|
18
|
-
directive;
|
|
19
|
-
constructor(directive, options = {}) {
|
|
20
|
-
super(`SYSTEMD_TS_INVALID_EXEC_DIRECTIVE`, `${directive} must use an absolute executable path for systemd`, options);
|
|
21
|
-
this.directive = directive;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
var NoUnitsProvidedError = class extends SystemdTsError {
|
|
25
|
-
operation;
|
|
26
|
-
constructor(operation, options = {}) {
|
|
27
|
-
super(`SYSTEMD_TS_NO_UNITS_PROVIDED`, `${operation} requires at least one service or timer`, options);
|
|
28
|
-
this.operation = operation;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
var ExecutableInferenceError = class extends SystemdTsError {
|
|
32
|
-
constructor(options = {}) {
|
|
33
|
-
super(`SYSTEMD_TS_EXECUTABLE_INFERENCE`, `Could not infer the calling module path for defineExecutable(); pass { modulePath } explicitly`, options);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
var UnitMaterializationError = class extends SystemdTsError {
|
|
37
|
-
operation;
|
|
38
|
-
reason;
|
|
39
|
-
unitName;
|
|
40
|
-
unitPath;
|
|
41
|
-
constructor(message, options = {}) {
|
|
42
|
-
super(`SYSTEMD_TS_UNIT_MATERIALIZATION`, message, options);
|
|
43
|
-
this.operation = options.operation;
|
|
44
|
-
this.reason = options.reason;
|
|
45
|
-
this.unitName = options.unitName;
|
|
46
|
-
this.unitPath = options.unitPath;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
var UnitEnableError = class extends SystemdTsError {
|
|
50
|
-
args;
|
|
51
|
-
command;
|
|
52
|
-
environmentReason;
|
|
53
|
-
exitCode;
|
|
54
|
-
stage;
|
|
55
|
-
stderr;
|
|
56
|
-
stdout;
|
|
57
|
-
unitName;
|
|
58
|
-
unitPath;
|
|
59
|
-
constructor(message, options = {}) {
|
|
60
|
-
super(`SYSTEMD_TS_UNIT_ENABLE`, message, options);
|
|
61
|
-
const details = extractCommandErrorDetails(options.cause);
|
|
62
|
-
this.args = options.args;
|
|
63
|
-
this.command = options.command;
|
|
64
|
-
this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
|
|
65
|
-
this.exitCode = details.exitCode;
|
|
66
|
-
this.stage = options.stage;
|
|
67
|
-
this.stderr = details.stderr;
|
|
68
|
-
this.stdout = details.stdout;
|
|
69
|
-
this.unitName = options.unitName;
|
|
70
|
-
this.unitPath = options.unitPath;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
var UnitStartError = class extends SystemdTsError {
|
|
74
|
-
args;
|
|
75
|
-
command;
|
|
76
|
-
diagnostics;
|
|
77
|
-
environmentReason;
|
|
78
|
-
exitCode;
|
|
79
|
-
stage;
|
|
80
|
-
stderr;
|
|
81
|
-
stdout;
|
|
82
|
-
unitName;
|
|
83
|
-
unitPath;
|
|
84
|
-
constructor(message, options = {}) {
|
|
85
|
-
super(`SYSTEMD_TS_UNIT_START`, message, options);
|
|
86
|
-
const details = extractCommandErrorDetails(options.cause);
|
|
87
|
-
this.args = options.args;
|
|
88
|
-
this.command = options.command;
|
|
89
|
-
this.diagnostics = options.diagnostics;
|
|
90
|
-
this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
|
|
91
|
-
this.exitCode = details.exitCode;
|
|
92
|
-
this.stage = options.stage;
|
|
93
|
-
this.stderr = details.stderr;
|
|
94
|
-
this.stdout = details.stdout;
|
|
95
|
-
this.unitName = options.unitName;
|
|
96
|
-
this.unitPath = options.unitPath;
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
var UnitLogsReadError = class extends SystemdTsError {
|
|
100
|
-
args;
|
|
101
|
-
command;
|
|
102
|
-
environmentReason;
|
|
103
|
-
exitCode;
|
|
104
|
-
reason;
|
|
105
|
-
stage;
|
|
106
|
-
stderr;
|
|
107
|
-
stdout;
|
|
108
|
-
unitName;
|
|
109
|
-
unitPath;
|
|
110
|
-
constructor(message, options = {}) {
|
|
111
|
-
super(`SYSTEMD_TS_UNIT_LOGS`, message, options);
|
|
112
|
-
const details = extractCommandErrorDetails(options.cause);
|
|
113
|
-
this.args = options.args;
|
|
114
|
-
this.command = options.command;
|
|
115
|
-
this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
|
|
116
|
-
this.exitCode = details.exitCode;
|
|
117
|
-
this.reason = options.reason;
|
|
118
|
-
this.stage = options.stage;
|
|
119
|
-
this.stderr = details.stderr;
|
|
120
|
-
this.stdout = details.stdout;
|
|
121
|
-
this.unitName = options.unitName;
|
|
122
|
-
this.unitPath = options.unitPath;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
var NotifySendError = class extends SystemdTsError {
|
|
126
|
-
args;
|
|
127
|
-
command;
|
|
128
|
-
environmentReason;
|
|
129
|
-
exitCode;
|
|
130
|
-
reason;
|
|
131
|
-
stage;
|
|
132
|
-
stderr;
|
|
133
|
-
stdout;
|
|
134
|
-
constructor(message, options = {}) {
|
|
135
|
-
super(`SYSTEMD_TS_NOTIFY_SEND`, message, options);
|
|
136
|
-
const details = extractCommandErrorDetails(options.cause);
|
|
137
|
-
this.args = options.args;
|
|
138
|
-
this.command = options.command;
|
|
139
|
-
this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
|
|
140
|
-
this.exitCode = details.exitCode;
|
|
141
|
-
this.reason = options.reason;
|
|
142
|
-
this.stage = options.stage;
|
|
143
|
-
this.stderr = details.stderr;
|
|
144
|
-
this.stdout = details.stdout;
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
var SystemctlCommandError = class extends SystemdTsError {
|
|
148
|
-
args;
|
|
149
|
-
command;
|
|
150
|
-
environmentReason;
|
|
151
|
-
exitCode;
|
|
152
|
-
operation;
|
|
153
|
-
reason;
|
|
154
|
-
stage;
|
|
155
|
-
stderr;
|
|
156
|
-
stdout;
|
|
157
|
-
constructor(message, options = {}) {
|
|
158
|
-
super(`SYSTEMD_TS_SYSTEMCTL`, message, options);
|
|
159
|
-
const details = extractCommandErrorDetails(options.cause);
|
|
160
|
-
this.args = options.args;
|
|
161
|
-
this.command = options.command;
|
|
162
|
-
this.environmentReason = options.environmentReason ?? classifyCommandEnvironmentReason(options.cause);
|
|
163
|
-
this.exitCode = details.exitCode;
|
|
164
|
-
this.operation = options.operation;
|
|
165
|
-
this.reason = options.reason;
|
|
166
|
-
this.stage = options.stage;
|
|
167
|
-
this.stderr = details.stderr;
|
|
168
|
-
this.stdout = details.stdout;
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
function classifyCommandEnvironmentReason(cause) {
|
|
172
|
-
if (cause === null || typeof cause !== `object`) return;
|
|
173
|
-
const record = cause;
|
|
174
|
-
if (record[`code`] === `ENOENT`) return `missing-command`;
|
|
175
|
-
if (record[`code`] === `EACCES` || record[`code`] === `EPERM`) return `permission-denied`;
|
|
176
|
-
const combinedOutput = [record[`stderr`], record[`stdout`]].filter((value) => typeof value === `string` && value.length > 0).join(`\n`);
|
|
177
|
-
if (combinedOutput.includes(`Failed to connect to bus`) || combinedOutput.includes(`System has not been booted with systemd`) || combinedOutput.includes(`Failed to get D-Bus connection`)) return `manager-unavailable`;
|
|
178
|
-
}
|
|
179
|
-
function classifyMaterializationReason(cause) {
|
|
180
|
-
if (cause === null || typeof cause !== `object`) return;
|
|
181
|
-
const record = cause;
|
|
182
|
-
if (record[`code`] === `EACCES` || record[`code`] === `EPERM` || record[`code`] === `EROFS`) return `permission-denied`;
|
|
183
|
-
if (record[`code`] === `EEXIST` || record[`code`] === `ENOTDIR` || record[`code`] === `EISDIR`) return `invalid-unit-directory`;
|
|
184
|
-
return `file-system-failed`;
|
|
185
|
-
}
|
|
186
|
-
function extractCommandErrorDetails(cause) {
|
|
187
|
-
if (cause === null || typeof cause !== `object`) return {
|
|
188
|
-
exitCode: void 0,
|
|
189
|
-
stderr: void 0,
|
|
190
|
-
stdout: void 0
|
|
191
|
-
};
|
|
192
|
-
const record = cause;
|
|
193
|
-
return {
|
|
194
|
-
exitCode: typeof record[`code`] === `number` ? record[`code`] : void 0,
|
|
195
|
-
stderr: typeof record[`stderr`] === `string` ? record[`stderr`] : void 0,
|
|
196
|
-
stdout: typeof record[`stdout`] === `string` ? record[`stdout`] : void 0
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
//#endregion
|
|
200
|
-
//#region src/main/executable.ts
|
|
201
|
-
const currentModulePath = fileURLToPath(import.meta.url);
|
|
202
|
-
/**
|
|
203
|
-
* An immutable description of a runnable module entrypoint that can be used in
|
|
204
|
-
* executable-valued systemd unit directives.
|
|
205
|
-
*
|
|
206
|
-
* `Executable` captures three pieces of information:
|
|
207
|
-
* - the absolute runtime binary that should launch the module
|
|
208
|
-
* - the absolute path to the module itself
|
|
209
|
-
* - any additional arguments that should be passed after the module path
|
|
210
|
-
*
|
|
211
|
-
* Most consumers will create one via {@link defineExecutable} and then pass it
|
|
212
|
-
* into a {@link SystemdService} directive such as `ExecStart`, `ExecStop`, or
|
|
213
|
-
* `ExecReload`.
|
|
214
|
-
*/
|
|
215
|
-
var Executable = class {
|
|
216
|
-
/** Additional arguments passed after the module path. */
|
|
217
|
-
args;
|
|
218
|
-
/** Inference issue captured when no explicit module path was provided. */
|
|
219
|
-
inferenceError;
|
|
220
|
-
/** Absolute path to the module that should be executed. */
|
|
221
|
-
modulePath;
|
|
222
|
-
/** Absolute path to the runtime binary that should launch the module. */
|
|
223
|
-
runtimeEntrypoint;
|
|
224
|
-
/**
|
|
225
|
-
* Creates an executable description.
|
|
226
|
-
*
|
|
227
|
-
* When `modulePath` is omitted, the calling module is inferred from the
|
|
228
|
-
* current stack so `defineExecutable()` can be used inline from the module
|
|
229
|
-
* that should become runnable.
|
|
230
|
-
*
|
|
231
|
-
* When `runtimeEntrypoint` is omitted, the current process executable is
|
|
232
|
-
* used. This works well for the common case where the same runtime that is
|
|
233
|
-
* evaluating your module should also be used by systemd.
|
|
234
|
-
*/
|
|
235
|
-
constructor(options = {}) {
|
|
236
|
-
this.runtimeEntrypoint = options.runtimeEntrypoint ?? process.execPath;
|
|
237
|
-
const inferredModulePath = options.modulePath ?? tryInferCallerModulePath();
|
|
238
|
-
this.inferenceError = inferredModulePath === void 0 && options.modulePath === void 0 ? new ExecutableInferenceError() : void 0;
|
|
239
|
-
this.modulePath = inferredModulePath ?? ``;
|
|
240
|
-
this.args = Object.freeze([...options.args ?? []]);
|
|
241
|
-
Object.freeze(this);
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Returns the executable as raw command parts.
|
|
245
|
-
*
|
|
246
|
-
* The first element is always the runtime entrypoint, followed by the module
|
|
247
|
-
* path and any configured arguments.
|
|
248
|
-
*/
|
|
249
|
-
toCommandParts() {
|
|
250
|
-
if (this.inferenceError !== void 0) return {
|
|
251
|
-
ok: false,
|
|
252
|
-
error: this.inferenceError
|
|
253
|
-
};
|
|
254
|
-
return {
|
|
255
|
-
ok: true,
|
|
256
|
-
value: [
|
|
257
|
-
this.runtimeEntrypoint,
|
|
258
|
-
this.modulePath,
|
|
259
|
-
...this.args
|
|
260
|
-
]
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Renders the executable as a shell-quoted command string suitable for
|
|
265
|
-
* executable-valued systemd directives such as `ExecStart=`.
|
|
266
|
-
*/
|
|
267
|
-
toExecStart() {
|
|
268
|
-
const commandParts = this.toCommandParts();
|
|
269
|
-
if (!commandParts.ok) return commandParts;
|
|
270
|
-
return {
|
|
271
|
-
ok: true,
|
|
272
|
-
value: commandParts.value.map(shellQuote).join(` `)
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
/**
|
|
277
|
-
* Defines a module as a runnable executable and returns its immutable
|
|
278
|
-
* `Executable` description.
|
|
279
|
-
*
|
|
280
|
-
* This helper is designed for the pattern:
|
|
281
|
-
*
|
|
282
|
-
* ```ts
|
|
283
|
-
* export default defineExecutable(async () => {
|
|
284
|
-
* // do work here
|
|
285
|
-
* });
|
|
286
|
-
* ```
|
|
287
|
-
*
|
|
288
|
-
* When the defining module is executed as the main entrypoint, `fn` is invoked.
|
|
289
|
-
* When the module is merely imported, `fn` is not run and only the executable
|
|
290
|
-
* description is returned.
|
|
291
|
-
*
|
|
292
|
-
* Pass `options.runtimeEntrypoint` to override the default runtime binary when
|
|
293
|
-
* the current process is not the exact runtime you want systemd to use.
|
|
294
|
-
*/
|
|
295
|
-
function defineExecutable(fn, options = {}) {
|
|
296
|
-
const executable = new Executable(options);
|
|
297
|
-
if (executable.inferenceError === void 0 && isMainModule(executable.modulePath)) Promise.resolve(fn()).catch((error) => {
|
|
298
|
-
process.exitCode = 1;
|
|
299
|
-
throw error;
|
|
300
|
-
});
|
|
301
|
-
return executable;
|
|
302
|
-
}
|
|
303
|
-
function tryInferCallerModulePath() {
|
|
304
|
-
const stack = (/* @__PURE__ */ new Error()).stack ?? ``;
|
|
305
|
-
for (const line of stack.split(`\n`).slice(1)) {
|
|
306
|
-
const candidate = extractStackPath(line);
|
|
307
|
-
if (candidate === void 0 || candidate === currentModulePath) continue;
|
|
308
|
-
return candidate;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
function extractStackPath(line) {
|
|
312
|
-
const fileUrlMatch = line.match(/(file:\/\/\/[^)\s:]+(?:\.[cm]?[jt]s)?)/u);
|
|
313
|
-
if (fileUrlMatch !== null) return fileURLToPath(fileUrlMatch[1]);
|
|
314
|
-
const pathMatch = line.match(/(\/[^)\s:]+(?:\.[cm]?[jt]s)?)/u);
|
|
315
|
-
if (pathMatch !== null) return pathMatch[1];
|
|
316
|
-
}
|
|
317
|
-
function isMainModule(modulePath) {
|
|
318
|
-
const mainArg = process.argv[1];
|
|
319
|
-
if (mainArg === void 0) return false;
|
|
320
|
-
return normalizeFilePath(mainArg) === normalizeFilePath(modulePath);
|
|
321
|
-
}
|
|
322
|
-
function shellQuote(value) {
|
|
323
|
-
return `'${value.replaceAll(`'`, `'\\''`)}'`;
|
|
324
|
-
}
|
|
325
|
-
function normalizeFilePath(path) {
|
|
326
|
-
try {
|
|
327
|
-
return realpathSync(path);
|
|
328
|
-
} catch {
|
|
329
|
-
return path;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
//#endregion
|
|
333
|
-
//#region src/main/internal.ts
|
|
334
|
-
var internal_exports = /* @__PURE__ */ __exportAll({
|
|
335
|
-
cloneUnitSection: () => cloneUnitSection,
|
|
336
|
-
defaultCommandExecutor: () => defaultCommandExecutor,
|
|
337
|
-
defaultUnitDirForScope: () => defaultUnitDirForScope,
|
|
338
|
-
extractCommandOutput: () => extractCommandOutput,
|
|
339
|
-
fileExists: () => fileExists,
|
|
340
|
-
freezeUnitOptions: () => freezeUnitOptions,
|
|
341
|
-
normalizeUnitName: () => normalizeUnitName,
|
|
342
|
-
parseStartStatus: () => parseStartStatus,
|
|
343
|
-
renderUnitFile: () => renderUnitFile,
|
|
344
|
-
resolveTimerTargetUnit: () => resolveTimerTargetUnit,
|
|
345
|
-
sendNotify: () => sendNotify,
|
|
346
|
-
validateServiceSection: () => validateServiceSection
|
|
347
|
-
});
|
|
348
|
-
const EXEC_DIRECTIVE_KEYS = [
|
|
349
|
-
`ExecCondition`,
|
|
350
|
-
`ExecReload`,
|
|
351
|
-
`ExecReloadPost`,
|
|
352
|
-
`ExecStart`,
|
|
353
|
-
`ExecStartPost`,
|
|
354
|
-
`ExecStartPre`,
|
|
355
|
-
`ExecStop`,
|
|
356
|
-
`ExecStopPost`
|
|
357
|
-
];
|
|
358
|
-
const execFileAsync = promisify(execFile);
|
|
359
|
-
function normalizeUnitName(name, suffix) {
|
|
360
|
-
return name.endsWith(suffix) ? name.slice(0, -suffix.length) : name;
|
|
361
|
-
}
|
|
362
|
-
function resolveTimerTargetUnit(options) {
|
|
363
|
-
const explicitTarget = options.timer[`Unit`];
|
|
364
|
-
if (typeof explicitTarget === `string` && explicitTarget.length > 0) return explicitTarget;
|
|
365
|
-
return `${normalizeUnitName(options.name, `.timer`)}.service`;
|
|
366
|
-
}
|
|
367
|
-
function defaultUnitDirForScope(scope) {
|
|
368
|
-
if (scope === `user`) return `${process.env[`HOME`] ?? `~`}/.config/systemd/user`;
|
|
369
|
-
return `/etc/systemd/system`;
|
|
370
|
-
}
|
|
371
|
-
function freezeUnitOptions(options) {
|
|
372
|
-
return Object.freeze({
|
|
373
|
-
...options,
|
|
374
|
-
...options.unit === void 0 ? {} : { unit: cloneUnitSection(options.unit) },
|
|
375
|
-
...options.install === void 0 ? {} : { install: cloneUnitSection(options.install) },
|
|
376
|
-
...hasServiceSection(options) ? { service: cloneUnitSection(options.service) } : {},
|
|
377
|
-
...hasTimerSection(options) ? { timer: cloneUnitSection(options.timer) } : {}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
function cloneUnitSection(section) {
|
|
381
|
-
if (section === void 0) return section;
|
|
382
|
-
const entries = Object.entries(section).map(([key, value]) => {
|
|
383
|
-
if (isUnitValueList(value)) return [key, Object.freeze([...value])];
|
|
384
|
-
return [key, value];
|
|
385
|
-
});
|
|
386
|
-
return Object.freeze(Object.fromEntries(entries));
|
|
387
|
-
}
|
|
388
|
-
function validateServiceSection(service) {
|
|
389
|
-
for (const key of EXEC_DIRECTIVE_KEYS) {
|
|
390
|
-
const validation = assertAbsoluteExecValue(key, service[key]);
|
|
391
|
-
if (!validation.ok) return validation;
|
|
392
|
-
}
|
|
393
|
-
return {
|
|
394
|
-
ok: true,
|
|
395
|
-
value: void 0
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function parseStartStatus(unit, output) {
|
|
399
|
-
const properties = Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
400
|
-
const separatorIndex = line.indexOf(`=`);
|
|
401
|
-
if (separatorIndex === -1) return [line, ``];
|
|
402
|
-
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
|
|
403
|
-
}));
|
|
404
|
-
return {
|
|
405
|
-
activeState: properties[`ActiveState`] ?? `unknown`,
|
|
406
|
-
execMainStatus: properties[`ExecMainStatus`] === void 0 || properties[`ExecMainStatus`] === `` ? void 0 : Number(properties[`ExecMainStatus`]),
|
|
407
|
-
result: properties[`Result`] ?? `unknown`,
|
|
408
|
-
subState: properties[`SubState`] ?? `unknown`,
|
|
409
|
-
unit: properties[`Id`] ?? unit
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
function renderUnitFile(sections) {
|
|
413
|
-
try {
|
|
414
|
-
const renderedSections = sections.flatMap(([sectionName, section]) => {
|
|
415
|
-
if (section === void 0) return [];
|
|
416
|
-
const lines = [];
|
|
417
|
-
for (const [key, value] of Object.entries(section)) {
|
|
418
|
-
if (value === void 0) continue;
|
|
419
|
-
if (isUnitValueList(value)) {
|
|
420
|
-
for (const entry of value) {
|
|
421
|
-
const rendered = stringifyUnitValue(entry);
|
|
422
|
-
if (!rendered.ok) throw rendered.error;
|
|
423
|
-
lines.push(`${key}=${rendered.value}`);
|
|
424
|
-
}
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
const rendered = stringifyUnitValue(value);
|
|
428
|
-
if (!rendered.ok) throw rendered.error;
|
|
429
|
-
lines.push(`${key}=${rendered.value}`);
|
|
430
|
-
}
|
|
431
|
-
if (lines.length === 0) return [];
|
|
432
|
-
return [
|
|
433
|
-
`[${sectionName}]`,
|
|
434
|
-
...lines,
|
|
435
|
-
``
|
|
436
|
-
];
|
|
437
|
-
}).join(`\n`);
|
|
438
|
-
return {
|
|
439
|
-
ok: true,
|
|
440
|
-
value: renderedSections.endsWith(`\n`) ? renderedSections : `${renderedSections}\n`
|
|
441
|
-
};
|
|
442
|
-
} catch (error) {
|
|
443
|
-
return {
|
|
444
|
-
ok: false,
|
|
445
|
-
error: error instanceof ExecutableInferenceError ? error : new ExecutableInferenceError({ cause: error })
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
async function defaultCommandExecutor(command, args, options = {}) {
|
|
450
|
-
const result = await execFileAsync(command, [...args], options.env === void 0 ? {} : { env: {
|
|
451
|
-
...process.env,
|
|
452
|
-
...options.env
|
|
453
|
-
} });
|
|
454
|
-
return {
|
|
455
|
-
stderr: result.stderr,
|
|
456
|
-
stdout: result.stdout
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
async function fileExists(path) {
|
|
460
|
-
try {
|
|
461
|
-
await access(path);
|
|
462
|
-
return true;
|
|
463
|
-
} catch {
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
async function sendNotify(state, options) {
|
|
468
|
-
const args = [`--no-block`, state];
|
|
469
|
-
if (options.pid !== void 0) args.push(`MAINPID=${options.pid}`);
|
|
470
|
-
if (options.status !== void 0) args.push(`STATUS=${options.status}`);
|
|
471
|
-
if (options.executor !== void 0) {
|
|
472
|
-
try {
|
|
473
|
-
await options.executor(`systemd-notify`, args, options.socketPath === void 0 ? {} : { env: { NOTIFY_SOCKET: options.socketPath } });
|
|
474
|
-
} catch (cause) {
|
|
475
|
-
throw new NotifySendError(`Failed to send systemd notification through the configured executor`, {
|
|
476
|
-
args,
|
|
477
|
-
cause,
|
|
478
|
-
command: `systemd-notify`,
|
|
479
|
-
reason: `executor-failed`,
|
|
480
|
-
stage: `executor`
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
try {
|
|
486
|
-
await execFileAsync(`systemd-notify`, args, { env: {
|
|
487
|
-
...process.env,
|
|
488
|
-
...options.socketPath === void 0 ? {} : { NOTIFY_SOCKET: options.socketPath }
|
|
489
|
-
} });
|
|
490
|
-
} catch (cause) {
|
|
491
|
-
throw new NotifySendError(`Failed to send systemd notification with systemd-notify`, {
|
|
492
|
-
args,
|
|
493
|
-
cause,
|
|
494
|
-
command: `systemd-notify`,
|
|
495
|
-
reason: `systemd-notify-failed`,
|
|
496
|
-
stage: `systemd-notify`
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
function extractCommandOutput(cause) {
|
|
501
|
-
if (cause === null || typeof cause !== `object`) return;
|
|
502
|
-
const record = cause;
|
|
503
|
-
const stdout = typeof record[`stdout`] === `string` ? record[`stdout`] : void 0;
|
|
504
|
-
const stderr = typeof record[`stderr`] === `string` ? record[`stderr`] : void 0;
|
|
505
|
-
if (stdout === void 0 && stderr === void 0) return;
|
|
506
|
-
return {
|
|
507
|
-
stderr: stderr ?? ``,
|
|
508
|
-
stdout: stdout ?? ``
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function hasServiceSection(options) {
|
|
512
|
-
return `service` in options;
|
|
513
|
-
}
|
|
514
|
-
function hasTimerSection(options) {
|
|
515
|
-
return `timer` in options;
|
|
516
|
-
}
|
|
517
|
-
function assertAbsoluteExecValue(key, value) {
|
|
518
|
-
if (isUnitValueList(value)) {
|
|
519
|
-
for (const entry of value) {
|
|
520
|
-
const validation = assertAbsoluteExecEntry(key, entry);
|
|
521
|
-
if (!validation.ok) return validation;
|
|
522
|
-
}
|
|
523
|
-
return {
|
|
524
|
-
ok: true,
|
|
525
|
-
value: void 0
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
return assertAbsoluteExecEntry(key, value);
|
|
529
|
-
}
|
|
530
|
-
function assertAbsoluteExecEntry(key, value) {
|
|
531
|
-
if (typeof value === `string` && value.length > 0 && !isAbsoluteExecCommand(value)) return {
|
|
532
|
-
ok: false,
|
|
533
|
-
error: new InvalidExecDirectiveError(key)
|
|
534
|
-
};
|
|
535
|
-
if (value instanceof Executable && !value.runtimeEntrypoint.startsWith(`/`)) return {
|
|
536
|
-
ok: false,
|
|
537
|
-
error: new InvalidExecDirectiveError(key)
|
|
538
|
-
};
|
|
539
|
-
return {
|
|
540
|
-
ok: true,
|
|
541
|
-
value: void 0
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
function stringifyUnitValue(value) {
|
|
545
|
-
if (value instanceof Executable) return value.toExecStart();
|
|
546
|
-
if (typeof value === `boolean`) return {
|
|
547
|
-
ok: true,
|
|
548
|
-
value: value ? `true` : `false`
|
|
549
|
-
};
|
|
550
|
-
return {
|
|
551
|
-
ok: true,
|
|
552
|
-
value: String(value)
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
function isUnitValueList(value) {
|
|
556
|
-
return Array.isArray(value);
|
|
557
|
-
}
|
|
558
|
-
function isAbsoluteExecCommand(value) {
|
|
559
|
-
let index = 0;
|
|
560
|
-
let hasPrivilegePrefix = false;
|
|
561
|
-
while (index < value.length) {
|
|
562
|
-
const prefix = value[index];
|
|
563
|
-
if (prefix === `@` || prefix === `-` || prefix === `:`) {
|
|
564
|
-
index += 1;
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
if (prefix === `+`) {
|
|
568
|
-
if (hasPrivilegePrefix) return false;
|
|
569
|
-
hasPrivilegePrefix = true;
|
|
570
|
-
index += 1;
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
if (prefix === `!`) {
|
|
574
|
-
if (hasPrivilegePrefix) return false;
|
|
575
|
-
hasPrivilegePrefix = true;
|
|
576
|
-
index += value[index + 1] === `!` ? 2 : 1;
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
return value[index] === `/`;
|
|
582
|
-
}
|
|
583
|
-
//#endregion
|
|
584
|
-
//#region src/main/notify.ts
|
|
585
|
-
/**
|
|
586
|
-
* Helpers for sending `sd_notify` state updates to systemd.
|
|
587
|
-
*
|
|
588
|
-
* These helpers send notification payloads to the socket identified by
|
|
589
|
-
* `NOTIFY_SOCKET`, or to `options.socketPath` when one is provided explicitly.
|
|
590
|
-
* They are useful both in real services and in tests that want to observe
|
|
591
|
-
* readiness or watchdog traffic directly.
|
|
592
|
-
*
|
|
593
|
-
* Source:
|
|
594
|
-
* - https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html
|
|
595
|
-
* - https://www.freedesktop.org/software/systemd/man/latest/systemd-notify.html
|
|
596
|
-
*/
|
|
597
|
-
const notify = {
|
|
598
|
-
/**
|
|
599
|
-
* Sends `READY=1` to systemd.
|
|
600
|
-
*
|
|
601
|
-
* Use this when a `Type=notify` service has completed its startup work and is
|
|
602
|
-
* ready to be considered fully started. If `options.status` is provided, it is
|
|
603
|
-
* sent as an additional `STATUS=...` field.
|
|
604
|
-
*/
|
|
605
|
-
async ready(options = {}) {
|
|
606
|
-
try {
|
|
607
|
-
await sendNotify(`READY=1`, options);
|
|
608
|
-
return {
|
|
609
|
-
ok: true,
|
|
610
|
-
value: void 0
|
|
611
|
-
};
|
|
612
|
-
} catch (error) {
|
|
613
|
-
return {
|
|
614
|
-
ok: false,
|
|
615
|
-
error: error instanceof NotifySendError ? error : new NotifySendError(`Failed to send READY=1 notification`, {
|
|
616
|
-
cause: error,
|
|
617
|
-
stage: `ready`
|
|
618
|
-
})
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
/**
|
|
623
|
-
* Sends `WATCHDOG=1` to systemd.
|
|
624
|
-
*
|
|
625
|
-
* Use this when a service configured with `WatchdogSec=` needs to emit a
|
|
626
|
-
* watchdog heartbeat. If `options.pid` is provided, it is sent as
|
|
627
|
-
* `MAINPID=...`, and `options.status` is forwarded as `STATUS=...`.
|
|
628
|
-
*/
|
|
629
|
-
async watchdog(options = {}) {
|
|
630
|
-
try {
|
|
631
|
-
await sendNotify(`WATCHDOG=1`, options);
|
|
632
|
-
return {
|
|
633
|
-
ok: true,
|
|
634
|
-
value: void 0
|
|
635
|
-
};
|
|
636
|
-
} catch (error) {
|
|
637
|
-
return {
|
|
638
|
-
ok: false,
|
|
639
|
-
error: error instanceof NotifySendError ? error : new NotifySendError(`Failed to send WATCHDOG=1 notification`, {
|
|
640
|
-
cause: error,
|
|
641
|
-
stage: `watchdog`
|
|
642
|
-
})
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
//#endregion
|
|
648
|
-
//#region src/main/systemd-service.ts
|
|
649
|
-
/**
|
|
650
|
-
* An immutable definition of a `.service` unit.
|
|
651
|
-
*
|
|
652
|
-
* `SystemdService` is a pure value object: it captures the intended unit name
|
|
653
|
-
* and section contents, but it does not write files or talk to `systemd`
|
|
654
|
-
* directly. Operational actions such as installation or startup belong on
|
|
655
|
-
* {@link Systemd}.
|
|
656
|
-
*
|
|
657
|
-
* Source:
|
|
658
|
-
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
|
|
659
|
-
*/
|
|
660
|
-
var SystemdService = class {
|
|
661
|
-
/** Optional `[Install]` section for enable-time relationships. */
|
|
662
|
-
install;
|
|
663
|
-
/** The normalized base unit name, without the `.service` suffix. */
|
|
664
|
-
name;
|
|
665
|
-
/** The fully frozen original options used to construct this service. */
|
|
666
|
-
options;
|
|
667
|
-
/** The `[Service]` section payload. */
|
|
668
|
-
service;
|
|
669
|
-
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
670
|
-
unit;
|
|
671
|
-
/**
|
|
672
|
-
* Creates an immutable service definition.
|
|
673
|
-
*
|
|
674
|
-
* The constructor preserves literal types where possible and rejects unknown
|
|
675
|
-
* directive names within the provided sections, while still allowing custom
|
|
676
|
-
* `X-...` extension directives.
|
|
677
|
-
*/
|
|
678
|
-
constructor(options) {
|
|
679
|
-
this.options = freezeUnitOptions(options);
|
|
680
|
-
this.name = normalizeUnitName(options.name, `.service`);
|
|
681
|
-
this.unit = cloneUnitSection(options.unit);
|
|
682
|
-
this.service = cloneUnitSection(options.service) ?? {};
|
|
683
|
-
this.install = cloneUnitSection(options.install);
|
|
684
|
-
Object.freeze(this);
|
|
685
|
-
}
|
|
686
|
-
/** The canonical unit filename, including the `.service` suffix. */
|
|
687
|
-
get filename() {
|
|
688
|
-
return `${this.name}.service`;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Renders the service as a complete unit file.
|
|
692
|
-
*
|
|
693
|
-
* Rendering also validates executable-valued directives such as `ExecStart`
|
|
694
|
-
* and `ExecStop`, ensuring they use absolute runtime entrypoints as required
|
|
695
|
-
* by systemd.
|
|
696
|
-
*/
|
|
697
|
-
render() {
|
|
698
|
-
const validation = validateServiceSection(this.service);
|
|
699
|
-
if (!validation.ok) return validation;
|
|
700
|
-
return renderUnitFile([
|
|
701
|
-
[`Unit`, this.unit],
|
|
702
|
-
[`Service`, this.service],
|
|
703
|
-
[`Install`, this.install]
|
|
704
|
-
]);
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
//#endregion
|
|
708
|
-
//#region src/main/systemd-timer.ts
|
|
709
|
-
/**
|
|
710
|
-
* An immutable definition of a `.timer` unit.
|
|
711
|
-
*
|
|
712
|
-
* Like {@link SystemdService}, this is a pure value object. It models the timer
|
|
713
|
-
* configuration and its attachment target, but does not write files or interact
|
|
714
|
-
* with the service manager directly.
|
|
715
|
-
*
|
|
716
|
-
* Source:
|
|
717
|
-
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html
|
|
718
|
-
*/
|
|
719
|
-
var SystemdTimer = class {
|
|
720
|
-
/** Optional `[Install]` section for enable-time relationships. */
|
|
721
|
-
install;
|
|
722
|
-
/** The normalized base unit name, without the `.timer` suffix. */
|
|
723
|
-
name;
|
|
724
|
-
/** The fully frozen original options used to construct this timer. */
|
|
725
|
-
options;
|
|
726
|
-
/** The attached service basename inferred from `targetUnit`. */
|
|
727
|
-
targetServiceName;
|
|
728
|
-
/** The unit name this timer activates, explicit or implicit. */
|
|
729
|
-
targetUnit;
|
|
730
|
-
/** The `[Timer]` section payload. */
|
|
731
|
-
timer;
|
|
732
|
-
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
733
|
-
unit;
|
|
734
|
-
/**
|
|
735
|
-
* Creates an immutable timer definition.
|
|
736
|
-
*
|
|
737
|
-
* If no explicit `timer.Unit` is provided, the target defaults to the service
|
|
738
|
-
* with the same basename, matching systemd's native timer behavior.
|
|
739
|
-
*/
|
|
740
|
-
constructor(options) {
|
|
741
|
-
this.options = freezeUnitOptions(options);
|
|
742
|
-
this.name = normalizeUnitName(options.name, `.timer`);
|
|
743
|
-
this.unit = cloneUnitSection(options.unit);
|
|
744
|
-
this.timer = cloneUnitSection(options.timer) ?? {};
|
|
745
|
-
this.install = cloneUnitSection(options.install);
|
|
746
|
-
this.targetUnit = resolveTimerTargetUnit(options);
|
|
747
|
-
this.targetServiceName = normalizeUnitName(this.targetUnit, `.service`);
|
|
748
|
-
Object.freeze(this);
|
|
749
|
-
}
|
|
750
|
-
/** The canonical unit filename, including the `.timer` suffix. */
|
|
751
|
-
get filename() {
|
|
752
|
-
return `${this.name}.timer`;
|
|
753
|
-
}
|
|
754
|
-
/** Renders the timer as a complete unit file. */
|
|
755
|
-
render() {
|
|
756
|
-
return renderUnitFile([
|
|
757
|
-
[`Unit`, this.unit],
|
|
758
|
-
[`Timer`, this.timer],
|
|
759
|
-
[`Install`, this.install]
|
|
760
|
-
]);
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
//#endregion
|
|
764
|
-
//#region src/main/systemctl.ts
|
|
765
|
-
/**
|
|
766
|
-
* A TypeScript-first client for invoking `systemctl` with structured argument
|
|
767
|
-
* vectors instead of shell-composed command strings.
|
|
768
|
-
*
|
|
769
|
-
* This lower-level primitive is useful both on its own and as the subprocess
|
|
770
|
-
* transport underneath higher-level abstractions such as {@link Systemd}.
|
|
771
|
-
*/
|
|
772
|
-
var Systemctl = class {
|
|
773
|
-
/** Command executor used for `systemctl` subprocesses. */
|
|
774
|
-
executor;
|
|
775
|
-
/** The target manager scope, either `system` or `user`. */
|
|
776
|
-
scope;
|
|
777
|
-
constructor(options = {}) {
|
|
778
|
-
this.scope = options.scope ?? `system`;
|
|
779
|
-
this.executor = options.executor ?? defaultCommandExecutor;
|
|
780
|
-
Object.freeze(this);
|
|
781
|
-
}
|
|
782
|
-
/** Reloads the targeted systemd manager. */
|
|
783
|
-
async daemonReload() {
|
|
784
|
-
return this.run(`daemon-reload`);
|
|
785
|
-
}
|
|
786
|
-
/** Enables a unit by filename. */
|
|
787
|
-
async enable(unit) {
|
|
788
|
-
return this.run(`enable`, unit);
|
|
789
|
-
}
|
|
790
|
-
/** Disables a unit by filename. */
|
|
791
|
-
async disable(unit) {
|
|
792
|
-
return this.run(`disable`, unit);
|
|
793
|
-
}
|
|
794
|
-
/** Links a unit file path into the targeted manager. */
|
|
795
|
-
async link(path) {
|
|
796
|
-
return this.run(`link`, path);
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Checks the install-time enablement state of a unit file.
|
|
800
|
-
*
|
|
801
|
-
* Source:
|
|
802
|
-
* - systemd v260.1, `systemctl(1)`
|
|
803
|
-
*/
|
|
804
|
-
async isEnabled(unit, options = {}) {
|
|
805
|
-
const output = await this.run(`is-enabled`, ...options.full ? [`--full`] : [], ...options.quiet ? [`--quiet`] : [], unit);
|
|
806
|
-
if (!output.ok) return output;
|
|
807
|
-
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Checks the runtime active state of a unit.
|
|
811
|
-
*
|
|
812
|
-
* Unless `quiet` is set, this returns the state string printed by
|
|
813
|
-
* `systemctl is-active`.
|
|
814
|
-
*/
|
|
815
|
-
async isActive(unit, options = {}) {
|
|
816
|
-
const output = await this.run(`is-active`, ...options.quiet ? [`--quiet`] : [], unit);
|
|
817
|
-
if (!output.ok) return output;
|
|
818
|
-
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Checks whether a unit is in the failed state.
|
|
822
|
-
*
|
|
823
|
-
* Unless `quiet` is set, this returns the state string printed by
|
|
824
|
-
* `systemctl is-failed`.
|
|
825
|
-
*/
|
|
826
|
-
async isFailed(unit, options = {}) {
|
|
827
|
-
const output = await this.run(`is-failed`, ...options.quiet ? [`--quiet`] : [], unit);
|
|
828
|
-
if (!output.ok) return output;
|
|
829
|
-
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Queries the overall manager state reported by `systemctl is-system-running`.
|
|
833
|
-
*
|
|
834
|
-
* Source:
|
|
835
|
-
* - systemd v260.1, `systemctl(1)`
|
|
836
|
-
*/
|
|
837
|
-
async isSystemRunning(options = {}) {
|
|
838
|
-
const output = await this.run(`is-system-running`, ...options.wait ? [`--wait`] : []);
|
|
839
|
-
if (!output.ok) return output;
|
|
840
|
-
return ok$1(firstNonEmptyOutputLine(output.value));
|
|
841
|
-
}
|
|
842
|
-
/**
|
|
843
|
-
* Queries `systemctl show` for a unit.
|
|
844
|
-
*
|
|
845
|
-
* When `properties` are provided, only those properties are requested.
|
|
846
|
-
*/
|
|
847
|
-
async show(unit, options = {}) {
|
|
848
|
-
const propertyFlag = options.properties === void 0 || options.properties.length === 0 ? [] : [`--property=${options.properties.join(`,`)}`];
|
|
849
|
-
return this.run(`show`, unit, ...propertyFlag);
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Queries `systemctl show` and parses the result into a key-value object.
|
|
853
|
-
*
|
|
854
|
-
* Properties are returned exactly as printed by `systemctl show`.
|
|
855
|
-
*/
|
|
856
|
-
async showProperties(unit, options = {}) {
|
|
857
|
-
const output = await this.show(unit, options);
|
|
858
|
-
if (!output.ok) return output;
|
|
859
|
-
return ok$1(parseSystemctlShowOutput(output.value.stdout));
|
|
860
|
-
}
|
|
861
|
-
/**
|
|
862
|
-
* Queries the standard status-related `show` properties for a unit and
|
|
863
|
-
* returns a parsed snapshot.
|
|
864
|
-
*/
|
|
865
|
-
async showStatus(unit) {
|
|
866
|
-
return this.showServiceStatus(unit);
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Queries the standard service-status `show` properties for a service unit
|
|
870
|
-
* and returns a parsed snapshot.
|
|
871
|
-
*/
|
|
872
|
-
async showServiceStatus(unit) {
|
|
873
|
-
const output = await this.show(unit, { properties: [
|
|
874
|
-
`Id`,
|
|
875
|
-
`ActiveState`,
|
|
876
|
-
`SubState`,
|
|
877
|
-
`Result`,
|
|
878
|
-
`ExecMainStatus`
|
|
879
|
-
] });
|
|
880
|
-
if (!output.ok) return output;
|
|
881
|
-
return ok$1(parseStartStatus(unit, output.value.stdout));
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Queries `systemctl list-timers` and returns the observed JSON payload
|
|
885
|
-
* shape directly, with raw integer timestamps left intact.
|
|
886
|
-
*/
|
|
887
|
-
async listTimers(options = {}) {
|
|
888
|
-
const args = [
|
|
889
|
-
`list-timers`,
|
|
890
|
-
...options.all ? [`--all`] : [],
|
|
891
|
-
...options.noPager ?? true ? [`--no-pager`] : [],
|
|
892
|
-
`--output=json`,
|
|
893
|
-
...options.patterns ?? []
|
|
894
|
-
];
|
|
895
|
-
const output = await this.run(...args);
|
|
896
|
-
if (!output.ok) return output;
|
|
897
|
-
try {
|
|
898
|
-
return ok$1(JSON.parse(output.value.stdout));
|
|
899
|
-
} catch (cause) {
|
|
900
|
-
return err$1(new SystemctlCommandError(`Failed to parse JSON output from systemctl list-timers`, {
|
|
901
|
-
args: [...this.scopeArgs(), ...args],
|
|
902
|
-
cause,
|
|
903
|
-
command: `systemctl`,
|
|
904
|
-
operation: `list-timers`,
|
|
905
|
-
reason: `invalid-json`
|
|
906
|
-
}));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
/** Starts a unit by filename. */
|
|
910
|
-
async start(unit) {
|
|
911
|
-
return this.run(`start`, unit);
|
|
912
|
-
}
|
|
913
|
-
/** Stops a unit by filename. */
|
|
914
|
-
async stop(unit) {
|
|
915
|
-
return this.run(`stop`, unit);
|
|
916
|
-
}
|
|
917
|
-
/** Restarts a unit by filename. */
|
|
918
|
-
async restart(unit) {
|
|
919
|
-
return this.run(`restart`, unit);
|
|
920
|
-
}
|
|
921
|
-
/**
|
|
922
|
-
* Resets the failed state for matching units, or for all units when no
|
|
923
|
-
* patterns are provided.
|
|
924
|
-
*/
|
|
925
|
-
async resetFailed(patterns = []) {
|
|
926
|
-
return this.run(`reset-failed`, ...patterns);
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Queries `systemctl status` for a unit.
|
|
930
|
-
*
|
|
931
|
-
* This is primarily a human-oriented command and is best paired with
|
|
932
|
-
* `show()` when structured state is needed.
|
|
933
|
-
*/
|
|
934
|
-
async status(unit, options = {}) {
|
|
935
|
-
const args = [`status`, unit];
|
|
936
|
-
if (options.noPager ?? true) args.push(`--no-pager`);
|
|
937
|
-
if (options.lines !== void 0) args.push(`--lines`, String(options.lines));
|
|
938
|
-
return this.run(...args);
|
|
939
|
-
}
|
|
940
|
-
/** Executes a raw `systemctl` invocation with scope-aware arguments. */
|
|
941
|
-
async run(...args) {
|
|
942
|
-
const scopedArgs = [...this.scopeArgs(), ...args];
|
|
943
|
-
try {
|
|
944
|
-
return ok$1(await this.executor(`systemctl`, scopedArgs));
|
|
945
|
-
} catch (cause) {
|
|
946
|
-
return err$1(new SystemctlCommandError(`Failed to run systemctl ${args[0] ?? `command`}`, {
|
|
947
|
-
args: scopedArgs,
|
|
948
|
-
cause,
|
|
949
|
-
command: `systemctl`,
|
|
950
|
-
operation: args[0],
|
|
951
|
-
reason: `executor-failed`
|
|
952
|
-
}));
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
scopeArgs() {
|
|
956
|
-
return this.scope === `user` ? [`--user`] : [];
|
|
957
|
-
}
|
|
958
|
-
};
|
|
959
|
-
function ok$1(value) {
|
|
960
|
-
return {
|
|
961
|
-
ok: true,
|
|
962
|
-
value
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
function err$1(error) {
|
|
966
|
-
return {
|
|
967
|
-
ok: false,
|
|
968
|
-
error
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
function parseSystemctlShowOutput(output) {
|
|
972
|
-
return Object.freeze(Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
973
|
-
const separatorIndex = line.indexOf(`=`);
|
|
974
|
-
if (separatorIndex === -1) return [line, ``];
|
|
975
|
-
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)];
|
|
976
|
-
})));
|
|
977
|
-
}
|
|
978
|
-
function firstNonEmptyOutputLine(output) {
|
|
979
|
-
return [output.stdout, output.stderr].flatMap((value) => value.split(`\n`)).map((value) => value.trim()).find((value) => value.length > 0) ?? ``;
|
|
980
|
-
}
|
|
981
|
-
//#endregion
|
|
982
|
-
//#region src/main/systemd.ts
|
|
983
|
-
/**
|
|
984
|
-
* A successful systemd unit materialization.
|
|
985
|
-
*
|
|
986
|
-
* It records the target directory and the on-disk path associated with each
|
|
987
|
-
* materialized unit.
|
|
988
|
-
*/
|
|
989
|
-
var SystemdMaterialization = class {
|
|
990
|
-
/** The directory units were written into. */
|
|
991
|
-
directory;
|
|
992
|
-
/** The materialized units together with their resolved on-disk paths. */
|
|
993
|
-
materialized;
|
|
994
|
-
constructor(directory, materialized) {
|
|
995
|
-
this.directory = directory;
|
|
996
|
-
this.materialized = Object.freeze([...materialized]);
|
|
997
|
-
Object.freeze(this);
|
|
998
|
-
}
|
|
999
|
-
};
|
|
1000
|
-
/**
|
|
1001
|
-
* A configured interface to a specific systemd environment.
|
|
1002
|
-
*
|
|
1003
|
-
* `Systemd` combines unit-file materialization with a command execution
|
|
1004
|
-
* strategy. It knows which scope it is targeting, where unit files should be
|
|
1005
|
-
* written, and how `systemctl` should be invoked.
|
|
1006
|
-
*
|
|
1007
|
-
* This abstraction is intentionally close to `systemctl` concepts while still
|
|
1008
|
-
* accepting full unit definitions instead of loose unit-name strings.
|
|
1009
|
-
*/
|
|
1010
|
-
var Systemd = class {
|
|
1011
|
-
/** Command executor used for `systemctl` and related subprocess calls. */
|
|
1012
|
-
executor;
|
|
1013
|
-
/** Whether units should be linked into systemd before manager operations. */
|
|
1014
|
-
linkUnits;
|
|
1015
|
-
/** The target manager scope, either `system` or `user`. */
|
|
1016
|
-
scope;
|
|
1017
|
-
/** The low-level `systemctl` client for this configured target. */
|
|
1018
|
-
systemctl;
|
|
1019
|
-
/** Higher-level `systemd-ts` upkeep helpers for application-owned units. */
|
|
1020
|
-
ts;
|
|
1021
|
-
/** The directory used when materializing unit files. */
|
|
1022
|
-
unitDir;
|
|
1023
|
-
/**
|
|
1024
|
-
* Creates a configured `Systemd` facade.
|
|
1025
|
-
*
|
|
1026
|
-
* By default, this targets the system scope and `/etc/systemd/system`. Use
|
|
1027
|
-
* `scope: "user"` or an explicit `unitDir` to target a different manager or
|
|
1028
|
-
* unit-file location.
|
|
1029
|
-
*/
|
|
1030
|
-
constructor(options = {}) {
|
|
1031
|
-
this.scope = options.scope ?? `system`;
|
|
1032
|
-
this.unitDir = options.unitDir ?? defaultUnitDirForScope(this.scope);
|
|
1033
|
-
this.linkUnits = options.linkUnits ?? false;
|
|
1034
|
-
this.executor = options.executor ?? defaultCommandExecutor;
|
|
1035
|
-
this.systemctl = new Systemctl({
|
|
1036
|
-
executor: this.executor,
|
|
1037
|
-
scope: this.scope
|
|
1038
|
-
});
|
|
1039
|
-
this.ts = new SystemdTs(this);
|
|
1040
|
-
Object.freeze(this);
|
|
1041
|
-
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Renders and writes one or more units into this instance's `unitDir`.
|
|
1044
|
-
*
|
|
1045
|
-
* This is intentionally a file-materialization step. It does not enable or
|
|
1046
|
-
* start the units on its own.
|
|
1047
|
-
*/
|
|
1048
|
-
async materialize(...units) {
|
|
1049
|
-
return this.materializeUnits(units);
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Enables one or more units via `systemctl enable`.
|
|
1053
|
-
*
|
|
1054
|
-
* When `linkUnits` is enabled, this first links materialized unit files into the
|
|
1055
|
-
* target manager and reloads systemd so the units are visible before the
|
|
1056
|
-
* enable operation runs.
|
|
1057
|
-
*/
|
|
1058
|
-
async enable(...units) {
|
|
1059
|
-
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.enable()`));
|
|
1060
|
-
const scopeArgs = this.scopeArgs();
|
|
1061
|
-
try {
|
|
1062
|
-
await this.prepareUnits(scopeArgs, units, `enable`);
|
|
1063
|
-
} catch (cause) {
|
|
1064
|
-
if (cause instanceof UnitEnableError) return err(cause);
|
|
1065
|
-
return err(new UnitEnableError(`Failed to prepare units for enable`, {
|
|
1066
|
-
cause,
|
|
1067
|
-
stage: `prepare`,
|
|
1068
|
-
unitName: units[0]?.filename
|
|
1069
|
-
}));
|
|
1070
|
-
}
|
|
1071
|
-
for (const unit of units) {
|
|
1072
|
-
const args = [
|
|
1073
|
-
...scopeArgs,
|
|
1074
|
-
`enable`,
|
|
1075
|
-
unit.filename
|
|
1076
|
-
];
|
|
1077
|
-
const enabled = await this.systemctl.enable(unit.filename);
|
|
1078
|
-
if (!enabled.ok) {
|
|
1079
|
-
const errorOptions = {
|
|
1080
|
-
args,
|
|
1081
|
-
cause: enabled.error,
|
|
1082
|
-
command: `systemctl`,
|
|
1083
|
-
stage: `enable`,
|
|
1084
|
-
unitName: unit.filename,
|
|
1085
|
-
...enabled.error.environmentReason === void 0 ? {} : { environmentReason: enabled.error.environmentReason }
|
|
1086
|
-
};
|
|
1087
|
-
return err(new UnitEnableError(`Failed to enable ${unit.filename}`, errorOptions));
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return ok(void 0);
|
|
1091
|
-
}
|
|
1092
|
-
/**
|
|
1093
|
-
* Starts a unit and returns a parsed `systemctl show` snapshot of its final
|
|
1094
|
-
* observed state.
|
|
1095
|
-
*
|
|
1096
|
-
* For oneshot services, a successful result commonly means `ActiveState` is
|
|
1097
|
-
* already back to `inactive` by the time the status snapshot is collected.
|
|
1098
|
-
*/
|
|
1099
|
-
async start(unit) {
|
|
1100
|
-
const scopeArgs = this.scopeArgs();
|
|
1101
|
-
try {
|
|
1102
|
-
await this.prepareUnits(scopeArgs, [unit], `start`);
|
|
1103
|
-
} catch (cause) {
|
|
1104
|
-
if (cause instanceof UnitStartError) return err(cause);
|
|
1105
|
-
return err(new UnitStartError(`Failed to prepare ${unit.filename} for start`, {
|
|
1106
|
-
cause,
|
|
1107
|
-
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
1108
|
-
stage: `prepare`,
|
|
1109
|
-
unitName: unit.filename
|
|
1110
|
-
}));
|
|
1111
|
-
}
|
|
1112
|
-
const startArgs = [
|
|
1113
|
-
...scopeArgs,
|
|
1114
|
-
`start`,
|
|
1115
|
-
unit.filename
|
|
1116
|
-
];
|
|
1117
|
-
const started = await this.systemctl.start(unit.filename);
|
|
1118
|
-
if (!started.ok) {
|
|
1119
|
-
const errorOptions = {
|
|
1120
|
-
args: startArgs,
|
|
1121
|
-
cause: started.error,
|
|
1122
|
-
command: `systemctl`,
|
|
1123
|
-
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
1124
|
-
stage: `start`,
|
|
1125
|
-
unitName: unit.filename,
|
|
1126
|
-
...started.error.environmentReason === void 0 ? {} : { environmentReason: started.error.environmentReason }
|
|
1127
|
-
};
|
|
1128
|
-
return err(new UnitStartError(`Failed to start ${unit.filename}`, errorOptions));
|
|
1129
|
-
}
|
|
1130
|
-
const status = await this.systemctl.showServiceStatus(unit.filename);
|
|
1131
|
-
if (!status.ok) {
|
|
1132
|
-
const errorOptions = {
|
|
1133
|
-
args: [
|
|
1134
|
-
...scopeArgs,
|
|
1135
|
-
`show`,
|
|
1136
|
-
unit.filename,
|
|
1137
|
-
`--property=Id,ActiveState,SubState,Result,ExecMainStatus`
|
|
1138
|
-
],
|
|
1139
|
-
cause: status.error,
|
|
1140
|
-
command: `systemctl`,
|
|
1141
|
-
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
1142
|
-
stage: `show-status`,
|
|
1143
|
-
unitName: unit.filename,
|
|
1144
|
-
...status.error.environmentReason === void 0 ? {} : { environmentReason: status.error.environmentReason }
|
|
1145
|
-
};
|
|
1146
|
-
return err(new UnitStartError(`Started ${unit.filename} but failed to query its status`, errorOptions));
|
|
1147
|
-
}
|
|
1148
|
-
return ok(status.value);
|
|
1149
|
-
}
|
|
1150
|
-
/**
|
|
1151
|
-
* Reads recent output for a managed unit.
|
|
1152
|
-
*
|
|
1153
|
-
* If the unit is configured with file-backed `StandardOutput` or
|
|
1154
|
-
* `StandardError`, this reads directly from that file. Otherwise it falls back
|
|
1155
|
-
* to `systemctl status --lines ...`.
|
|
1156
|
-
*/
|
|
1157
|
-
async logs(unit, options) {
|
|
1158
|
-
const fileLogPath = resolveUnitLogPath(unit);
|
|
1159
|
-
if (fileLogPath !== void 0) try {
|
|
1160
|
-
return ok(tailLines(await readFile(fileLogPath, `utf8`), options?.lines ?? 50));
|
|
1161
|
-
} catch (cause) {
|
|
1162
|
-
return err(new UnitLogsReadError(`Failed to read logs for ${unit.filename} from ${fileLogPath}`, {
|
|
1163
|
-
cause,
|
|
1164
|
-
reason: isMissingPathError(cause) ? `missing-log-file` : `log-file-read-failed`,
|
|
1165
|
-
stage: `read-log-file`,
|
|
1166
|
-
unitName: unit.filename,
|
|
1167
|
-
unitPath: fileLogPath
|
|
1168
|
-
}));
|
|
1169
|
-
}
|
|
1170
|
-
const scopeArgs = this.scopeArgs();
|
|
1171
|
-
const lines = options?.lines ?? 50;
|
|
1172
|
-
const logs = await this.systemctl.status(unit.filename, { lines });
|
|
1173
|
-
if (logs.ok) return ok(joinCommandOutput(logs.value));
|
|
1174
|
-
const errorOutput = extractCommandOutput(logs.error);
|
|
1175
|
-
if (errorOutput !== void 0) return ok(joinCommandOutput(errorOutput));
|
|
1176
|
-
const args = [
|
|
1177
|
-
...scopeArgs,
|
|
1178
|
-
`status`,
|
|
1179
|
-
unit.filename,
|
|
1180
|
-
`--no-pager`,
|
|
1181
|
-
`--lines`,
|
|
1182
|
-
String(lines)
|
|
1183
|
-
];
|
|
1184
|
-
return err(new UnitLogsReadError(`Failed to query logs for ${unit.filename} from systemctl status`, {
|
|
1185
|
-
args,
|
|
1186
|
-
cause: logs.error,
|
|
1187
|
-
command: `systemctl`,
|
|
1188
|
-
reason: `status-command-failed`,
|
|
1189
|
-
stage: `status`,
|
|
1190
|
-
unitName: unit.filename
|
|
1191
|
-
}));
|
|
1192
|
-
}
|
|
1193
|
-
/** Returns the on-disk unit-file path this instance uses for the given unit. */
|
|
1194
|
-
pathFor(unit) {
|
|
1195
|
-
return join(this.unitDir, unit.filename);
|
|
1196
|
-
}
|
|
1197
|
-
async prepareUnits(scopeArgs, units, operation) {
|
|
1198
|
-
if (this.linkUnits) {
|
|
1199
|
-
const linkPaths = await this.collectLinkPaths(units);
|
|
1200
|
-
for (const path of linkPaths) {
|
|
1201
|
-
const args = [
|
|
1202
|
-
...scopeArgs,
|
|
1203
|
-
`link`,
|
|
1204
|
-
path
|
|
1205
|
-
];
|
|
1206
|
-
const linked = await this.systemctl.link(path);
|
|
1207
|
-
if (!linked.ok) {
|
|
1208
|
-
const linkedUnit = units.find((candidate) => this.pathFor(candidate) === path);
|
|
1209
|
-
const errorContext = {
|
|
1210
|
-
args,
|
|
1211
|
-
cause: linked.error,
|
|
1212
|
-
command: `systemctl`,
|
|
1213
|
-
stage: `link`,
|
|
1214
|
-
unitPath: path,
|
|
1215
|
-
...linked.error.environmentReason === void 0 ? {} : { environmentReason: linked.error.environmentReason },
|
|
1216
|
-
...linkedUnit === void 0 ? {} : { unitName: linkedUnit.filename }
|
|
1217
|
-
};
|
|
1218
|
-
throw operation === `enable` ? new UnitEnableError(`Failed to link ${path} before enable`, errorContext) : new UnitStartError(`Failed to link ${path} before start`, errorContext);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
const args = [...scopeArgs, `daemon-reload`];
|
|
1223
|
-
const reloaded = await this.systemctl.daemonReload();
|
|
1224
|
-
if (!reloaded.ok) {
|
|
1225
|
-
const errorContext = {
|
|
1226
|
-
args,
|
|
1227
|
-
cause: reloaded.error,
|
|
1228
|
-
command: `systemctl`,
|
|
1229
|
-
stage: `daemon-reload`,
|
|
1230
|
-
...reloaded.error.environmentReason === void 0 ? {} : { environmentReason: reloaded.error.environmentReason },
|
|
1231
|
-
...units[0]?.filename === void 0 ? {} : { unitName: units[0].filename }
|
|
1232
|
-
};
|
|
1233
|
-
throw operation === `enable` ? new UnitEnableError(`Failed to reload systemd before enable`, errorContext) : new UnitStartError(`Failed to reload systemd before start`, errorContext);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
scopeArgs() {
|
|
1237
|
-
return this.scope === `user` ? [`--user`] : [];
|
|
1238
|
-
}
|
|
1239
|
-
async collectStartDiagnostics(scopeArgs, unitName) {
|
|
1240
|
-
const diagnostics = {};
|
|
1241
|
-
const showArgs = [
|
|
1242
|
-
...scopeArgs,
|
|
1243
|
-
`show`,
|
|
1244
|
-
unitName,
|
|
1245
|
-
`--property=Id,ActiveState,SubState,Result,ExecMainStatus`
|
|
1246
|
-
];
|
|
1247
|
-
const show = await this.tryBestEffortCommand(`systemctl`, showArgs);
|
|
1248
|
-
if (show !== void 0) {
|
|
1249
|
-
const showOutput = joinCommandOutput(show);
|
|
1250
|
-
if (showOutput.length > 0) diagnostics.showOutput = showOutput;
|
|
1251
|
-
if (show.stdout.length > 0) diagnostics.showStatus = parseStartStatus(unitName, show.stdout);
|
|
1252
|
-
}
|
|
1253
|
-
const statusArgs = [
|
|
1254
|
-
...scopeArgs,
|
|
1255
|
-
`status`,
|
|
1256
|
-
unitName,
|
|
1257
|
-
`--no-pager`,
|
|
1258
|
-
`--lines`,
|
|
1259
|
-
`20`
|
|
1260
|
-
];
|
|
1261
|
-
const status = await this.tryBestEffortCommand(`systemctl`, statusArgs);
|
|
1262
|
-
if (status !== void 0) {
|
|
1263
|
-
const statusOutput = joinCommandOutput(status);
|
|
1264
|
-
if (statusOutput.length > 0) diagnostics.statusOutput = statusOutput;
|
|
1265
|
-
}
|
|
1266
|
-
return diagnostics;
|
|
1267
|
-
}
|
|
1268
|
-
async collectLinkPaths(units) {
|
|
1269
|
-
const paths = /* @__PURE__ */ new Set();
|
|
1270
|
-
for (const unit of units) {
|
|
1271
|
-
paths.add(this.pathFor(unit));
|
|
1272
|
-
if (unit instanceof SystemdTimer) {
|
|
1273
|
-
const targetPath = join(this.unitDir, unit.targetUnit);
|
|
1274
|
-
if (await fileExists(targetPath)) paths.add(targetPath);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
return [...paths];
|
|
1278
|
-
}
|
|
1279
|
-
async materializeUnits(units) {
|
|
1280
|
-
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.materialize()`));
|
|
1281
|
-
try {
|
|
1282
|
-
await writeUnitDirectory(this.unitDir);
|
|
1283
|
-
} catch (cause) {
|
|
1284
|
-
const reason = classifyMaterializationReason(cause);
|
|
1285
|
-
return err(new UnitMaterializationError(`Failed to create unit directory ${this.unitDir}`, {
|
|
1286
|
-
cause,
|
|
1287
|
-
operation: `create-directory`,
|
|
1288
|
-
...reason === void 0 ? {} : { reason },
|
|
1289
|
-
unitPath: this.unitDir
|
|
1290
|
-
}));
|
|
1291
|
-
}
|
|
1292
|
-
const materialized = [];
|
|
1293
|
-
for (const unit of units) {
|
|
1294
|
-
const path = join(this.unitDir, unit.filename);
|
|
1295
|
-
const rendered = unit.render();
|
|
1296
|
-
if (!rendered.ok) return err(rendered.error);
|
|
1297
|
-
try {
|
|
1298
|
-
await writeFile(path, rendered.value, `utf8`);
|
|
1299
|
-
} catch (cause) {
|
|
1300
|
-
if (cause instanceof InvalidExecDirectiveError) return err(cause);
|
|
1301
|
-
const reason = classifyMaterializationReason(cause);
|
|
1302
|
-
return err(new UnitMaterializationError(`Failed to materialize ${unit.filename} into ${path}`, {
|
|
1303
|
-
cause,
|
|
1304
|
-
operation: `write-file`,
|
|
1305
|
-
...reason === void 0 ? {} : { reason },
|
|
1306
|
-
unitName: unit.filename,
|
|
1307
|
-
unitPath: path
|
|
1308
|
-
}));
|
|
1309
|
-
}
|
|
1310
|
-
materialized.push({
|
|
1311
|
-
path,
|
|
1312
|
-
unit
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
return ok(new SystemdMaterialization(this.unitDir, materialized));
|
|
1316
|
-
}
|
|
1317
|
-
async tryBestEffortCommand(command, args) {
|
|
1318
|
-
try {
|
|
1319
|
-
return await this.executor(command, args);
|
|
1320
|
-
} catch (cause) {
|
|
1321
|
-
const output = extractCommandOutput(cause);
|
|
1322
|
-
if (output !== void 0) return output;
|
|
1323
|
-
return;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
/**
|
|
1328
|
-
* Opinionated `systemd-ts` upkeep helpers for application-owned units.
|
|
1329
|
-
*
|
|
1330
|
-
* This layer is intentionally analogous to a subset of `portablectl`'s
|
|
1331
|
-
* ownership workflow, but it operates on regular materialized units instead of
|
|
1332
|
-
* portable service images.
|
|
1333
|
-
*/
|
|
1334
|
-
var SystemdTs = class {
|
|
1335
|
-
systemd;
|
|
1336
|
-
constructor(systemd) {
|
|
1337
|
-
this.systemd = systemd;
|
|
1338
|
-
Object.freeze(this);
|
|
1339
|
-
}
|
|
1340
|
-
async attach(units, options) {
|
|
1341
|
-
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.ts.attach()`));
|
|
1342
|
-
const described = await this.describeUnits(units);
|
|
1343
|
-
if (!described.ok) return described;
|
|
1344
|
-
const manifestPath = this.manifestPathFor(options.owner);
|
|
1345
|
-
const prior = await this.readManifest(options.owner);
|
|
1346
|
-
const diff = await this.diffManifest(prior, described.value);
|
|
1347
|
-
const materialized = await this.systemd.materialize(...units);
|
|
1348
|
-
if (!materialized.ok) return materialized;
|
|
1349
|
-
const enabled = [];
|
|
1350
|
-
if (options.enable ?? false) {
|
|
1351
|
-
const result = await this.systemd.enable(...units);
|
|
1352
|
-
if (!result.ok) return result;
|
|
1353
|
-
enabled.push(...units.map((unit) => unit.filename));
|
|
1354
|
-
}
|
|
1355
|
-
const started = [];
|
|
1356
|
-
if (options.start ?? false) for (const unit of units) {
|
|
1357
|
-
const result = await this.systemd.start(unit);
|
|
1358
|
-
if (!result.ok) return result;
|
|
1359
|
-
started.push(unit.filename);
|
|
1360
|
-
}
|
|
1361
|
-
await this.writeManifest(options.owner, described.value);
|
|
1362
|
-
return ok({
|
|
1363
|
-
added: diff.added,
|
|
1364
|
-
directory: this.systemd.unitDir,
|
|
1365
|
-
enabled,
|
|
1366
|
-
manifestPath,
|
|
1367
|
-
owner: options.owner,
|
|
1368
|
-
removed: diff.removed,
|
|
1369
|
-
restarted: [],
|
|
1370
|
-
started,
|
|
1371
|
-
unchanged: diff.unchanged,
|
|
1372
|
-
updated: diff.updated
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
async detach(selector, options = {}) {
|
|
1376
|
-
const manifest = await this.readManifest(selector.owner);
|
|
1377
|
-
if (manifest === void 0) return ok({
|
|
1378
|
-
deleted: [],
|
|
1379
|
-
detached: [],
|
|
1380
|
-
disabled: [],
|
|
1381
|
-
owner: selector.owner,
|
|
1382
|
-
stopped: []
|
|
1383
|
-
});
|
|
1384
|
-
const filenames = manifest.units.map((unit) => unit.filename);
|
|
1385
|
-
const deleted = [];
|
|
1386
|
-
const disabled = [];
|
|
1387
|
-
const stopped = [];
|
|
1388
|
-
if (options.stop ?? true) {
|
|
1389
|
-
await this.runSystemctl(`stop`, filenames);
|
|
1390
|
-
stopped.push(...filenames);
|
|
1391
|
-
}
|
|
1392
|
-
if (options.disable ?? true) {
|
|
1393
|
-
await this.runSystemctl(`disable`, filenames);
|
|
1394
|
-
disabled.push(...filenames);
|
|
1395
|
-
}
|
|
1396
|
-
if (options.deleteUnitFiles ?? true) {
|
|
1397
|
-
for (const unit of manifest.units) {
|
|
1398
|
-
await this.removeManagedUnitPath(unit.path);
|
|
1399
|
-
await rm(unit.path, { force: true });
|
|
1400
|
-
deleted.push(unit.filename);
|
|
1401
|
-
}
|
|
1402
|
-
await this.runSystemctl(`daemon-reload`);
|
|
1403
|
-
}
|
|
1404
|
-
await rm(this.manifestPathFor(selector.owner), { force: true });
|
|
1405
|
-
return ok({
|
|
1406
|
-
deleted,
|
|
1407
|
-
detached: filenames,
|
|
1408
|
-
disabled,
|
|
1409
|
-
owner: selector.owner,
|
|
1410
|
-
stopped
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
async reattach(units, options) {
|
|
1414
|
-
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.ts.reattach()`));
|
|
1415
|
-
const described = await this.describeUnits(units);
|
|
1416
|
-
if (!described.ok) return described;
|
|
1417
|
-
const manifestPath = this.manifestPathFor(options.owner);
|
|
1418
|
-
const prior = await this.readManifest(options.owner);
|
|
1419
|
-
const diff = await this.diffManifest(prior, described.value);
|
|
1420
|
-
if ((options.stopRemoved ?? true) && diff.removed.length > 0) await this.runSystemctl(`stop`, diff.removed);
|
|
1421
|
-
if ((options.disableRemoved ?? true) && diff.removed.length > 0) await this.runSystemctl(`disable`, diff.removed);
|
|
1422
|
-
if ((options.prune ?? true) && prior !== void 0) {
|
|
1423
|
-
for (const unit of prior.units) {
|
|
1424
|
-
if (!diff.removed.includes(unit.filename)) continue;
|
|
1425
|
-
await this.removeManagedUnitPath(unit.path);
|
|
1426
|
-
await rm(unit.path, { force: true });
|
|
1427
|
-
}
|
|
1428
|
-
if (diff.removed.length > 0) await this.runSystemctl(`daemon-reload`);
|
|
1429
|
-
}
|
|
1430
|
-
const materialized = await this.systemd.materialize(...units);
|
|
1431
|
-
if (!materialized.ok) return materialized;
|
|
1432
|
-
const enabled = [];
|
|
1433
|
-
if (options.enable ?? false) {
|
|
1434
|
-
const result = await this.systemd.enable(...units);
|
|
1435
|
-
if (!result.ok) return result;
|
|
1436
|
-
enabled.push(...units.map((unit) => unit.filename));
|
|
1437
|
-
}
|
|
1438
|
-
const restarted = [];
|
|
1439
|
-
if (options.restartUpdated ?? false) {
|
|
1440
|
-
const restartable = units.filter((unit) => diff.updated.includes(unit.filename)).map((unit) => unit.filename);
|
|
1441
|
-
if (restartable.length > 0) {
|
|
1442
|
-
await this.runSystemctl(`restart`, restartable);
|
|
1443
|
-
restarted.push(...restartable);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
const started = [];
|
|
1447
|
-
if (options.start ?? false) for (const unit of units) {
|
|
1448
|
-
if (restarted.includes(unit.filename)) continue;
|
|
1449
|
-
const result = await this.systemd.start(unit);
|
|
1450
|
-
if (!result.ok) return result;
|
|
1451
|
-
started.push(unit.filename);
|
|
1452
|
-
}
|
|
1453
|
-
await this.writeManifest(options.owner, described.value);
|
|
1454
|
-
return ok({
|
|
1455
|
-
added: diff.added,
|
|
1456
|
-
directory: this.systemd.unitDir,
|
|
1457
|
-
enabled,
|
|
1458
|
-
manifestPath,
|
|
1459
|
-
owner: options.owner,
|
|
1460
|
-
removed: diff.removed,
|
|
1461
|
-
restarted,
|
|
1462
|
-
started,
|
|
1463
|
-
unchanged: diff.unchanged,
|
|
1464
|
-
updated: diff.updated
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
async describeUnits(units) {
|
|
1468
|
-
const described = [];
|
|
1469
|
-
for (const unit of units) {
|
|
1470
|
-
const rendered = unit.render();
|
|
1471
|
-
if (!rendered.ok) return rendered;
|
|
1472
|
-
described.push({
|
|
1473
|
-
filename: unit.filename,
|
|
1474
|
-
path: this.systemd.pathFor(unit),
|
|
1475
|
-
rendered: rendered.value
|
|
1476
|
-
});
|
|
1477
|
-
}
|
|
1478
|
-
return ok(described);
|
|
1479
|
-
}
|
|
1480
|
-
async diffManifest(prior, desired) {
|
|
1481
|
-
const priorUnits = new Map(prior?.units.map((unit) => [unit.filename, unit.path]) ?? []);
|
|
1482
|
-
const desiredNames = new Set(desired.map((unit) => unit.filename));
|
|
1483
|
-
const added = [];
|
|
1484
|
-
const removed = [...priorUnits.keys()].filter((filename) => !desiredNames.has(filename));
|
|
1485
|
-
const unchanged = [];
|
|
1486
|
-
const updated = [];
|
|
1487
|
-
for (const unit of desired) {
|
|
1488
|
-
const priorPath = priorUnits.get(unit.filename);
|
|
1489
|
-
if (priorPath === void 0) {
|
|
1490
|
-
added.push(unit.filename);
|
|
1491
|
-
continue;
|
|
1492
|
-
}
|
|
1493
|
-
try {
|
|
1494
|
-
if (await readFile(priorPath, `utf8`) === unit.rendered) {
|
|
1495
|
-
unchanged.push(unit.filename);
|
|
1496
|
-
continue;
|
|
1497
|
-
}
|
|
1498
|
-
} catch {
|
|
1499
|
-
added.push(unit.filename);
|
|
1500
|
-
continue;
|
|
1501
|
-
}
|
|
1502
|
-
updated.push(unit.filename);
|
|
1503
|
-
}
|
|
1504
|
-
return {
|
|
1505
|
-
added,
|
|
1506
|
-
removed,
|
|
1507
|
-
unchanged,
|
|
1508
|
-
updated
|
|
1509
|
-
};
|
|
1510
|
-
}
|
|
1511
|
-
async manifestDir() {
|
|
1512
|
-
const directory = join(this.systemd.unitDir, `.systemd-ts`);
|
|
1513
|
-
await mkdir(directory, { recursive: true });
|
|
1514
|
-
return directory;
|
|
1515
|
-
}
|
|
1516
|
-
manifestPathFor(owner) {
|
|
1517
|
-
return join(this.systemd.unitDir, `.systemd-ts`, `${encodeURIComponent(owner)}.json`);
|
|
1518
|
-
}
|
|
1519
|
-
async readManifest(owner) {
|
|
1520
|
-
try {
|
|
1521
|
-
const content = await readFile(this.manifestPathFor(owner), `utf8`);
|
|
1522
|
-
return JSON.parse(content);
|
|
1523
|
-
} catch {
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
async runSystemctl(action, units = []) {
|
|
1528
|
-
const args = action === `daemon-reload` ? [...this.scopeArgs(), action] : [
|
|
1529
|
-
...this.scopeArgs(),
|
|
1530
|
-
action,
|
|
1531
|
-
...units
|
|
1532
|
-
];
|
|
1533
|
-
await this.systemd.executor(`systemctl`, args);
|
|
1534
|
-
}
|
|
1535
|
-
async removeManagedUnitPath(path) {
|
|
1536
|
-
await this.systemd.executor(`rm`, [`-f`, path]);
|
|
1537
|
-
}
|
|
1538
|
-
scopeArgs() {
|
|
1539
|
-
return this.systemd.scope === `user` ? [`--user`] : [];
|
|
1540
|
-
}
|
|
1541
|
-
async writeManifest(owner, units) {
|
|
1542
|
-
await this.manifestDir();
|
|
1543
|
-
const manifest = {
|
|
1544
|
-
owner,
|
|
1545
|
-
units: units.map((unit) => ({
|
|
1546
|
-
filename: unit.filename,
|
|
1547
|
-
path: unit.path
|
|
1548
|
-
})),
|
|
1549
|
-
version: 1
|
|
1550
|
-
};
|
|
1551
|
-
await writeFile(this.manifestPathFor(owner), JSON.stringify(manifest, null, 2), `utf8`);
|
|
1552
|
-
}
|
|
1553
|
-
};
|
|
1554
|
-
function ok(value) {
|
|
1555
|
-
return {
|
|
1556
|
-
ok: true,
|
|
1557
|
-
value
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
function err(error) {
|
|
1561
|
-
return {
|
|
1562
|
-
ok: false,
|
|
1563
|
-
error
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
function joinCommandOutput(output) {
|
|
1567
|
-
return [output.stdout, output.stderr].filter((value) => value.length > 0).join(`\n`);
|
|
1568
|
-
}
|
|
1569
|
-
let lazyDefaultSystemd;
|
|
1570
|
-
/**
|
|
1571
|
-
* Returns the lazily created default `Systemd` instance.
|
|
1572
|
-
*
|
|
1573
|
-
* The default instance uses the library defaults for scope, unit directory, and
|
|
1574
|
-
* command execution.
|
|
1575
|
-
*/
|
|
1576
|
-
function defaultSystemd() {
|
|
1577
|
-
lazyDefaultSystemd ??= new Systemd();
|
|
1578
|
-
return lazyDefaultSystemd;
|
|
1579
|
-
}
|
|
1580
|
-
function resolveUnitLogPath(unit) {
|
|
1581
|
-
if (!(unit instanceof SystemdService)) return;
|
|
1582
|
-
for (const key of [`StandardOutput`, `StandardError`]) {
|
|
1583
|
-
const value = unit.service[key];
|
|
1584
|
-
if (typeof value !== `string`) continue;
|
|
1585
|
-
const path = parseFileLogPath(value);
|
|
1586
|
-
if (path !== void 0) return path;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
function parseFileLogPath(value) {
|
|
1590
|
-
for (const prefix of [`append:`, `file:`]) if (value.startsWith(prefix)) return value.slice(prefix.length);
|
|
1591
|
-
}
|
|
1592
|
-
function tailLines(output, lines) {
|
|
1593
|
-
return output.split(`\n`).slice(-lines).join(`\n`);
|
|
1594
|
-
}
|
|
1595
|
-
function isMissingPathError(cause) {
|
|
1596
|
-
if (cause === null || typeof cause !== `object`) return false;
|
|
1597
|
-
return cause[`code`] === `ENOENT`;
|
|
1598
|
-
}
|
|
1599
|
-
async function writeUnitDirectory(path) {
|
|
1600
|
-
await mkdir(path, { recursive: true });
|
|
1601
|
-
}
|
|
1602
|
-
//#endregion
|
|
1603
|
-
//#region src/main/index.ts
|
|
1604
|
-
/**
|
|
1605
|
-
* Internal helpers are exposed for advanced use, but they are not a stable public API.
|
|
1606
|
-
* Expect breaking changes here outside the package's normal compatibility guarantees.
|
|
1607
|
-
*/
|
|
1608
|
-
const Internal = internal_exports;
|
|
1609
|
-
//#endregion
|
|
1
|
+
import { _ as SystemdTsError, a as defaultSystemd, b as UnitMaterializationError, c as SystemdService, d as defineExecutable, f as ExecutableInferenceError, g as SystemctlCommandError, h as NotifySendError, i as SystemdTs, l as notify, m as NoUnitsProvidedError, n as Systemd, o as Systemctl, p as InvalidExecDirectiveError, r as SystemdMaterialization, s as SystemdTimer, t as Internal, u as Executable, v as UnitEnableError, x as UnitStartError, y as UnitLogsReadError } from "./main-BXq7TQdg.mjs";
|
|
1610
2
|
export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemctl, SystemctlCommandError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTs, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };
|