@push.rocks/smartdaemon 2.18.0 → 2.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.systemdservicedefinition.d.ts +9 -1
- package/dist_ts/classes.systemdservicedefinition.js +4 -1
- package/dist_ts/classes.systemdtransientscope.d.ts +26 -2
- package/dist_ts/classes.systemdtransientscope.js +29 -5
- package/dist_ts/classes.systemdunit.d.ts +44 -2
- package/dist_ts/classes.systemdunit.js +57 -2
- package/dist_ts/classes.systemdunitfile.d.ts +5 -0
- package/dist_ts/classes.systemdunitfile.js +10 -1
- package/dist_ts/smartdaemon.plugins.d.ts +1 -1
- package/dist_ts/smartdaemon.plugins.js +1 -1
- package/dist_ts/systemd.managerversion.d.ts +30 -0
- package/dist_ts/systemd.managerversion.js +23 -0
- package/package.json +8 -9
- package/readme.md +67 -11
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.systemdservicedefinition.ts +12 -1
- package/ts/classes.systemdtransientscope.ts +45 -5
- package/ts/classes.systemdunit.ts +72 -2
- package/ts/classes.systemdunitfile.ts +9 -0
- package/ts/smartdaemon.plugins.ts +1 -1
- package/ts/systemd.managerversion.ts +38 -0
package/readme.md
CHANGED
|
@@ -249,7 +249,10 @@ unit was started right after the release and stored descriptors again, which a c
|
|
|
249
249
|
read as a failed clean. A refused clean, or one that could not be run, rejects with
|
|
250
250
|
`command_failed`; so does a clean whose `systemctl` call exceeded `commandTimeoutMs`, whose outcome
|
|
251
251
|
is unknown because the manager may have released the store before the client was killed. After
|
|
252
|
-
`command_failed` a caller must `inspect()` the unit before treating the store as kept.
|
|
252
|
+
`command_failed` a caller must `inspect()` the unit before treating the store as kept. Cleaning a
|
|
253
|
+
store needs systemd 254 or later: the call reads the manager's version first and refuses an older
|
|
254
|
+
manager with `SystemdUnitManagerTooOldError` (`manager_too_old`), an unreadable version with
|
|
255
|
+
`version_unknown` and an unanswered read with `command_failed`, each without running the clean.
|
|
253
256
|
|
|
254
257
|
`inspectConfiguration()` reports the selected `fragmentPath`, opaque `dropInPaths`
|
|
255
258
|
list and `needsReload` flag. `reloadConfiguration()` explicitly reloads the manager's
|
|
@@ -269,10 +272,12 @@ extension a running start requested), `limitNoFile` and `limitNoFileSoft`
|
|
|
269
272
|
(`0` without a store), `fileDescriptorStorePreserve` (`no`, `yes` or `restart`) and `environment`, the
|
|
270
273
|
unit's own `Environment=` variables as a frozen name-to-value object (variables the manager gives
|
|
271
274
|
every unit, `EnvironmentFile=` and `PassEnvironment=` are not included). A missing, repeated,
|
|
272
|
-
foreign or malformed property rejects with `invalid_state
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
foreign or malformed property rejects with `invalid_state`; this includes an `Environment=` value
|
|
276
|
+
holding a control character, which `systemctl show` prints as a C escape. `inspectServiceSettings()`
|
|
277
|
+
needs systemd 254 or later, the first to report `FileDescriptorStorePreserve`: it reads the manager's
|
|
278
|
+
version first and refuses an older manager with `SystemdUnitManagerTooOldError` (`manager_too_old`)
|
|
279
|
+
and an unreadable version with `version_unknown`, both before the unit is read (see *The manager's
|
|
280
|
+
version* below). Every environment a `SystemdServiceDefinition` accepts
|
|
276
281
|
reads back. Reload changed files first; the snapshot describes what the manager loaded.
|
|
277
282
|
|
|
278
283
|
`execStart` lists every `ExecStart=` command of the loaded unit, drop-ins included, in the order
|
|
@@ -318,6 +323,43 @@ target and the actual network provider; inspect the provider's `requires` and
|
|
|
318
323
|
`after` as well. Recheck the exact file digest, selected fragment, absence of
|
|
319
324
|
drop-ins and `needsReload === false` before permitting dependent activation.
|
|
320
325
|
|
|
326
|
+
#### The manager's version
|
|
327
|
+
|
|
328
|
+
`inspectManagerVersion()` reads the running manager's version through the unit's scope — the
|
|
329
|
+
system manager, or the calling user's manager for `scope: 'user'` — with `systemctl show
|
|
330
|
+
--property=Version`, the same `systemctlPath`, environment and timeout as every other call. It
|
|
331
|
+
resolves with a frozen `ISystemdManagerVersion`: `major` (`255`) and `version`, the manager's
|
|
332
|
+
`Version` property verbatim (`255.4-1ubuntu8.17`). This is the running manager's version, which after
|
|
333
|
+
a package upgrade differs from `systemctl --version` until the manager re-executes. A manager that
|
|
334
|
+
does not answer rejects with `command_failed`; an answer that is not a single `Version=` line with a
|
|
335
|
+
version starting with a major of one to four digits rejects with `version_unknown`.
|
|
336
|
+
|
|
337
|
+
`requireManagerVersion(requiredMajor)` resolves with that version when its `major` is at least
|
|
338
|
+
`requiredMajor` (an integer from 1 to 9999; anything else rejects with `invalid_options` before any
|
|
339
|
+
command) and otherwise rejects with a `SystemdUnitManagerTooOldError`: a `SystemdUnitError` with code
|
|
340
|
+
`manager_too_old` that carries the `managerVersion` it read and the `requiredMajor`. Calls that need a
|
|
341
|
+
newer manager use it and fail closed: `inspectServiceSettings()`, `cleanFileDescriptorStore()` and
|
|
342
|
+
installing a definition with a `requiredManagerMajor` (see `SystemdUnitFile.install()`), so none fails
|
|
343
|
+
late with a generic code. `runInTransientScope()` refuses an older user manager the same way with its
|
|
344
|
+
own `SystemdTransientScopeManagerTooOldError`.
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { SystemdUnit, SystemdUnitManagerTooOldError } from '@push.rocks/smartdaemon';
|
|
348
|
+
|
|
349
|
+
const unit = new SystemdUnit({ unitName: 'pallet-node.service' });
|
|
350
|
+
console.log((await unit.inspectManagerVersion()).major); // 255
|
|
351
|
+
try {
|
|
352
|
+
await unit.inspectServiceSettings();
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof SystemdUnitManagerTooOldError) {
|
|
355
|
+
// e.g. 'systemd 253 (253.5-1) running, 254 required'
|
|
356
|
+
console.error(`systemd ${error.managerVersion.major} (${error.managerVersion.version}) running, ` +
|
|
357
|
+
`${error.requiredMajor} required`);
|
|
358
|
+
}
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
321
363
|
### Installing a direct systemd service definition
|
|
322
364
|
|
|
323
365
|
`SystemdServiceDefinition` encodes a literal executable and argument list, with
|
|
@@ -584,8 +626,11 @@ loads an explicit `NotifyAccess=none` beside a store as `main`, so `'none'` is r
|
|
|
584
626
|
(systemd's default) once it is neither active nor about to restart, and `'yes'` only when the
|
|
585
627
|
unit leaves the manager's memory or `SystemdUnit.cleanFileDescriptorStore()` releases it. A reboot
|
|
586
628
|
empties the store; a `systemctl soft-reboot` keeps the running kernel, so a preserved store and the
|
|
587
|
-
descriptors in it pass on to the next userspace boot. Systemd reads `FileDescriptorStorePreserve=` from version 254
|
|
588
|
-
an older manager
|
|
629
|
+
descriptors in it pass on to the next userspace boot. Systemd reads `FileDescriptorStorePreserve=` from version 254
|
|
630
|
+
and an older manager would ignore the line, so a definition with `fileDescriptorStorePreserve` has
|
|
631
|
+
`requiredManagerMajor` 254 and `SystemdUnitFile.install()` refuses it on an older manager with
|
|
632
|
+
`SystemdUnitManagerTooOldError` (`manager_too_old`) before touching anything; every other definition
|
|
633
|
+
has `requiredManagerMajor` null. Sending `FDSTORE=1` needs an `AF_UNIX` socket, so a definition
|
|
589
634
|
with a store or a notify access other than `'none'` that restricts its socket families must allow
|
|
590
635
|
`AF_UNIX`. Omitted, each option renders nothing.
|
|
591
636
|
|
|
@@ -820,7 +865,11 @@ directories and regular single-link mode-0644 files, retains changed definitions
|
|
|
820
865
|
as `<unit>.previous-<sha256>`, flushes them before atomic replacement, reloads
|
|
821
866
|
the manager and checks its selected fragment and final bytes. A reload failure
|
|
822
867
|
leaves the new file and retained previous definition in place; recovery requires
|
|
823
|
-
inspection, never an automatic rollback.
|
|
868
|
+
inspection, never an automatic rollback. A definition whose `requiredManagerMajor` is not null is
|
|
869
|
+
admitted only on a manager at least that new, read with `SystemdUnit.requireManagerVersion()` before
|
|
870
|
+
any directory, file or other `systemctl` effect; that read's refusal propagates unchanged as the
|
|
871
|
+
unit's `SystemdUnitError`: `manager_too_old` (a `SystemdUnitManagerTooOldError`), `version_unknown`,
|
|
872
|
+
or `command_failed` when the manager does not answer. Filesystem and definition errors use
|
|
824
873
|
`SystemdUnitFileError` with a local code; every `unsafe_path` refusal is a
|
|
825
874
|
`SystemdUnitFileUnsafePathError` naming the offending `path` and its `reason`, and
|
|
826
875
|
every `unsafe_unit` refusal is a `SystemdUnitFileUnsafeUnitError` naming its `reason`
|
|
@@ -893,7 +942,8 @@ refused options), the `stderr` read so far and one of these codes:
|
|
|
893
942
|
| --- | --- |
|
|
894
943
|
| `invalid_options` | Malformed options, or a runtime directory that is not a private directory of the caller |
|
|
895
944
|
| `unsupported_platform` | The process is not running on Linux |
|
|
896
|
-
| `user_manager_unavailable` | No `XDG_RUNTIME_DIR` (or `runtimeDirectory`), a runtime directory that does not exist, or a manager that does not answer `systemctl --user` |
|
|
945
|
+
| `user_manager_unavailable` | No `XDG_RUNTIME_DIR` (or `runtimeDirectory`), a runtime directory that does not exist, or a manager that does not answer `systemctl --user show --property=Version` with a version as `SystemdUnit.inspectManagerVersion()` reads it |
|
|
946
|
+
| `manager_too_old` | The user manager is older than systemd 254; a `SystemdTransientScopeManagerTooOldError` naming its `managerVersion` and the `requiredMajor`. Nothing was started |
|
|
897
947
|
| `scope_start_failed` | systemd-run exited without reporting the scope running; the program has not run |
|
|
898
948
|
| `timed_out` | The manager probe or the program did not finish and close its output within `commandTimeoutMs` |
|
|
899
949
|
| `output_limit` | The program's stdout or stderr exceeded 1 MiB |
|
|
@@ -905,8 +955,11 @@ its daemon's output); otherwise the call runs into its timeout. On `timed_out` a
|
|
|
905
955
|
the scope. `commandTimeoutMs` (default 120000, maximum 600000) bounds the probe
|
|
906
956
|
and the program each. Trusted owning code can set `runtimeDirectory` and the
|
|
907
957
|
absolute `systemdRunPath` and `systemctlPath`. systemd-run reports the started
|
|
908
|
-
scope as its first stderr line, which systemd 254 and later print
|
|
909
|
-
|
|
958
|
+
scope as its first stderr line, which systemd 254 and later print, and systemd-run
|
|
959
|
+
before 254 lacks `--expand-environment`. The manager probe's version stands for the
|
|
960
|
+
installation's: a user manager older than 254 is refused with `manager_too_old`
|
|
961
|
+
before systemd-run starts. A systemd-run older than the manager it addresses still
|
|
962
|
+
fails with `scope_start_failed`.
|
|
910
963
|
If the program cannot be executed after the scope started, systemd-run reports
|
|
911
964
|
that as the program's exit status 1.
|
|
912
965
|
|
|
@@ -1085,6 +1138,9 @@ console.log(template); // View generated systemd unit file
|
|
|
1085
1138
|
## 🏗️ System Requirements
|
|
1086
1139
|
|
|
1087
1140
|
- **Operating System**: Linux with systemd (Ubuntu 16.04+, Debian 9+, CentOS 7+, RHEL 7+, Fedora, Arch, etc.)
|
|
1141
|
+
- **systemd 254 or later** for `SystemdUnit.inspectServiceSettings()`, `SystemdUnit.cleanFileDescriptorStore()`,
|
|
1142
|
+
`runInTransientScope()` and for installing a definition with `fileDescriptorStorePreserve` (Debian 13,
|
|
1143
|
+
Ubuntu 24.04, Fedora 39 and later ship it); an older manager is refused by name with `manager_too_old`
|
|
1088
1144
|
- **Node.js**: Version 14.x or higher
|
|
1089
1145
|
- **Permissions**: Either root access or properly configured sudo
|
|
1090
1146
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as plugins from './smartdaemon.plugins.js';
|
|
|
2
2
|
import { environmentByteLimit, environmentVariableLimit, isCleanText, renderServiceRelations, systemdAddressFamilies,
|
|
3
3
|
systemdCapabilities, type TSystemdServiceStartup, type ISystemdServiceDependencies, type ISystemdServiceInstallTargets,
|
|
4
4
|
type ISystemdServiceCredential, type TSystemdCapability, type TSystemdAddressFamily } from './systemd.definition.js';
|
|
5
|
+
import { fileDescriptorStorePreserveMajor } from './systemd.managerversion.js';
|
|
5
6
|
export type { ISystemdServiceStartup, ISystemdServiceNotifyStartup, TSystemdServiceStartup,
|
|
6
7
|
ISystemdServiceDependencies, ISystemdServiceInstallTargets, ISystemdServiceCredential, TSystemdCapability,
|
|
7
8
|
TSystemdAddressFamily } from './systemd.definition.js';
|
|
@@ -65,7 +66,9 @@ interface ISystemdServiceDefinitionBaseOptions {
|
|
|
65
66
|
* When the manager releases the store (`FileDescriptorStorePreserve=`, systemd 254 or later):
|
|
66
67
|
* `'no'` when the service stops, `'restart'` (systemd's default) once it is neither active nor
|
|
67
68
|
* about to restart, `'yes'` only when the unit leaves the manager's memory or is cleaned with
|
|
68
|
-
* `SystemdUnit.cleanFileDescriptorStore()`. Requires `fileDescriptorStoreMax`.
|
|
69
|
+
* `SystemdUnit.cleanFileDescriptorStore()`. Requires `fileDescriptorStoreMax`. Sets
|
|
70
|
+
* `requiredManagerMajor` to 254, so `SystemdUnitFile.install()` refuses an older manager, which
|
|
71
|
+
* would ignore the line.
|
|
69
72
|
*/
|
|
70
73
|
fileDescriptorStorePreserve?: 'no' | 'yes' | 'restart';
|
|
71
74
|
/**
|
|
@@ -172,6 +175,12 @@ export class SystemdServiceDefinition {
|
|
|
172
175
|
public readonly scope: 'system' | 'user';
|
|
173
176
|
public readonly content: string;
|
|
174
177
|
public readonly sha256: string;
|
|
178
|
+
/**
|
|
179
|
+
* The oldest systemd major whose manager reads every directive this definition renders, when it
|
|
180
|
+
* renders one an older manager would ignore: 254 with `fileDescriptorStorePreserve`; otherwise
|
|
181
|
+
* null. `SystemdUnitFile.install()` refuses a definition on an older manager.
|
|
182
|
+
*/
|
|
183
|
+
public readonly requiredManagerMajor: number | null;
|
|
175
184
|
|
|
176
185
|
constructor(options: ISystemdServiceDefinitionOptions) {
|
|
177
186
|
const fail = (option: TSystemdServiceDefinitionOption): never => {
|
|
@@ -508,6 +517,8 @@ export class SystemdServiceDefinition {
|
|
|
508
517
|
].join('\n');
|
|
509
518
|
requireValue(Buffer.byteLength(this.content) <= 16384, 'options');
|
|
510
519
|
this.sha256 = plugins.crypto.createHash('sha256').update(this.content).digest('hex');
|
|
520
|
+
this.requiredManagerMajor = options.fileDescriptorStorePreserve !== undefined
|
|
521
|
+
? fileDescriptorStorePreserveMajor : null;
|
|
511
522
|
Object.freeze(this);
|
|
512
523
|
}
|
|
513
524
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import * as plugins from './smartdaemon.plugins.js';
|
|
2
2
|
import { isCleanText } from './systemd.definition.js';
|
|
3
3
|
import { captureUserEnvironment, inspectRuntimeDirectory, isSafeDirectory } from './systemd.userenvironment.js';
|
|
4
|
+
import {
|
|
5
|
+
type ISystemdManagerVersion,
|
|
6
|
+
managerVersionArguments,
|
|
7
|
+
parseManagerVersion,
|
|
8
|
+
transientScopeMajor,
|
|
9
|
+
} from './systemd.managerversion.js';
|
|
4
10
|
|
|
5
11
|
export interface ISystemdTransientScopeOptions {
|
|
6
12
|
/** The calling user's manager; the only manager a transient scope is started in. */
|
|
@@ -51,8 +57,16 @@ export type TSystemdTransientScopeErrorCode =
|
|
|
51
57
|
| 'invalid_options'
|
|
52
58
|
/** The process is not running on Linux. */
|
|
53
59
|
| 'unsupported_platform'
|
|
54
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* No runtime directory is set or it does not exist, or the manager does not answer systemctl
|
|
62
|
+
* with a version it reports as systemd does (`SystemdUnit.inspectManagerVersion()`).
|
|
63
|
+
*/
|
|
55
64
|
| 'user_manager_unavailable'
|
|
65
|
+
/**
|
|
66
|
+
* The user manager is older than systemd 254, whose systemd-run this call needs; always a
|
|
67
|
+
* `SystemdTransientScopeManagerTooOldError`. Nothing was started.
|
|
68
|
+
*/
|
|
69
|
+
| 'manager_too_old'
|
|
56
70
|
/** systemd-run exited without reporting the scope running; the program has not run. */
|
|
57
71
|
| 'scope_start_failed'
|
|
58
72
|
/** The manager probe or the program did not finish, and close its output, in time. */
|
|
@@ -74,6 +88,24 @@ export class SystemdTransientScopeError extends Error {
|
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
/** Every `manager_too_old` refusal: names the user manager's version and the one required. */
|
|
92
|
+
export class SystemdTransientScopeManagerTooOldError extends SystemdTransientScopeError {
|
|
93
|
+
declare public readonly code: 'manager_too_old';
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
unitName: string,
|
|
97
|
+
/** The user manager's version the probe read. */
|
|
98
|
+
public readonly managerVersion: ISystemdManagerVersion,
|
|
99
|
+
/** The oldest systemd major the call needs. */
|
|
100
|
+
public readonly requiredMajor: number,
|
|
101
|
+
) {
|
|
102
|
+
super('manager_too_old', unitName, '');
|
|
103
|
+
this.message = `Systemd transient scope failed: manager_too_old (${unitName}: systemd ` +
|
|
104
|
+
`${managerVersion.major} running, ${requiredMajor} required).`;
|
|
105
|
+
this.name = 'SystemdTransientScopeManagerTooOldError';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
const outputLimit = 1048576;
|
|
78
110
|
const reservedName = (name: string) =>
|
|
79
111
|
name === 'XDG_RUNTIME_DIR' || name === 'INVOCATION_ID' || name.startsWith('SYSTEMD_') || name.startsWith('DBUS_');
|
|
@@ -227,7 +259,9 @@ const programStderr = (stderr: string, unitName: string): string | undefined =>
|
|
|
227
259
|
* must not keep the program's stdout or stderr open, or the call runs into its timeout.
|
|
228
260
|
*
|
|
229
261
|
* On `timed_out` and `output_limit` the direct process is killed; processes it started stay in
|
|
230
|
-
* the scope. A program that exits with a failure status is a result, not an error.
|
|
262
|
+
* the scope. A program that exits with a failure status is a result, not an error. A user manager
|
|
263
|
+
* older than systemd 254 is refused with `SystemdTransientScopeManagerTooOldError` before
|
|
264
|
+
* systemd-run is started.
|
|
231
265
|
*/
|
|
232
266
|
export const runInTransientScope = async (
|
|
233
267
|
options: ISystemdTransientScopeOptions,
|
|
@@ -247,13 +281,19 @@ export const runInTransientScope = async (
|
|
|
247
281
|
if (admission !== 'private') fail('invalid_options');
|
|
248
282
|
|
|
249
283
|
const probe = await runProcess(validated.systemctlPath,
|
|
250
|
-
['--user', '--no-ask-password', '--no-pager',
|
|
284
|
+
['--user', '--no-ask-password', '--no-pager', ...managerVersionArguments],
|
|
251
285
|
{ PATH: '/usr/bin:/bin', LANG: 'C', LC_ALL: 'C', XDG_RUNTIME_DIR: runtimeDirectory! }, validated.timeoutMs);
|
|
252
286
|
if (probe.kind === 'timed_out') fail('timed_out', probe.stderr);
|
|
253
|
-
|
|
254
|
-
|
|
287
|
+
const managerVersion = probe.kind === 'exited' && probe.code === 0 ? parseManagerVersion(probe.stdout) : undefined;
|
|
288
|
+
if (managerVersion === undefined) {
|
|
289
|
+
return fail('user_manager_unavailable', probe.kind === 'spawn_failed' ? '' : probe.stderr,
|
|
255
290
|
probe.kind === 'spawn_failed' ? probe.error : undefined);
|
|
256
291
|
}
|
|
292
|
+
// systemd-run before 254 knows no --expand-environment; the running user manager's version is the
|
|
293
|
+
// one this call can read before starting anything, so it stands for the installation's.
|
|
294
|
+
if (managerVersion.major < transientScopeMajor) {
|
|
295
|
+
throw new SystemdTransientScopeManagerTooOldError(unitName, managerVersion, transientScopeMajor);
|
|
296
|
+
}
|
|
257
297
|
|
|
258
298
|
// systemd-run registers its own process as the scope, then executes the program in place with
|
|
259
299
|
// its environment, so that environment is the program's own plus INVOCATION_ID.
|
|
@@ -8,6 +8,14 @@ import {
|
|
|
8
8
|
isUserManagerEnvironment,
|
|
9
9
|
} from './systemd.userenvironment.js';
|
|
10
10
|
import { environmentByteLimit, environmentVariableLimit, isCleanText } from './systemd.definition.js';
|
|
11
|
+
import {
|
|
12
|
+
type ISystemdManagerVersion,
|
|
13
|
+
fileDescriptorStoreCleanMajor,
|
|
14
|
+
fileDescriptorStorePreserveMajor,
|
|
15
|
+
managerVersionArguments,
|
|
16
|
+
parseManagerVersion,
|
|
17
|
+
} from './systemd.managerversion.js';
|
|
18
|
+
export type { ISystemdManagerVersion } from './systemd.managerversion.js';
|
|
11
19
|
|
|
12
20
|
export interface ISystemdUnitOptions {
|
|
13
21
|
unitName: string;
|
|
@@ -171,8 +179,17 @@ export class SystemdUnitError extends Error {
|
|
|
171
179
|
/**
|
|
172
180
|
* `released_uninspected`: only `cleanFileDescriptorStore()`. The manager released the store, and
|
|
173
181
|
* the snapshot read afterwards failed; `cause` is that inspection's error.
|
|
182
|
+
* `version_unknown`: the manager answered, but its `Version` property is not one this package
|
|
183
|
+
* reads, so a call that needs a minimum manager version refuses.
|
|
184
|
+
* `manager_too_old`: always a `SystemdUnitManagerTooOldError`, naming both versions.
|
|
174
185
|
*/
|
|
175
|
-
public readonly code:
|
|
186
|
+
public readonly code:
|
|
187
|
+
| 'invalid_options'
|
|
188
|
+
| 'command_failed'
|
|
189
|
+
| 'invalid_state'
|
|
190
|
+
| 'released_uninspected'
|
|
191
|
+
| 'version_unknown'
|
|
192
|
+
| 'manager_too_old',
|
|
176
193
|
options?: ErrorOptions,
|
|
177
194
|
) {
|
|
178
195
|
super(`Systemd unit operation failed: ${code}.`, options);
|
|
@@ -180,6 +197,23 @@ export class SystemdUnitError extends Error {
|
|
|
180
197
|
}
|
|
181
198
|
}
|
|
182
199
|
|
|
200
|
+
/** Every `manager_too_old` refusal: the running manager is older than the call needs. */
|
|
201
|
+
export class SystemdUnitManagerTooOldError extends SystemdUnitError {
|
|
202
|
+
declare public readonly code: 'manager_too_old';
|
|
203
|
+
|
|
204
|
+
constructor(
|
|
205
|
+
/** The running manager's version the refusal read. */
|
|
206
|
+
public readonly managerVersion: ISystemdManagerVersion,
|
|
207
|
+
/** The oldest systemd major the refused call needs. */
|
|
208
|
+
public readonly requiredMajor: number,
|
|
209
|
+
) {
|
|
210
|
+
super('manager_too_old');
|
|
211
|
+
this.message = `Systemd unit operation failed: manager_too_old (systemd ${managerVersion.major} ` +
|
|
212
|
+
`running, ${requiredMajor} required).`;
|
|
213
|
+
this.name = 'SystemdUnitManagerTooOldError';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
183
217
|
// systemd's format_timespan() units, largest first; a year and a month are 365.25 and 30.44 days.
|
|
184
218
|
const timespanUnits = new Map<string, bigint>([
|
|
185
219
|
['y', 31557600000000n], ['month', 2629800000000n], ['w', 604800000000n], ['d', 86400000000n],
|
|
@@ -514,11 +548,42 @@ export class SystemdUnit {
|
|
|
514
548
|
});
|
|
515
549
|
}
|
|
516
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Reads the running manager's version (`systemctl show --property=Version`) through the unit's
|
|
553
|
+
* scope: the system manager, or the calling user's manager for user scope. A manager that does
|
|
554
|
+
* not answer rejects with `command_failed`; an answer this package cannot read, with
|
|
555
|
+
* `version_unknown`.
|
|
556
|
+
*/
|
|
557
|
+
public async inspectManagerVersion(): Promise<ISystemdManagerVersion> {
|
|
558
|
+
const version = parseManagerVersion(await this.command([...managerVersionArguments]));
|
|
559
|
+
if (version === undefined) throw new SystemdUnitError('version_unknown');
|
|
560
|
+
return version;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Resolves with the running manager's version when its major is at least `requiredMajor`, an
|
|
565
|
+
* integer from 1 to 9999; otherwise rejects with a `SystemdUnitManagerTooOldError`. Fails closed:
|
|
566
|
+
* an unreadable version rejects with `version_unknown`, an unanswered read with
|
|
567
|
+
* `command_failed`, and a malformed `requiredMajor` with `invalid_options` before any command.
|
|
568
|
+
*/
|
|
569
|
+
public async requireManagerVersion(requiredMajor: number): Promise<ISystemdManagerVersion> {
|
|
570
|
+
if (!Number.isSafeInteger(requiredMajor) || requiredMajor < 1 || requiredMajor > 9999) {
|
|
571
|
+
throw new SystemdUnitError('invalid_options');
|
|
572
|
+
}
|
|
573
|
+
const version = await this.inspectManagerVersion();
|
|
574
|
+
if (version.major < requiredMajor) throw new SystemdUnitManagerTooOldError(version, requiredMajor);
|
|
575
|
+
return version;
|
|
576
|
+
}
|
|
577
|
+
|
|
517
578
|
/**
|
|
518
579
|
* Reports the loaded service settings that a definition renders, so an owner can verify an
|
|
519
580
|
* installed unit carries them. Reads only; the manager must have reloaded changed files first.
|
|
581
|
+
* Needs systemd 254 or later, which first reports `FileDescriptorStorePreserve`: an older manager
|
|
582
|
+
* is refused with `SystemdUnitManagerTooOldError` before the unit is read, and so is a manager
|
|
583
|
+
* whose version is unreadable, with `version_unknown`.
|
|
520
584
|
*/
|
|
521
585
|
public async inspectServiceSettings(): Promise<ISystemdServiceSettings> {
|
|
586
|
+
await this.requireManagerVersion(fileDescriptorStorePreserveMajor);
|
|
522
587
|
const keys = ['Id', 'Type', 'NotifyAccess', 'Restart', 'RestartUSec', 'KillMode', 'TimeoutStopUSec',
|
|
523
588
|
'TimeoutStartUSec', 'RestartPreventExitStatus', 'SuccessExitStatus', 'StartLimitIntervalUSec', 'StartLimitBurst',
|
|
524
589
|
'Delegate', 'LimitNOFILE', 'LimitNOFILESoft', 'TasksMax', 'OOMScoreAdjust', 'FileDescriptorStoreMax',
|
|
@@ -530,7 +595,7 @@ export class SystemdUnit {
|
|
|
530
595
|
if (separator < 1 || !keys.includes(key) || fields.has(key)) throw new SystemdUnitError('invalid_state');
|
|
531
596
|
fields.set(key, line.slice(separator + 1));
|
|
532
597
|
}
|
|
533
|
-
//
|
|
598
|
+
// A manager that omits a requested property it should know fails this count check.
|
|
534
599
|
if (fields.size !== keys.length || fields.get('Id') !== this.unitName) throw new SystemdUnitError('invalid_state');
|
|
535
600
|
const mode = (key: string, empty = false): string => {
|
|
536
601
|
const value = fields.get(key)!;
|
|
@@ -596,8 +661,13 @@ export class SystemdUnit {
|
|
|
596
661
|
* or that could not be run rejects with `command_failed`; so does one whose `systemctl` call
|
|
597
662
|
* exceeded `commandTimeoutMs`, whose outcome is then unknown, so after `command_failed` a caller
|
|
598
663
|
* must `inspect()` the unit before treating the store as kept.
|
|
664
|
+
*
|
|
665
|
+
* Needs systemd 254 or later, the first that cleans a store: the manager's version is read first,
|
|
666
|
+
* and an older manager is refused with `SystemdUnitManagerTooOldError`, an unreadable version with
|
|
667
|
+
* `version_unknown` and an unanswered read with `command_failed`, each before any clean is run.
|
|
599
668
|
*/
|
|
600
669
|
public async cleanFileDescriptorStore(): Promise<ISystemdUnitState> {
|
|
670
|
+
await this.requireManagerVersion(fileDescriptorStoreCleanMajor);
|
|
601
671
|
await this.command(['clean', '--what=fdstore', this.unitName]);
|
|
602
672
|
try {
|
|
603
673
|
return await this.inspect();
|
|
@@ -270,6 +270,11 @@ export class SystemdUnitFile {
|
|
|
270
270
|
/**
|
|
271
271
|
* Expected previous digest is mandatory; null admits an absent file. Exact desired bytes are idempotent.
|
|
272
272
|
* Retains changed definitions beside the unit before replacement. A failed reload never rolls code back.
|
|
273
|
+
*
|
|
274
|
+
* A definition with a `requiredManagerMajor` is admitted only on a manager at least that new,
|
|
275
|
+
* read before any directory, file or other systemctl effect: an older manager rejects with
|
|
276
|
+
* the unit's `SystemdUnitManagerTooOldError` (`manager_too_old`), an unreadable version with
|
|
277
|
+
* `SystemdUnitError` `version_unknown` and a failed read with `SystemdUnitError` `command_failed`.
|
|
273
278
|
*/
|
|
274
279
|
public async install(
|
|
275
280
|
definition: SystemdServiceDefinition,
|
|
@@ -288,6 +293,10 @@ export class SystemdUnitFile {
|
|
|
288
293
|
// instead of a wrapped write_failed.
|
|
289
294
|
const maskDirectories = this.maskDirectories();
|
|
290
295
|
if (maskDirectories === null) throw new SystemdUnitFileError('invalid_options');
|
|
296
|
+
// A manager older than the definition needs would load the unit without the newer directives.
|
|
297
|
+
if (definition.requiredManagerMajor !== null) {
|
|
298
|
+
await this.unit.requireManagerVersion(definition.requiredManagerMajor);
|
|
299
|
+
}
|
|
291
300
|
requireValue(!this.busy, 'busy');
|
|
292
301
|
this.busy = true;
|
|
293
302
|
let temporary: string | undefined;
|
|
@@ -14,7 +14,7 @@ export { path, childProcess, fs, fsConstants, crypto, util, os };
|
|
|
14
14
|
import * as lik from '@push.rocks/lik';
|
|
15
15
|
import * as smartfs from '@push.rocks/smartfs';
|
|
16
16
|
import * as smartlog from '@push.rocks/smartlog';
|
|
17
|
-
import * as smartlogDestinationLocal from '@push.rocks/smartlog
|
|
17
|
+
import * as smartlogDestinationLocal from '@push.rocks/smartlog/destination-local';
|
|
18
18
|
import * as smartmarkdown from '@push.rocks/smartmarkdown/iso';
|
|
19
19
|
import * as smartpath from '@push.rocks/smartpath';
|
|
20
20
|
import * as smartshell from '@push.rocks/smartshell';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The version of a running systemd manager, as the manager itself reports it. */
|
|
2
|
+
export interface ISystemdManagerVersion {
|
|
3
|
+
/** systemd's major version: the leading number of `version`, `255` for `255.4-1ubuntu8.17`. */
|
|
4
|
+
readonly major: number;
|
|
5
|
+
/**
|
|
6
|
+
* The manager's `Version` property verbatim, as its build names it: `255.4-1ubuntu8.17`,
|
|
7
|
+
* `253.5-1`, `256~rc3` or `257.1-1.fc41`. It is the running manager's version, which differs
|
|
8
|
+
* from `systemctl --version` after an upgrade until the manager re-executes.
|
|
9
|
+
*/
|
|
10
|
+
readonly version: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Internal: the systemd major that first reads `FileDescriptorStorePreserve=` and reports it. */
|
|
14
|
+
export const fileDescriptorStorePreserveMajor = 254;
|
|
15
|
+
|
|
16
|
+
/** Internal: the systemd major whose `systemctl clean` first releases a file descriptor store (`--what=fdstore`). */
|
|
17
|
+
export const fileDescriptorStoreCleanMajor = 254;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal: the systemd major whose `systemd-run` first takes `--expand-environment` and reports a
|
|
21
|
+
* started scope as `runInTransientScope()` reads it.
|
|
22
|
+
*/
|
|
23
|
+
export const transientScopeMajor = 254;
|
|
24
|
+
|
|
25
|
+
/** Internal: the `systemctl` verb and argument that print the manager's `Version` property. */
|
|
26
|
+
export const managerVersionArguments: readonly string[] = ['show', '--property=Version'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Internal: parses the output of `systemctl show --property=Version`, exactly one
|
|
30
|
+
* `Version=<version>` line. The version is a major of 1–4 digits without a leading zero,
|
|
31
|
+
* optionally followed by a separator (`.`, `~`, `-`, `+`, `^` or `_`) and printable ASCII, at
|
|
32
|
+
* most 256 characters in all. Anything else, `systemctl --version`'s `systemd 255 (255.4-1)`
|
|
33
|
+
* format included, is refused with undefined.
|
|
34
|
+
*/
|
|
35
|
+
export const parseManagerVersion = (stdout: string): ISystemdManagerVersion | undefined => {
|
|
36
|
+
const match = /^Version=(([1-9][0-9]{0,3})(?:[.~+^_-][\x21-\x7e]{0,251})?)\n$/.exec(stdout);
|
|
37
|
+
return match ? Object.freeze({ major: Number(match[2]), version: match[1] }) : undefined;
|
|
38
|
+
};
|