systemd-ts 0.3.1 → 0.3.2
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/dist/index-DeNBLyGA.d.mts +2 -1
- package/dist/main-BXq7TQdg.mjs +2 -0
- package/dist/main-BXq7TQdg.mjs.map +1 -0
- package/dist/test.d.mts +2 -1
- package/dist/test.mjs +2 -0
- package/dist/test.mjs.map +1 -0
- package/package.json +3 -2
- package/src/main/errors.ts +348 -0
- package/src/main/executable.ts +175 -0
- package/src/main/index.ts +76 -0
- package/src/main/internal.ts +406 -0
- package/src/main/notify.ts +67 -0
- package/src/main/systemctl.ts +326 -0
- package/src/main/systemd-service.ts +80 -0
- package/src/main/systemd-timer.ts +80 -0
- package/src/main/systemd.ts +991 -0
- package/src/main/types.ts +1231 -0
- package/src/test/harness.ts +106 -0
- package/src/test/host.ts +676 -0
- package/src/test/index.ts +17 -0
- package/src/test/sandbox.ts +94 -0
- package/src/test/systemd-container.Dockerfile +30 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { defaultCommandExecutor, parseStartStatus } from "./internal.ts";
|
|
2
|
+
import { SystemctlCommandError } from "./errors.ts";
|
|
3
|
+
import type { Result } from "./internal.ts";
|
|
4
|
+
import type {
|
|
5
|
+
SystemctlActiveState,
|
|
6
|
+
CommandExecutor,
|
|
7
|
+
CommandOutput,
|
|
8
|
+
StartStatus,
|
|
9
|
+
SystemctlEnablementState,
|
|
10
|
+
SystemctlIsEnabledOptions,
|
|
11
|
+
SystemctlIsSystemRunningOptions,
|
|
12
|
+
SystemctlListTimersOptions,
|
|
13
|
+
SystemctlOptions,
|
|
14
|
+
SystemctlServiceStatus,
|
|
15
|
+
SystemctlStateQueryOptions,
|
|
16
|
+
SystemctlShowOptions,
|
|
17
|
+
SystemctlStatusOptions,
|
|
18
|
+
SystemctlSystemRunningState,
|
|
19
|
+
SystemctlTimerListEntry,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A TypeScript-first client for invoking `systemctl` with structured argument
|
|
24
|
+
* vectors instead of shell-composed command strings.
|
|
25
|
+
*
|
|
26
|
+
* This lower-level primitive is useful both on its own and as the subprocess
|
|
27
|
+
* transport underneath higher-level abstractions such as {@link Systemd}.
|
|
28
|
+
*/
|
|
29
|
+
export class Systemctl {
|
|
30
|
+
/** Command executor used for `systemctl` subprocesses. */
|
|
31
|
+
public readonly executor: CommandExecutor;
|
|
32
|
+
/** The target manager scope, either `system` or `user`. */
|
|
33
|
+
public readonly scope: `system` | `user`;
|
|
34
|
+
|
|
35
|
+
public constructor(options: SystemctlOptions = {}) {
|
|
36
|
+
this.scope = options.scope ?? `system`;
|
|
37
|
+
this.executor = options.executor ?? defaultCommandExecutor;
|
|
38
|
+
Object.freeze(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Reloads the targeted systemd manager. */
|
|
42
|
+
public async daemonReload(): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
43
|
+
return this.run(`daemon-reload`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Enables a unit by filename. */
|
|
47
|
+
public async enable(unit: string): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
48
|
+
return this.run(`enable`, unit);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Disables a unit by filename. */
|
|
52
|
+
public async disable(unit: string): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
53
|
+
return this.run(`disable`, unit);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Links a unit file path into the targeted manager. */
|
|
57
|
+
public async link(path: string): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
58
|
+
return this.run(`link`, path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Checks the install-time enablement state of a unit file.
|
|
63
|
+
*
|
|
64
|
+
* Source:
|
|
65
|
+
* - systemd v260.1, `systemctl(1)`
|
|
66
|
+
*/
|
|
67
|
+
public async isEnabled(
|
|
68
|
+
unit: string,
|
|
69
|
+
options: SystemctlIsEnabledOptions = {},
|
|
70
|
+
): Promise<Result<SystemctlEnablementState, SystemctlCommandError>> {
|
|
71
|
+
const output = await this.run(
|
|
72
|
+
`is-enabled`,
|
|
73
|
+
...(options.full ? [`--full`] : []),
|
|
74
|
+
...(options.quiet ? [`--quiet`] : []),
|
|
75
|
+
unit,
|
|
76
|
+
);
|
|
77
|
+
if (!output.ok) {
|
|
78
|
+
return output;
|
|
79
|
+
}
|
|
80
|
+
return ok(firstNonEmptyOutputLine(output.value) as SystemctlEnablementState);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Checks the runtime active state of a unit.
|
|
85
|
+
*
|
|
86
|
+
* Unless `quiet` is set, this returns the state string printed by
|
|
87
|
+
* `systemctl is-active`.
|
|
88
|
+
*/
|
|
89
|
+
public async isActive(
|
|
90
|
+
unit: string,
|
|
91
|
+
options: SystemctlStateQueryOptions = {},
|
|
92
|
+
): Promise<Result<SystemctlActiveState, SystemctlCommandError>> {
|
|
93
|
+
const output = await this.run(`is-active`, ...(options.quiet ? [`--quiet`] : []), unit);
|
|
94
|
+
if (!output.ok) {
|
|
95
|
+
return output;
|
|
96
|
+
}
|
|
97
|
+
return ok(firstNonEmptyOutputLine(output.value) as SystemctlActiveState);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Checks whether a unit is in the failed state.
|
|
102
|
+
*
|
|
103
|
+
* Unless `quiet` is set, this returns the state string printed by
|
|
104
|
+
* `systemctl is-failed`.
|
|
105
|
+
*/
|
|
106
|
+
public async isFailed(
|
|
107
|
+
unit: string,
|
|
108
|
+
options: SystemctlStateQueryOptions = {},
|
|
109
|
+
): Promise<Result<SystemctlActiveState, SystemctlCommandError>> {
|
|
110
|
+
const output = await this.run(`is-failed`, ...(options.quiet ? [`--quiet`] : []), unit);
|
|
111
|
+
if (!output.ok) {
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
return ok(firstNonEmptyOutputLine(output.value) as SystemctlActiveState);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Queries the overall manager state reported by `systemctl is-system-running`.
|
|
119
|
+
*
|
|
120
|
+
* Source:
|
|
121
|
+
* - systemd v260.1, `systemctl(1)`
|
|
122
|
+
*/
|
|
123
|
+
public async isSystemRunning(
|
|
124
|
+
options: SystemctlIsSystemRunningOptions = {},
|
|
125
|
+
): Promise<Result<SystemctlSystemRunningState, SystemctlCommandError>> {
|
|
126
|
+
const output = await this.run(`is-system-running`, ...(options.wait ? [`--wait`] : []));
|
|
127
|
+
if (!output.ok) {
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
return ok(firstNonEmptyOutputLine(output.value) as SystemctlSystemRunningState);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Queries `systemctl show` for a unit.
|
|
135
|
+
*
|
|
136
|
+
* When `properties` are provided, only those properties are requested.
|
|
137
|
+
*/
|
|
138
|
+
public async show(
|
|
139
|
+
unit: string,
|
|
140
|
+
options: SystemctlShowOptions = {},
|
|
141
|
+
): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
142
|
+
const propertyFlag =
|
|
143
|
+
options.properties === undefined || options.properties.length === 0
|
|
144
|
+
? []
|
|
145
|
+
: [`--property=${options.properties.join(`,`)}`];
|
|
146
|
+
return this.run(`show`, unit, ...propertyFlag);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Queries `systemctl show` and parses the result into a key-value object.
|
|
151
|
+
*
|
|
152
|
+
* Properties are returned exactly as printed by `systemctl show`.
|
|
153
|
+
*/
|
|
154
|
+
public async showProperties(
|
|
155
|
+
unit: string,
|
|
156
|
+
options: SystemctlShowOptions = {},
|
|
157
|
+
): Promise<Result<Readonly<Record<string, string>>, SystemctlCommandError>> {
|
|
158
|
+
const output = await this.show(unit, options);
|
|
159
|
+
if (!output.ok) {
|
|
160
|
+
return output;
|
|
161
|
+
}
|
|
162
|
+
return ok(parseSystemctlShowOutput(output.value.stdout));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Queries the standard status-related `show` properties for a unit and
|
|
167
|
+
* returns a parsed snapshot.
|
|
168
|
+
*/
|
|
169
|
+
public async showStatus(unit: string): Promise<Result<StartStatus, SystemctlCommandError>> {
|
|
170
|
+
return this.showServiceStatus(unit);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Queries the standard service-status `show` properties for a service unit
|
|
175
|
+
* and returns a parsed snapshot.
|
|
176
|
+
*/
|
|
177
|
+
public async showServiceStatus(
|
|
178
|
+
unit: string,
|
|
179
|
+
): Promise<Result<SystemctlServiceStatus, SystemctlCommandError>> {
|
|
180
|
+
const output = await this.show(unit, {
|
|
181
|
+
properties: [`Id`, `ActiveState`, `SubState`, `Result`, `ExecMainStatus`],
|
|
182
|
+
});
|
|
183
|
+
if (!output.ok) {
|
|
184
|
+
return output;
|
|
185
|
+
}
|
|
186
|
+
return ok(parseStartStatus(unit, output.value.stdout));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Queries `systemctl list-timers` and returns the observed JSON payload
|
|
191
|
+
* shape directly, with raw integer timestamps left intact.
|
|
192
|
+
*/
|
|
193
|
+
public async listTimers(
|
|
194
|
+
options: SystemctlListTimersOptions = {},
|
|
195
|
+
): Promise<Result<readonly SystemctlTimerListEntry[], SystemctlCommandError>> {
|
|
196
|
+
const args = [
|
|
197
|
+
`list-timers`,
|
|
198
|
+
...(options.all ? [`--all`] : []),
|
|
199
|
+
...((options.noPager ?? true) ? [`--no-pager`] : []),
|
|
200
|
+
`--output=json`,
|
|
201
|
+
...(options.patterns ?? []),
|
|
202
|
+
] as const;
|
|
203
|
+
const output = await this.run(...args);
|
|
204
|
+
if (!output.ok) {
|
|
205
|
+
return output;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return ok(JSON.parse(output.value.stdout) as readonly SystemctlTimerListEntry[]);
|
|
209
|
+
} catch (cause) {
|
|
210
|
+
return err(
|
|
211
|
+
new SystemctlCommandError(`Failed to parse JSON output from systemctl list-timers`, {
|
|
212
|
+
args: [...this.scopeArgs(), ...args],
|
|
213
|
+
cause,
|
|
214
|
+
command: `systemctl`,
|
|
215
|
+
operation: `list-timers`,
|
|
216
|
+
reason: `invalid-json`,
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Starts a unit by filename. */
|
|
223
|
+
public async start(unit: string): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
224
|
+
return this.run(`start`, unit);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Stops a unit by filename. */
|
|
228
|
+
public async stop(unit: string): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
229
|
+
return this.run(`stop`, unit);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Restarts a unit by filename. */
|
|
233
|
+
public async restart(unit: string): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
234
|
+
return this.run(`restart`, unit);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resets the failed state for matching units, or for all units when no
|
|
239
|
+
* patterns are provided.
|
|
240
|
+
*/
|
|
241
|
+
public async resetFailed(
|
|
242
|
+
patterns: readonly string[] = [],
|
|
243
|
+
): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
244
|
+
return this.run(`reset-failed`, ...patterns);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Queries `systemctl status` for a unit.
|
|
249
|
+
*
|
|
250
|
+
* This is primarily a human-oriented command and is best paired with
|
|
251
|
+
* `show()` when structured state is needed.
|
|
252
|
+
*/
|
|
253
|
+
public async status(
|
|
254
|
+
unit: string,
|
|
255
|
+
options: SystemctlStatusOptions = {},
|
|
256
|
+
): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
257
|
+
const args = [`status`, unit] as string[];
|
|
258
|
+
if (options.noPager ?? true) {
|
|
259
|
+
args.push(`--no-pager`);
|
|
260
|
+
}
|
|
261
|
+
if (options.lines !== undefined) {
|
|
262
|
+
args.push(`--lines`, String(options.lines));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return this.run(...args);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Executes a raw `systemctl` invocation with scope-aware arguments. */
|
|
269
|
+
public async run(
|
|
270
|
+
...args: readonly string[]
|
|
271
|
+
): Promise<Result<CommandOutput, SystemctlCommandError>> {
|
|
272
|
+
const scopedArgs = [...this.scopeArgs(), ...args];
|
|
273
|
+
try {
|
|
274
|
+
return ok(await this.executor(`systemctl`, scopedArgs));
|
|
275
|
+
} catch (cause) {
|
|
276
|
+
return err(
|
|
277
|
+
new SystemctlCommandError(`Failed to run systemctl ${args[0] ?? `command`}`, {
|
|
278
|
+
args: scopedArgs,
|
|
279
|
+
cause,
|
|
280
|
+
command: `systemctl`,
|
|
281
|
+
operation: args[0],
|
|
282
|
+
reason: `executor-failed`,
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private scopeArgs(): readonly string[] {
|
|
289
|
+
return this.scope === `user` ? [`--user`] : [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function ok<TValue>(value: TValue): Result<TValue, never> {
|
|
294
|
+
return { ok: true, value };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function err<TError>(error: TError): Result<never, TError> {
|
|
298
|
+
return { ok: false, error };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseSystemctlShowOutput(output: string): Readonly<Record<string, string>> {
|
|
302
|
+
return Object.freeze(
|
|
303
|
+
Object.fromEntries(
|
|
304
|
+
output
|
|
305
|
+
.split(`\n`)
|
|
306
|
+
.map((line) => line.trim())
|
|
307
|
+
.filter((line) => line.length > 0)
|
|
308
|
+
.map((line) => {
|
|
309
|
+
const separatorIndex = line.indexOf(`=`);
|
|
310
|
+
if (separatorIndex === -1) {
|
|
311
|
+
return [line, ``] as const;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return [line.slice(0, separatorIndex), line.slice(separatorIndex + 1)] as const;
|
|
315
|
+
}),
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function firstNonEmptyOutputLine(output: CommandOutput): string {
|
|
321
|
+
const line = [output.stdout, output.stderr]
|
|
322
|
+
.flatMap((value) => value.split(`\n`))
|
|
323
|
+
.map((value) => value.trim())
|
|
324
|
+
.find((value) => value.length > 0);
|
|
325
|
+
return line ?? ``;
|
|
326
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneUnitSection,
|
|
3
|
+
freezeUnitOptions,
|
|
4
|
+
normalizeUnitName,
|
|
5
|
+
renderUnitFile,
|
|
6
|
+
validateServiceSection,
|
|
7
|
+
} from "./internal.ts";
|
|
8
|
+
import type { ExecutableInferenceError, InvalidExecDirectiveError } from "./errors.ts";
|
|
9
|
+
import type { Result } from "./internal.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ExactSystemdServiceOptions,
|
|
12
|
+
ServiceBaseName,
|
|
13
|
+
ServiceFilename,
|
|
14
|
+
SystemdServiceOptions,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An immutable definition of a `.service` unit.
|
|
19
|
+
*
|
|
20
|
+
* `SystemdService` is a pure value object: it captures the intended unit name
|
|
21
|
+
* and section contents, but it does not write files or talk to `systemd`
|
|
22
|
+
* directly. Operational actions such as installation or startup belong on
|
|
23
|
+
* {@link Systemd}.
|
|
24
|
+
*
|
|
25
|
+
* Source:
|
|
26
|
+
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html
|
|
27
|
+
*/
|
|
28
|
+
export class SystemdService<const TOptions extends SystemdServiceOptions = SystemdServiceOptions> {
|
|
29
|
+
/** Optional `[Install]` section for enable-time relationships. */
|
|
30
|
+
public readonly install: TOptions[`install`] | undefined;
|
|
31
|
+
/** The normalized base unit name, without the `.service` suffix. */
|
|
32
|
+
public readonly name: ServiceBaseName<TOptions[`name`]>;
|
|
33
|
+
/** The fully frozen original options used to construct this service. */
|
|
34
|
+
public readonly options: Readonly<TOptions>;
|
|
35
|
+
/** The `[Service]` section payload. */
|
|
36
|
+
public readonly service: TOptions[`service`];
|
|
37
|
+
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
38
|
+
public readonly unit: TOptions[`unit`] | undefined;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates an immutable service definition.
|
|
42
|
+
*
|
|
43
|
+
* The constructor preserves literal types where possible and rejects unknown
|
|
44
|
+
* directive names within the provided sections, while still allowing custom
|
|
45
|
+
* `X-...` extension directives.
|
|
46
|
+
*/
|
|
47
|
+
public constructor(options: TOptions & ExactSystemdServiceOptions<TOptions>) {
|
|
48
|
+
this.options = freezeUnitOptions(options as unknown as TOptions);
|
|
49
|
+
this.name = normalizeUnitName(options.name, `.service`) as ServiceBaseName<TOptions[`name`]>;
|
|
50
|
+
this.unit = cloneUnitSection(options.unit) as TOptions[`unit`] | undefined;
|
|
51
|
+
this.service = (cloneUnitSection(options.service) ?? {}) as TOptions[`service`];
|
|
52
|
+
this.install = cloneUnitSection(options.install) as TOptions[`install`] | undefined;
|
|
53
|
+
Object.freeze(this);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The canonical unit filename, including the `.service` suffix. */
|
|
57
|
+
public get filename(): ServiceFilename<TOptions[`name`]> {
|
|
58
|
+
return `${this.name}.service` as ServiceFilename<TOptions[`name`]>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Renders the service as a complete unit file.
|
|
63
|
+
*
|
|
64
|
+
* Rendering also validates executable-valued directives such as `ExecStart`
|
|
65
|
+
* and `ExecStop`, ensuring they use absolute runtime entrypoints as required
|
|
66
|
+
* by systemd.
|
|
67
|
+
*/
|
|
68
|
+
public render(): Result<string, ExecutableInferenceError | InvalidExecDirectiveError> {
|
|
69
|
+
const validation = validateServiceSection(this.service);
|
|
70
|
+
if (!validation.ok) {
|
|
71
|
+
return validation;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return renderUnitFile([
|
|
75
|
+
[`Unit`, this.unit],
|
|
76
|
+
[`Service`, this.service],
|
|
77
|
+
[`Install`, this.install],
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneUnitSection,
|
|
3
|
+
freezeUnitOptions,
|
|
4
|
+
normalizeUnitName,
|
|
5
|
+
renderUnitFile,
|
|
6
|
+
resolveTimerTargetUnit,
|
|
7
|
+
} from "./internal.ts";
|
|
8
|
+
import type { ExecutableInferenceError } from "./errors.ts";
|
|
9
|
+
import type { Result } from "./internal.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ExactSystemdTimerOptions,
|
|
12
|
+
SystemdTimerOptions,
|
|
13
|
+
TimerBaseName,
|
|
14
|
+
TimerFilename,
|
|
15
|
+
TimerTargetServiceName,
|
|
16
|
+
TimerTargetUnit,
|
|
17
|
+
} from "./types.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An immutable definition of a `.timer` unit.
|
|
21
|
+
*
|
|
22
|
+
* Like {@link SystemdService}, this is a pure value object. It models the timer
|
|
23
|
+
* configuration and its attachment target, but does not write files or interact
|
|
24
|
+
* with the service manager directly.
|
|
25
|
+
*
|
|
26
|
+
* Source:
|
|
27
|
+
* - https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html
|
|
28
|
+
*/
|
|
29
|
+
export class SystemdTimer<const TOptions extends SystemdTimerOptions = SystemdTimerOptions> {
|
|
30
|
+
/** Optional `[Install]` section for enable-time relationships. */
|
|
31
|
+
public readonly install: TOptions[`install`] | undefined;
|
|
32
|
+
/** The normalized base unit name, without the `.timer` suffix. */
|
|
33
|
+
public readonly name: TimerBaseName<TOptions[`name`]>;
|
|
34
|
+
/** The fully frozen original options used to construct this timer. */
|
|
35
|
+
public readonly options: Readonly<TOptions>;
|
|
36
|
+
/** The attached service basename inferred from `targetUnit`. */
|
|
37
|
+
public readonly targetServiceName: TimerTargetServiceName<TOptions>;
|
|
38
|
+
/** The unit name this timer activates, explicit or implicit. */
|
|
39
|
+
public readonly targetUnit: TimerTargetUnit<TOptions>;
|
|
40
|
+
/** The `[Timer]` section payload. */
|
|
41
|
+
public readonly timer: TOptions[`timer`];
|
|
42
|
+
/** Optional `[Unit]` section metadata and dependency configuration. */
|
|
43
|
+
public readonly unit: TOptions[`unit`] | undefined;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates an immutable timer definition.
|
|
47
|
+
*
|
|
48
|
+
* If no explicit `timer.Unit` is provided, the target defaults to the service
|
|
49
|
+
* with the same basename, matching systemd's native timer behavior.
|
|
50
|
+
*/
|
|
51
|
+
public constructor(options: TOptions & ExactSystemdTimerOptions<TOptions>) {
|
|
52
|
+
this.options = freezeUnitOptions(options as unknown as TOptions);
|
|
53
|
+
this.name = normalizeUnitName(options.name, `.timer`) as TimerBaseName<TOptions[`name`]>;
|
|
54
|
+
this.unit = cloneUnitSection(options.unit) as TOptions[`unit`] | undefined;
|
|
55
|
+
this.timer = (cloneUnitSection(options.timer) ?? {}) as TOptions[`timer`];
|
|
56
|
+
this.install = cloneUnitSection(options.install) as TOptions[`install`] | undefined;
|
|
57
|
+
this.targetUnit = resolveTimerTargetUnit(
|
|
58
|
+
options as unknown as TOptions,
|
|
59
|
+
) as TimerTargetUnit<TOptions>;
|
|
60
|
+
this.targetServiceName = normalizeUnitName(
|
|
61
|
+
this.targetUnit,
|
|
62
|
+
`.service`,
|
|
63
|
+
) as TimerTargetServiceName<TOptions>;
|
|
64
|
+
Object.freeze(this);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The canonical unit filename, including the `.timer` suffix. */
|
|
68
|
+
public get filename(): TimerFilename<TOptions[`name`]> {
|
|
69
|
+
return `${this.name}.timer` as TimerFilename<TOptions[`name`]>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Renders the timer as a complete unit file. */
|
|
73
|
+
public render(): Result<string, ExecutableInferenceError> {
|
|
74
|
+
return renderUnitFile([
|
|
75
|
+
[`Unit`, this.unit],
|
|
76
|
+
[`Timer`, this.timer],
|
|
77
|
+
[`Install`, this.install],
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
}
|