systemd-ts 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -25
- package/dist/index.d.mts +461 -117
- package/dist/index.mjs +1179 -242
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -2,9 +2,201 @@ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
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
|
|
8
200
|
//#region src/main/executable.ts
|
|
9
201
|
const currentModulePath = fileURLToPath(import.meta.url);
|
|
10
202
|
/**
|
|
@@ -23,6 +215,8 @@ const currentModulePath = fileURLToPath(import.meta.url);
|
|
|
23
215
|
var Executable = class {
|
|
24
216
|
/** Additional arguments passed after the module path. */
|
|
25
217
|
args;
|
|
218
|
+
/** Inference issue captured when no explicit module path was provided. */
|
|
219
|
+
inferenceError;
|
|
26
220
|
/** Absolute path to the module that should be executed. */
|
|
27
221
|
modulePath;
|
|
28
222
|
/** Absolute path to the runtime binary that should launch the module. */
|
|
@@ -40,7 +234,9 @@ var Executable = class {
|
|
|
40
234
|
*/
|
|
41
235
|
constructor(options = {}) {
|
|
42
236
|
this.runtimeEntrypoint = options.runtimeEntrypoint ?? process.execPath;
|
|
43
|
-
|
|
237
|
+
const inferredModulePath = options.modulePath ?? tryInferCallerModulePath();
|
|
238
|
+
this.inferenceError = inferredModulePath === void 0 && options.modulePath === void 0 ? new ExecutableInferenceError() : void 0;
|
|
239
|
+
this.modulePath = inferredModulePath ?? ``;
|
|
44
240
|
this.args = Object.freeze([...options.args ?? []]);
|
|
45
241
|
Object.freeze(this);
|
|
46
242
|
}
|
|
@@ -51,18 +247,30 @@ var Executable = class {
|
|
|
51
247
|
* path and any configured arguments.
|
|
52
248
|
*/
|
|
53
249
|
toCommandParts() {
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
};
|
|
59
262
|
}
|
|
60
263
|
/**
|
|
61
264
|
* Renders the executable as a shell-quoted command string suitable for
|
|
62
265
|
* executable-valued systemd directives such as `ExecStart=`.
|
|
63
266
|
*/
|
|
64
267
|
toExecStart() {
|
|
65
|
-
|
|
268
|
+
const commandParts = this.toCommandParts();
|
|
269
|
+
if (!commandParts.ok) return commandParts;
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
value: commandParts.value.map(shellQuote).join(` `)
|
|
273
|
+
};
|
|
66
274
|
}
|
|
67
275
|
};
|
|
68
276
|
/**
|
|
@@ -86,20 +294,19 @@ var Executable = class {
|
|
|
86
294
|
*/
|
|
87
295
|
function defineExecutable(fn, options = {}) {
|
|
88
296
|
const executable = new Executable(options);
|
|
89
|
-
if (isMainModule(executable.modulePath)) Promise.resolve(fn()).catch((error) => {
|
|
297
|
+
if (executable.inferenceError === void 0 && isMainModule(executable.modulePath)) Promise.resolve(fn()).catch((error) => {
|
|
90
298
|
process.exitCode = 1;
|
|
91
299
|
throw error;
|
|
92
300
|
});
|
|
93
301
|
return executable;
|
|
94
302
|
}
|
|
95
|
-
function
|
|
303
|
+
function tryInferCallerModulePath() {
|
|
96
304
|
const stack = (/* @__PURE__ */ new Error()).stack ?? ``;
|
|
97
305
|
for (const line of stack.split(`\n`).slice(1)) {
|
|
98
306
|
const candidate = extractStackPath(line);
|
|
99
307
|
if (candidate === void 0 || candidate === currentModulePath) continue;
|
|
100
308
|
return candidate;
|
|
101
309
|
}
|
|
102
|
-
throw new Error(`Could not infer the calling module path for defineExecutable(); pass { modulePath } explicitly`);
|
|
103
310
|
}
|
|
104
311
|
function extractStackPath(line) {
|
|
105
312
|
const fileUrlMatch = line.match(/(file:\/\/\/[^)\s:]+(?:\.[cm]?[jt]s)?)/u);
|
|
@@ -112,7 +319,7 @@ function isMainModule(modulePath) {
|
|
|
112
319
|
if (mainArg === void 0) return false;
|
|
113
320
|
return normalizeFilePath(mainArg) === normalizeFilePath(modulePath);
|
|
114
321
|
}
|
|
115
|
-
function shellQuote
|
|
322
|
+
function shellQuote(value) {
|
|
116
323
|
return `'${value.replaceAll(`'`, `'\\''`)}'`;
|
|
117
324
|
}
|
|
118
325
|
function normalizeFilePath(path) {
|
|
@@ -123,135 +330,19 @@ function normalizeFilePath(path) {
|
|
|
123
330
|
}
|
|
124
331
|
}
|
|
125
332
|
//#endregion
|
|
126
|
-
//#region src/main/systemd-service.ts
|
|
127
|
-
/**
|
|
128
|
-
* An immutable definition of a `.service` unit.
|
|
129
|
-
*
|
|
130
|
-
* `SystemdService` is a pure value object: it captures the intended unit name
|
|
131
|
-
* and section contents, but it does not write files or talk to `systemd`
|
|
132
|
-
* directly. Operational actions such as installation or startup belong on
|
|
133
|
-
* {@link Systemd}.
|
|
134
|
-
*
|
|
135
|
-
* Source:
|
|
136
|
-
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
|
|
137
|
-
*/
|
|
138
|
-
var SystemdService = class {
|
|
139
|
-
/** Optional `[Install]` section for enable-time relationships. */
|
|
140
|
-
install;
|
|
141
|
-
/** The normalized base unit name, without the `.service` suffix. */
|
|
142
|
-
name;
|
|
143
|
-
/** The fully frozen original options used to construct this service. */
|
|
144
|
-
options;
|
|
145
|
-
/** The `[Service]` section payload. */
|
|
146
|
-
service;
|
|
147
|
-
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
148
|
-
unit;
|
|
149
|
-
/**
|
|
150
|
-
* Creates an immutable service definition.
|
|
151
|
-
*
|
|
152
|
-
* The constructor preserves literal types where possible and rejects unknown
|
|
153
|
-
* directive names within the provided sections, while still allowing custom
|
|
154
|
-
* `X-...` extension directives.
|
|
155
|
-
*/
|
|
156
|
-
constructor(options) {
|
|
157
|
-
this.options = freezeUnitOptions(options);
|
|
158
|
-
this.name = normalizeUnitName(options.name, `.service`);
|
|
159
|
-
this.unit = cloneUnitSection(options.unit);
|
|
160
|
-
this.service = cloneUnitSection(options.service) ?? {};
|
|
161
|
-
this.install = cloneUnitSection(options.install);
|
|
162
|
-
Object.freeze(this);
|
|
163
|
-
}
|
|
164
|
-
/** The canonical unit filename, including the `.service` suffix. */
|
|
165
|
-
get filename() {
|
|
166
|
-
return `${this.name}.service`;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Renders the service as a complete unit file.
|
|
170
|
-
*
|
|
171
|
-
* Rendering also validates executable-valued directives such as `ExecStart`
|
|
172
|
-
* and `ExecStop`, ensuring they use absolute runtime entrypoints as required
|
|
173
|
-
* by systemd.
|
|
174
|
-
*/
|
|
175
|
-
render() {
|
|
176
|
-
validateServiceSection(this.service);
|
|
177
|
-
return renderUnitFile([
|
|
178
|
-
[`Unit`, this.unit],
|
|
179
|
-
[`Service`, this.service],
|
|
180
|
-
[`Install`, this.install]
|
|
181
|
-
]);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
//#endregion
|
|
185
|
-
//#region src/main/systemd-timer.ts
|
|
186
|
-
/**
|
|
187
|
-
* An immutable definition of a `.timer` unit.
|
|
188
|
-
*
|
|
189
|
-
* Like {@link SystemdService}, this is a pure value object. It models the timer
|
|
190
|
-
* configuration and its attachment target, but does not write files or interact
|
|
191
|
-
* with the service manager directly.
|
|
192
|
-
*
|
|
193
|
-
* Source:
|
|
194
|
-
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html
|
|
195
|
-
*/
|
|
196
|
-
var SystemdTimer = class {
|
|
197
|
-
/** Optional `[Install]` section for enable-time relationships. */
|
|
198
|
-
install;
|
|
199
|
-
/** The normalized base unit name, without the `.timer` suffix. */
|
|
200
|
-
name;
|
|
201
|
-
/** The fully frozen original options used to construct this timer. */
|
|
202
|
-
options;
|
|
203
|
-
/** The attached service basename inferred from `targetUnit`. */
|
|
204
|
-
targetServiceName;
|
|
205
|
-
/** The unit name this timer activates, explicit or implicit. */
|
|
206
|
-
targetUnit;
|
|
207
|
-
/** The `[Timer]` section payload. */
|
|
208
|
-
timer;
|
|
209
|
-
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
210
|
-
unit;
|
|
211
|
-
/**
|
|
212
|
-
* Creates an immutable timer definition.
|
|
213
|
-
*
|
|
214
|
-
* If no explicit `timer.Unit` is provided, the target defaults to the service
|
|
215
|
-
* with the same basename, matching systemd's native timer behavior.
|
|
216
|
-
*/
|
|
217
|
-
constructor(options) {
|
|
218
|
-
this.options = freezeUnitOptions(options);
|
|
219
|
-
this.name = normalizeUnitName(options.name, `.timer`);
|
|
220
|
-
this.unit = cloneUnitSection(options.unit);
|
|
221
|
-
this.timer = cloneUnitSection(options.timer) ?? {};
|
|
222
|
-
this.install = cloneUnitSection(options.install);
|
|
223
|
-
this.targetUnit = resolveTimerTargetUnit(options);
|
|
224
|
-
this.targetServiceName = normalizeUnitName(this.targetUnit, `.service`);
|
|
225
|
-
Object.freeze(this);
|
|
226
|
-
}
|
|
227
|
-
/** The canonical unit filename, including the `.timer` suffix. */
|
|
228
|
-
get filename() {
|
|
229
|
-
return `${this.name}.timer`;
|
|
230
|
-
}
|
|
231
|
-
/** Renders the timer as a complete unit file. */
|
|
232
|
-
render() {
|
|
233
|
-
return renderUnitFile([
|
|
234
|
-
[`Unit`, this.unit],
|
|
235
|
-
[`Timer`, this.timer],
|
|
236
|
-
[`Install`, this.install]
|
|
237
|
-
]);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
//#endregion
|
|
241
333
|
//#region src/main/internal.ts
|
|
242
334
|
var internal_exports = /* @__PURE__ */ __exportAll({
|
|
243
|
-
assertInstallableTogether: () => assertInstallableTogether,
|
|
244
335
|
cloneUnitSection: () => cloneUnitSection,
|
|
245
336
|
defaultCommandExecutor: () => defaultCommandExecutor,
|
|
246
337
|
defaultUnitDirForScope: () => defaultUnitDirForScope,
|
|
338
|
+
extractCommandOutput: () => extractCommandOutput,
|
|
247
339
|
fileExists: () => fileExists,
|
|
248
340
|
freezeUnitOptions: () => freezeUnitOptions,
|
|
249
341
|
normalizeUnitName: () => normalizeUnitName,
|
|
250
|
-
|
|
342
|
+
parseStartStatus: () => parseStartStatus,
|
|
251
343
|
renderUnitFile: () => renderUnitFile,
|
|
252
344
|
resolveTimerTargetUnit: () => resolveTimerTargetUnit,
|
|
253
345
|
sendNotify: () => sendNotify,
|
|
254
|
-
shellQuote: () => shellQuote,
|
|
255
346
|
validateServiceSection: () => validateServiceSection
|
|
256
347
|
});
|
|
257
348
|
const EXEC_DIRECTIVE_KEYS = [
|
|
@@ -295,17 +386,16 @@ function cloneUnitSection(section) {
|
|
|
295
386
|
return Object.freeze(Object.fromEntries(entries));
|
|
296
387
|
}
|
|
297
388
|
function validateServiceSection(service) {
|
|
298
|
-
for (const key of EXEC_DIRECTIVE_KEYS)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const installedServices = new Set(units.filter((unit) => unit instanceof SystemdService).map((unit) => unit.name));
|
|
302
|
-
if (installedServices.size === 0) return;
|
|
303
|
-
for (const unit of units) {
|
|
304
|
-
if (!(unit instanceof SystemdTimer)) continue;
|
|
305
|
-
if (!installedServices.has(unit.targetServiceName)) throw new Error(`Cannot install ${unit.filename} alongside unrelated services: expected ${unit.targetUnit}`);
|
|
389
|
+
for (const key of EXEC_DIRECTIVE_KEYS) {
|
|
390
|
+
const validation = assertAbsoluteExecValue(key, service[key]);
|
|
391
|
+
if (!validation.ok) return validation;
|
|
306
392
|
}
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
value: void 0
|
|
396
|
+
};
|
|
307
397
|
}
|
|
308
|
-
function
|
|
398
|
+
function parseStartStatus(unit, output) {
|
|
309
399
|
const properties = Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
310
400
|
const separatorIndex = line.indexOf(`=`);
|
|
311
401
|
if (separatorIndex === -1) return [line, ``];
|
|
@@ -320,27 +410,47 @@ function parseStartResult(unit, output) {
|
|
|
320
410
|
};
|
|
321
411
|
}
|
|
322
412
|
function renderUnitFile(sections) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
}
|
|
341
448
|
}
|
|
342
|
-
async function defaultCommandExecutor(command, args) {
|
|
343
|
-
const result = await execFileAsync(command, [...args]
|
|
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
|
+
} });
|
|
344
454
|
return {
|
|
345
455
|
stderr: result.stderr,
|
|
346
456
|
stdout: result.stdout
|
|
@@ -355,18 +465,48 @@ async function fileExists(path) {
|
|
|
355
465
|
}
|
|
356
466
|
}
|
|
357
467
|
async function sendNotify(state, options) {
|
|
358
|
-
const args = [state];
|
|
468
|
+
const args = [`--no-block`, state];
|
|
359
469
|
if (options.pid !== void 0) args.push(`MAINPID=${options.pid}`);
|
|
360
470
|
if (options.status !== void 0) args.push(`STATUS=${options.status}`);
|
|
361
471
|
if (options.executor !== void 0) {
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
}
|
|
364
483
|
return;
|
|
365
484
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
};
|
|
370
510
|
}
|
|
371
511
|
function hasServiceSection(options) {
|
|
372
512
|
return `service` in options;
|
|
@@ -376,19 +516,41 @@ function hasTimerSection(options) {
|
|
|
376
516
|
}
|
|
377
517
|
function assertAbsoluteExecValue(key, value) {
|
|
378
518
|
if (isUnitValueList(value)) {
|
|
379
|
-
for (const entry of value)
|
|
380
|
-
|
|
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
|
+
};
|
|
381
527
|
}
|
|
382
|
-
assertAbsoluteExecEntry(key, value);
|
|
528
|
+
return assertAbsoluteExecEntry(key, value);
|
|
383
529
|
}
|
|
384
530
|
function assertAbsoluteExecEntry(key, value) {
|
|
385
|
-
if (typeof value === `string` && value.length > 0 && !isAbsoluteExecCommand(value))
|
|
386
|
-
|
|
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
|
+
};
|
|
387
543
|
}
|
|
388
544
|
function stringifyUnitValue(value) {
|
|
389
545
|
if (value instanceof Executable) return value.toExecStart();
|
|
390
|
-
if (typeof value === `boolean`) return
|
|
391
|
-
|
|
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
|
+
};
|
|
392
554
|
}
|
|
393
555
|
function isUnitValueList(value) {
|
|
394
556
|
return Array.isArray(value);
|
|
@@ -418,9 +580,6 @@ function isAbsoluteExecCommand(value) {
|
|
|
418
580
|
}
|
|
419
581
|
return value[index] === `/`;
|
|
420
582
|
}
|
|
421
|
-
function buildNotifyShellCommand(args, socketPath) {
|
|
422
|
-
return `${socketPath === void 0 ? `` : `NOTIFY_SOCKET=${shellQuote(socketPath)} `}${[`systemd-notify`, ...args].map(shellQuote).join(` `)}`;
|
|
423
|
-
}
|
|
424
583
|
//#endregion
|
|
425
584
|
//#region src/main/notify.ts
|
|
426
585
|
/**
|
|
@@ -444,7 +603,21 @@ const notify = {
|
|
|
444
603
|
* sent as an additional `STATUS=...` field.
|
|
445
604
|
*/
|
|
446
605
|
async ready(options = {}) {
|
|
447
|
-
|
|
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
|
+
}
|
|
448
621
|
},
|
|
449
622
|
/**
|
|
450
623
|
* Sends `WATCHDOG=1` to systemd.
|
|
@@ -454,35 +627,375 @@ const notify = {
|
|
|
454
627
|
* `MAINPID=...`, and `options.status` is forwarded as `STATUS=...`.
|
|
455
628
|
*/
|
|
456
629
|
async watchdog(options = {}) {
|
|
457
|
-
|
|
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
|
+
]);
|
|
458
705
|
}
|
|
459
706
|
};
|
|
460
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
|
|
461
982
|
//#region src/main/systemd.ts
|
|
462
983
|
/**
|
|
463
|
-
*
|
|
984
|
+
* A successful systemd unit materialization.
|
|
464
985
|
*
|
|
465
|
-
* It records the
|
|
466
|
-
*
|
|
986
|
+
* It records the target directory and the on-disk path associated with each
|
|
987
|
+
* materialized unit.
|
|
467
988
|
*/
|
|
468
|
-
var
|
|
989
|
+
var SystemdMaterialization = class {
|
|
469
990
|
/** The directory units were written into. */
|
|
470
991
|
directory;
|
|
471
|
-
/** The
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
constructor(directory, installed) {
|
|
992
|
+
/** The materialized units together with their resolved on-disk paths. */
|
|
993
|
+
materialized;
|
|
994
|
+
constructor(directory, materialized) {
|
|
475
995
|
this.directory = directory;
|
|
476
|
-
this.
|
|
477
|
-
this.pathByFilename = new Map(installed.map((entry) => [entry.unit.filename, entry.path]));
|
|
996
|
+
this.materialized = Object.freeze([...materialized]);
|
|
478
997
|
Object.freeze(this);
|
|
479
998
|
}
|
|
480
|
-
/** Returns the installed on-disk path for a previously installed unit. */
|
|
481
|
-
pathFor(unit) {
|
|
482
|
-
const path = this.pathByFilename.get(unit.filename);
|
|
483
|
-
if (path === void 0) throw new Error(`No installed path is recorded for ${unit.filename}`);
|
|
484
|
-
return path;
|
|
485
|
-
}
|
|
486
999
|
};
|
|
487
1000
|
/**
|
|
488
1001
|
* A configured interface to a specific systemd environment.
|
|
@@ -501,6 +1014,10 @@ var Systemd = class {
|
|
|
501
1014
|
linkUnits;
|
|
502
1015
|
/** The target manager scope, either `system` or `user`. */
|
|
503
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;
|
|
504
1021
|
/** The directory used when materializing unit files. */
|
|
505
1022
|
unitDir;
|
|
506
1023
|
/**
|
|
@@ -515,47 +1032,62 @@ var Systemd = class {
|
|
|
515
1032
|
this.unitDir = options.unitDir ?? defaultUnitDirForScope(this.scope);
|
|
516
1033
|
this.linkUnits = options.linkUnits ?? false;
|
|
517
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);
|
|
518
1040
|
Object.freeze(this);
|
|
519
1041
|
}
|
|
520
1042
|
/**
|
|
521
1043
|
* Renders and writes one or more units into this instance's `unitDir`.
|
|
522
1044
|
*
|
|
523
1045
|
* This is intentionally a file-materialization step. It does not enable or
|
|
524
|
-
* start the units on its own.
|
|
525
|
-
* together, compile-time and runtime attachment checks ensure the timer points
|
|
526
|
-
* at one of the accompanying services.
|
|
1046
|
+
* start the units on its own.
|
|
527
1047
|
*/
|
|
528
|
-
async
|
|
529
|
-
|
|
530
|
-
assertInstallableTogether(units);
|
|
531
|
-
await writeUnitDirectory(this.unitDir);
|
|
532
|
-
const installed = [];
|
|
533
|
-
for (const unit of units) {
|
|
534
|
-
const path = join(this.unitDir, unit.filename);
|
|
535
|
-
await writeFile(path, unit.render(), `utf8`);
|
|
536
|
-
installed.push({
|
|
537
|
-
path,
|
|
538
|
-
unit
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
return new SystemdInstallResult(this.unitDir, installed);
|
|
1048
|
+
async materialize(...units) {
|
|
1049
|
+
return this.materializeUnits(units);
|
|
542
1050
|
}
|
|
543
1051
|
/**
|
|
544
1052
|
* Enables one or more units via `systemctl enable`.
|
|
545
1053
|
*
|
|
546
|
-
* When `linkUnits` is enabled, this first links
|
|
1054
|
+
* When `linkUnits` is enabled, this first links materialized unit files into the
|
|
547
1055
|
* target manager and reloads systemd so the units are visible before the
|
|
548
1056
|
* enable operation runs.
|
|
549
1057
|
*/
|
|
550
1058
|
async enable(...units) {
|
|
551
|
-
if (units.length === 0)
|
|
1059
|
+
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.enable()`));
|
|
552
1060
|
const scopeArgs = this.scopeArgs();
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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);
|
|
559
1091
|
}
|
|
560
1092
|
/**
|
|
561
1093
|
* Starts a unit and returns a parsed `systemctl show` snapshot of its final
|
|
@@ -566,19 +1098,54 @@ var Systemd = class {
|
|
|
566
1098
|
*/
|
|
567
1099
|
async start(unit) {
|
|
568
1100
|
const scopeArgs = this.scopeArgs();
|
|
569
|
-
|
|
570
|
-
|
|
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 = [
|
|
571
1113
|
...scopeArgs,
|
|
572
1114
|
`start`,
|
|
573
1115
|
unit.filename
|
|
574
|
-
]
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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);
|
|
582
1149
|
}
|
|
583
1150
|
/**
|
|
584
1151
|
* Reads recent output for a managed unit.
|
|
@@ -589,38 +1156,115 @@ var Systemd = class {
|
|
|
589
1156
|
*/
|
|
590
1157
|
async logs(unit, options) {
|
|
591
1158
|
const fileLogPath = resolveUnitLogPath(unit);
|
|
592
|
-
if (fileLogPath !== void 0)
|
|
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
|
+
}
|
|
593
1170
|
const scopeArgs = this.scopeArgs();
|
|
594
1171
|
const lines = options?.lines ?? 50;
|
|
595
|
-
const
|
|
596
|
-
|
|
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 = [
|
|
597
1177
|
...scopeArgs,
|
|
598
1178
|
`status`,
|
|
599
1179
|
unit.filename,
|
|
600
1180
|
`--no-pager`,
|
|
601
1181
|
`--lines`,
|
|
602
1182
|
String(lines)
|
|
603
|
-
]
|
|
604
|
-
return (
|
|
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
|
+
}));
|
|
605
1192
|
}
|
|
606
1193
|
/** Returns the on-disk unit-file path this instance uses for the given unit. */
|
|
607
1194
|
pathFor(unit) {
|
|
608
1195
|
return join(this.unitDir, unit.filename);
|
|
609
1196
|
}
|
|
610
|
-
async prepareUnits(scopeArgs, units) {
|
|
1197
|
+
async prepareUnits(scopeArgs, units, operation) {
|
|
611
1198
|
if (this.linkUnits) {
|
|
612
1199
|
const linkPaths = await this.collectLinkPaths(units);
|
|
613
|
-
for (const path of linkPaths)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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);
|
|
618
1234
|
}
|
|
619
|
-
await this.executor(`systemctl`, [...scopeArgs, `daemon-reload`]);
|
|
620
1235
|
}
|
|
621
1236
|
scopeArgs() {
|
|
622
1237
|
return this.scope === `user` ? [`--user`] : [];
|
|
623
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
|
+
}
|
|
624
1268
|
async collectLinkPaths(units) {
|
|
625
1269
|
const paths = /* @__PURE__ */ new Set();
|
|
626
1270
|
for (const unit of units) {
|
|
@@ -632,7 +1276,296 @@ var Systemd = class {
|
|
|
632
1276
|
}
|
|
633
1277
|
return [...paths];
|
|
634
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
|
+
}
|
|
635
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
|
+
}
|
|
636
1569
|
let lazyDefaultSystemd;
|
|
637
1570
|
/**
|
|
638
1571
|
* Returns the lazily created default `Systemd` instance.
|
|
@@ -659,6 +1592,10 @@ function parseFileLogPath(value) {
|
|
|
659
1592
|
function tailLines(output, lines) {
|
|
660
1593
|
return output.split(`\n`).slice(-lines).join(`\n`);
|
|
661
1594
|
}
|
|
1595
|
+
function isMissingPathError(cause) {
|
|
1596
|
+
if (cause === null || typeof cause !== `object`) return false;
|
|
1597
|
+
return cause[`code`] === `ENOENT`;
|
|
1598
|
+
}
|
|
662
1599
|
async function writeUnitDirectory(path) {
|
|
663
1600
|
await mkdir(path, { recursive: true });
|
|
664
1601
|
}
|
|
@@ -670,4 +1607,4 @@ async function writeUnitDirectory(path) {
|
|
|
670
1607
|
*/
|
|
671
1608
|
const Internal = internal_exports;
|
|
672
1609
|
//#endregion
|
|
673
|
-
export { Executable, Internal, Systemd,
|
|
1610
|
+
export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemctl, SystemctlCommandError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTs, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };
|