@push.rocks/smartdaemon 2.20.1 → 2.22.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.systemdservicedefinition.d.ts +16 -4
- package/dist_ts/classes.systemdservicedefinition.js +33 -7
- package/dist_ts/classes.systemdtransientscope.js +1 -1
- package/dist_ts/classes.systemdunit.d.ts +7 -0
- package/dist_ts/classes.systemdunit.js +11 -1
- package/dist_ts/classes.systemdunitfile.d.ts +26 -0
- package/dist_ts/classes.systemdunitfile.js +125 -9
- package/dist_ts/smartdaemon.classes.systemdmanager.js +1 -1
- package/dist_ts/smartdaemon.paths.js +1 -1
- package/dist_ts/systemd.definition.d.ts +16 -3
- package/dist_ts/systemd.definition.js +1 -1
- package/dist_ts/systemd.managerversion.d.ts +2 -0
- package/dist_ts/systemd.managerversion.js +3 -1
- package/dist_ts/systemd.userenvironment.d.ts +1 -1
- package/dist_ts_systemdruntime/classes.systemdnotifier.d.ts +82 -0
- package/dist_ts_systemdruntime/classes.systemdnotifier.js +217 -0
- package/dist_ts_systemdruntime/classes.systemdruntimeerror.d.ts +31 -0
- package/dist_ts_systemdruntime/classes.systemdruntimeerror.js +9 -0
- package/dist_ts_systemdruntime/index.d.ts +5 -0
- package/dist_ts_systemdruntime/index.js +14 -0
- package/dist_ts_systemdruntime/systemdruntime.libc.d.ts +87 -0
- package/dist_ts_systemdruntime/systemdruntime.libc.js +92 -0
- package/dist_ts_systemdruntime/systemdruntime.listen.d.ts +46 -0
- package/dist_ts_systemdruntime/systemdruntime.listen.js +136 -0
- package/dist_ts_systemdruntime/systemdruntime.validation.d.ts +13 -0
- package/dist_ts_systemdruntime/systemdruntime.validation.js +29 -0
- package/package.json +18 -6
- package/readme.md +186 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.systemdservicedefinition.ts +47 -10
- package/ts/classes.systemdunit.ts +11 -0
- package/ts/classes.systemdunitfile.ts +124 -7
- package/ts/systemd.definition.ts +12 -0
- package/ts/systemd.managerversion.ts +3 -0
- package/ts_systemdruntime/classes.systemdnotifier.ts +287 -0
- package/ts_systemdruntime/classes.systemdruntimeerror.ts +53 -0
- package/ts_systemdruntime/index.ts +22 -0
- package/ts_systemdruntime/systemdruntime.libc.ts +121 -0
- package/ts_systemdruntime/systemdruntime.listen.ts +153 -0
- package/ts_systemdruntime/systemdruntime.validation.ts +41 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
|
|
2
|
+
import { libcConstants, systemdLibc } from './systemdruntime.libc.js';
|
|
3
|
+
import { assertSystemdDescriptor, isSystemdDescriptorName } from './systemdruntime.validation.js';
|
|
4
|
+
/** sd_listen_fds(3): `SD_LISTEN_FDS_START`, the first passed descriptor. */
|
|
5
|
+
export const systemdListenDescriptorsStart = 3;
|
|
6
|
+
/**
|
|
7
|
+
* Marks one open descriptor close-on-exec (`fcntl(F_SETFD, FD_CLOEXEC)`) and verifies the flag,
|
|
8
|
+
* so no child process inherits it. Throws `close_on_exec_failed` with `errno` when the descriptor
|
|
9
|
+
* is not open or the flag does not hold.
|
|
10
|
+
*/
|
|
11
|
+
export const setCloseOnExec = (descriptorArg) => {
|
|
12
|
+
assertSystemdDescriptor(descriptorArg);
|
|
13
|
+
const libc = systemdLibc();
|
|
14
|
+
const { fcntl } = libc.functions;
|
|
15
|
+
const flags = fcntl(descriptorArg, libcConstants.F_GETFD, 0);
|
|
16
|
+
if (flags < 0) {
|
|
17
|
+
throw new SystemdRuntimeError('close_on_exec_failed', `Descriptor ${descriptorArg} cannot be read.`, {
|
|
18
|
+
errno: libc.errno(),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (fcntl(descriptorArg, libcConstants.F_SETFD, flags | libcConstants.FD_CLOEXEC) < 0) {
|
|
22
|
+
throw new SystemdRuntimeError('close_on_exec_failed', `Descriptor ${descriptorArg} cannot be marked close-on-exec.`, {
|
|
23
|
+
errno: libc.errno(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if ((fcntl(descriptorArg, libcConstants.F_GETFD, 0) & libcConstants.FD_CLOEXEC) === 0) {
|
|
27
|
+
throw new SystemdRuntimeError('close_on_exec_failed', `Descriptor ${descriptorArg} did not keep close-on-exec.`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const positiveIntegerPattern = /^[1-9][0-9]*$/;
|
|
31
|
+
/** sd_listen_fds(3) refuses a count above `INT_MAX - SD_LISTEN_FDS_START`. */
|
|
32
|
+
const listenDescriptorsMaximum = 2_147_483_647 - systemdListenDescriptorsStart;
|
|
33
|
+
const invalidListen = (messageArg, errnoArg) => new SystemdRuntimeError('listen_environment_invalid', messageArg, errnoArg === undefined ? {} : { errno: errnoArg });
|
|
34
|
+
/**
|
|
35
|
+
* The process's hard `RLIMIT_NOFILE` (`getrlimit(2)`), or null for `RLIM_INFINITY`. No descriptor
|
|
36
|
+
* the manager passed can lie at or above it: systemd moves passed descriptors into place with
|
|
37
|
+
* `fcntl(F_DUPFD)` after applying the unit's limits (`shift_fds()` after `setrlimit_closest_all()` in
|
|
38
|
+
* v255 `src/core/exec-invoke.c`), `F_DUPFD` refuses a number at or above the soft limit, the soft
|
|
39
|
+
* limit never exceeds the hard one, and an unprivileged process cannot raise the hard one.
|
|
40
|
+
*/
|
|
41
|
+
const descriptorHardLimit = () => {
|
|
42
|
+
const libc = systemdLibc();
|
|
43
|
+
const rlimit = Buffer.alloc(16);
|
|
44
|
+
if (libc.functions.getrlimit(libcConstants.RLIMIT_NOFILE, rlimit) !== 0) {
|
|
45
|
+
throw invalidListen('getrlimit(RLIMIT_NOFILE) failed, so LISTEN_FDS cannot be bounded.', libc.errno());
|
|
46
|
+
}
|
|
47
|
+
const hard = rlimit.readBigUInt64LE(8);
|
|
48
|
+
return hard === 0xffffffffffffffffn ? null : hard;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Takes the descriptors the service manager passed this process (sd_listen_fds_with_names(3)):
|
|
52
|
+
* socket-activation sockets and descriptor-store entries, numbered from 3.
|
|
53
|
+
*
|
|
54
|
+
* `LISTEN_PID`, `LISTEN_FDS` and `LISTEN_FDNAMES` always leave the environment, so no child sees
|
|
55
|
+
* them. Without them, or with `LISTEN_PID` naming another process (variables inherited from a
|
|
56
|
+
* parent), it returns an empty list and touches no descriptor. For this process every passed
|
|
57
|
+
* descriptor is marked close-on-exec before the list returns. Without `LISTEN_FDNAMES` each name is
|
|
58
|
+
* `unknown`, as in systemd.
|
|
59
|
+
*
|
|
60
|
+
* Throws `listen_environment_invalid` before touching any descriptor for a partial or malformed set
|
|
61
|
+
* (a count that is not a positive integer, a count whose last descriptor, `2 + count`, is at or
|
|
62
|
+
* above the process's hard `RLIMIT_NOFILE`, a names list whose length differs from the count, an
|
|
63
|
+
* invalid name), and with `errno` when `getrlimit(2)` fails. Descriptors are then marked one by one
|
|
64
|
+
* from 3, and memory grows only with descriptors proven open: the first passed descriptor that is
|
|
65
|
+
* not open throws `close_on_exec_failed`, as `sd_listen_fds(3)` fails on it; descriptors before it
|
|
66
|
+
* keep the flag.
|
|
67
|
+
*/
|
|
68
|
+
export const takeSystemdListenDescriptors = (optionsArg = {}) => {
|
|
69
|
+
const env = optionsArg.env ?? process.env;
|
|
70
|
+
const pid = optionsArg.pid ?? process.pid;
|
|
71
|
+
const listenPid = env.LISTEN_PID;
|
|
72
|
+
const listenFds = env.LISTEN_FDS;
|
|
73
|
+
const listenFdNames = env.LISTEN_FDNAMES;
|
|
74
|
+
delete env.LISTEN_PID;
|
|
75
|
+
delete env.LISTEN_FDS;
|
|
76
|
+
delete env.LISTEN_FDNAMES;
|
|
77
|
+
if (listenPid === undefined && listenFds === undefined && listenFdNames === undefined)
|
|
78
|
+
return Object.freeze([]);
|
|
79
|
+
if (listenPid === undefined || !positiveIntegerPattern.test(listenPid)) {
|
|
80
|
+
throw invalidListen('LISTEN_PID must name a process.');
|
|
81
|
+
}
|
|
82
|
+
if (listenPid !== String(pid))
|
|
83
|
+
return Object.freeze([]);
|
|
84
|
+
if (listenFds === undefined || !positiveIntegerPattern.test(listenFds) || Number(listenFds) > listenDescriptorsMaximum) {
|
|
85
|
+
throw invalidListen('LISTEN_FDS must be a positive descriptor count.');
|
|
86
|
+
}
|
|
87
|
+
const count = Number(listenFds);
|
|
88
|
+
// The cheap early bound: the last passed descriptor is 2 + count.
|
|
89
|
+
const hardLimit = descriptorHardLimit();
|
|
90
|
+
if (hardLimit !== null && BigInt(systemdListenDescriptorsStart + count) > hardLimit) {
|
|
91
|
+
throw invalidListen(`LISTEN_FDS passes ${count} descriptors; descriptor ${systemdListenDescriptorsStart + count - 1} is at or above this process's hard RLIMIT_NOFILE (${hardLimit}).`);
|
|
92
|
+
}
|
|
93
|
+
// Names, when given, are checked before any descriptor; the list is bounded by the variable's length.
|
|
94
|
+
const names = listenFdNames === undefined ? null : listenFdNames.split(':');
|
|
95
|
+
if (names !== null) {
|
|
96
|
+
if (names.length !== count)
|
|
97
|
+
throw invalidListen(`LISTEN_FDNAMES names ${names.length} descriptors; LISTEN_FDS passes ${count}.`);
|
|
98
|
+
if (!names.every((name) => isSystemdDescriptorName(name, false)))
|
|
99
|
+
throw invalidListen('LISTEN_FDNAMES holds an invalid name.');
|
|
100
|
+
}
|
|
101
|
+
// Nothing scales with the count before a descriptor is proven open: as in sd_listen_fds(3), the
|
|
102
|
+
// first descriptor that is not open ends the walk with close_on_exec_failed.
|
|
103
|
+
const descriptors = [];
|
|
104
|
+
for (let index = 0; index < count; index++) {
|
|
105
|
+
const descriptor = systemdListenDescriptorsStart + index;
|
|
106
|
+
setCloseOnExec(descriptor);
|
|
107
|
+
descriptors.push(Object.freeze({ descriptor, name: names === null ? 'unknown' : names[index] }));
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze(descriptors);
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The watchdog interval the manager expects `WATCHDOG=1` within (`WATCHDOG_USEC`, sd_watchdog_enabled(3)),
|
|
113
|
+
* in microseconds; null when the unit has no watchdog or `WATCHDOG_PID` names another process.
|
|
114
|
+
* Reads the environment without changing it. Throws `watchdog_environment_invalid` for a malformed
|
|
115
|
+
* value.
|
|
116
|
+
*/
|
|
117
|
+
export const watchdogIntervalUsec = (optionsArg = {}) => {
|
|
118
|
+
const env = optionsArg.env ?? process.env;
|
|
119
|
+
const pid = optionsArg.pid ?? process.pid;
|
|
120
|
+
const usec = env.WATCHDOG_USEC;
|
|
121
|
+
const watchdogPid = env.WATCHDOG_PID;
|
|
122
|
+
if (usec === undefined)
|
|
123
|
+
return null;
|
|
124
|
+
if (!positiveIntegerPattern.test(usec) || !Number.isSafeInteger(Number(usec))) {
|
|
125
|
+
throw new SystemdRuntimeError('watchdog_environment_invalid', 'WATCHDOG_USEC must be a positive integer.');
|
|
126
|
+
}
|
|
127
|
+
if (watchdogPid !== undefined) {
|
|
128
|
+
if (!positiveIntegerPattern.test(watchdogPid)) {
|
|
129
|
+
throw new SystemdRuntimeError('watchdog_environment_invalid', 'WATCHDOG_PID must name a process.');
|
|
130
|
+
}
|
|
131
|
+
if (watchdogPid !== String(pid))
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return Number(usec);
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZHJ1bnRpbWUubGlzdGVuLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfc3lzdGVtZHJ1bnRpbWUvc3lzdGVtZHJ1bnRpbWUubGlzdGVuLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBQ3ZFLE9BQU8sRUFBRSxhQUFhLEVBQUUsV0FBVyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDdEUsT0FBTyxFQUFFLHVCQUF1QixFQUFFLHVCQUF1QixFQUFFLE1BQU0sZ0NBQWdDLENBQUM7QUFFbEcsNEVBQTRFO0FBQzVFLE1BQU0sQ0FBQyxNQUFNLDZCQUE2QixHQUFHLENBQUMsQ0FBQztBQWdCL0M7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxDQUFDLGFBQXFCLEVBQVEsRUFBRTtJQUM1RCx1QkFBdUIsQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUN2QyxNQUFNLElBQUksR0FBRyxXQUFXLEVBQUUsQ0FBQztJQUMzQixNQUFNLEVBQUUsS0FBSyxFQUFFLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQztJQUNqQyxNQUFNLEtBQUssR0FBRyxLQUFLLENBQUMsYUFBYSxFQUFFLGFBQWEsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDN0QsSUFBSSxLQUFLLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDZCxNQUFNLElBQUksbUJBQW1CLENBQUMsc0JBQXNCLEVBQUUsY0FBYyxhQUFhLGtCQUFrQixFQUFFO1lBQ25HLEtBQUssRUFBRSxJQUFJLENBQUMsS0FBSyxFQUFFO1NBQ3BCLENBQUMsQ0FBQztJQUNMLENBQUM7SUFDRCxJQUFJLEtBQUssQ0FBQyxhQUFhLEVBQUUsYUFBYSxDQUFDLE9BQU8sRUFBRSxLQUFLLEdBQUcsYUFBYSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3RGLE1BQU0sSUFBSSxtQkFBbUIs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
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** `FDNAME_MAX`: the longest descriptor name systemd accepts. */
|
|
2
|
+
export declare const descriptorNameMaximumLength = 255;
|
|
3
|
+
/**
|
|
4
|
+
* systemd's `fdname_is_valid()` (src/basic/fd-util.c, v255): printable ASCII except `:`, at most
|
|
5
|
+
* 255 characters. systemd also admits the empty name; `nonEmptyArg` refuses it where a name has to
|
|
6
|
+
* identify a descriptor.
|
|
7
|
+
*/
|
|
8
|
+
export declare const isSystemdDescriptorName: (nameArg: unknown, nonEmptyArg: boolean) => nameArg is string;
|
|
9
|
+
/** An `AF_UNIX` notification address: an absolute path or an abstract name written `@name`. */
|
|
10
|
+
export declare const isSystemdNotifySocketAddress: (addressArg: unknown) => addressArg is string;
|
|
11
|
+
export declare const assertSystemdNotifySocketAddress: (addressArg: string) => void;
|
|
12
|
+
export declare const isSystemdDescriptor: (descriptorArg: unknown) => descriptorArg is number;
|
|
13
|
+
export declare const assertSystemdDescriptor: (descriptorArg: number) => void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
|
|
2
|
+
import { socketAddressMaximumBytes } from './systemdruntime.libc.js';
|
|
3
|
+
/** `FDNAME_MAX`: the longest descriptor name systemd accepts. */
|
|
4
|
+
export const descriptorNameMaximumLength = 255;
|
|
5
|
+
/**
|
|
6
|
+
* systemd's `fdname_is_valid()` (src/basic/fd-util.c, v255): printable ASCII except `:`, at most
|
|
7
|
+
* 255 characters. systemd also admits the empty name; `nonEmptyArg` refuses it where a name has to
|
|
8
|
+
* identify a descriptor.
|
|
9
|
+
*/
|
|
10
|
+
export const isSystemdDescriptorName = (nameArg, nonEmptyArg) => typeof nameArg === 'string' && nameArg.length <= descriptorNameMaximumLength &&
|
|
11
|
+
(!nonEmptyArg || nameArg.length > 0) && /^[\x20-\x39\x3b-\x7e]*$/.test(nameArg);
|
|
12
|
+
/** An `AF_UNIX` notification address: an absolute path or an abstract name written `@name`. */
|
|
13
|
+
export const isSystemdNotifySocketAddress = (addressArg) => typeof addressArg === 'string' && /^[/@]./s.test(addressArg) && !addressArg.includes('\0') &&
|
|
14
|
+
Buffer.byteLength(addressArg, 'utf8') <= socketAddressMaximumBytes;
|
|
15
|
+
export const assertSystemdNotifySocketAddress = (addressArg) => {
|
|
16
|
+
if (!isSystemdNotifySocketAddress(addressArg)) {
|
|
17
|
+
throw new SystemdRuntimeError('notify_socket_invalid', 'NOTIFY_SOCKET must be an absolute AF_UNIX path or an abstract name (@name) of at most 107 bytes without NUL.');
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/** The largest descriptor number the kernel can hand out (`INT_MAX`). */
|
|
21
|
+
const descriptorMaximum = 2_147_483_647;
|
|
22
|
+
export const isSystemdDescriptor = (descriptorArg) => typeof descriptorArg === 'number' && Number.isSafeInteger(descriptorArg) && descriptorArg >= 0 &&
|
|
23
|
+
descriptorArg <= descriptorMaximum;
|
|
24
|
+
export const assertSystemdDescriptor = (descriptorArg) => {
|
|
25
|
+
if (!isSystemdDescriptor(descriptorArg)) {
|
|
26
|
+
throw new SystemdRuntimeError('descriptor_invalid', `${String(descriptorArg)} is not a descriptor number.`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZHJ1bnRpbWUudmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3N5c3RlbWRydW50aW1lL3N5c3RlbWRydW50aW1lLnZhbGlkYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDdkUsT0FBTyxFQUFFLHlCQUF5QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFFckUsaUVBQWlFO0FBQ2pFLE1BQU0sQ0FBQyxNQUFNLDJCQUEyQixHQUFHLEdBQUcsQ0FBQztBQUUvQzs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsQ0FBQyxPQUFnQixFQUFFLFdBQW9CLEVBQXFCLEVBQUUsQ0FDbkcsT0FBTyxPQUFPLEtBQUssUUFBUSxJQUFJLE9BQU8sQ0FBQyxNQUFNLElBQUksMkJBQTJCO0lBQzVFLENBQUMsQ0FBQyxXQUFXLElBQUksT0FBTyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsSUFBSSx5QkFBeUIsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUM7QUFFbEYsK0ZBQStGO0FBQy9GLE1BQU0sQ0FBQyxNQUFNLDRCQUE0QixHQUFHLENBQUMsVUFBbUIsRUFBd0IsRUFBRSxDQUN4RixPQUFPLFVBQVUsS0FBSyxRQUFRLElBQUksU0FBUyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDO0lBQzFGLE1BQU0sQ0FBQyxVQUFVLENBQUMsVUFBVSxFQUFFLE1BQU0sQ0FBQyxJQUFJLHlCQUF5QixDQUFDO0FBRXJFLE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUFHLENBQUMsVUFBa0IsRUFBUSxFQUFFO0lBQzNFLElBQUksQ0FBQyw0QkFBNEIsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDO1FBQzlDLE1BQU0sSUFBSSxtQkFBbUIsQ0FDM0IsdUJBQXVCLEVBQ3ZCLDhHQUE4RyxDQUMvRyxDQUFDO0lBQ0osQ0FBQztBQUNILENBQUMsQ0FBQztBQUVGLHlFQUF5RTtBQUN6RSxNQUFNLGlCQUFpQixHQUFHLGFBQWEsQ0FBQztBQUV4QyxNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBRyxDQUFDLGFBQXNCLEVBQTJCLEVBQUUsQ0FDckYsT0FBTyxhQUFhLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxhQUFhLENBQUMsYUFBYSxDQUFDLElBQUksYUFBYSxJQUFJLENBQUM7SUFDOUYsYUFBYSxJQUFJLGlCQUFpQixDQUFDO0FBRXJDLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsYUFBcUIsRUFBUSxFQUFFO0lBQ3JFLElBQUksQ0FBQyxtQkFBbUIsQ0FBQyxhQUFhLENBQUMsRUFBRSxDQUFDO1FBQ3hDLE1BQU0sSUFBSSxtQkFBbUIsQ0FBQyxvQkFBb0IsRUFBRSxHQUFHLE1BQU0sQ0FBQyxhQUFhLENBQUMsOEJBQThCLENBQUMsQ0FBQztJQUM5RyxDQUFDO0FBQ0gsQ0FBQyxDQUFDIn0=
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartdaemon",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Start scripts as long running daemons and manage them.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
7
7
|
"typings": "dist_ts/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist_ts/index.d.ts",
|
|
11
|
+
"default": "./dist_ts/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./systemd-runtime": {
|
|
14
|
+
"types": "./dist_ts_systemdruntime/index.d.ts",
|
|
15
|
+
"default": "./dist_ts_systemdruntime/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
8
19
|
"type": "module",
|
|
9
20
|
"author": "Task Venture Capital GmbH <hello@task.vc>",
|
|
10
21
|
"license": "MIT",
|
|
@@ -26,10 +37,10 @@
|
|
|
26
37
|
},
|
|
27
38
|
"homepage": "https://code.foss.global/push.rocks/smartdaemon",
|
|
28
39
|
"devDependencies": {
|
|
29
|
-
"@git.zone/tsbuild": "^
|
|
30
|
-
"@git.zone/tsrun": "^3.0.
|
|
31
|
-
"@git.zone/tstest": "^7.1
|
|
32
|
-
"@types/node": "^26.6.
|
|
40
|
+
"@git.zone/tsbuild": "^6.0.2",
|
|
41
|
+
"@git.zone/tsrun": "^3.0.3",
|
|
42
|
+
"@git.zone/tstest": "^7.2.1",
|
|
43
|
+
"@types/node": "^26.6.4"
|
|
33
44
|
},
|
|
34
45
|
"dependencies": {
|
|
35
46
|
"@push.rocks/lik": "^6.4.4",
|
|
@@ -41,6 +52,7 @@
|
|
|
41
52
|
},
|
|
42
53
|
"files": [
|
|
43
54
|
"ts/**/*",
|
|
55
|
+
"ts_systemdruntime/**/*",
|
|
44
56
|
"ts_web/**/*",
|
|
45
57
|
"dist/**/*",
|
|
46
58
|
"dist_*/**/*",
|
|
@@ -58,7 +70,7 @@
|
|
|
58
70
|
],
|
|
59
71
|
"scripts": {
|
|
60
72
|
"test": "tstest test/ --verbose --timeout 60",
|
|
61
|
-
"build": "tsbuild --web",
|
|
73
|
+
"build": "tsbuild tsfolders --web",
|
|
62
74
|
"format": "gitzone format",
|
|
63
75
|
"buildDocs": "tsdoc"
|
|
64
76
|
}
|
package/readme.md
CHANGED
|
@@ -338,6 +338,12 @@ installation scope, after any required offline commissioning. A failed or timed
|
|
|
338
338
|
out command can leave partial enablement; inspect and retry the exact operation
|
|
339
339
|
without assuming cancellation or undoing the application's retained protection.
|
|
340
340
|
|
|
341
|
+
`disable()` is its counterpart: it removes the enablement links the unit's `[Install]`
|
|
342
|
+
section names (`systemctl disable`) and returns a fresh snapshot. It does not stop a running
|
|
343
|
+
unit, mask, unmask or remove the definition, so a disabled service keeps running until it is
|
|
344
|
+
stopped. A failed or timed out command can leave partial enablement as well; inspect and retry.
|
|
345
|
+
Disable a unit before `SystemdUnitFile.uninstall()` removes its definition.
|
|
346
|
+
|
|
341
347
|
`inspectRelationships()` returns immutable sorted arrays for `before`, `after`,
|
|
342
348
|
`requires`, `wants`, `conflicts`, `requiredBy` and `wantedBy` from the loaded manager
|
|
343
349
|
graph. Literal systemd hex escapes are preserved. The bounded snapshot rejects
|
|
@@ -391,7 +397,7 @@ try {
|
|
|
391
397
|
explicit restart policy, kill mode and stop timeout. System-scope definitions require
|
|
392
398
|
`user` and `group`; user-scope definitions reject both directives because the user
|
|
393
399
|
manager already runs under the calling identity. It defaults to `Type=exec`,
|
|
394
|
-
journal output and null stdin. It introduces no shell; percent specifiers and
|
|
400
|
+
journal output (see `standardOutput` and `standardError` below) and null stdin. It introduces no shell; percent specifiers and
|
|
395
401
|
argument environment expansion are escaped. Paths must be absolute and normalized.
|
|
396
402
|
Descriptions, paths and arguments reject control characters, lone surrogates and Unicode
|
|
397
403
|
noncharacters (U+FDD0–U+FDEF and every code point ending in FFFE or FFFF), which systemd
|
|
@@ -464,6 +470,7 @@ A user-scope `SystemdUnitFile` validates each value where it is read:
|
|
|
464
470
|
calls need, plus a well-formed config root (`XDG_CONFIG_HOME`, or `~/.config`), and
|
|
465
471
|
rejects anything missing or malformed with `invalid_options` before touching any
|
|
466
472
|
directory or running `systemctl`.
|
|
473
|
+
- `uninstall()` needs the same and refuses the same way, before removing anything.
|
|
467
474
|
|
|
468
475
|
An owner can therefore ask whether its definition is installed from a context without
|
|
469
476
|
a login session:
|
|
@@ -631,6 +638,41 @@ and `%` doubled, so systemd applies no specifier; `Environment=` never expands `
|
|
|
631
638
|
taken literally. An empty object renders nothing. Systemd merges these variables over its own
|
|
632
639
|
(`HOME`, `USER`, `INVOCATION_ID`, …), so a name such as `NOTIFY_SOCKET` overrides the manager's value.
|
|
633
640
|
|
|
641
|
+
`standardOutput` and `standardError` say where the service's output streams go
|
|
642
|
+
(`StandardOutput=`, `StandardError=`): `'journal'`, the default, `'null'`, which discards the
|
|
643
|
+
stream, or `{ append: path }`, which appends it to a file (`append:`, systemd 240 and later).
|
|
644
|
+
Omitted, both render `journal` as before, so existing definitions keep their bytes.
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
const controller = new SystemdServiceDefinition({
|
|
648
|
+
scope: 'user',
|
|
649
|
+
unitName: 'app-controller.service',
|
|
650
|
+
description: 'App controller',
|
|
651
|
+
executable: '/home/owner/.local/share/pnpm/app',
|
|
652
|
+
args: ['serve'],
|
|
653
|
+
workingDirectory: '/home/owner',
|
|
654
|
+
restart: 'on-failure',
|
|
655
|
+
killMode: 'process',
|
|
656
|
+
timeoutStopSeconds: 90,
|
|
657
|
+
standardOutput: 'null',
|
|
658
|
+
standardError: { append: '/home/owner/.config/app/logs/controller.log' },
|
|
659
|
+
});
|
|
660
|
+
// StandardOutput=null
|
|
661
|
+
// StandardError=append:/home/owner/.config/app/logs/controller.log
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
The path must be absolute and normalized, without a backslash, surrounding whitespace or a
|
|
665
|
+
trailing slash; `%` is doubled, so systemd applies no specifier. The service manager opens the file
|
|
666
|
+
before it invokes the executable and before it switches to the service's `User=`, so with its own
|
|
667
|
+
privileges: as root under the system manager, as the user under a user manager. It creates a missing
|
|
668
|
+
file owned by that user with mode 0666 masked by the unit's umask (0644 under the usual umask 0022).
|
|
669
|
+
Its directory must already exist, or the service fails to start. A caller that needs another owner
|
|
670
|
+
or a private file creates it with the owner and mode it wants first; an existing file keeps both.
|
|
671
|
+
Systemd reads `append:` from version 240 and an older manager would ignore the line, so a definition
|
|
672
|
+
with an `append` output has `requiredManagerMajor` 240 (254 with `fileDescriptorStorePreserve` as
|
|
673
|
+
well) and `SystemdUnitFile.install()` refuses it on an older manager with `manager_too_old`. The manager reports only the kind of output (`append`) as a property, not the path, so
|
|
674
|
+
`inspectServiceSettings()` does not report it: verify an installed definition by its digest.
|
|
675
|
+
|
|
634
676
|
#### Notify access and the file descriptor store
|
|
635
677
|
|
|
636
678
|
A service that keeps a descriptor across its own restarts, such as a network namespace a child
|
|
@@ -654,8 +696,9 @@ empties the store; a `systemctl soft-reboot` keeps the running kernel, so a pres
|
|
|
654
696
|
descriptors in it pass on to the next userspace boot. Systemd reads `FileDescriptorStorePreserve=` from version 254
|
|
655
697
|
and an older manager would ignore the line, so a definition with `fileDescriptorStorePreserve` has
|
|
656
698
|
`requiredManagerMajor` 254 and `SystemdUnitFile.install()` refuses it on an older manager with
|
|
657
|
-
`SystemdUnitManagerTooOldError` (`manager_too_old`) before touching anything
|
|
658
|
-
|
|
699
|
+
`SystemdUnitManagerTooOldError` (`manager_too_old`) before touching anything. `requiredManagerMajor` is
|
|
700
|
+
the highest major any rendered line needs (240 for an `append` output, see above); a definition with
|
|
701
|
+
neither has it null. Sending `FDSTORE=1` needs an `AF_UNIX` socket, so a definition
|
|
659
702
|
with a store or a notify access other than `'none'` that restricts its socket families must allow
|
|
660
703
|
`AF_UNIX`. Omitted, each option renders nothing.
|
|
661
704
|
|
|
@@ -910,6 +953,42 @@ user scope requires `expectedUid` to match the caller's actual uid. The manager'
|
|
|
910
953
|
selected fragment must match the installed file after reload, so an unrecognized
|
|
911
954
|
custom user unit path fails closed.
|
|
912
955
|
|
|
956
|
+
#### Removing an installed definition
|
|
957
|
+
|
|
958
|
+
`uninstall(expectedSha256)` removes what `install()` wrote: the unit file, when it holds exactly
|
|
959
|
+
the bytes `expectedSha256` digests, and every previous definition retained beside it
|
|
960
|
+
(`<unit>.previous-<sha256>`), then reloads the manager and verifies its view.
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
const unit = new SystemdUnit({ scope: 'user', unitName: 'authswitch.service' });
|
|
964
|
+
const userFile = new SystemdUnitFile({ scope: 'user', unitName: 'authswitch.service' });
|
|
965
|
+
await unit.stop();
|
|
966
|
+
await unit.disable();
|
|
967
|
+
const installed = await userFile.inspect();
|
|
968
|
+
if (installed) {
|
|
969
|
+
const result = await userFile.uninstall(installed.sha256);
|
|
970
|
+
console.log(result.removed, result.masked, result.removedPaths);
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
It admits the unit only at rest (`systemdUnitQuiescence()`) with no job queued, no pending reload
|
|
975
|
+
and no drop-ins, while the manager still selects this file, and refuses with the same
|
|
976
|
+
`SystemdUnitFileUnsafeUnitError` reasons as installation. Removal admits fewer unit-file states: a
|
|
977
|
+
loaded unit must be `disabled`, so no enablement link outlives its definition (an enabled one is
|
|
978
|
+
refused with `unexpected_file_state`; disable it first), a masked unit keeps its mask, and a unit the
|
|
979
|
+
manager does not find is admitted. A stopped service whose `killMode: 'process'` left processes
|
|
980
|
+
running still has a control group and is refused with `active` until they end.
|
|
981
|
+
|
|
982
|
+
Every file is checked as installation checks the unit file (regular, single link, mode 0644, owned
|
|
983
|
+
by the expected uid) and is removed only while its name still holds the file that was checked; a
|
|
984
|
+
retained definition whose bytes do not match the digest in its name, or a unit file with other bytes
|
|
985
|
+
than expected, refuses the removal with `changed` before anything is removed. After removal the
|
|
986
|
+
manager must report the unit not found (still masked, for a masked unit) and at rest, or the call
|
|
987
|
+
rejects with `reload_failed`, the files already removed and the cause attached. With nothing to
|
|
988
|
+
remove the call resolves `{ removed: false, … }` without reloading the manager. `removed` reports
|
|
989
|
+
whether the unit file itself was removed and `removedPaths` every file that was. Removal never
|
|
990
|
+
stops, disables or unmasks the unit.
|
|
991
|
+
|
|
913
992
|
With `killMode: 'mixed'`, the main process owns graceful child shutdown and must
|
|
914
993
|
remain alive until its children have joined. Systemd sends SIGTERM to the main
|
|
915
994
|
process first and can send SIGKILL to the whole cgroup when the main process exits
|
|
@@ -919,6 +998,106 @@ other process in the unit survives, so the main process must own their shutdown
|
|
|
919
998
|
deliberate retention (as with `Delegate=yes` workloads). A successful systemd start still requires application readiness
|
|
920
999
|
verification, and a completed stop alone does not prove database drain.
|
|
921
1000
|
|
|
1001
|
+
### The service side: notifications and passed descriptors (`/systemd-runtime`, Node.js 26)
|
|
1002
|
+
|
|
1003
|
+
The definitions above tell the manager what to expect; a Node.js main process answers it through
|
|
1004
|
+
the `@push.rocks/smartdaemon/systemd-runtime` subpath: `sd_notify(3)` messages (with descriptors
|
|
1005
|
+
for the store) and the descriptors the manager passes (`LISTEN_FDS`). Node.js has no `AF_UNIX`
|
|
1006
|
+
datagram socket and no `fcntl(2)`, so the subpath calls glibc through `node:ffi`:
|
|
1007
|
+
|
|
1008
|
+
- It requires **Node.js 26 or later**. Importing it on an older Node.js throws
|
|
1009
|
+
`SystemdRuntimeError` with code `runtime_unsupported`; the main entry is unaffected and keeps
|
|
1010
|
+
its Node.js range. The package is ES modules: CommonJS can `require()` either entry only on a
|
|
1011
|
+
Node.js that loads ES modules through `require()` by default (20.19+, 22.12+, 23+), where the
|
|
1012
|
+
subpath throws the same `runtime_unsupported` below 26; an older Node.js throws `ERR_REQUIRE_ESM`.
|
|
1013
|
+
- It runs on Linux x64 and arm64 with glibc (`libc.so.6`). Elsewhere, and with `node:ffi`
|
|
1014
|
+
unavailable (`--no-experimental-ffi`) or denied by the permission model, the first call that
|
|
1015
|
+
needs libc throws `runtime_unsupported`. Under the permission model pass `--allow-ffi`.
|
|
1016
|
+
- `node:ffi` is experimental: loading it prints one `ExperimentalWarning`. A single executable
|
|
1017
|
+
application suppresses it through its `execArgv` (`--disable-warning=ExperimentalWarning`).
|
|
1018
|
+
- libc is opened on first use. Outside systemd nothing is loaded: without `NOTIFY_SOCKET`
|
|
1019
|
+
`SystemdNotifier.fromEnvironment()` returns null, and without `LISTEN_*` variables
|
|
1020
|
+
`takeSystemdListenDescriptors()` returns an empty list.
|
|
1021
|
+
|
|
1022
|
+
```typescript
|
|
1023
|
+
import {
|
|
1024
|
+
SystemdNotifier,
|
|
1025
|
+
takeSystemdListenDescriptors,
|
|
1026
|
+
watchdogIntervalUsec,
|
|
1027
|
+
} from '@push.rocks/smartdaemon/systemd-runtime';
|
|
1028
|
+
|
|
1029
|
+
// Descriptors passed for this process, numbered from 3, already close-on-exec.
|
|
1030
|
+
const passed = takeSystemdListenDescriptors(); // [{ descriptor: 3, name: 'pallet-router' }]
|
|
1031
|
+
const namespaceDescriptor = passed.find(({ name }) => name === 'pallet-router')?.descriptor ?? createNamespace();
|
|
1032
|
+
|
|
1033
|
+
// null when no service manager listens; the caller decides what that means.
|
|
1034
|
+
const notifier = SystemdNotifier.fromEnvironment({ take: true });
|
|
1035
|
+
if (notifier) {
|
|
1036
|
+
notifier.extendTimeout(60_000_000, 'replaying journal');
|
|
1037
|
+
notifier.replaceDescriptors('pallet-router', [namespaceDescriptor], { poll: false });
|
|
1038
|
+
notifier.ready('serving');
|
|
1039
|
+
const interval = watchdogIntervalUsec();
|
|
1040
|
+
if (interval !== null) setInterval(() => notifier.watchdog(), interval / 2000);
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
| Call | Sends or does |
|
|
1045
|
+
| --- | --- |
|
|
1046
|
+
| `ready(status?)` | `READY=1` (and `STATUS=`) |
|
|
1047
|
+
| `status(text)` | `STATUS=` |
|
|
1048
|
+
| `stopping(status?)` | `STOPPING=1` |
|
|
1049
|
+
| `reloading(status?)` | `RELOADING=1` with `MONOTONIC_USEC=` (`CLOCK_MONOTONIC`); send `ready()` when done |
|
|
1050
|
+
| `watchdog()` | `WATCHDOG=1` |
|
|
1051
|
+
| `extendTimeout(usec, status?)` | `EXTEND_TIMEOUT_USEC=` |
|
|
1052
|
+
| `storeDescriptors(name, fds, { poll? })` | `FDSTORE=1`, `FDNAME=`, `FDPOLL=0` for `poll: false`; descriptors as `SCM_RIGHTS` |
|
|
1053
|
+
| `removeDescriptors(name)` | `FDSTOREREMOVE=1`, `FDNAME=` |
|
|
1054
|
+
| `replaceDescriptors(name, fds, { poll? })` | remove, then store: two messages, handled in order |
|
|
1055
|
+
| `send(fields, fds?)` | any `NAME=value` fields in insertion order |
|
|
1056
|
+
| `takeSystemdNotifySocket()` | removes `NOTIFY_SOCKET` from the environment and returns it validated, or null |
|
|
1057
|
+
| `takeSystemdListenDescriptors()` | removes `LISTEN_PID`/`LISTEN_FDS`/`LISTEN_FDNAMES`; for this process marks every passed descriptor close-on-exec and returns it with its name (`unknown` without names) |
|
|
1058
|
+
| `setCloseOnExec(fd)` | `fcntl(F_SETFD, FD_CLOEXEC)`, verified |
|
|
1059
|
+
| `watchdogIntervalUsec()` | `WATCHDOG_USEC` for this process (`WATCHDOG_PID`), or null |
|
|
1060
|
+
|
|
1061
|
+
Every call is synchronous and either completes or throws `SystemdRuntimeError` (`code`, and
|
|
1062
|
+
`errno` when a libc call failed); nothing is dropped silently. systemd ignores a message it cannot
|
|
1063
|
+
parse without telling the sender, so the notifier refuses it first: field names are upper-case,
|
|
1064
|
+
values carry no newline or NUL, a message is at most 4096 bytes (`notify_field_invalid`); a
|
|
1065
|
+
descriptor name is 1–255 printable ASCII characters without `:` (`descriptor_name_invalid`); one
|
|
1066
|
+
message carries 1–253 descriptors (`descriptor_invalid`). `NOTIFY_SOCKET` must be an absolute
|
|
1067
|
+
path or an abstract name (`@name`) of at most 107 bytes; anything else, `vsock:` addresses
|
|
1068
|
+
included, is `notify_socket_invalid`. A send the kernel refuses is `notify_failed` with its errno
|
|
1069
|
+
(`ENOENT` for a missing path, `ECONNREFUSED` for an abstract name nobody holds). `LISTEN_*`
|
|
1070
|
+
variables naming another process are removed and nothing else is touched; a partial or malformed
|
|
1071
|
+
set (`LISTEN_FDS` not a positive integer, a names list of another length) is
|
|
1072
|
+
`listen_environment_invalid` before any descriptor is touched, and a passed descriptor that is not
|
|
1073
|
+
open is `close_on_exec_failed`.
|
|
1074
|
+
|
|
1075
|
+
`LISTEN_FDS` is also bounded by the process's hard `RLIMIT_NOFILE` before any descriptor is touched:
|
|
1076
|
+
a count whose last descriptor (`2 + count`) is at or above that limit is
|
|
1077
|
+
`listen_environment_invalid`. No descriptor systemd passed can lie at or above the hard limit,
|
|
1078
|
+
because it moves them into place with `fcntl(F_DUPFD)` (which refuses a number at or above the soft
|
|
1079
|
+
limit) after applying the unit's limits; a wrapper that lowers its hard limit below a passed
|
|
1080
|
+
descriptor before starting Node.js is refused. The hard limit can be as large as `fs.nr_open`
|
|
1081
|
+
(`LimitNOFILE=infinity`), so nothing is allocated per counted descriptor up front: like
|
|
1082
|
+
`sd_listen_fds(3)`, the descriptors are marked one by one from 3, the first that is not open throws
|
|
1083
|
+
`close_on_exec_failed` (the ones before it keep the flag), and the list grows only with open
|
|
1084
|
+
descriptors.
|
|
1085
|
+
|
|
1086
|
+
Sends are blocking `sendmsg(2)` calls on a fresh `AF_UNIX` datagram socket, the same as
|
|
1087
|
+
`sd_notify(3)`: when the manager's receive queue is full, the call waits for room and the event loop
|
|
1088
|
+
stalls for that time.
|
|
1089
|
+
|
|
1090
|
+
`node:ffi` has no errno API: the subpath reads glibc's thread-local `errno` through
|
|
1091
|
+
`__errno_location()` immediately after the failed call. Node.js itself calls libuv's
|
|
1092
|
+
`uv_disable_stdio_inheritance()` at startup (`InitializeNodeWithArgsInternal()` in `src/node.cc`;
|
|
1093
|
+
`deps/uv/src/unix/core.c`, Node.js 25.9.0 and 26.11.1), which marks descriptors 0–15 close-on-exec
|
|
1094
|
+
and continues past 15 until the first descriptor that is not open. The subpath still sets and
|
|
1095
|
+
verifies the flag on every passed descriptor, as `sd_listen_fds(3)` does.
|
|
1096
|
+
|
|
1097
|
+
`test/test.systemdruntimeunit.node.ts` proves the subpath under a real `Type=notify` unit of the
|
|
1098
|
+
caller's user manager (`systemd-run --user`), and skips with the reason without Node.js 26 or a
|
|
1099
|
+
user manager.
|
|
1100
|
+
|
|
922
1101
|
### Starting a program in its own transient scope
|
|
923
1102
|
|
|
924
1103
|
A service that launches a long-lived program keeps that program in its own
|
|
@@ -1172,7 +1351,11 @@ console.log(template); // View generated systemd unit file
|
|
|
1172
1351
|
- **systemd 254 or later** for `SystemdUnit.inspectServiceSettings()`, `SystemdUnit.cleanFileDescriptorStore()`,
|
|
1173
1352
|
`runInTransientScope()` and for installing a definition with `fileDescriptorStorePreserve` (Debian 13,
|
|
1174
1353
|
Ubuntu 24.04, Fedora 39 and later ship it); an older manager is refused by name with `manager_too_old`
|
|
1354
|
+
- **systemd 240 or later** for installing a definition with an `append` output (`standardOutput` or
|
|
1355
|
+
`standardError`), refused the same way
|
|
1175
1356
|
- **Node.js**: Version 14.x or higher
|
|
1357
|
+
- **Node.js 26 or later** for the `@push.rocks/smartdaemon/systemd-runtime` subpath (`node:ffi`), on Linux
|
|
1358
|
+
x64 or arm64 with glibc
|
|
1176
1359
|
- **Permissions**: Either root access or properly configured sudo
|
|
1177
1360
|
|
|
1178
1361
|
## ⚙️ Generated Systemd Service Files
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
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
|
-
type ISystemdServiceCredential, type TSystemdCapability, type TSystemdAddressFamily
|
|
5
|
-
|
|
4
|
+
type ISystemdServiceCredential, type TSystemdCapability, type TSystemdAddressFamily,
|
|
5
|
+
type TSystemdServiceOutput } from './systemd.definition.js';
|
|
6
|
+
import { fileDescriptorStorePreserveMajor, outputAppendMajor } from './systemd.managerversion.js';
|
|
6
7
|
export type { ISystemdServiceStartup, ISystemdServiceNotifyStartup, TSystemdServiceStartup,
|
|
7
8
|
ISystemdServiceDependencies, ISystemdServiceInstallTargets, ISystemdServiceCredential, TSystemdCapability,
|
|
8
|
-
TSystemdAddressFamily } from './systemd.definition.js';
|
|
9
|
+
TSystemdAddressFamily, TSystemdServiceOutput } from './systemd.definition.js';
|
|
9
10
|
export { systemdCapabilities, systemdAddressFamilies } from './systemd.definition.js';
|
|
10
11
|
|
|
11
12
|
interface ISystemdServiceDefinitionBaseOptions {
|
|
@@ -78,6 +79,17 @@ interface ISystemdServiceDefinitionBaseOptions {
|
|
|
78
79
|
* 64 variables and 8192 UTF-8 bytes of `NAME=value` in total. They are taken literally: `%` is escaped, and `Environment=` never expands `$`.
|
|
79
80
|
*/
|
|
80
81
|
environment?: Readonly<Record<string, string>>;
|
|
82
|
+
/**
|
|
83
|
+
* Where the service's standard output goes (`StandardOutput=`): `'journal'` (the default),
|
|
84
|
+
* `'null'`, or `{ append: path }`, an absolute normalized path without a backslash, surrounding
|
|
85
|
+
* whitespace or trailing slash, whose file systemd appends to (`append:`, systemd 240 and later).
|
|
86
|
+
* An `append` output sets `requiredManagerMajor` to at least 240, so `SystemdUnitFile.install()`
|
|
87
|
+
* refuses an older manager, which would ignore the line. Omitted, the unit renders
|
|
88
|
+
* `StandardOutput=journal` as before.
|
|
89
|
+
*/
|
|
90
|
+
standardOutput?: TSystemdServiceOutput;
|
|
91
|
+
/** Where the service's standard error goes (`StandardError=`), as `standardOutput`; omitted, `journal`. */
|
|
92
|
+
standardError?: TSystemdServiceOutput;
|
|
81
93
|
/**
|
|
82
94
|
* System scope only. Runs the service as a transient user and group allocated at start
|
|
83
95
|
* (`DynamicUser=`), named by `user` and `group`, which then must not be `root` and have at most
|
|
@@ -177,8 +189,9 @@ export class SystemdServiceDefinition {
|
|
|
177
189
|
public readonly sha256: string;
|
|
178
190
|
/**
|
|
179
191
|
* 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
|
|
181
|
-
* null. `SystemdUnitFile.install()` refuses a
|
|
192
|
+
* renders one an older manager would ignore: the highest of 254 with `fileDescriptorStorePreserve`
|
|
193
|
+
* and 240 with an `append` output; otherwise null. `SystemdUnitFile.install()` refuses a
|
|
194
|
+
* definition on an older manager.
|
|
182
195
|
*/
|
|
183
196
|
public readonly requiredManagerMajor: number | null;
|
|
184
197
|
|
|
@@ -217,7 +230,8 @@ export class SystemdServiceDefinition {
|
|
|
217
230
|
const optionalKeys: TSystemdServiceDefinitionOption[] = ['scope', 'startup', 'dependencies', 'install',
|
|
218
231
|
'restartSeconds', 'restartPreventExitStatus', 'successExitStatus', 'startLimitIntervalSeconds',
|
|
219
232
|
'startLimitBurst', 'delegate', 'limitNoFile', 'tasksMax', 'oomScoreAdjust', 'notifyAccess',
|
|
220
|
-
'fileDescriptorStoreMax', 'fileDescriptorStorePreserve', 'environment',
|
|
233
|
+
'fileDescriptorStoreMax', 'fileDescriptorStorePreserve', 'environment', 'standardOutput', 'standardError',
|
|
234
|
+
...systemOnlyOptions];
|
|
221
235
|
// Every own key is a known option held by an enumerable data property, so no accessor or trap runs.
|
|
222
236
|
for (const key of Reflect.ownKeys(options)) {
|
|
223
237
|
requireValue(typeof key === 'string' && [...keys, ...optionalKeys, 'user', 'group'].includes(key as never), 'options');
|
|
@@ -334,6 +348,25 @@ export class SystemdServiceDefinition {
|
|
|
334
348
|
service.push(...[...assignments.keys()].sort()
|
|
335
349
|
.map((name) => `Environment=${quote(assignments.get(name)!)}`));
|
|
336
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* An output stream's directive value, read once through the object's own data descriptor. An
|
|
353
|
+
* `append:` path is taken raw after specifier expansion, so only `%` is escaped.
|
|
354
|
+
*/
|
|
355
|
+
const output = (value: unknown, option: 'standardOutput' | 'standardError'): string => {
|
|
356
|
+
if (value === undefined || value === 'journal') return 'journal';
|
|
357
|
+
if (value === 'null') return 'null';
|
|
358
|
+
plainObject(value, option);
|
|
359
|
+
const entryKeys = Reflect.ownKeys(value as object);
|
|
360
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, 'append');
|
|
361
|
+
requireValue(entryKeys.length === 1 && entryKeys[0] === 'append' && descriptor?.enumerable &&
|
|
362
|
+
'value' in descriptor, option);
|
|
363
|
+
const file: unknown = descriptor!.value;
|
|
364
|
+
requireValue(absolute(file) && !file.endsWith('/') && file.trim() === file &&
|
|
365
|
+
!file.includes('\\'), option);
|
|
366
|
+
return `append:${(file as string).replaceAll('%', '%%')}`;
|
|
367
|
+
};
|
|
368
|
+
const standardOutput = output(options.standardOutput, 'standardOutput');
|
|
369
|
+
const standardError = output(options.standardError, 'standardError');
|
|
337
370
|
// Every system-only option is refused by name in a user manager's service.
|
|
338
371
|
for (const option of systemOnlyOptions) {
|
|
339
372
|
requireValue(scope === 'system' || options[option] === undefined, option);
|
|
@@ -508,8 +541,8 @@ export class SystemdServiceDefinition {
|
|
|
508
541
|
`TimeoutStopSec=${options.timeoutStopSeconds}s`,
|
|
509
542
|
...service,
|
|
510
543
|
'StandardInput=null',
|
|
511
|
-
|
|
512
|
-
|
|
544
|
+
`StandardOutput=${standardOutput}`,
|
|
545
|
+
`StandardError=${standardError}`,
|
|
513
546
|
'',
|
|
514
547
|
'[Install]',
|
|
515
548
|
...relations.install,
|
|
@@ -517,8 +550,12 @@ export class SystemdServiceDefinition {
|
|
|
517
550
|
].join('\n');
|
|
518
551
|
requireValue(Buffer.byteLength(this.content) <= 16384, 'options');
|
|
519
552
|
this.sha256 = plugins.crypto.createHash('sha256').update(this.content).digest('hex');
|
|
520
|
-
|
|
521
|
-
|
|
553
|
+
// An older manager ignores a directive it does not read, so the newest one any line needs counts.
|
|
554
|
+
const requiredMajors = [
|
|
555
|
+
...(options.fileDescriptorStorePreserve !== undefined ? [fileDescriptorStorePreserveMajor] : []),
|
|
556
|
+
...([standardOutput, standardError].some((value) => value.startsWith('append:')) ? [outputAppendMajor] : []),
|
|
557
|
+
];
|
|
558
|
+
this.requiredManagerMajor = requiredMajors.length > 0 ? Math.max(...requiredMajors) : null;
|
|
522
559
|
Object.freeze(this);
|
|
523
560
|
}
|
|
524
561
|
}
|
|
@@ -539,6 +539,17 @@ export class SystemdUnit {
|
|
|
539
539
|
return this.inspect();
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Persistently disables the unit: removes the enablement links its `[Install]` section names
|
|
544
|
+
* (`systemctl disable`, which reloads the manager), then inspects. Never stops, masks, unmasks or
|
|
545
|
+
* removes the definition; a running unit keeps running. A failed or timed out command can leave
|
|
546
|
+
* partial enablement: inspect and retry the exact operation.
|
|
547
|
+
*/
|
|
548
|
+
public async disable(): Promise<ISystemdUnitState> {
|
|
549
|
+
await this.command(['disable', this.unitName]);
|
|
550
|
+
return this.inspect();
|
|
551
|
+
}
|
|
552
|
+
|
|
542
553
|
/** Loaded manager graph, including reverse edges. Enablement alone does not prove every required link. */
|
|
543
554
|
public async inspectRelationships(): Promise<ISystemdUnitRelationships> {
|
|
544
555
|
const keys = ['Id', 'Before', 'After', 'Requires', 'Wants', 'Conflicts', 'RequiredBy', 'WantedBy'];
|