@push.rocks/smartdaemon 2.21.0 → 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.systemdtransientscope.js +1 -1
- package/dist_ts/classes.systemdunit.js +1 -1
- package/dist_ts/classes.systemdunitfile.js +1 -1
- package/dist_ts/smartdaemon.classes.systemdmanager.js +1 -1
- package/dist_ts/smartdaemon.paths.js +1 -1
- package/dist_ts/systemd.definition.d.ts +3 -3
- 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 +102 -0
- package/ts/00_commitinfo_data.ts +1 -1
- 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
|
@@ -998,6 +998,106 @@ other process in the unit survives, so the main process must own their shutdown
|
|
|
998
998
|
deliberate retention (as with `Delegate=yes` workloads). A successful systemd start still requires application readiness
|
|
999
999
|
verification, and a completed stop alone does not prove database drain.
|
|
1000
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
|
+
|
|
1001
1101
|
### Starting a program in its own transient scope
|
|
1002
1102
|
|
|
1003
1103
|
A service that launches a long-lived program keeps that program in its own
|
|
@@ -1254,6 +1354,8 @@ console.log(template); // View generated systemd unit file
|
|
|
1254
1354
|
- **systemd 240 or later** for installing a definition with an `append` output (`standardOutput` or
|
|
1255
1355
|
`standardError`), refused the same way
|
|
1256
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
|
|
1257
1359
|
- **Permissions**: Either root access or properly configured sudo
|
|
1258
1360
|
|
|
1259
1361
|
## ⚙️ Generated Systemd Service Files
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
|
|
2
|
+
import {
|
|
3
|
+
controlSpace,
|
|
4
|
+
descriptorsPerMessageMaximum,
|
|
5
|
+
libcConstants,
|
|
6
|
+
socketAddressLength,
|
|
7
|
+
systemdLibc,
|
|
8
|
+
writeSocketAddress,
|
|
9
|
+
} from './systemdruntime.libc.js';
|
|
10
|
+
import {
|
|
11
|
+
assertSystemdDescriptor,
|
|
12
|
+
assertSystemdNotifySocketAddress,
|
|
13
|
+
isSystemdDescriptorName,
|
|
14
|
+
} from './systemdruntime.validation.js';
|
|
15
|
+
|
|
16
|
+
/** `NOTIFY_BUFFER_MAX` (`PIPE_BUF`): systemd ignores a longer notification message. */
|
|
17
|
+
export const notifyMessageMaximumBytes = 4096;
|
|
18
|
+
|
|
19
|
+
export interface ISystemdEnvironmentOptions {
|
|
20
|
+
/** The environment to read; `process.env` by default. */
|
|
21
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ISystemdNotifierEnvironmentOptions extends ISystemdEnvironmentOptions {
|
|
25
|
+
/** Remove `NOTIFY_SOCKET` from the environment, so no child inherits it. Default `false`. */
|
|
26
|
+
readonly take?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ISystemdStoreDescriptorsOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Whether the manager watches the stored descriptors and drops one that reports `POLLHUP` or
|
|
32
|
+
* `POLLERR` (`FDPOLL=`). Default `true`, as in systemd; `false` sends `FDPOLL=0`, for descriptors
|
|
33
|
+
* that are not pollable or always report hang-up, such as namespace descriptors.
|
|
34
|
+
*/
|
|
35
|
+
readonly poll?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Takes `NOTIFY_SOCKET` out of the environment: the validated address, or null when it is unset or
|
|
40
|
+
* empty. An address that is set but invalid is removed as well and then refused
|
|
41
|
+
* (`notify_socket_invalid`).
|
|
42
|
+
*/
|
|
43
|
+
export const takeSystemdNotifySocket = (optionsArg: ISystemdEnvironmentOptions = {}): string | null => {
|
|
44
|
+
const env = optionsArg.env ?? process.env;
|
|
45
|
+
const address = env.NOTIFY_SOCKET;
|
|
46
|
+
delete env.NOTIFY_SOCKET;
|
|
47
|
+
if (address === undefined || address === '') return null;
|
|
48
|
+
assertSystemdNotifySocketAddress(address);
|
|
49
|
+
return address;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const fieldNamePattern = /^[A-Z][A-Z0-9_]*$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sends `sd_notify(3)` messages to a service manager's notification socket: one `AF_UNIX`
|
|
56
|
+
* `SOCK_DGRAM` datagram per message, descriptors attached as `SCM_RIGHTS`. Every call is synchronous
|
|
57
|
+
* and either delivers the whole message to the socket or throws a `SystemdRuntimeError`; whether
|
|
58
|
+
* the manager acts on it depends on the unit's `NotifyAccess=`. Fields and descriptor names are
|
|
59
|
+
* validated before sending, because systemd ignores a message it cannot parse without telling the
|
|
60
|
+
* sender.
|
|
61
|
+
*/
|
|
62
|
+
export class SystemdNotifier {
|
|
63
|
+
/**
|
|
64
|
+
* The notifier for the unit that started this process, or null when `NOTIFY_SOCKET` is unset or
|
|
65
|
+
* empty — the process was not started by a service manager that listens. The caller decides what
|
|
66
|
+
* that means; nothing is sent silently nowhere. An invalid `NOTIFY_SOCKET` throws
|
|
67
|
+
* `notify_socket_invalid`.
|
|
68
|
+
*/
|
|
69
|
+
public static fromEnvironment(optionsArg: ISystemdNotifierEnvironmentOptions = {}): SystemdNotifier | null {
|
|
70
|
+
const env = optionsArg.env ?? process.env;
|
|
71
|
+
const address = optionsArg.take === true ? takeSystemdNotifySocket({ env }) : env.NOTIFY_SOCKET;
|
|
72
|
+
if (address === undefined || address === null || address === '') return null;
|
|
73
|
+
return new SystemdNotifier(address);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The notification socket: an absolute path, or an abstract name written `@name`. */
|
|
77
|
+
public readonly socketAddress: string;
|
|
78
|
+
|
|
79
|
+
constructor(socketAddressArg: string) {
|
|
80
|
+
assertSystemdNotifySocketAddress(socketAddressArg);
|
|
81
|
+
this.socketAddress = socketAddressArg;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `READY=1`, with an optional `STATUS=`. */
|
|
85
|
+
public ready(statusArg?: string): void {
|
|
86
|
+
this.send({ READY: '1', ...(statusArg === undefined ? {} : { STATUS: statusArg }) });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `STATUS=`: free-form text the manager shows as the unit's status. */
|
|
90
|
+
public status(textArg: string): void {
|
|
91
|
+
this.send({ STATUS: textArg });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `STOPPING=1`, with an optional `STATUS=`. */
|
|
95
|
+
public stopping(statusArg?: string): void {
|
|
96
|
+
this.send({ STOPPING: '1', ...(statusArg === undefined ? {} : { STATUS: statusArg }) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `RELOADING=1` with `MONOTONIC_USEC=` (`CLOCK_MONOTONIC` now), with an optional `STATUS=`. The
|
|
101
|
+
* service sends `ready()` once the reload finished.
|
|
102
|
+
*/
|
|
103
|
+
public reloading(statusArg?: string): void {
|
|
104
|
+
this.send({
|
|
105
|
+
RELOADING: '1',
|
|
106
|
+
MONOTONIC_USEC: String(monotonicMicroseconds()),
|
|
107
|
+
...(statusArg === undefined ? {} : { STATUS: statusArg }),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `WATCHDOG=1`: keeps a `WatchdogSec=` unit alive. */
|
|
112
|
+
public watchdog(): void {
|
|
113
|
+
this.send({ WATCHDOG: '1' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* `EXTEND_TIMEOUT_USEC=`: asks the manager to wait `microsecondsArg` longer for the current start,
|
|
118
|
+
* runtime-start, stop or watchdog deadline, with an optional `STATUS=`.
|
|
119
|
+
*/
|
|
120
|
+
public extendTimeout(microsecondsArg: number, statusArg?: string): void {
|
|
121
|
+
if (!Number.isSafeInteger(microsecondsArg) || microsecondsArg <= 0) {
|
|
122
|
+
throw new SystemdRuntimeError('notify_field_invalid', 'EXTEND_TIMEOUT_USEC must be a positive integer.');
|
|
123
|
+
}
|
|
124
|
+
this.send({
|
|
125
|
+
EXTEND_TIMEOUT_USEC: String(microsecondsArg),
|
|
126
|
+
...(statusArg === undefined ? {} : { STATUS: statusArg }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `FDSTORE=1` with `FDNAME=`: the manager keeps duplicates of `descriptorsArg` (1–253) under
|
|
132
|
+
* `nameArg` in the unit's descriptor store (`FileDescriptorStoreMax=`) and passes them to the
|
|
133
|
+
* next main process through `LISTEN_FDS`. Storing does not replace descriptors already stored
|
|
134
|
+
* under the name; `replaceDescriptors()` does.
|
|
135
|
+
*/
|
|
136
|
+
public storeDescriptors(
|
|
137
|
+
nameArg: string,
|
|
138
|
+
descriptorsArg: readonly number[],
|
|
139
|
+
optionsArg: ISystemdStoreDescriptorsOptions = {},
|
|
140
|
+
): void {
|
|
141
|
+
assertDescriptorName(nameArg);
|
|
142
|
+
assertStoredDescriptors(descriptorsArg);
|
|
143
|
+
this.send(
|
|
144
|
+
{ FDSTORE: '1', FDNAME: nameArg, ...(optionsArg.poll === false ? { FDPOLL: '0' } : {}) },
|
|
145
|
+
descriptorsArg,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `FDSTOREREMOVE=1` with `FDNAME=`: the manager closes every descriptor stored under `nameArg`. */
|
|
150
|
+
public removeDescriptors(nameArg: string): void {
|
|
151
|
+
assertDescriptorName(nameArg);
|
|
152
|
+
this.send({ FDSTOREREMOVE: '1', FDNAME: nameArg });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Removes the descriptors stored under `nameArg`, then stores `descriptorsArg` under it: two
|
|
157
|
+
* messages, handled by the manager in order. Between them the store holds nothing under the name.
|
|
158
|
+
*/
|
|
159
|
+
public replaceDescriptors(
|
|
160
|
+
nameArg: string,
|
|
161
|
+
descriptorsArg: readonly number[],
|
|
162
|
+
optionsArg: ISystemdStoreDescriptorsOptions = {},
|
|
163
|
+
): void {
|
|
164
|
+
assertDescriptorName(nameArg);
|
|
165
|
+
assertStoredDescriptors(descriptorsArg);
|
|
166
|
+
this.removeDescriptors(nameArg);
|
|
167
|
+
this.storeDescriptors(nameArg, descriptorsArg, optionsArg);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sends one message: `NAME=value` fields in insertion order, joined by newlines, and up to 253
|
|
172
|
+
* descriptors. Names are upper-case (`A–Z`, digits, `_`); values carry no newline or NUL; the
|
|
173
|
+
* message is at most 4096 bytes.
|
|
174
|
+
*/
|
|
175
|
+
public send(fieldsArg: Readonly<Record<string, string>>, descriptorsArg: readonly number[] = []): void {
|
|
176
|
+
const entries = Object.entries(fieldsArg);
|
|
177
|
+
if (entries.length === 0) {
|
|
178
|
+
throw new SystemdRuntimeError('notify_field_invalid', 'A notification needs at least one field.');
|
|
179
|
+
}
|
|
180
|
+
for (const [name, value] of entries) {
|
|
181
|
+
if (!fieldNamePattern.test(name)) {
|
|
182
|
+
throw new SystemdRuntimeError('notify_field_invalid', `${JSON.stringify(name)} is not a notification field name.`);
|
|
183
|
+
}
|
|
184
|
+
if (typeof value !== 'string' || /[\n\0]/.test(value)) {
|
|
185
|
+
throw new SystemdRuntimeError('notify_field_invalid', `The ${name} value must be a string without newline or NUL.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
assertDescriptorList(descriptorsArg);
|
|
189
|
+
const payload = Buffer.from(entries.map(([name, value]) => `${name}=${value}`).join('\n'), 'utf8');
|
|
190
|
+
if (payload.length > notifyMessageMaximumBytes) {
|
|
191
|
+
throw new SystemdRuntimeError(
|
|
192
|
+
'notify_field_invalid',
|
|
193
|
+
`The message is ${payload.length} bytes; systemd reads at most ${notifyMessageMaximumBytes}.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
sendDatagram(this.socketAddress, payload, descriptorsArg);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const assertDescriptorName = (nameArg: string): void => {
|
|
201
|
+
if (!isSystemdDescriptorName(nameArg, true)) {
|
|
202
|
+
throw new SystemdRuntimeError(
|
|
203
|
+
'descriptor_name_invalid',
|
|
204
|
+
'A descriptor name is 1–255 printable ASCII characters without ":".',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** At most 253 valid descriptor numbers. */
|
|
210
|
+
const assertDescriptorList = (descriptorsArg: readonly number[]): void => {
|
|
211
|
+
if (descriptorsArg.length > descriptorsPerMessageMaximum) {
|
|
212
|
+
throw new SystemdRuntimeError(
|
|
213
|
+
'descriptor_invalid',
|
|
214
|
+
`One message carries at most ${descriptorsPerMessageMaximum} descriptors.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
descriptorsArg.forEach(assertSystemdDescriptor);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** 1–253 valid descriptor numbers, as `FDSTORE=1` needs. */
|
|
221
|
+
const assertStoredDescriptors = (descriptorsArg: readonly number[]): void => {
|
|
222
|
+
if (descriptorsArg.length === 0) {
|
|
223
|
+
throw new SystemdRuntimeError('descriptor_invalid', 'FDSTORE=1 needs at least one descriptor.');
|
|
224
|
+
}
|
|
225
|
+
assertDescriptorList(descriptorsArg);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** `CLOCK_MONOTONIC` in microseconds, as `MONOTONIC_USEC=` expects. */
|
|
229
|
+
const monotonicMicroseconds = (): bigint => {
|
|
230
|
+
const libc = systemdLibc();
|
|
231
|
+
const timespec = Buffer.alloc(16);
|
|
232
|
+
if (libc.functions.clock_gettime(libcConstants.CLOCK_MONOTONIC, timespec) !== 0) {
|
|
233
|
+
throw new SystemdRuntimeError('notify_failed', 'clock_gettime(CLOCK_MONOTONIC) failed.', { errno: libc.errno() });
|
|
234
|
+
}
|
|
235
|
+
return timespec.readBigInt64LE(0) * 1_000_000n + timespec.readBigInt64LE(8) / 1_000n;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// One block holds the whole message, so every pointer inside it stays valid while the block is
|
|
239
|
+
// borrowed by the call: msghdr, iovec, sockaddr_un, control data, payload.
|
|
240
|
+
const iovecOffset = 56;
|
|
241
|
+
const addressOffset = 72;
|
|
242
|
+
const controlOffset = 184;
|
|
243
|
+
|
|
244
|
+
const sendDatagram = (addressArg: string, payloadArg: Buffer, descriptorsArg: readonly number[]): void => {
|
|
245
|
+
const libc = systemdLibc();
|
|
246
|
+
const space = descriptorsArg.length === 0 ? 0 : controlSpace(descriptorsArg.length);
|
|
247
|
+
const payloadOffset = controlOffset + space;
|
|
248
|
+
const block = Buffer.alloc(payloadOffset + payloadArg.length);
|
|
249
|
+
const base = libc.ffi.getRawPointer(block);
|
|
250
|
+
writeSocketAddress(block, addressOffset, addressArg);
|
|
251
|
+
payloadArg.copy(block, payloadOffset);
|
|
252
|
+
block.writeBigUInt64LE(base + BigInt(addressOffset), 0);
|
|
253
|
+
block.writeUInt32LE(socketAddressLength(addressArg), 8);
|
|
254
|
+
block.writeBigUInt64LE(base + BigInt(iovecOffset), 16);
|
|
255
|
+
block.writeBigUInt64LE(1n, 24);
|
|
256
|
+
block.writeBigUInt64LE(base + BigInt(payloadOffset), iovecOffset);
|
|
257
|
+
block.writeBigUInt64LE(BigInt(payloadArg.length), iovecOffset + 8);
|
|
258
|
+
if (space > 0) {
|
|
259
|
+
block.writeBigUInt64LE(base + BigInt(controlOffset), 32);
|
|
260
|
+
block.writeBigUInt64LE(BigInt(space), 40);
|
|
261
|
+
block.writeBigUInt64LE(BigInt(16 + 4 * descriptorsArg.length), controlOffset);
|
|
262
|
+
block.writeInt32LE(libcConstants.SOL_SOCKET, controlOffset + 8);
|
|
263
|
+
block.writeInt32LE(libcConstants.SCM_RIGHTS, controlOffset + 12);
|
|
264
|
+
descriptorsArg.forEach((descriptor, index) => block.writeInt32LE(descriptor, controlOffset + 16 + 4 * index));
|
|
265
|
+
}
|
|
266
|
+
const socket = libc.functions.socket(libcConstants.AF_UNIX, libcConstants.SOCK_DGRAM | libcConstants.SOCK_CLOEXEC, 0);
|
|
267
|
+
if (socket < 0) {
|
|
268
|
+
throw new SystemdRuntimeError('notify_failed', 'socket(AF_UNIX, SOCK_DGRAM) failed.', { errno: libc.errno() });
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
let sent = libc.functions.sendmsg(socket, block, libcConstants.MSG_NOSIGNAL);
|
|
272
|
+
let errno = sent < 0n ? libc.errno() : 0;
|
|
273
|
+
while (sent < 0n && errno === libcConstants.EINTR) {
|
|
274
|
+
sent = libc.functions.sendmsg(socket, block, libcConstants.MSG_NOSIGNAL);
|
|
275
|
+
errno = sent < 0n ? libc.errno() : 0;
|
|
276
|
+
}
|
|
277
|
+
if (sent < 0n) {
|
|
278
|
+
throw new SystemdRuntimeError('notify_failed', `sendmsg to ${addressArg} failed.`, { errno });
|
|
279
|
+
}
|
|
280
|
+
if (sent !== BigInt(payloadArg.length)) {
|
|
281
|
+
throw new SystemdRuntimeError('notify_failed', `sendmsg to ${addressArg} sent ${sent} of ${payloadArg.length} bytes.`);
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
// Linux releases the descriptor whatever close(2) reports; the socket is this call's alone.
|
|
285
|
+
libc.functions.close(socket);
|
|
286
|
+
}
|
|
287
|
+
};
|