@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,53 @@
1
+ /**
2
+ * Why a systemd runtime operation refused or failed:
3
+ *
4
+ * - `runtime_unsupported`: the runtime cannot make the call: Node.js older than 26, a platform
5
+ * other than Linux on x64 or arm64, a C library other than glibc (`libc.so.6`), `node:ffi`
6
+ * disabled (`--no-experimental-ffi`) or denied by the permission model (no `--allow-ffi`).
7
+ * - `notify_socket_invalid`: `NOTIFY_SOCKET` is not an `AF_UNIX` path or abstract name this
8
+ * module can address (`vsock:` addresses included).
9
+ * - `notify_field_invalid`: a notification field name or value systemd would not parse, or a
10
+ * message larger than systemd reads (4096 bytes).
11
+ * - `notify_failed`: the message could not be sent (`errno` names why).
12
+ * - `descriptor_name_invalid`: an `FDNAME=` systemd would refuse.
13
+ * - `descriptor_invalid`: a descriptor number that cannot exist, or a count outside 1–253.
14
+ * - `listen_environment_invalid`: `LISTEN_PID`, `LISTEN_FDS` or `LISTEN_FDNAMES` is malformed,
15
+ * `LISTEN_FDS` names a descriptor at or above the process's hard `RLIMIT_NOFILE`, or
16
+ * `getrlimit(RLIMIT_NOFILE)` failed, so the count cannot be bounded (`errno` names why).
17
+ * - `watchdog_environment_invalid`: `WATCHDOG_USEC` or `WATCHDOG_PID` is malformed.
18
+ * - `close_on_exec_failed`: a descriptor could not be marked close-on-exec (`errno` names why).
19
+ */
20
+ export type TSystemdRuntimeErrorCode =
21
+ | 'runtime_unsupported'
22
+ | 'notify_socket_invalid'
23
+ | 'notify_field_invalid'
24
+ | 'notify_failed'
25
+ | 'descriptor_name_invalid'
26
+ | 'descriptor_invalid'
27
+ | 'listen_environment_invalid'
28
+ | 'watchdog_environment_invalid'
29
+ | 'close_on_exec_failed';
30
+
31
+ export interface ISystemdRuntimeErrorOptions {
32
+ /** The C library's `errno` after the failed call, when a call failed. */
33
+ readonly errno?: number;
34
+ readonly cause?: unknown;
35
+ }
36
+
37
+ export class SystemdRuntimeError extends Error {
38
+ /** The C library's `errno` after the failed call, or null when no call failed. */
39
+ public readonly errno: number | null;
40
+
41
+ constructor(
42
+ public readonly code: TSystemdRuntimeErrorCode,
43
+ messageArg: string,
44
+ optionsArg: ISystemdRuntimeErrorOptions = {},
45
+ ) {
46
+ super(
47
+ `${messageArg}${optionsArg.errno === undefined ? '' : ` (errno ${optionsArg.errno})`}`,
48
+ optionsArg.cause === undefined ? undefined : { cause: optionsArg.cause },
49
+ );
50
+ this.name = 'SystemdRuntimeError';
51
+ this.errno = optionsArg.errno ?? null;
52
+ }
53
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@push.rocks/smartdaemon/systemd-runtime`: the service side of systemd for a Node.js main
3
+ * process — `sd_notify(3)` messages with descriptors, `LISTEN_FDS` adoption with close-on-exec, and
4
+ * the watchdog interval — through glibc calls made with `node:ffi`. Requires Node.js 26 or later:
5
+ * importing it on an older Node.js throws `SystemdRuntimeError` `runtime_unsupported`.
6
+ */
7
+ import { assertSystemdRuntimeNode } from './systemdruntime.libc.js';
8
+
9
+ export * from './classes.systemdruntimeerror.js';
10
+ export * from './classes.systemdnotifier.js';
11
+ export {
12
+ type ISystemdListenDescriptor,
13
+ type ISystemdProcessEnvironmentOptions,
14
+ setCloseOnExec,
15
+ systemdListenDescriptorsStart,
16
+ takeSystemdListenDescriptors,
17
+ watchdogIntervalUsec,
18
+ } from './systemdruntime.listen.js';
19
+ export { systemdRuntimeNodeMajor } from './systemdruntime.libc.js';
20
+ export { isSystemdDescriptorName, isSystemdNotifySocketAddress } from './systemdruntime.validation.js';
21
+
22
+ assertSystemdRuntimeNode();
@@ -0,0 +1,121 @@
1
+ import type * as ffi from 'node:ffi';
2
+ import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
3
+
4
+ /** The first Node.js major with `node:ffi`, which this module calls libc through. */
5
+ export const systemdRuntimeNodeMajor = 26;
6
+
7
+ /** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; the subpath runs it on import. */
8
+ export const assertSystemdRuntimeNode = (versionArg: string = process.versions.node): void => {
9
+ const major = Number(/^(\d+)\./.exec(versionArg)?.[1]);
10
+ if (!(major >= systemdRuntimeNodeMajor)) {
11
+ throw new SystemdRuntimeError(
12
+ 'runtime_unsupported',
13
+ `@push.rocks/smartdaemon/systemd-runtime requires Node.js ${systemdRuntimeNodeMajor} or later; this is Node.js ${versionArg}.`,
14
+ );
15
+ }
16
+ };
17
+
18
+ /**
19
+ * The generic Linux values both qualified targets (x86_64 and aarch64, glibc) use. The structure
20
+ * layouts below are the LP64 glibc ones, identical on both:
21
+ *
22
+ * - `sockaddr_un`: `sa_family_t` (u16) at 0, `sun_path[108]` at 2.
23
+ * - `iovec` (16 bytes): `iov_base` at 0, `iov_len` at 8.
24
+ * - `msghdr` (56 bytes): `msg_name` at 0, `msg_namelen` (u32) at 8, `msg_iov` at 16, `msg_iovlen`
25
+ * at 24, `msg_control` at 32, `msg_controllen` at 40, `msg_flags` at 48.
26
+ * - `cmsghdr`: `cmsg_len` (size_t) at 0, `cmsg_level` at 8, `cmsg_type` at 12, data at 16;
27
+ * `CMSG_LEN(n)` is 16 + n and `CMSG_SPACE(n)` 16 + n rounded up to 8.
28
+ * - `timespec` (16 bytes): `tv_sec` at 0, `tv_nsec` at 8.
29
+ * - `rlimit` (16 bytes): `rlim_cur` (u64) at 0, `rlim_max` (u64) at 8; `RLIM_INFINITY` is all ones.
30
+ */
31
+ export const libcConstants = {
32
+ AF_UNIX: 1,
33
+ SOCK_DGRAM: 2,
34
+ SOCK_CLOEXEC: 0o2000000,
35
+ SOL_SOCKET: 1,
36
+ SCM_RIGHTS: 1,
37
+ MSG_NOSIGNAL: 0x4000,
38
+ F_GETFD: 1,
39
+ F_SETFD: 2,
40
+ FD_CLOEXEC: 1,
41
+ CLOCK_MONOTONIC: 1,
42
+ EINTR: 4,
43
+ RLIMIT_NOFILE: 7,
44
+ } as const;
45
+
46
+ /** `sun_path` is 108 bytes; an address of up to 107 bytes keeps a path's terminating NUL. */
47
+ export const socketAddressMaximumBytes = 107;
48
+
49
+ /** The kernel's `SCM_MAX_FD`: the most descriptors one `SCM_RIGHTS` message carries. */
50
+ export const descriptorsPerMessageMaximum = 253;
51
+
52
+ const libcFunctions = {
53
+ socket: { arguments: ['int32', 'int32', 'int32'], return: 'int32' },
54
+ sendmsg: { arguments: ['int32', 'pointer', 'int32'], return: 'int64' },
55
+ close: { arguments: ['int32'], return: 'int32' },
56
+ // fcntl(2) is variadic; its third argument is an int for F_GETFD and F_SETFD.
57
+ fcntl: { arguments: ['int32', 'int32', 'int32'], return: 'int32' },
58
+ clock_gettime: { arguments: ['int32', 'pointer'], return: 'int32' },
59
+ getrlimit: { arguments: ['int32', 'pointer'], return: 'int32' },
60
+ __errno_location: { arguments: [], return: 'pointer' },
61
+ } as const satisfies ffi.FunctionDefinitions;
62
+
63
+ export interface ISystemdLibc {
64
+ readonly functions: ffi.WrappedFunctionsFromDefinitions<typeof libcFunctions>;
65
+ readonly ffi: typeof ffi;
66
+ /**
67
+ * The calling thread's `errno`, read through glibc's `__errno_location()`. `node:ffi` has no errno
68
+ * API; read it immediately after the failed call, before anything else runs on the thread.
69
+ */
70
+ errno(): number;
71
+ }
72
+
73
+ let loaded: ISystemdLibc | null = null;
74
+
75
+ /**
76
+ * glibc, opened through `node:ffi` on first use and kept for the process lifetime. Loading
77
+ * `node:ffi` emits Node's ExperimentalWarning once.
78
+ */
79
+ export const systemdLibc = (): ISystemdLibc => {
80
+ if (loaded !== null) return loaded;
81
+ if (process.platform !== 'linux' || (process.arch !== 'x64' && process.arch !== 'arm64')) {
82
+ throw new SystemdRuntimeError(
83
+ 'runtime_unsupported',
84
+ `The systemd runtime supports Linux on x64 and arm64; this is ${process.platform} on ${process.arch}.`,
85
+ );
86
+ }
87
+ const ffiModule: typeof ffi | undefined = process.getBuiltinModule('node:ffi');
88
+ if (ffiModule === undefined) {
89
+ throw new SystemdRuntimeError('runtime_unsupported', 'node:ffi is not available (disabled by --no-experimental-ffi?).');
90
+ }
91
+ let functions: ffi.WrappedFunctionsFromDefinitions<typeof libcFunctions>;
92
+ try {
93
+ functions = ffiModule.dlopen('libc.so.6', libcFunctions).functions;
94
+ } catch (error) {
95
+ throw new SystemdRuntimeError(
96
+ 'runtime_unsupported',
97
+ 'glibc (libc.so.6) could not be opened through node:ffi; under the permission model pass --allow-ffi.',
98
+ { cause: error },
99
+ );
100
+ }
101
+ loaded = Object.freeze({
102
+ functions,
103
+ ffi: ffiModule,
104
+ errno: () => ffiModule.getInt32(functions.__errno_location(), 0),
105
+ });
106
+ return loaded;
107
+ };
108
+
109
+ /** The `msg_namelen` of an `AF_UNIX` address: a path keeps its NUL; an abstract name has none. */
110
+ export const socketAddressLength = (addressArg: string): number =>
111
+ 2 + Buffer.byteLength(addressArg, 'utf8') + (addressArg.startsWith('/') ? 1 : 0);
112
+
113
+ /** Writes a `sockaddr_un` at `offsetArg`; an abstract name's leading `@` becomes its NUL. */
114
+ export const writeSocketAddress = (bufferArg: Buffer, offsetArg: number, addressArg: string): void => {
115
+ bufferArg.writeUInt16LE(libcConstants.AF_UNIX, offsetArg);
116
+ bufferArg.write(addressArg, offsetArg + 2, 'utf8');
117
+ if (addressArg.startsWith('@')) bufferArg[offsetArg + 2] = 0;
118
+ };
119
+
120
+ /** `CMSG_SPACE` for `countArg` descriptors. */
121
+ export const controlSpace = (countArg: number): number => 16 + ((4 * countArg + 7) & ~7);
@@ -0,0 +1,153 @@
1
+ import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
2
+ import { libcConstants, systemdLibc } from './systemdruntime.libc.js';
3
+ import { assertSystemdDescriptor, isSystemdDescriptorName } from './systemdruntime.validation.js';
4
+
5
+ /** sd_listen_fds(3): `SD_LISTEN_FDS_START`, the first passed descriptor. */
6
+ export const systemdListenDescriptorsStart = 3;
7
+
8
+ export interface ISystemdProcessEnvironmentOptions {
9
+ /** The environment to read; `process.env` by default. */
10
+ readonly env?: NodeJS.ProcessEnv;
11
+ /** The process the variables must name; `process.pid` by default. */
12
+ readonly pid?: number;
13
+ }
14
+
15
+ /** One descriptor the service manager passed: its number and its `LISTEN_FDNAMES` entry. */
16
+ export interface ISystemdListenDescriptor {
17
+ readonly descriptor: number;
18
+ /** The `FDNAME=` it was stored under, the socket unit's `FileDescriptorName=`, or `unknown`. */
19
+ readonly name: string;
20
+ }
21
+
22
+ /**
23
+ * Marks one open descriptor close-on-exec (`fcntl(F_SETFD, FD_CLOEXEC)`) and verifies the flag,
24
+ * so no child process inherits it. Throws `close_on_exec_failed` with `errno` when the descriptor
25
+ * is not open or the flag does not hold.
26
+ */
27
+ export const setCloseOnExec = (descriptorArg: number): void => {
28
+ assertSystemdDescriptor(descriptorArg);
29
+ const libc = systemdLibc();
30
+ const { fcntl } = libc.functions;
31
+ const flags = fcntl(descriptorArg, libcConstants.F_GETFD, 0);
32
+ if (flags < 0) {
33
+ throw new SystemdRuntimeError('close_on_exec_failed', `Descriptor ${descriptorArg} cannot be read.`, {
34
+ errno: libc.errno(),
35
+ });
36
+ }
37
+ if (fcntl(descriptorArg, libcConstants.F_SETFD, flags | libcConstants.FD_CLOEXEC) < 0) {
38
+ throw new SystemdRuntimeError('close_on_exec_failed', `Descriptor ${descriptorArg} cannot be marked close-on-exec.`, {
39
+ errno: libc.errno(),
40
+ });
41
+ }
42
+ if ((fcntl(descriptorArg, libcConstants.F_GETFD, 0) & libcConstants.FD_CLOEXEC) === 0) {
43
+ throw new SystemdRuntimeError('close_on_exec_failed', `Descriptor ${descriptorArg} did not keep close-on-exec.`);
44
+ }
45
+ };
46
+
47
+ const positiveIntegerPattern = /^[1-9][0-9]*$/;
48
+ /** sd_listen_fds(3) refuses a count above `INT_MAX - SD_LISTEN_FDS_START`. */
49
+ const listenDescriptorsMaximum = 2_147_483_647 - systemdListenDescriptorsStart;
50
+
51
+ const invalidListen = (messageArg: string, errnoArg?: number) =>
52
+ new SystemdRuntimeError('listen_environment_invalid', messageArg, errnoArg === undefined ? {} : { errno: errnoArg });
53
+
54
+ /**
55
+ * The process's hard `RLIMIT_NOFILE` (`getrlimit(2)`), or null for `RLIM_INFINITY`. No descriptor
56
+ * the manager passed can lie at or above it: systemd moves passed descriptors into place with
57
+ * `fcntl(F_DUPFD)` after applying the unit's limits (`shift_fds()` after `setrlimit_closest_all()` in
58
+ * v255 `src/core/exec-invoke.c`), `F_DUPFD` refuses a number at or above the soft limit, the soft
59
+ * limit never exceeds the hard one, and an unprivileged process cannot raise the hard one.
60
+ */
61
+ const descriptorHardLimit = (): bigint | null => {
62
+ const libc = systemdLibc();
63
+ const rlimit = Buffer.alloc(16);
64
+ if (libc.functions.getrlimit(libcConstants.RLIMIT_NOFILE, rlimit) !== 0) {
65
+ throw invalidListen('getrlimit(RLIMIT_NOFILE) failed, so LISTEN_FDS cannot be bounded.', libc.errno());
66
+ }
67
+ const hard = rlimit.readBigUInt64LE(8);
68
+ return hard === 0xffff_ffff_ffff_ffffn ? null : hard;
69
+ };
70
+
71
+ /**
72
+ * Takes the descriptors the service manager passed this process (sd_listen_fds_with_names(3)):
73
+ * socket-activation sockets and descriptor-store entries, numbered from 3.
74
+ *
75
+ * `LISTEN_PID`, `LISTEN_FDS` and `LISTEN_FDNAMES` always leave the environment, so no child sees
76
+ * them. Without them, or with `LISTEN_PID` naming another process (variables inherited from a
77
+ * parent), it returns an empty list and touches no descriptor. For this process every passed
78
+ * descriptor is marked close-on-exec before the list returns. Without `LISTEN_FDNAMES` each name is
79
+ * `unknown`, as in systemd.
80
+ *
81
+ * Throws `listen_environment_invalid` before touching any descriptor for a partial or malformed set
82
+ * (a count that is not a positive integer, a count whose last descriptor, `2 + count`, is at or
83
+ * above the process's hard `RLIMIT_NOFILE`, a names list whose length differs from the count, an
84
+ * invalid name), and with `errno` when `getrlimit(2)` fails. Descriptors are then marked one by one
85
+ * from 3, and memory grows only with descriptors proven open: the first passed descriptor that is
86
+ * not open throws `close_on_exec_failed`, as `sd_listen_fds(3)` fails on it; descriptors before it
87
+ * keep the flag.
88
+ */
89
+ export const takeSystemdListenDescriptors = (
90
+ optionsArg: ISystemdProcessEnvironmentOptions = {},
91
+ ): readonly ISystemdListenDescriptor[] => {
92
+ const env = optionsArg.env ?? process.env;
93
+ const pid = optionsArg.pid ?? process.pid;
94
+ const listenPid = env.LISTEN_PID;
95
+ const listenFds = env.LISTEN_FDS;
96
+ const listenFdNames = env.LISTEN_FDNAMES;
97
+ delete env.LISTEN_PID;
98
+ delete env.LISTEN_FDS;
99
+ delete env.LISTEN_FDNAMES;
100
+ if (listenPid === undefined && listenFds === undefined && listenFdNames === undefined) return Object.freeze([]);
101
+ if (listenPid === undefined || !positiveIntegerPattern.test(listenPid)) {
102
+ throw invalidListen('LISTEN_PID must name a process.');
103
+ }
104
+ if (listenPid !== String(pid)) return Object.freeze([]);
105
+ if (listenFds === undefined || !positiveIntegerPattern.test(listenFds) || Number(listenFds) > listenDescriptorsMaximum) {
106
+ throw invalidListen('LISTEN_FDS must be a positive descriptor count.');
107
+ }
108
+ const count = Number(listenFds);
109
+ // The cheap early bound: the last passed descriptor is 2 + count.
110
+ const hardLimit = descriptorHardLimit();
111
+ if (hardLimit !== null && BigInt(systemdListenDescriptorsStart + count) > hardLimit) {
112
+ throw invalidListen(`LISTEN_FDS passes ${count} descriptors; descriptor ${systemdListenDescriptorsStart + count - 1} is at or above this process's hard RLIMIT_NOFILE (${hardLimit}).`);
113
+ }
114
+ // Names, when given, are checked before any descriptor; the list is bounded by the variable's length.
115
+ const names = listenFdNames === undefined ? null : listenFdNames.split(':');
116
+ if (names !== null) {
117
+ if (names.length !== count) throw invalidListen(`LISTEN_FDNAMES names ${names.length} descriptors; LISTEN_FDS passes ${count}.`);
118
+ if (!names.every((name) => isSystemdDescriptorName(name, false))) throw invalidListen('LISTEN_FDNAMES holds an invalid name.');
119
+ }
120
+ // Nothing scales with the count before a descriptor is proven open: as in sd_listen_fds(3), the
121
+ // first descriptor that is not open ends the walk with close_on_exec_failed.
122
+ const descriptors: ISystemdListenDescriptor[] = [];
123
+ for (let index = 0; index < count; index++) {
124
+ const descriptor = systemdListenDescriptorsStart + index;
125
+ setCloseOnExec(descriptor);
126
+ descriptors.push(Object.freeze({ descriptor, name: names === null ? 'unknown' : names[index] }));
127
+ }
128
+ return Object.freeze(descriptors);
129
+ };
130
+
131
+ /**
132
+ * The watchdog interval the manager expects `WATCHDOG=1` within (`WATCHDOG_USEC`, sd_watchdog_enabled(3)),
133
+ * in microseconds; null when the unit has no watchdog or `WATCHDOG_PID` names another process.
134
+ * Reads the environment without changing it. Throws `watchdog_environment_invalid` for a malformed
135
+ * value.
136
+ */
137
+ export const watchdogIntervalUsec = (optionsArg: ISystemdProcessEnvironmentOptions = {}): number | null => {
138
+ const env = optionsArg.env ?? process.env;
139
+ const pid = optionsArg.pid ?? process.pid;
140
+ const usec = env.WATCHDOG_USEC;
141
+ const watchdogPid = env.WATCHDOG_PID;
142
+ if (usec === undefined) return null;
143
+ if (!positiveIntegerPattern.test(usec) || !Number.isSafeInteger(Number(usec))) {
144
+ throw new SystemdRuntimeError('watchdog_environment_invalid', 'WATCHDOG_USEC must be a positive integer.');
145
+ }
146
+ if (watchdogPid !== undefined) {
147
+ if (!positiveIntegerPattern.test(watchdogPid)) {
148
+ throw new SystemdRuntimeError('watchdog_environment_invalid', 'WATCHDOG_PID must name a process.');
149
+ }
150
+ if (watchdogPid !== String(pid)) return null;
151
+ }
152
+ return Number(usec);
153
+ };
@@ -0,0 +1,41 @@
1
+ import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
2
+ import { socketAddressMaximumBytes } from './systemdruntime.libc.js';
3
+
4
+ /** `FDNAME_MAX`: the longest descriptor name systemd accepts. */
5
+ export const descriptorNameMaximumLength = 255;
6
+
7
+ /**
8
+ * systemd's `fdname_is_valid()` (src/basic/fd-util.c, v255): printable ASCII except `:`, at most
9
+ * 255 characters. systemd also admits the empty name; `nonEmptyArg` refuses it where a name has to
10
+ * identify a descriptor.
11
+ */
12
+ export const isSystemdDescriptorName = (nameArg: unknown, nonEmptyArg: boolean): nameArg is string =>
13
+ typeof nameArg === 'string' && nameArg.length <= descriptorNameMaximumLength &&
14
+ (!nonEmptyArg || nameArg.length > 0) && /^[\x20-\x39\x3b-\x7e]*$/.test(nameArg);
15
+
16
+ /** An `AF_UNIX` notification address: an absolute path or an abstract name written `@name`. */
17
+ export const isSystemdNotifySocketAddress = (addressArg: unknown): addressArg is string =>
18
+ typeof addressArg === 'string' && /^[/@]./s.test(addressArg) && !addressArg.includes('\0') &&
19
+ Buffer.byteLength(addressArg, 'utf8') <= socketAddressMaximumBytes;
20
+
21
+ export const assertSystemdNotifySocketAddress = (addressArg: string): void => {
22
+ if (!isSystemdNotifySocketAddress(addressArg)) {
23
+ throw new SystemdRuntimeError(
24
+ 'notify_socket_invalid',
25
+ 'NOTIFY_SOCKET must be an absolute AF_UNIX path or an abstract name (@name) of at most 107 bytes without NUL.',
26
+ );
27
+ }
28
+ };
29
+
30
+ /** The largest descriptor number the kernel can hand out (`INT_MAX`). */
31
+ const descriptorMaximum = 2_147_483_647;
32
+
33
+ export const isSystemdDescriptor = (descriptorArg: unknown): descriptorArg is number =>
34
+ typeof descriptorArg === 'number' && Number.isSafeInteger(descriptorArg) && descriptorArg >= 0 &&
35
+ descriptorArg <= descriptorMaximum;
36
+
37
+ export const assertSystemdDescriptor = (descriptorArg: number): void => {
38
+ if (!isSystemdDescriptor(descriptorArg)) {
39
+ throw new SystemdRuntimeError('descriptor_invalid', `${String(descriptorArg)} is not a descriptor number.`);
40
+ }
41
+ };