@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.
Files changed (29) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.systemdtransientscope.js +1 -1
  3. package/dist_ts/classes.systemdunit.js +1 -1
  4. package/dist_ts/classes.systemdunitfile.js +1 -1
  5. package/dist_ts/smartdaemon.classes.systemdmanager.js +1 -1
  6. package/dist_ts/smartdaemon.paths.js +1 -1
  7. package/dist_ts/systemd.definition.d.ts +3 -3
  8. package/dist_ts/systemd.userenvironment.d.ts +1 -1
  9. package/dist_ts_systemdruntime/classes.systemdnotifier.d.ts +82 -0
  10. package/dist_ts_systemdruntime/classes.systemdnotifier.js +217 -0
  11. package/dist_ts_systemdruntime/classes.systemdruntimeerror.d.ts +31 -0
  12. package/dist_ts_systemdruntime/classes.systemdruntimeerror.js +9 -0
  13. package/dist_ts_systemdruntime/index.d.ts +5 -0
  14. package/dist_ts_systemdruntime/index.js +14 -0
  15. package/dist_ts_systemdruntime/systemdruntime.libc.d.ts +87 -0
  16. package/dist_ts_systemdruntime/systemdruntime.libc.js +92 -0
  17. package/dist_ts_systemdruntime/systemdruntime.listen.d.ts +46 -0
  18. package/dist_ts_systemdruntime/systemdruntime.listen.js +136 -0
  19. package/dist_ts_systemdruntime/systemdruntime.validation.d.ts +13 -0
  20. package/dist_ts_systemdruntime/systemdruntime.validation.js +29 -0
  21. package/package.json +18 -6
  22. package/readme.md +102 -0
  23. package/ts/00_commitinfo_data.ts +1 -1
  24. package/ts_systemdruntime/classes.systemdnotifier.ts +287 -0
  25. package/ts_systemdruntime/classes.systemdruntimeerror.ts +53 -0
  26. package/ts_systemdruntime/index.ts +22 -0
  27. package/ts_systemdruntime/systemdruntime.libc.ts +121 -0
  28. package/ts_systemdruntime/systemdruntime.listen.ts +153 -0
  29. 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.21.0",
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": "^5.1.2",
30
- "@git.zone/tsrun": "^3.0.2",
31
- "@git.zone/tstest": "^7.1.0",
32
- "@types/node": "^26.6.3"
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
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdaemon',
6
- version: '2.21.0',
6
+ version: '2.22.0',
7
7
  description: 'Start scripts as long running daemons and manage them.'
8
8
  }
@@ -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
+ };