@push.rocks/smartdaemon 3.0.0 → 3.2.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.
@@ -0,0 +1,48 @@
1
+ import {
2
+ assertLibcNode,
3
+ libcConstants,
4
+ systemdLibc,
5
+ type ISystemdLibc,
6
+ type TLibcUnsupportedError,
7
+ } from '../ts_systemdruntime/systemdruntime.libc.js';
8
+ import { isSystemdDescriptor } from '../ts_systemdruntime/systemdruntime.validation.js';
9
+ import { FileLockError } from './filelock.classes.filelockerror.js';
10
+
11
+ const fileLockUnsupported: TLibcUnsupportedError = (messageArg, causeArg) =>
12
+ new FileLockError('runtime_unsupported', messageArg, causeArg === undefined ? {} : { cause: causeArg });
13
+
14
+ /** Internal: refuses a Node.js older than 26; the subpath runs it on import. */
15
+ export const assertFileLockNode = (versionArg: string = process.versions.node): void =>
16
+ assertLibcNode('file-lock', fileLockUnsupported, versionArg);
17
+
18
+ const fileLockLibc = (descriptorArg: number): ISystemdLibc => {
19
+ if (!isSystemdDescriptor(descriptorArg)) {
20
+ throw new FileLockError('descriptor_invalid', `${String(descriptorArg)} is not a descriptor number.`);
21
+ }
22
+ return systemdLibc(fileLockUnsupported);
23
+ };
24
+
25
+ /**
26
+ * Takes the exclusive `flock(2)` lock on the open file description behind `descriptorArg`
27
+ * (`LOCK_EX | LOCK_NB`), without waiting. Returns true when the lock is granted, also when this
28
+ * open file description already holds it, and false only when another open file description holds
29
+ * a conflicting lock (`EWOULDBLOCK`). Throws `FileLockError` for every other failure.
30
+ */
31
+ export const tryLockFile = (descriptorArg: number): boolean => {
32
+ const libc = fileLockLibc(descriptorArg);
33
+ if (libc.functions.flock(descriptorArg, libcConstants.LOCK_EX | libcConstants.LOCK_NB) === 0) return true;
34
+ const errno = libc.errno();
35
+ if (errno === libcConstants.EWOULDBLOCK) return false;
36
+ throw new FileLockError('lock_failed', `Descriptor ${descriptorArg} cannot be locked.`, { errno });
37
+ };
38
+
39
+ /**
40
+ * Releases the `flock(2)` lock of the open file description behind `descriptorArg` (`LOCK_UN`).
41
+ * Releasing a description that holds no lock succeeds. Throws `FileLockError` on failure.
42
+ */
43
+ export const unlockFile = (descriptorArg: number): void => {
44
+ const libc = fileLockLibc(descriptorArg);
45
+ if (libc.functions.flock(descriptorArg, libcConstants.LOCK_UN) !== 0) {
46
+ throw new FileLockError('unlock_failed', `Descriptor ${descriptorArg} cannot be unlocked.`, { errno: libc.errno() });
47
+ }
48
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@push.rocks/smartdaemon/file-lock`: the exclusive, advisory `flock(2)` lock on a descriptor the
3
+ * caller opened, through glibc called with `node:ffi`. There is no blocking lock: a caller that must
4
+ * wait polls `tryLockFile` on a timer. Requires Node.js 26 or later: importing it on an older
5
+ * Node.js throws `FileLockError` `runtime_unsupported`.
6
+ */
7
+ import { assertFileLockNode } from './filelock.flock.js';
8
+
9
+ export * from './filelock.classes.filelockerror.js';
10
+ export { tryLockFile, unlockFile } from './filelock.flock.js';
11
+
12
+ assertFileLockNode();
@@ -4,17 +4,33 @@ import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
4
4
  /** The first Node.js major with `node:ffi`, which this module calls libc through. */
5
5
  export const systemdRuntimeNodeMajor = 26;
6
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 => {
7
+ /**
8
+ * Internal: builds the `runtime_unsupported` error of a subpath that calls libc through this
9
+ * loader (`systemd-runtime`, `file-lock`), so each subpath refuses with its own error class.
10
+ */
11
+ export type TLibcUnsupportedError = (messageArg: string, causeArg?: unknown) => Error;
12
+
13
+ const systemdRuntimeUnsupported: TLibcUnsupportedError = (messageArg, causeArg) =>
14
+ new SystemdRuntimeError('runtime_unsupported', messageArg, causeArg === undefined ? {} : { cause: causeArg });
15
+
16
+ /** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; each libc subpath runs it on import. */
17
+ export const assertLibcNode = (
18
+ subpathArg: string,
19
+ unsupportedArg: TLibcUnsupportedError,
20
+ versionArg: string = process.versions.node,
21
+ ): void => {
9
22
  const major = Number(/^(\d+)\./.exec(versionArg)?.[1]);
10
23
  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}.`,
24
+ throw unsupportedArg(
25
+ `@push.rocks/smartdaemon/${subpathArg} requires Node.js ${systemdRuntimeNodeMajor} or later; this is Node.js ${versionArg}.`,
14
26
  );
15
27
  }
