@push.rocks/smartdaemon 2.21.0 → 3.0.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 +2 -2
- 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/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/smartdaemon.classes.commanderror.d.ts +25 -0
- package/dist_ts/smartdaemon.classes.commanderror.js +27 -0
- package/dist_ts/smartdaemon.classes.systemdmanager.d.ts +12 -0
- package/dist_ts/smartdaemon.classes.systemdmanager.js +63 -30
- package/dist_ts/smartdaemon.paths.js +1 -1
- package/dist_ts/smartdaemon.plugins.d.ts +1 -3
- package/dist_ts/smartdaemon.plugins.js +2 -4
- 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 +19 -8
- package/readme.md +119 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +1 -0
- package/ts/smartdaemon.classes.commanderror.ts +23 -0
- package/ts/smartdaemon.classes.systemdmanager.ts +78 -34
- package/ts/smartdaemon.plugins.ts +0 -4
- 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
- package/dist_ts/smartdamon.logging.d.ts +0 -2
- package/dist_ts/smartdamon.logging.js +0 -14
- package/ts/smartdamon.logging.ts +0 -15
|
@@ -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
|
+
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import * as plugins from './smartdaemon.plugins.js';
|
|
2
|
-
export const logger = new plugins.smartlog.Smartlog({
|
|
3
|
-
logContext: {
|
|
4
|
-
company: 'Some Company',
|
|
5
|
-
companyunit: 'Some CompanyUnit',
|
|
6
|
-
containerName: 'Some Containername',
|
|
7
|
-
environment: 'local',
|
|
8
|
-
runtime: 'node',
|
|
9
|
-
zone: 'gitzone',
|
|
10
|
-
},
|
|
11
|
-
minimumLogLevel: 'silly',
|
|
12
|
-
});
|
|
13
|
-
logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal());
|
|
14
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRkYW1vbi5sb2dnaW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvc21hcnRkYW1vbi5sb2dnaW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sMEJBQTBCLENBQUM7QUFFcEQsTUFBTSxDQUFDLE1BQU0sTUFBTSxHQUFHLElBQUksT0FBTyxDQUFDLFFBQVEsQ0FBQyxRQUFRLENBQUM7SUFDbEQsVUFBVSxFQUFFO1FBQ1YsT0FBTyxFQUFFLGNBQWM7UUFDdkIsV0FBVyxFQUFFLGtCQUFrQjtRQUMvQixhQUFhLEVBQUUsb0JBQW9CO1FBQ25DLFdBQVcsRUFBRSxPQUFPO1FBQ3BCLE9BQU8sRUFBRSxNQUFNO1FBQ2YsSUFBSSxFQUFFLFNBQVM7S0FDaEI7SUFDRCxlQUFlLEVBQUUsT0FBTztDQUN6QixDQUFDLENBQUM7QUFFSCxNQUFNLENBQUMsaUJBQWlCLENBQUMsSUFBSSxPQUFPLENBQUMsd0JBQXdCLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDIn0=
|
package/ts/smartdamon.logging.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import * as plugins from './smartdaemon.plugins.js';
|
|
2
|
-
|
|
3
|
-
export const logger = new plugins.smartlog.Smartlog({
|
|
4
|
-
logContext: {
|
|
5
|
-
company: 'Some Company',
|
|
6
|
-
companyunit: 'Some CompanyUnit',
|
|
7
|
-
containerName: 'Some Containername',
|
|
8
|
-
environment: 'local',
|
|
9
|
-
runtime: 'node',
|
|
10
|
-
zone: 'gitzone',
|
|
11
|
-
},
|
|
12
|
-
minimumLogLevel: 'silly',
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
logger.addLogDestination(new plugins.smartlogDestinationLocal.DestinationLocal());
|