systemd-ts 0.1.0 → 0.2.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 +70 -0
- package/dist/index.d.mts +1075 -546
- package/dist/index.mjs +676 -224
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,174 @@ import { execFile } from "node:child_process";
|
|
|
5
5
|
import { access, mkdir, readFile, 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
|
+
function classifyCommandEnvironmentReason(cause) {
|
|
148
|
+
if (cause === null || typeof cause !== `object`) return;
|
|
149
|
+
const record = cause;
|
|
150
|
+
if (record[`code`] === `ENOENT`) return `missing-command`;
|
|
151
|
+
if (record[`code`] === `EACCES` || record[`code`] === `EPERM`) return `permission-denied`;
|
|
152
|
+
const combinedOutput = [record[`stderr`], record[`stdout`]].filter((value) => typeof value === `string` && value.length > 0).join(`\n`);
|
|
153
|
+
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`;
|
|
154
|
+
}
|
|
155
|
+
function classifyMaterializationReason(cause) {
|
|
156
|
+
if (cause === null || typeof cause !== `object`) return;
|
|
157
|
+
const record = cause;
|
|
158
|
+
if (record[`code`] === `EACCES` || record[`code`] === `EPERM` || record[`code`] === `EROFS`) return `permission-denied`;
|
|
159
|
+
if (record[`code`] === `EEXIST` || record[`code`] === `ENOTDIR` || record[`code`] === `EISDIR`) return `invalid-unit-directory`;
|
|
160
|
+
return `file-system-failed`;
|
|
161
|
+
}
|
|
162
|
+
function extractCommandErrorDetails(cause) {
|
|
163
|
+
if (cause === null || typeof cause !== `object`) return {
|
|
164
|
+
exitCode: void 0,
|
|
165
|
+
stderr: void 0,
|
|
166
|
+
stdout: void 0
|
|
167
|
+
};
|
|
168
|
+
const record = cause;
|
|
169
|
+
return {
|
|
170
|
+
exitCode: typeof record[`code`] === `number` ? record[`code`] : void 0,
|
|
171
|
+
stderr: typeof record[`stderr`] === `string` ? record[`stderr`] : void 0,
|
|
172
|
+
stdout: typeof record[`stdout`] === `string` ? record[`stdout`] : void 0
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
8
176
|
//#region src/main/executable.ts
|
|
9
177
|
const currentModulePath = fileURLToPath(import.meta.url);
|
|
10
178
|
/**
|
|
@@ -23,6 +191,8 @@ const currentModulePath = fileURLToPath(import.meta.url);
|
|
|
23
191
|
var Executable = class {
|
|
24
192
|
/** Additional arguments passed after the module path. */
|
|
25
193
|
args;
|
|
194
|
+
/** Inference issue captured when no explicit module path was provided. */
|
|
195
|
+
inferenceError;
|
|
26
196
|
/** Absolute path to the module that should be executed. */
|
|
27
197
|
modulePath;
|
|
28
198
|
/** Absolute path to the runtime binary that should launch the module. */
|
|
@@ -40,7 +210,9 @@ var Executable = class {
|
|
|
40
210
|
*/
|
|
41
211
|
constructor(options = {}) {
|
|
42
212
|
this.runtimeEntrypoint = options.runtimeEntrypoint ?? process.execPath;
|
|
43
|
-
|
|
213
|
+
const inferredModulePath = options.modulePath ?? tryInferCallerModulePath();
|
|
214
|
+
this.inferenceError = inferredModulePath === void 0 && options.modulePath === void 0 ? new ExecutableInferenceError() : void 0;
|
|
215
|
+
this.modulePath = inferredModulePath ?? ``;
|
|
44
216
|
this.args = Object.freeze([...options.args ?? []]);
|
|
45
217
|
Object.freeze(this);
|
|
46
218
|
}
|
|
@@ -51,18 +223,30 @@ var Executable = class {
|
|
|
51
223
|
* path and any configured arguments.
|
|
52
224
|
*/
|
|
53
225
|
toCommandParts() {
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
226
|
+
if (this.inferenceError !== void 0) return {
|
|
227
|
+
ok: false,
|
|
228
|
+
error: this.inferenceError
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
value: [
|
|
233
|
+
this.runtimeEntrypoint,
|
|
234
|
+
this.modulePath,
|
|
235
|
+
...this.args
|
|
236
|
+
]
|
|
237
|
+
};
|
|
59
238
|
}
|
|
60
239
|
/**
|
|
61
240
|
* Renders the executable as a shell-quoted command string suitable for
|
|
62
241
|
* executable-valued systemd directives such as `ExecStart=`.
|
|
63
242
|
*/
|
|
64
243
|
toExecStart() {
|
|
65
|
-
|
|
244
|
+
const commandParts = this.toCommandParts();
|
|
245
|
+
if (!commandParts.ok) return commandParts;
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
value: commandParts.value.map(shellQuote$1).join(` `)
|
|
249
|
+
};
|
|
66
250
|
}
|
|
67
251
|
};
|
|
68
252
|
/**
|
|
@@ -86,20 +270,19 @@ var Executable = class {
|
|
|
86
270
|
*/
|
|
87
271
|
function defineExecutable(fn, options = {}) {
|
|
88
272
|
const executable = new Executable(options);
|
|
89
|
-
if (isMainModule(executable.modulePath)) Promise.resolve(fn()).catch((error) => {
|
|
273
|
+
if (executable.inferenceError === void 0 && isMainModule(executable.modulePath)) Promise.resolve(fn()).catch((error) => {
|
|
90
274
|
process.exitCode = 1;
|
|
91
275
|
throw error;
|
|
92
276
|
});
|
|
93
277
|
return executable;
|
|
94
278
|
}
|
|
95
|
-
function
|
|
279
|
+
function tryInferCallerModulePath() {
|
|
96
280
|
const stack = (/* @__PURE__ */ new Error()).stack ?? ``;
|
|
97
281
|
for (const line of stack.split(`\n`).slice(1)) {
|
|
98
282
|
const candidate = extractStackPath(line);
|
|
99
283
|
if (candidate === void 0 || candidate === currentModulePath) continue;
|
|
100
284
|
return candidate;
|
|
101
285
|
}
|
|
102
|
-
throw new Error(`Could not infer the calling module path for defineExecutable(); pass { modulePath } explicitly`);
|
|
103
286
|
}
|
|
104
287
|
function extractStackPath(line) {
|
|
105
288
|
const fileUrlMatch = line.match(/(file:\/\/\/[^)\s:]+(?:\.[cm]?[jt]s)?)/u);
|
|
@@ -123,131 +306,15 @@ function normalizeFilePath(path) {
|
|
|
123
306
|
}
|
|
124
307
|
}
|
|
125
308
|
//#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
309
|
//#region src/main/internal.ts
|
|
242
310
|
var internal_exports = /* @__PURE__ */ __exportAll({
|
|
243
|
-
assertInstallableTogether: () => assertInstallableTogether,
|
|
244
311
|
cloneUnitSection: () => cloneUnitSection,
|
|
245
312
|
defaultCommandExecutor: () => defaultCommandExecutor,
|
|
246
313
|
defaultUnitDirForScope: () => defaultUnitDirForScope,
|
|
247
314
|
fileExists: () => fileExists,
|
|
248
315
|
freezeUnitOptions: () => freezeUnitOptions,
|
|
249
316
|
normalizeUnitName: () => normalizeUnitName,
|
|
250
|
-
|
|
317
|
+
parseStartStatus: () => parseStartStatus,
|
|
251
318
|
renderUnitFile: () => renderUnitFile,
|
|
252
319
|
resolveTimerTargetUnit: () => resolveTimerTargetUnit,
|
|
253
320
|
sendNotify: () => sendNotify,
|
|
@@ -295,17 +362,16 @@ function cloneUnitSection(section) {
|
|
|
295
362
|
return Object.freeze(Object.fromEntries(entries));
|
|
296
363
|
}
|
|
297
364
|
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}`);
|
|
365
|
+
for (const key of EXEC_DIRECTIVE_KEYS) {
|
|
366
|
+
const validation = assertAbsoluteExecValue(key, service[key]);
|
|
367
|
+
if (!validation.ok) return validation;
|
|
306
368
|
}
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
value: void 0
|
|
372
|
+
};
|
|
307
373
|
}
|
|
308
|
-
function
|
|
374
|
+
function parseStartStatus(unit, output) {
|
|
309
375
|
const properties = Object.fromEntries(output.split(`\n`).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
310
376
|
const separatorIndex = line.indexOf(`=`);
|
|
311
377
|
if (separatorIndex === -1) return [line, ``];
|
|
@@ -320,21 +386,41 @@ function parseStartResult(unit, output) {
|
|
|
320
386
|
};
|
|
321
387
|
}
|
|
322
388
|
function renderUnitFile(sections) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
389
|
+
try {
|
|
390
|
+
const renderedSections = sections.flatMap(([sectionName, section]) => {
|
|
391
|
+
if (section === void 0) return [];
|
|
392
|
+
const lines = [];
|
|
393
|
+
for (const [key, value] of Object.entries(section)) {
|
|
394
|
+
if (value === void 0) continue;
|
|
395
|
+
if (isUnitValueList(value)) {
|
|
396
|
+
for (const entry of value) {
|
|
397
|
+
const rendered = stringifyUnitValue(entry);
|
|
398
|
+
if (!rendered.ok) throw rendered.error;
|
|
399
|
+
lines.push(`${key}=${rendered.value}`);
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const rendered = stringifyUnitValue(value);
|
|
404
|
+
if (!rendered.ok) throw rendered.error;
|
|
405
|
+
lines.push(`${key}=${rendered.value}`);
|
|
406
|
+
}
|
|
407
|
+
if (lines.length === 0) return [];
|
|
408
|
+
return [
|
|
409
|
+
`[${sectionName}]`,
|
|
410
|
+
...lines,
|
|
411
|
+
``
|
|
412
|
+
];
|
|
413
|
+
}).join(`\n`);
|
|
414
|
+
return {
|
|
415
|
+
ok: true,
|
|
416
|
+
value: renderedSections.endsWith(`\n`) ? renderedSections : `${renderedSections}\n`
|
|
417
|
+
};
|
|
418
|
+
} catch (error) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
error: error instanceof ExecutableInferenceError ? error : new ExecutableInferenceError({ cause: error })
|
|
422
|
+
};
|
|
423
|
+
}
|
|
338
424
|
}
|
|
339
425
|
function shellQuote(value) {
|
|
340
426
|
return `'${value.replaceAll(`'`, `'\\''`)}'`;
|
|
@@ -360,13 +446,33 @@ async function sendNotify(state, options) {
|
|
|
360
446
|
if (options.status !== void 0) args.push(`STATUS=${options.status}`);
|
|
361
447
|
if (options.executor !== void 0) {
|
|
362
448
|
const command = buildNotifyShellCommand(args, options.socketPath);
|
|
363
|
-
|
|
449
|
+
try {
|
|
450
|
+
await options.executor(`bash`, [`-lc`, command]);
|
|
451
|
+
} catch (cause) {
|
|
452
|
+
throw new NotifySendError(`Failed to send systemd notification through the configured executor`, {
|
|
453
|
+
args: [`-lc`, command],
|
|
454
|
+
cause,
|
|
455
|
+
command: `bash`,
|
|
456
|
+
reason: `executor-failed`,
|
|
457
|
+
stage: `executor`
|
|
458
|
+
});
|
|
459
|
+
}
|
|
364
460
|
return;
|
|
365
461
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
462
|
+
try {
|
|
463
|
+
await execFileAsync(`systemd-notify`, args, { env: {
|
|
464
|
+
...process.env,
|
|
465
|
+
...options.socketPath === void 0 ? {} : { NOTIFY_SOCKET: options.socketPath }
|
|
466
|
+
} });
|
|
467
|
+
} catch (cause) {
|
|
468
|
+
throw new NotifySendError(`Failed to send systemd notification with systemd-notify`, {
|
|
469
|
+
args,
|
|
470
|
+
cause,
|
|
471
|
+
command: `systemd-notify`,
|
|
472
|
+
reason: `systemd-notify-failed`,
|
|
473
|
+
stage: `systemd-notify`
|
|
474
|
+
});
|
|
475
|
+
}
|
|
370
476
|
}
|
|
371
477
|
function hasServiceSection(options) {
|
|
372
478
|
return `service` in options;
|
|
@@ -376,19 +482,41 @@ function hasTimerSection(options) {
|
|
|
376
482
|
}
|
|
377
483
|
function assertAbsoluteExecValue(key, value) {
|
|
378
484
|
if (isUnitValueList(value)) {
|
|
379
|
-
for (const entry of value)
|
|
380
|
-
|
|
485
|
+
for (const entry of value) {
|
|
486
|
+
const validation = assertAbsoluteExecEntry(key, entry);
|
|
487
|
+
if (!validation.ok) return validation;
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
value: void 0
|
|
492
|
+
};
|
|
381
493
|
}
|
|
382
|
-
assertAbsoluteExecEntry(key, value);
|
|
494
|
+
return assertAbsoluteExecEntry(key, value);
|
|
383
495
|
}
|
|
384
496
|
function assertAbsoluteExecEntry(key, value) {
|
|
385
|
-
if (typeof value === `string` && value.length > 0 && !isAbsoluteExecCommand(value))
|
|
386
|
-
|
|
497
|
+
if (typeof value === `string` && value.length > 0 && !isAbsoluteExecCommand(value)) return {
|
|
498
|
+
ok: false,
|
|
499
|
+
error: new InvalidExecDirectiveError(key)
|
|
500
|
+
};
|
|
501
|
+
if (value instanceof Executable && !value.runtimeEntrypoint.startsWith(`/`)) return {
|
|
502
|
+
ok: false,
|
|
503
|
+
error: new InvalidExecDirectiveError(key)
|
|
504
|
+
};
|
|
505
|
+
return {
|
|
506
|
+
ok: true,
|
|
507
|
+
value: void 0
|
|
508
|
+
};
|
|
387
509
|
}
|
|
388
510
|
function stringifyUnitValue(value) {
|
|
389
511
|
if (value instanceof Executable) return value.toExecStart();
|
|
390
|
-
if (typeof value === `boolean`) return
|
|
391
|
-
|
|
512
|
+
if (typeof value === `boolean`) return {
|
|
513
|
+
ok: true,
|
|
514
|
+
value: value ? `true` : `false`
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
value: String(value)
|
|
519
|
+
};
|
|
392
520
|
}
|
|
393
521
|
function isUnitValueList(value) {
|
|
394
522
|
return Array.isArray(value);
|
|
@@ -444,7 +572,21 @@ const notify = {
|
|
|
444
572
|
* sent as an additional `STATUS=...` field.
|
|
445
573
|
*/
|
|
446
574
|
async ready(options = {}) {
|
|
447
|
-
|
|
575
|
+
try {
|
|
576
|
+
await sendNotify(`READY=1`, options);
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
value: void 0
|
|
580
|
+
};
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
error: error instanceof NotifySendError ? error : new NotifySendError(`Failed to send READY=1 notification`, {
|
|
585
|
+
cause: error,
|
|
586
|
+
stage: `ready`
|
|
587
|
+
})
|
|
588
|
+
};
|
|
589
|
+
}
|
|
448
590
|
},
|
|
449
591
|
/**
|
|
450
592
|
* Sends `WATCHDOG=1` to systemd.
|
|
@@ -454,35 +596,157 @@ const notify = {
|
|
|
454
596
|
* `MAINPID=...`, and `options.status` is forwarded as `STATUS=...`.
|
|
455
597
|
*/
|
|
456
598
|
async watchdog(options = {}) {
|
|
457
|
-
|
|
599
|
+
try {
|
|
600
|
+
await sendNotify(`WATCHDOG=1`, options);
|
|
601
|
+
return {
|
|
602
|
+
ok: true,
|
|
603
|
+
value: void 0
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
return {
|
|
607
|
+
ok: false,
|
|
608
|
+
error: error instanceof NotifySendError ? error : new NotifySendError(`Failed to send WATCHDOG=1 notification`, {
|
|
609
|
+
cause: error,
|
|
610
|
+
stage: `watchdog`
|
|
611
|
+
})
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/main/systemd-service.ts
|
|
618
|
+
/**
|
|
619
|
+
* An immutable definition of a `.service` unit.
|
|
620
|
+
*
|
|
621
|
+
* `SystemdService` is a pure value object: it captures the intended unit name
|
|
622
|
+
* and section contents, but it does not write files or talk to `systemd`
|
|
623
|
+
* directly. Operational actions such as installation or startup belong on
|
|
624
|
+
* {@link Systemd}.
|
|
625
|
+
*
|
|
626
|
+
* Source:
|
|
627
|
+
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
|
|
628
|
+
*/
|
|
629
|
+
var SystemdService = class {
|
|
630
|
+
/** Optional `[Install]` section for enable-time relationships. */
|
|
631
|
+
install;
|
|
632
|
+
/** The normalized base unit name, without the `.service` suffix. */
|
|
633
|
+
name;
|
|
634
|
+
/** The fully frozen original options used to construct this service. */
|
|
635
|
+
options;
|
|
636
|
+
/** The `[Service]` section payload. */
|
|
637
|
+
service;
|
|
638
|
+
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
639
|
+
unit;
|
|
640
|
+
/**
|
|
641
|
+
* Creates an immutable service definition.
|
|
642
|
+
*
|
|
643
|
+
* The constructor preserves literal types where possible and rejects unknown
|
|
644
|
+
* directive names within the provided sections, while still allowing custom
|
|
645
|
+
* `X-...` extension directives.
|
|
646
|
+
*/
|
|
647
|
+
constructor(options) {
|
|
648
|
+
this.options = freezeUnitOptions(options);
|
|
649
|
+
this.name = normalizeUnitName(options.name, `.service`);
|
|
650
|
+
this.unit = cloneUnitSection(options.unit);
|
|
651
|
+
this.service = cloneUnitSection(options.service) ?? {};
|
|
652
|
+
this.install = cloneUnitSection(options.install);
|
|
653
|
+
Object.freeze(this);
|
|
654
|
+
}
|
|
655
|
+
/** The canonical unit filename, including the `.service` suffix. */
|
|
656
|
+
get filename() {
|
|
657
|
+
return `${this.name}.service`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Renders the service as a complete unit file.
|
|
661
|
+
*
|
|
662
|
+
* Rendering also validates executable-valued directives such as `ExecStart`
|
|
663
|
+
* and `ExecStop`, ensuring they use absolute runtime entrypoints as required
|
|
664
|
+
* by systemd.
|
|
665
|
+
*/
|
|
666
|
+
render() {
|
|
667
|
+
const validation = validateServiceSection(this.service);
|
|
668
|
+
if (!validation.ok) return validation;
|
|
669
|
+
return renderUnitFile([
|
|
670
|
+
[`Unit`, this.unit],
|
|
671
|
+
[`Service`, this.service],
|
|
672
|
+
[`Install`, this.install]
|
|
673
|
+
]);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region src/main/systemd-timer.ts
|
|
678
|
+
/**
|
|
679
|
+
* An immutable definition of a `.timer` unit.
|
|
680
|
+
*
|
|
681
|
+
* Like {@link SystemdService}, this is a pure value object. It models the timer
|
|
682
|
+
* configuration and its attachment target, but does not write files or interact
|
|
683
|
+
* with the service manager directly.
|
|
684
|
+
*
|
|
685
|
+
* Source:
|
|
686
|
+
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html
|
|
687
|
+
*/
|
|
688
|
+
var SystemdTimer = class {
|
|
689
|
+
/** Optional `[Install]` section for enable-time relationships. */
|
|
690
|
+
install;
|
|
691
|
+
/** The normalized base unit name, without the `.timer` suffix. */
|
|
692
|
+
name;
|
|
693
|
+
/** The fully frozen original options used to construct this timer. */
|
|
694
|
+
options;
|
|
695
|
+
/** The attached service basename inferred from `targetUnit`. */
|
|
696
|
+
targetServiceName;
|
|
697
|
+
/** The unit name this timer activates, explicit or implicit. */
|
|
698
|
+
targetUnit;
|
|
699
|
+
/** The `[Timer]` section payload. */
|
|
700
|
+
timer;
|
|
701
|
+
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
702
|
+
unit;
|
|
703
|
+
/**
|
|
704
|
+
* Creates an immutable timer definition.
|
|
705
|
+
*
|
|
706
|
+
* If no explicit `timer.Unit` is provided, the target defaults to the service
|
|
707
|
+
* with the same basename, matching systemd's native timer behavior.
|
|
708
|
+
*/
|
|
709
|
+
constructor(options) {
|
|
710
|
+
this.options = freezeUnitOptions(options);
|
|
711
|
+
this.name = normalizeUnitName(options.name, `.timer`);
|
|
712
|
+
this.unit = cloneUnitSection(options.unit);
|
|
713
|
+
this.timer = cloneUnitSection(options.timer) ?? {};
|
|
714
|
+
this.install = cloneUnitSection(options.install);
|
|
715
|
+
this.targetUnit = resolveTimerTargetUnit(options);
|
|
716
|
+
this.targetServiceName = normalizeUnitName(this.targetUnit, `.service`);
|
|
717
|
+
Object.freeze(this);
|
|
718
|
+
}
|
|
719
|
+
/** The canonical unit filename, including the `.timer` suffix. */
|
|
720
|
+
get filename() {
|
|
721
|
+
return `${this.name}.timer`;
|
|
722
|
+
}
|
|
723
|
+
/** Renders the timer as a complete unit file. */
|
|
724
|
+
render() {
|
|
725
|
+
return renderUnitFile([
|
|
726
|
+
[`Unit`, this.unit],
|
|
727
|
+
[`Timer`, this.timer],
|
|
728
|
+
[`Install`, this.install]
|
|
729
|
+
]);
|
|
458
730
|
}
|
|
459
731
|
};
|
|
460
732
|
//#endregion
|
|
461
733
|
//#region src/main/systemd.ts
|
|
462
734
|
/**
|
|
463
|
-
*
|
|
735
|
+
* A successful systemd unit materialization.
|
|
464
736
|
*
|
|
465
|
-
* It records the
|
|
466
|
-
*
|
|
737
|
+
* It records the target directory and the on-disk path associated with each
|
|
738
|
+
* materialized unit.
|
|
467
739
|
*/
|
|
468
|
-
var
|
|
740
|
+
var SystemdMaterialization = class {
|
|
469
741
|
/** The directory units were written into. */
|
|
470
742
|
directory;
|
|
471
|
-
/** The
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
constructor(directory, installed) {
|
|
743
|
+
/** The materialized units together with their resolved on-disk paths. */
|
|
744
|
+
materialized;
|
|
745
|
+
constructor(directory, materialized) {
|
|
475
746
|
this.directory = directory;
|
|
476
|
-
this.
|
|
477
|
-
this.pathByFilename = new Map(installed.map((entry) => [entry.unit.filename, entry.path]));
|
|
747
|
+
this.materialized = Object.freeze([...materialized]);
|
|
478
748
|
Object.freeze(this);
|
|
479
749
|
}
|
|
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
750
|
};
|
|
487
751
|
/**
|
|
488
752
|
* A configured interface to a specific systemd environment.
|
|
@@ -521,41 +785,50 @@ var Systemd = class {
|
|
|
521
785
|
* Renders and writes one or more units into this instance's `unitDir`.
|
|
522
786
|
*
|
|
523
787
|
* 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.
|
|
788
|
+
* start the units on its own.
|
|
527
789
|
*/
|
|
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);
|
|
790
|
+
async materialize(...units) {
|
|
791
|
+
return this.materializeUnits(units);
|
|
542
792
|
}
|
|
543
793
|
/**
|
|
544
794
|
* Enables one or more units via `systemctl enable`.
|
|
545
795
|
*
|
|
546
|
-
* When `linkUnits` is enabled, this first links
|
|
796
|
+
* When `linkUnits` is enabled, this first links materialized unit files into the
|
|
547
797
|
* target manager and reloads systemd so the units are visible before the
|
|
548
798
|
* enable operation runs.
|
|
549
799
|
*/
|
|
550
800
|
async enable(...units) {
|
|
551
|
-
if (units.length === 0)
|
|
801
|
+
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.enable()`));
|
|
552
802
|
const scopeArgs = this.scopeArgs();
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
803
|
+
try {
|
|
804
|
+
await this.prepareUnits(scopeArgs, units, `enable`);
|
|
805
|
+
} catch (cause) {
|
|
806
|
+
if (cause instanceof UnitEnableError) return err(cause);
|
|
807
|
+
return err(new UnitEnableError(`Failed to prepare units for enable`, {
|
|
808
|
+
cause,
|
|
809
|
+
stage: `prepare`,
|
|
810
|
+
unitName: units[0]?.filename
|
|
811
|
+
}));
|
|
812
|
+
}
|
|
813
|
+
for (const unit of units) {
|
|
814
|
+
const args = [
|
|
815
|
+
...scopeArgs,
|
|
816
|
+
`enable`,
|
|
817
|
+
unit.filename
|
|
818
|
+
];
|
|
819
|
+
try {
|
|
820
|
+
await this.executor(`systemctl`, args);
|
|
821
|
+
} catch (cause) {
|
|
822
|
+
return err(new UnitEnableError(`Failed to enable ${unit.filename}`, {
|
|
823
|
+
args,
|
|
824
|
+
cause,
|
|
825
|
+
command: `systemctl`,
|
|
826
|
+
stage: `enable`,
|
|
827
|
+
unitName: unit.filename
|
|
828
|
+
}));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return ok(void 0);
|
|
559
832
|
}
|
|
560
833
|
/**
|
|
561
834
|
* Starts a unit and returns a parsed `systemctl show` snapshot of its final
|
|
@@ -566,19 +839,54 @@ var Systemd = class {
|
|
|
566
839
|
*/
|
|
567
840
|
async start(unit) {
|
|
568
841
|
const scopeArgs = this.scopeArgs();
|
|
569
|
-
|
|
570
|
-
|
|
842
|
+
try {
|
|
843
|
+
await this.prepareUnits(scopeArgs, [unit], `start`);
|
|
844
|
+
} catch (cause) {
|
|
845
|
+
if (cause instanceof UnitStartError) return err(cause);
|
|
846
|
+
return err(new UnitStartError(`Failed to prepare ${unit.filename} for start`, {
|
|
847
|
+
cause,
|
|
848
|
+
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
849
|
+
stage: `prepare`,
|
|
850
|
+
unitName: unit.filename
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
853
|
+
const startArgs = [
|
|
571
854
|
...scopeArgs,
|
|
572
855
|
`start`,
|
|
573
856
|
unit.filename
|
|
574
|
-
]
|
|
575
|
-
|
|
857
|
+
];
|
|
858
|
+
try {
|
|
859
|
+
await this.executor(`systemctl`, startArgs);
|
|
860
|
+
} catch (cause) {
|
|
861
|
+
return err(new UnitStartError(`Failed to start ${unit.filename}`, {
|
|
862
|
+
args: startArgs,
|
|
863
|
+
cause,
|
|
864
|
+
command: `systemctl`,
|
|
865
|
+
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
866
|
+
stage: `start`,
|
|
867
|
+
unitName: unit.filename
|
|
868
|
+
}));
|
|
869
|
+
}
|
|
870
|
+
const statusArgs = [
|
|
576
871
|
...scopeArgs,
|
|
577
872
|
`show`,
|
|
578
873
|
unit.filename,
|
|
579
874
|
`--property=Id,ActiveState,SubState,Result,ExecMainStatus`
|
|
580
|
-
]
|
|
581
|
-
|
|
875
|
+
];
|
|
876
|
+
let status;
|
|
877
|
+
try {
|
|
878
|
+
status = await this.executor(`systemctl`, statusArgs);
|
|
879
|
+
} catch (cause) {
|
|
880
|
+
return err(new UnitStartError(`Started ${unit.filename} but failed to query its status`, {
|
|
881
|
+
args: statusArgs,
|
|
882
|
+
cause,
|
|
883
|
+
command: `systemctl`,
|
|
884
|
+
diagnostics: await this.collectStartDiagnostics(scopeArgs, unit.filename),
|
|
885
|
+
stage: `show-status`,
|
|
886
|
+
unitName: unit.filename
|
|
887
|
+
}));
|
|
888
|
+
}
|
|
889
|
+
return ok(parseStartStatus(unit.filename, status.stdout));
|
|
582
890
|
}
|
|
583
891
|
/**
|
|
584
892
|
* Reads recent output for a managed unit.
|
|
@@ -589,10 +897,20 @@ var Systemd = class {
|
|
|
589
897
|
*/
|
|
590
898
|
async logs(unit, options) {
|
|
591
899
|
const fileLogPath = resolveUnitLogPath(unit);
|
|
592
|
-
if (fileLogPath !== void 0)
|
|
900
|
+
if (fileLogPath !== void 0) try {
|
|
901
|
+
return ok(tailLines(await readFile(fileLogPath, `utf8`), options?.lines ?? 50));
|
|
902
|
+
} catch (cause) {
|
|
903
|
+
return err(new UnitLogsReadError(`Failed to read logs for ${unit.filename} from ${fileLogPath}`, {
|
|
904
|
+
cause,
|
|
905
|
+
reason: isMissingPathError(cause) ? `missing-log-file` : `log-file-read-failed`,
|
|
906
|
+
stage: `read-log-file`,
|
|
907
|
+
unitName: unit.filename,
|
|
908
|
+
unitPath: fileLogPath
|
|
909
|
+
}));
|
|
910
|
+
}
|
|
593
911
|
const scopeArgs = this.scopeArgs();
|
|
594
912
|
const lines = options?.lines ?? 50;
|
|
595
|
-
const
|
|
913
|
+
const args = [`-lc`, `${[
|
|
596
914
|
`systemctl`,
|
|
597
915
|
...scopeArgs,
|
|
598
916
|
`status`,
|
|
@@ -600,27 +918,100 @@ var Systemd = class {
|
|
|
600
918
|
`--no-pager`,
|
|
601
919
|
`--lines`,
|
|
602
920
|
String(lines)
|
|
603
|
-
].map(shellQuote).join(` `);
|
|
604
|
-
|
|
921
|
+
].map(shellQuote).join(` `)} 2>&1 || true`];
|
|
922
|
+
let logs;
|
|
923
|
+
try {
|
|
924
|
+
logs = await this.executor(`bash`, args);
|
|
925
|
+
} catch (cause) {
|
|
926
|
+
return err(new UnitLogsReadError(`Failed to query logs for ${unit.filename} from systemctl status`, {
|
|
927
|
+
args,
|
|
928
|
+
cause,
|
|
929
|
+
command: `bash`,
|
|
930
|
+
reason: `status-command-failed`,
|
|
931
|
+
stage: `status`,
|
|
932
|
+
unitName: unit.filename
|
|
933
|
+
}));
|
|
934
|
+
}
|
|
935
|
+
return ok([logs.stdout, logs.stderr].filter((value) => value.length > 0).join(`\n`));
|
|
605
936
|
}
|
|
606
937
|
/** Returns the on-disk unit-file path this instance uses for the given unit. */
|
|
607
938
|
pathFor(unit) {
|
|
608
939
|
return join(this.unitDir, unit.filename);
|
|
609
940
|
}
|
|
610
|
-
async prepareUnits(scopeArgs, units) {
|
|
941
|
+
async prepareUnits(scopeArgs, units, operation) {
|
|
611
942
|
if (this.linkUnits) {
|
|
612
943
|
const linkPaths = await this.collectLinkPaths(units);
|
|
613
|
-
for (const path of linkPaths)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
944
|
+
for (const path of linkPaths) {
|
|
945
|
+
const args = [
|
|
946
|
+
...scopeArgs,
|
|
947
|
+
`link`,
|
|
948
|
+
path
|
|
949
|
+
];
|
|
950
|
+
try {
|
|
951
|
+
await this.executor(`systemctl`, args);
|
|
952
|
+
} catch (cause) {
|
|
953
|
+
const linkedUnit = units.find((candidate) => this.pathFor(candidate) === path);
|
|
954
|
+
const errorContext = {
|
|
955
|
+
args,
|
|
956
|
+
cause,
|
|
957
|
+
command: `systemctl`,
|
|
958
|
+
stage: `link`,
|
|
959
|
+
...linkedUnit === void 0 ? {} : { unitName: linkedUnit.filename },
|
|
960
|
+
unitPath: path
|
|
961
|
+
};
|
|
962
|
+
throw operation === `enable` ? new UnitEnableError(`Failed to link ${path} before enable`, errorContext) : new UnitStartError(`Failed to link ${path} before start`, errorContext);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const args = [...scopeArgs, `daemon-reload`];
|
|
967
|
+
try {
|
|
968
|
+
await this.executor(`systemctl`, args);
|
|
969
|
+
} catch (cause) {
|
|
970
|
+
throw operation === `enable` ? new UnitEnableError(`Failed to reload systemd before enable`, {
|
|
971
|
+
args,
|
|
972
|
+
cause,
|
|
973
|
+
command: `systemctl`,
|
|
974
|
+
stage: `daemon-reload`,
|
|
975
|
+
unitName: units[0]?.filename
|
|
976
|
+
}) : new UnitStartError(`Failed to reload systemd before start`, {
|
|
977
|
+
args,
|
|
978
|
+
cause,
|
|
979
|
+
command: `systemctl`,
|
|
980
|
+
stage: `daemon-reload`,
|
|
981
|
+
unitName: units[0]?.filename
|
|
982
|
+
});
|
|
618
983
|
}
|
|
619
|
-
await this.executor(`systemctl`, [...scopeArgs, `daemon-reload`]);
|
|
620
984
|
}
|
|
621
985
|
scopeArgs() {
|
|
622
986
|
return this.scope === `user` ? [`--user`] : [];
|
|
623
987
|
}
|
|
988
|
+
async collectStartDiagnostics(scopeArgs, unitName) {
|
|
989
|
+
const diagnostics = {};
|
|
990
|
+
const showCommand = [
|
|
991
|
+
`systemctl`,
|
|
992
|
+
...scopeArgs,
|
|
993
|
+
`show`,
|
|
994
|
+
unitName,
|
|
995
|
+
`--property=Id,ActiveState,SubState,Result,ExecMainStatus`
|
|
996
|
+
].map(shellQuote).join(` `);
|
|
997
|
+
const show = await this.tryBestEffortCommand(`bash`, [`-lc`, `${showCommand} 2>&1 || true`]);
|
|
998
|
+
if (show !== void 0 && show.length > 0) {
|
|
999
|
+
diagnostics.showOutput = show;
|
|
1000
|
+
diagnostics.showStatus = parseStartStatus(unitName, show);
|
|
1001
|
+
}
|
|
1002
|
+
const statusCommand = [
|
|
1003
|
+
`systemctl`,
|
|
1004
|
+
...scopeArgs,
|
|
1005
|
+
`status`,
|
|
1006
|
+
unitName,
|
|
1007
|
+
`--no-pager`,
|
|
1008
|
+
`--lines`,
|
|
1009
|
+
`20`
|
|
1010
|
+
].map(shellQuote).join(` `);
|
|
1011
|
+
const status = await this.tryBestEffortCommand(`bash`, [`-lc`, `${statusCommand} 2>&1 || true`]);
|
|
1012
|
+
if (status !== void 0 && status.length > 0) diagnostics.statusOutput = status;
|
|
1013
|
+
return diagnostics;
|
|
1014
|
+
}
|
|
624
1015
|
async collectLinkPaths(units) {
|
|
625
1016
|
const paths = /* @__PURE__ */ new Set();
|
|
626
1017
|
for (const unit of units) {
|
|
@@ -632,7 +1023,64 @@ var Systemd = class {
|
|
|
632
1023
|
}
|
|
633
1024
|
return [...paths];
|
|
634
1025
|
}
|
|
1026
|
+
async materializeUnits(units) {
|
|
1027
|
+
if (units.length === 0) return err(new NoUnitsProvidedError(`Systemd.materialize()`));
|
|
1028
|
+
try {
|
|
1029
|
+
await writeUnitDirectory(this.unitDir);
|
|
1030
|
+
} catch (cause) {
|
|
1031
|
+
const reason = classifyMaterializationReason(cause);
|
|
1032
|
+
return err(new UnitMaterializationError(`Failed to create unit directory ${this.unitDir}`, {
|
|
1033
|
+
cause,
|
|
1034
|
+
operation: `create-directory`,
|
|
1035
|
+
...reason === void 0 ? {} : { reason },
|
|
1036
|
+
unitPath: this.unitDir
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
const materialized = [];
|
|
1040
|
+
for (const unit of units) {
|
|
1041
|
+
const path = join(this.unitDir, unit.filename);
|
|
1042
|
+
const rendered = unit.render();
|
|
1043
|
+
if (!rendered.ok) return err(rendered.error);
|
|
1044
|
+
try {
|
|
1045
|
+
await writeFile(path, rendered.value, `utf8`);
|
|
1046
|
+
} catch (cause) {
|
|
1047
|
+
if (cause instanceof InvalidExecDirectiveError) return err(cause);
|
|
1048
|
+
const reason = classifyMaterializationReason(cause);
|
|
1049
|
+
return err(new UnitMaterializationError(`Failed to materialize ${unit.filename} into ${path}`, {
|
|
1050
|
+
cause,
|
|
1051
|
+
operation: `write-file`,
|
|
1052
|
+
...reason === void 0 ? {} : { reason },
|
|
1053
|
+
unitName: unit.filename,
|
|
1054
|
+
unitPath: path
|
|
1055
|
+
}));
|
|
1056
|
+
}
|
|
1057
|
+
materialized.push({
|
|
1058
|
+
path,
|
|
1059
|
+
unit
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
return ok(new SystemdMaterialization(this.unitDir, materialized));
|
|
1063
|
+
}
|
|
1064
|
+
async tryBestEffortCommand(command, args) {
|
|
1065
|
+
try {
|
|
1066
|
+
return (await this.executor(command, args)).stdout;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
635
1071
|
};
|
|
1072
|
+
function ok(value) {
|
|
1073
|
+
return {
|
|
1074
|
+
ok: true,
|
|
1075
|
+
value
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function err(error) {
|
|
1079
|
+
return {
|
|
1080
|
+
ok: false,
|
|
1081
|
+
error
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
636
1084
|
let lazyDefaultSystemd;
|
|
637
1085
|
/**
|
|
638
1086
|
* Returns the lazily created default `Systemd` instance.
|
|
@@ -659,6 +1107,10 @@ function parseFileLogPath(value) {
|
|
|
659
1107
|
function tailLines(output, lines) {
|
|
660
1108
|
return output.split(`\n`).slice(-lines).join(`\n`);
|
|
661
1109
|
}
|
|
1110
|
+
function isMissingPathError(cause) {
|
|
1111
|
+
if (cause === null || typeof cause !== `object`) return false;
|
|
1112
|
+
return cause[`code`] === `ENOENT`;
|
|
1113
|
+
}
|
|
662
1114
|
async function writeUnitDirectory(path) {
|
|
663
1115
|
await mkdir(path, { recursive: true });
|
|
664
1116
|
}
|
|
@@ -670,4 +1122,4 @@ async function writeUnitDirectory(path) {
|
|
|
670
1122
|
*/
|
|
671
1123
|
const Internal = internal_exports;
|
|
672
1124
|
//#endregion
|
|
673
|
-
export { Executable, Internal, Systemd,
|
|
1125
|
+
export { Executable, ExecutableInferenceError, Internal, InvalidExecDirectiveError, NoUnitsProvidedError, NotifySendError, Systemd, SystemdMaterialization, SystemdService, SystemdTimer, SystemdTsError, UnitEnableError, UnitLogsReadError, UnitMaterializationError, UnitStartError, defaultSystemd, defineExecutable, notify };
|