16
28
  };
17
29
 
30
+ /** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; the subpath runs it on import. */
31
+ export const assertSystemdRuntimeNode = (versionArg: string = process.versions.node): void =>
32
+ assertLibcNode('systemd-runtime', systemdRuntimeUnsupported, versionArg);
33
+
18
34
  /**
19
35
  * The generic Linux values both qualified targets (x86_64 and aarch64, glibc) use. The structure
20
36
  * layouts below are the LP64 glibc ones, identical on both:
@@ -27,6 +43,8 @@ export const assertSystemdRuntimeNode = (versionArg: string = process.versions.n
27
43
  * `CMSG_LEN(n)` is 16 + n and `CMSG_SPACE(n)` 16 + n rounded up to 8.
28
44
  * - `timespec` (16 bytes): `tv_sec` at 0, `tv_nsec` at 8.
29
45
  * - `rlimit` (16 bytes): `rlim_cur` (u64) at 0, `rlim_max` (u64) at 8; `RLIM_INFINITY` is all ones.
46
+ *
47
+ * `EWOULDBLOCK` is `EAGAIN` (11) on both.
30
48
  */
31
49
  export const libcConstants = {
32
50
  AF_UNIX: 1,
@@ -40,7 +58,11 @@ export const libcConstants = {
40
58
  FD_CLOEXEC: 1,
41
59
  CLOCK_MONOTONIC: 1,
42
60
  EINTR: 4,
61
+ EWOULDBLOCK: 11,
43
62
  RLIMIT_NOFILE: 7,
63
+ LOCK_EX: 2,
64
+ LOCK_NB: 4,
65
+ LOCK_UN: 8,
44
66
  } as const;
45
67
 
46
68
  /** `sun_path` is 108 bytes; an address of up to 107 bytes keeps a path's terminating NUL. */
@@ -57,6 +79,7 @@ const libcFunctions = {
57
79
  fcntl: { arguments: ['int32', 'int32', 'int32'], return: 'int32' },
58
80
  clock_gettime: { arguments: ['int32', 'pointer'], return: 'int32' },
59
81
  getrlimit: { arguments: ['int32', 'pointer'], return: 'int32' },
82
+ flock: { arguments: ['int32', 'int32'], return: 'int32' },
60
83
  __errno_location: { arguments: [], return: 'pointer' },
61
84
  } as const satisfies ffi.FunctionDefinitions;
62
85
 
@@ -73,29 +96,27 @@ export interface ISystemdLibc {
73
96
  let loaded: ISystemdLibc | null = null;
74
97
 
75
98
  /**
76
- * glibc, opened through `node:ffi` on first use and kept for the process lifetime. Loading
77
- * `node:ffi` emits Node's ExperimentalWarning once.
99
+ * glibc, opened through `node:ffi` on first use and kept for the process lifetime: the one libc
100
+ * loader of the package, shared by the `systemd-runtime` and `file-lock` subpaths. A refusal is
101
+ * built by `unsupportedArg`, the calling subpath's `runtime_unsupported` error. Loading `node:ffi`
102
+ * emits Node's ExperimentalWarning once.
78
103
  */
79
- export const systemdLibc = (): ISystemdLibc => {
104
+ export const systemdLibc = (unsupportedArg: TLibcUnsupportedError = systemdRuntimeUnsupported): ISystemdLibc => {
80
105
  if (loaded !== null) return loaded;
81
106
  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
- );
107
+ throw unsupportedArg(`glibc calls through node:ffi support Linux on x64 and arm64; this is ${process.platform} on ${process.arch}.`);
86
108
  }
87
109
  const ffiModule: typeof ffi | undefined = process.getBuiltinModule('node:ffi');
88
110
  if (ffiModule === undefined) {
89
- throw new SystemdRuntimeError('runtime_unsupported', 'node:ffi is not available (disabled by --no-experimental-ffi?).');
111
+ throw unsupportedArg('node:ffi is not available (disabled by --no-experimental-ffi?).');
90
112
  }
91
113
  let functions: ffi.WrappedFunctionsFromDefinitions<typeof libcFunctions>;
92
114
  try {
93
115
  functions = ffiModule.dlopen('libc.so.6', libcFunctions).functions;
94
116
  } catch (error) {
95
- throw new SystemdRuntimeError(
96
- 'runtime_unsupported',
117
+ throw unsupportedArg(
97
118
  'glibc (libc.so.6) could not be opened through node:ffi; under the permission model pass --allow-ffi.',
98
- { cause: error },
119
+ error,
99
120
  );
100
121
  }
101
122
  loaded = Object.freeze({