@push.rocks/smartdaemon 3.1.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts_filelock/filelock.classes.filelockerror.d.ts +24 -0
- package/dist_ts_filelock/filelock.classes.filelockerror.js +9 -0
- package/dist_ts_filelock/filelock.flock.d.ts +14 -0
- package/dist_ts_filelock/filelock.flock.js +38 -0
- package/dist_ts_filelock/index.d.ts +2 -0
- package/dist_ts_filelock/index.js +11 -0
- package/dist_ts_systemdruntime/systemdruntime.libc.d.ts +22 -3
- package/dist_ts_systemdruntime/systemdruntime.libc.js +22 -10
- package/package.json +8 -3
- package/readme.md +66 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_filelock/filelock.classes.filelockerror.ts +37 -0
- package/ts_filelock/filelock.flock.ts +48 -0
- package/ts_filelock/index.ts +12 -0
- package/ts_systemdruntime/systemdruntime.libc.ts +37 -16
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartdaemon',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.2.0',
|
|
7
7
|
description: 'Start scripts as long running daemons and manage them.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx5QkFBeUI7SUFDL0IsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLHdEQUF3RDtDQUN0RSxDQUFBIn0=
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a file lock operation refused or failed:
|
|
3
|
+
*
|
|
4
|
+
* - `runtime_unsupported`: the lock cannot be called: Node.js older than 26, a platform other than
|
|
5
|
+
* Linux on x64 or arm64, a C library other than glibc (`libc.so.6`), `node:ffi` disabled
|
|
6
|
+
* (`--no-experimental-ffi`) or denied by the permission model (no `--allow-ffi`).
|
|
7
|
+
* - `descriptor_invalid`: a descriptor number that cannot exist (not an integer from 0 to
|
|
8
|
+
* `INT_MAX`); refused before any call.
|
|
9
|
+
* - `lock_failed`: `flock(LOCK_EX | LOCK_NB)` failed for a reason other than a conflicting lock
|
|
10
|
+
* (`errno` names why: `EBADF` for a descriptor that is not open, for example).
|
|
11
|
+
* - `unlock_failed`: `flock(LOCK_UN)` failed (`errno` names why).
|
|
12
|
+
*/
|
|
13
|
+
export type TFileLockErrorCode = 'runtime_unsupported' | 'descriptor_invalid' | 'lock_failed' | 'unlock_failed';
|
|
14
|
+
export interface IFileLockErrorOptions {
|
|
15
|
+
/** The C library's `errno` after the failed call, when a call failed. */
|
|
16
|
+
readonly errno?: number;
|
|
17
|
+
readonly cause?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare class FileLockError extends Error {
|
|
20
|
+
readonly code: TFileLockErrorCode;
|
|
21
|
+
/** The C library's `errno` after the failed call, or null when no call failed. */
|
|
22
|
+
readonly errno: number | null;
|
|
23
|
+
constructor(code: TFileLockErrorCode, messageArg: string, optionsArg?: IFileLockErrorOptions);
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class FileLockError extends Error {
|
|
2
|
+
constructor(code, messageArg, optionsArg = {}) {
|
|
3
|
+
super(`${messageArg}${optionsArg.errno === undefined ? '' : ` (errno ${optionsArg.errno})`}`, optionsArg.cause === undefined ? undefined : { cause: optionsArg.cause });
|
|
4
|
+
this.code = code;
|
|
5
|
+
this.name = 'FileLockError';
|
|
6
|
+
this.errno = optionsArg.errno ?? null;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZWxvY2suY2xhc3Nlcy5maWxlbG9ja2Vycm9yLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfZmlsZWxvY2svZmlsZWxvY2suY2xhc3Nlcy5maWxlbG9ja2Vycm9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQW9CQSxNQUFNLE9BQU8sYUFBYyxTQUFRLEtBQUs7SUFJdEMsWUFDa0IsSUFBd0IsRUFDeEMsVUFBa0IsRUFDbEIsVUFBVSxHQUEwQixFQUFFO1FBRXRDLEtBQUssQ0FDSCxHQUFHLFVBQVUsR0FBRyxVQUFVLENBQUMsS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLFVBQVUsQ0FBQyxLQUFLLEdBQUcsRUFBRSxFQUN0RixVQUFVLENBQUMsS0FBSyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLEtBQUssRUFBRSxVQUFVLENBQUMsS0FBSyxFQUFFLENBQ3pFLENBQUM7b0JBUGMsSUFBSTtRQVFwQixJQUFJLENBQUMsSUFBSSxHQUFHLGVBQWUsQ0FBQztRQUM1QixJQUFJLENBQUMsS0FBSyxHQUFHLFVBQVUsQ0FBQyxLQUFLLElBQUksSUFBSSxDQUFDO0lBQ3hDLENBQUM7Q0FDRiJ9
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Internal: refuses a Node.js older than 26; the subpath runs it on import. */
|
|
2
|
+
export declare const assertFileLockNode: (versionArg?: string) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Takes the exclusive `flock(2)` lock on the open file description behind `descriptorArg`
|
|
5
|
+
* (`LOCK_EX | LOCK_NB`), without waiting. Returns true when the lock is granted, also when this
|
|
6
|
+
* open file description already holds it, and false only when another open file description holds
|
|
7
|
+
* a conflicting lock (`EWOULDBLOCK`). Throws `FileLockError` for every other failure.
|
|
8
|
+
*/
|
|
9
|
+
export declare const tryLockFile: (descriptorArg: number) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Releases the `flock(2)` lock of the open file description behind `descriptorArg` (`LOCK_UN`).
|
|
12
|
+
* Releasing a description that holds no lock succeeds. Throws `FileLockError` on failure.
|
|
13
|
+
*/
|
|
14
|
+
export declare const unlockFile: (descriptorArg: number) => void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { assertLibcNode, libcConstants, systemdLibc, } from '../dist_ts_systemdruntime/systemdruntime.libc.js';
|
|
2
|
+
import { isSystemdDescriptor } from '../dist_ts_systemdruntime/systemdruntime.validation.js';
|
|
3
|
+
import { FileLockError } from './filelock.classes.filelockerror.js';
|
|
4
|
+
const fileLockUnsupported = (messageArg, causeArg) => new FileLockError('runtime_unsupported', messageArg, causeArg === undefined ? {} : { cause: causeArg });
|
|
5
|
+
/** Internal: refuses a Node.js older than 26; the subpath runs it on import. */
|
|
6
|
+
export const assertFileLockNode = (versionArg = process.versions.node) => assertLibcNode('file-lock', fileLockUnsupported, versionArg);
|
|
7
|
+
const fileLockLibc = (descriptorArg) => {
|
|
8
|
+
if (!isSystemdDescriptor(descriptorArg)) {
|
|
9
|
+
throw new FileLockError('descriptor_invalid', `${String(descriptorArg)} is not a descriptor number.`);
|
|
10
|
+
}
|
|
11
|
+
return systemdLibc(fileLockUnsupported);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Takes the exclusive `flock(2)` lock on the open file description behind `descriptorArg`
|
|
15
|
+
* (`LOCK_EX | LOCK_NB`), without waiting. Returns true when the lock is granted, also when this
|
|
16
|
+
* open file description already holds it, and false only when another open file description holds
|
|
17
|
+
* a conflicting lock (`EWOULDBLOCK`). Throws `FileLockError` for every other failure.
|
|
18
|
+
*/
|
|
19
|
+
export const tryLockFile = (descriptorArg) => {
|
|
20
|
+
const libc = fileLockLibc(descriptorArg);
|
|
21
|
+
if (libc.functions.flock(descriptorArg, libcConstants.LOCK_EX | libcConstants.LOCK_NB) === 0)
|
|
22
|
+
return true;
|
|
23
|
+
const errno = libc.errno();
|
|
24
|
+
if (errno === libcConstants.EWOULDBLOCK)
|
|
25
|
+
return false;
|
|
26
|
+
throw new FileLockError('lock_failed', `Descriptor ${descriptorArg} cannot be locked.`, { errno });
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Releases the `flock(2)` lock of the open file description behind `descriptorArg` (`LOCK_UN`).
|
|
30
|
+
* Releasing a description that holds no lock succeeds. Throws `FileLockError` on failure.
|
|
31
|
+
*/
|
|
32
|
+
export const unlockFile = (descriptorArg) => {
|
|
33
|
+
const libc = fileLockLibc(descriptorArg);
|
|
34
|
+
if (libc.functions.flock(descriptorArg, libcConstants.LOCK_UN) !== 0) {
|
|
35
|
+
throw new FileLockError('unlock_failed', `Descriptor ${descriptorArg} cannot be unlocked.`, { errno: libc.errno() });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZWxvY2suZmxvY2suanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19maWxlbG9jay9maWxlbG9jay5mbG9jay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsY0FBYyxFQUNkLGFBQWEsRUFDYixXQUFXLEdBR1osTUFBTSw2Q0FBNkMsQ0FBQztBQUNyRCxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxtREFBbUQsQ0FBQztBQUN4RixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0scUNBQXFDLENBQUM7QUFFcEUsTUFBTSxtQkFBbUIsR0FBMEIsQ0FBQyxVQUFVLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FDMUUsSUFBSSxhQUFhLENBQUMscUJBQXFCLEVBQUUsVUFBVSxFQUFFLFFBQVEsS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUUxRyxnRkFBZ0Y7QUFDaEYsTUFBTSxDQUFDLE1BQU0sa0JBQWtCLEdBQUcsQ0FBQyxVQUFVLEdBQVcsT0FBTyxDQUFDLFFBQVEsQ0FBQyxJQUFJLEVBQVEsRUFBRSxDQUNyRixjQUFjLENBQUMsV0FBVyxFQUFFLG1CQUFtQixFQUFFLFVBQVUsQ0FBQyxDQUFDO0FBRS9ELE1BQU0sWUFBWSxHQUFHLENBQUMsYUFBcUIsRUFBZ0IsRUFBRTtJQUMzRCxJQUFJLENBQUMsbUJBQW1CLENBQUMsYUFBYSxDQUFDLEVBQUUsQ0FBQztRQUN4QyxNQUFNLElBQUksYUFBYSxDQUFDLG9CQUFvQixFQUFFLEdBQUcsTUFBTSxDQUFDLGFBQWEsQ0FBQyw4QkFBOEIsQ0FBQyxDQUFDO0lBQ3hHLENBQUM7SUFDRCxPQUFPLFdBQVcsQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQzFDLENBQUMsQ0FBQztBQUVGOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sV0FBVyxHQUFHLENBQUMsYUFBcUIsRUFBVyxFQUFFO0lBQzVELE1BQU0sSUFBSSxHQUFHLFlBQVksQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUN6QyxJQUFJLElBQUksQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLGFBQWEsRUFBRSxhQUFhLENBQUMsT0FBTyxHQUFHLGFBQWEsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFDMUcsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQzNCLElBQUksS0FBSyxLQUFLLGFBQWEsQ0FBQyxXQUFXO1FBQUUsT0FBTyxLQUFLLENBQUM7SUFDdEQsTUFBTSxJQUFJLGFBQWEsQ0FBQyxhQUFhLEVBQUUsY0FBYyxhQUFhLG9CQUFvQixFQUFFLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztBQUNyRyxDQUFDLENBQUM7QUFFRjs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxVQUFVLEdBQUcsQ0FBQyxhQUFxQixFQUFRLEVBQUU7SUFDeEQsTUFBTSxJQUFJLEdBQUcsWUFBWSxDQUFDLGFBQWEsQ0FBQyxDQUFDO0lBQ3pDLElBQUksSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsYUFBYSxFQUFFLGFBQWEsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNyRSxNQUFNLElBQUksYUFBYSxDQUFDLGVBQWUsRUFBRSxjQUFjLGFBQWEsc0JBQXNCLEVBQUUsRUFBRSxLQUFLLEVBQUUsSUFBSSxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztJQUN2SCxDQUFDO0FBQ0gsQ0FBQyxDQUFDIn0=
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export * from './filelock.classes.filelockerror.js';
|
|
9
|
+
export { tryLockFile, unlockFile } from './filelock.flock.js';
|
|
10
|
+
assertFileLockNode();
|
|
11
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90c19maWxlbG9jay9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7R0FLRztBQUNILE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBRXpELGNBQWMscUNBQXFDLENBQUM7QUFDcEQsT0FBTyxFQUFFLFdBQVcsRUFBRSxVQUFVLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUU5RCxrQkFBa0IsRUFBRSxDQUFDIn0=
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type * as ffi from 'node:ffi';
|
|
2
2
|
/** The first Node.js major with `node:ffi`, which this module calls libc through. */
|
|
3
3
|
export declare const systemdRuntimeNodeMajor = 26;
|
|
4
|
+
/**
|
|
5
|
+
* Internal: builds the `runtime_unsupported` error of a subpath that calls libc through this
|
|
6
|
+
* loader (`systemd-runtime`, `file-lock`), so each subpath refuses with its own error class.
|
|
7
|
+
*/
|
|
8
|
+
export type TLibcUnsupportedError = (messageArg: string, causeArg?: unknown) => Error;
|
|
9
|
+
/** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; each libc subpath runs it on import. */
|
|
10
|
+
export declare const assertLibcNode: (subpathArg: string, unsupportedArg: TLibcUnsupportedError, versionArg?: string) => void;
|
|
4
11
|
/** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; the subpath runs it on import. */
|
|
5
12
|
export declare const assertSystemdRuntimeNode: (versionArg?: string) => void;
|
|
6
13
|
/**
|
|
@@ -15,6 +22,8 @@ export declare const assertSystemdRuntimeNode: (versionArg?: string) => void;
|
|
|
15
22
|
* `CMSG_LEN(n)` is 16 + n and `CMSG_SPACE(n)` 16 + n rounded up to 8.
|
|
16
23
|
* - `timespec` (16 bytes): `tv_sec` at 0, `tv_nsec` at 8.
|
|
17
24
|
* - `rlimit` (16 bytes): `rlim_cur` (u64) at 0, `rlim_max` (u64) at 8; `RLIM_INFINITY` is all ones.
|
|
25
|
+
*
|
|
26
|
+
* `EWOULDBLOCK` is `EAGAIN` (11) on both.
|
|
18
27
|
*/
|
|
19
28
|
export declare const libcConstants: {
|
|
20
29
|
readonly AF_UNIX: 1;
|
|
@@ -28,7 +37,11 @@ export declare const libcConstants: {
|
|
|
28
37
|
readonly FD_CLOEXEC: 1;
|
|
29
38
|
readonly CLOCK_MONOTONIC: 1;
|
|
30
39
|
readonly EINTR: 4;
|
|
40
|
+
readonly EWOULDBLOCK: 11;
|
|
31
41
|
readonly RLIMIT_NOFILE: 7;
|
|
42
|
+
readonly LOCK_EX: 2;
|
|
43
|
+
readonly LOCK_NB: 4;
|
|
44
|
+
readonly LOCK_UN: 8;
|
|
32
45
|
};
|
|
33
46
|
/** `sun_path` is 108 bytes; an address of up to 107 bytes keeps a path's terminating NUL. */
|
|
34
47
|
export declare const socketAddressMaximumBytes = 107;
|
|
@@ -59,6 +72,10 @@ declare const libcFunctions: {
|
|
|
59
72
|
readonly arguments: readonly ["int32", "pointer"];
|
|
60
73
|
readonly return: 'int32';
|
|
61
74
|
};
|
|
75
|
+
readonly flock: {
|
|
76
|
+
readonly arguments: readonly ["int32", "int32"];
|
|
77
|
+
readonly return: 'int32';
|
|
78
|
+
};
|
|
62
79
|
readonly __errno_location: {
|
|
63
80
|
readonly arguments: readonly [];
|
|
64
81
|
readonly return: 'pointer';
|
|
@@ -74,10 +91,12 @@ export interface ISystemdLibc {
|
|
|
74
91
|
errno(): number;
|
|
75
92
|
}
|
|
76
93
|
/**
|
|
77
|
-
* glibc, opened through `node:ffi` on first use and kept for the process lifetime
|
|
78
|
-
* `
|
|
94
|
+
* glibc, opened through `node:ffi` on first use and kept for the process lifetime: the one libc
|
|
95
|
+
* loader of the package, shared by the `systemd-runtime` and `file-lock` subpaths. A refusal is
|
|
96
|
+
* built by `unsupportedArg`, the calling subpath's `runtime_unsupported` error. Loading `node:ffi`
|
|
97
|
+
* emits Node's ExperimentalWarning once.
|
|
79
98
|
*/
|
|
80
|
-
export declare const systemdLibc: () => ISystemdLibc;
|
|
99
|
+
export declare const systemdLibc: (unsupportedArg?: TLibcUnsupportedError) => ISystemdLibc;
|
|
81
100
|
/** The `msg_namelen` of an `AF_UNIX` address: a path keeps its NUL; an abstract name has none. */
|
|
82
101
|
export declare const socketAddressLength: (addressArg: string) => number;
|
|
83
102
|
/** Writes a `sockaddr_un` at `offsetArg`; an abstract name's leading `@` becomes its NUL. */
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { SystemdRuntimeError } from './classes.systemdruntimeerror.js';
|
|
2
2
|
/** The first Node.js major with `node:ffi`, which this module calls libc through. */
|
|
3
3
|
export const systemdRuntimeNodeMajor = 26;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const systemdRuntimeUnsupported = (messageArg, causeArg) => new SystemdRuntimeError('runtime_unsupported', messageArg, causeArg === undefined ? {} : { cause: causeArg });
|
|
5
|
+
/** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; each libc subpath runs it on import. */
|
|
6
|
+
export const assertLibcNode = (subpathArg, unsupportedArg, versionArg = process.versions.node) => {
|
|
6
7
|
const major = Number(/^(\d+)\./.exec(versionArg)?.[1]);
|
|
7
8
|
if (!(major >= systemdRuntimeNodeMajor)) {
|
|
8
|
-
throw
|
|
9
|
+
throw unsupportedArg(`@push.rocks/smartdaemon/${subpathArg} requires Node.js ${systemdRuntimeNodeMajor} or later; this is Node.js ${versionArg}.`);
|
|
9
10
|
}
|
|
10
11
|
};
|
|
12
|
+
/** Internal: refuses a Node.js older than `systemdRuntimeNodeMajor`; the subpath runs it on import. */
|
|
13
|
+
export const assertSystemdRuntimeNode = (versionArg = process.versions.node) => assertLibcNode('systemd-runtime', systemdRuntimeUnsupported, versionArg);
|
|
11
14
|
/**
|
|
12
15
|
* The generic Linux values both qualified targets (x86_64 and aarch64, glibc) use. The structure
|
|
13
16
|
* layouts below are the LP64 glibc ones, identical on both:
|
|
@@ -20,6 +23,8 @@ export const assertSystemdRuntimeNode = (versionArg = process.versions.node) =>
|
|
|
20
23
|
* `CMSG_LEN(n)` is 16 + n and `CMSG_SPACE(n)` 16 + n rounded up to 8.
|
|
21
24
|
* - `timespec` (16 bytes): `tv_sec` at 0, `tv_nsec` at 8.
|
|
22
25
|
* - `rlimit` (16 bytes): `rlim_cur` (u64) at 0, `rlim_max` (u64) at 8; `RLIM_INFINITY` is all ones.
|
|
26
|
+
*
|
|
27
|
+
* `EWOULDBLOCK` is `EAGAIN` (11) on both.
|
|
23
28
|
*/
|
|
24
29
|
export const libcConstants = {
|
|
25
30
|
AF_UNIX: 1,
|
|
@@ -33,7 +38,11 @@ export const libcConstants = {
|
|
|
33
38
|
FD_CLOEXEC: 1,
|
|
34
39
|
CLOCK_MONOTONIC: 1,
|
|
35
40
|
EINTR: 4,
|
|
41
|
+
EWOULDBLOCK: 11,
|
|
36
42
|
RLIMIT_NOFILE: 7,
|
|
43
|
+
LOCK_EX: 2,
|
|
44
|
+
LOCK_NB: 4,
|
|
45
|
+
LOCK_UN: 8,
|
|
37
46
|
};
|
|
38
47
|
/** `sun_path` is 108 bytes; an address of up to 107 bytes keeps a path's terminating NUL. */
|
|
39
48
|
export const socketAddressMaximumBytes = 107;
|
|
@@ -47,29 +56,32 @@ const libcFunctions = {
|
|
|
47
56
|
fcntl: { arguments: ['int32', 'int32', 'int32'], return: 'int32' },
|
|
48
57
|
clock_gettime: { arguments: ['int32', 'pointer'], return: 'int32' },
|
|
49
58
|
getrlimit: { arguments: ['int32', 'pointer'], return: 'int32' },
|
|
59
|
+
flock: { arguments: ['int32', 'int32'], return: 'int32' },
|
|
50
60
|
__errno_location: { arguments: [], return: 'pointer' },
|
|
51
61
|
};
|
|
52
62
|
let loaded = null;
|
|
53
63
|
/**
|
|
54
|
-
* glibc, opened through `node:ffi` on first use and kept for the process lifetime
|
|
55
|
-
* `
|
|
64
|
+
* glibc, opened through `node:ffi` on first use and kept for the process lifetime: the one libc
|
|
65
|
+
* loader of the package, shared by the `systemd-runtime` and `file-lock` subpaths. A refusal is
|
|
66
|
+
* built by `unsupportedArg`, the calling subpath's `runtime_unsupported` error. Loading `node:ffi`
|
|
67
|
+
* emits Node's ExperimentalWarning once.
|
|
56
68
|
*/
|
|
57
|
-
export const systemdLibc = () => {
|
|
69
|
+
export const systemdLibc = (unsupportedArg = systemdRuntimeUnsupported) => {
|
|
58
70
|
if (loaded !== null)
|
|
59
71
|
return loaded;
|
|
60
72
|
if (process.platform !== 'linux' || (process.arch !== 'x64' && process.arch !== 'arm64')) {
|
|
61
|
-
throw
|
|
73
|
+
throw unsupportedArg(`glibc calls through node:ffi support Linux on x64 and arm64; this is ${process.platform} on ${process.arch}.`);
|
|
62
74
|
}
|
|
63
75
|
const ffiModule = process.getBuiltinModule('node:ffi');
|
|
64
76
|
if (ffiModule === undefined) {
|
|
65
|
-
throw
|
|
77
|
+
throw unsupportedArg('node:ffi is not available (disabled by --no-experimental-ffi?).');
|
|
66
78
|
}
|
|
67
79
|
let functions;
|
|
68
80
|
try {
|
|
69
81
|
functions = ffiModule.dlopen('libc.so.6', libcFunctions).functions;
|
|
70
82
|
}
|
|
71
83
|
catch (error) {
|
|
72
|
-
throw
|
|
84
|
+
throw unsupportedArg('glibc (libc.so.6) could not be opened through node:ffi; under the permission model pass --allow-ffi.', error);
|
|
73
85
|
}
|
|
74
86
|
loaded = Object.freeze({
|
|
75
87
|
functions,
|
|
@@ -89,4 +101,4 @@ export const writeSocketAddress = (bufferArg, offsetArg, addressArg) => {
|
|
|
89
101
|
};
|
|
90
102
|
/** `CMSG_SPACE` for `countArg` descriptors. */
|
|
91
103
|
export const controlSpace = (countArg) => 16 + ((4 * countArg + 7) & ~7);
|
|
92
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
104
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3lzdGVtZHJ1bnRpbWUubGliYy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX3N5c3RlbWRydW50aW1lL3N5c3RlbWRydW50aW1lLmxpYmMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFFdkUscUZBQXFGO0FBQ3JGLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLEVBQUUsQ0FBQztBQVExQyxNQUFNLHlCQUF5QixHQUEwQixDQUFDLFVBQVUsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUNoRixJQUFJLG1CQUFtQixDQUFDLHFCQUFxQixFQUFFLFVBQVUsRUFBRSxRQUFRLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7QUFFaEgsNkdBQTZHO0FBQzdHLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxDQUM1QixVQUFrQixFQUNsQixjQUFxQyxFQUNyQyxVQUFVLEdBQVcsT0FBTyxDQUFDLFFBQVEsQ0FBQyxJQUFJLEVBQ3BDLEVBQUU7SUFDUixNQUFNLEtBQUssR0FBRyxNQUFNLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDdkQsSUFBSSxDQUFDLENBQUMsS0FBSyxJQUFJLHVCQUF1QixDQUFDLEVBQUUsQ0FBQztRQUN4QyxNQUFNLGNBQWMsQ0FDbEIsMkJBQTJCLFVBQVUscUJBQXFCLHVCQUF1Qiw4QkFBOEIsVUFBVSxHQUFHLENBQzdILENBQUM7SUFDSixDQUFDO0FBQ0gsQ0FBQyxDQUFDO0FBRUYsdUdBQXVHO0FBQ3ZHLE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLENBQUMsVUFBVSxHQUFXLE9BQU8sQ0FBQyxRQUFRLENBQUMsSUFBSSxFQUFRLEVBQUUsQ0FDM0YsY0FBYyxDQUFD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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartdaemon",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Start scripts as long running daemons and manage them.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist_ts_systemdruntime/index.d.ts",
|
|
15
15
|
"default": "./dist_ts_systemdruntime/index.js"
|
|
16
16
|
},
|
|
17
|
+
"./file-lock": {
|
|
18
|
+
"types": "./dist_ts_filelock/index.d.ts",
|
|
19
|
+
"default": "./dist_ts_filelock/index.js"
|
|
20
|
+
},
|
|
17
21
|
"./package.json": "./package.json"
|
|
18
22
|
},
|
|
19
23
|
"type": "module",
|
|
@@ -37,8 +41,8 @@
|
|
|
37
41
|
},
|
|
38
42
|
"homepage": "https://code.foss.global/push.rocks/smartdaemon",
|
|
39
43
|
"devDependencies": {
|
|
40
|
-
"@git.zone/tsrepo": "^
|
|
41
|
-
"@types/node": "^26.6.
|
|
44
|
+
"@git.zone/tsrepo": "^2.1.0",
|
|
45
|
+
"@types/node": "^26.6.6"
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
44
48
|
"@push.rocks/lik": "^6.4.4",
|
|
@@ -50,6 +54,7 @@
|
|
|
50
54
|
"files": [
|
|
51
55
|
"ts/**/*",
|
|
52
56
|
"ts_systemdruntime/**/*",
|
|
57
|
+
"ts_filelock/**/*",
|
|
53
58
|
"ts_web/**/*",
|
|
54
59
|
"dist/**/*",
|
|
55
60
|
"dist_*/**/*",
|
package/readme.md
CHANGED
|
@@ -1155,6 +1155,70 @@ verifies the flag on every passed descriptor, as `sd_listen_fds(3)` does.
|
|
|
1155
1155
|
caller's user manager (`systemd-run --user`), and skips with the reason without Node.js 26 or a
|
|
1156
1156
|
user manager.
|
|
1157
1157
|
|
|
1158
|
+
### An exclusive file lock on an open descriptor (`/file-lock`, Node.js 26)
|
|
1159
|
+
|
|
1160
|
+
Node.js has no file lock. The `@push.rocks/smartdaemon/file-lock` subpath takes the kernel's
|
|
1161
|
+
`flock(2)` lock on a descriptor the caller opened, through the same glibc loader as
|
|
1162
|
+
`/systemd-runtime`, and nothing more: no path, no open, no class, no wait, no shared lock.
|
|
1163
|
+
|
|
1164
|
+
```typescript
|
|
1165
|
+
import * as fs from 'node:fs';
|
|
1166
|
+
import { FileLockError, tryLockFile, unlockFile } from '@push.rocks/smartdaemon/file-lock';
|
|
1167
|
+
|
|
1168
|
+
// The caller opens (and checks) the file itself; the lock belongs to this open file description.
|
|
1169
|
+
const descriptor = fs.openSync('/var/lib/example/.owner.lock', 'r+');
|
|
1170
|
+
if (!tryLockFile(descriptor)) throw new Error('another process owns /var/lib/example');
|
|
1171
|
+
try {
|
|
1172
|
+
// ... work only the owner may do ...
|
|
1173
|
+
} finally {
|
|
1174
|
+
unlockFile(descriptor); // or close the descriptor, or exit
|
|
1175
|
+
fs.closeSync(descriptor);
|
|
1176
|
+
}
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
| Call | Does |
|
|
1180
|
+
| --- | --- |
|
|
1181
|
+
| `tryLockFile(fd)` | `flock(fd, LOCK_EX \| LOCK_NB)`: true when granted (also when this description already holds it), false only when another holds a conflicting lock (`EWOULDBLOCK`) |
|
|
1182
|
+
| `unlockFile(fd)` | `flock(fd, LOCK_UN)`; releasing a description that holds no lock succeeds |
|
|
1183
|
+
|
|
1184
|
+
Both are synchronous and either complete or throw `FileLockError` (`code`, and `errno` when the
|
|
1185
|
+
libc call failed): `lock_failed` and `unlock_failed` carry the errno (`EBADF` for a descriptor that
|
|
1186
|
+
is not open), and a number that cannot be a descriptor (not an integer from 0 to `INT_MAX`) is
|
|
1187
|
+
`descriptor_invalid` before any call.
|
|
1188
|
+
|
|
1189
|
+
What the lock is:
|
|
1190
|
+
|
|
1191
|
+
- **Advisory and exclusive.** It excludes only other `flock(2)` locks; a process that does not ask
|
|
1192
|
+
is not stopped from reading or writing the file. `/proc/locks` shows it as `FLOCK ADVISORY WRITE`.
|
|
1193
|
+
- **Held by the open file description**, not by the process or the descriptor number. A `dup` of
|
|
1194
|
+
the descriptor shares it, and so does a descriptor a child inherits (through `stdio`, for
|
|
1195
|
+
example): the child keeps the lock after the parent closed its copy or died. A second `open` of
|
|
1196
|
+
the same file, even in the same process, is a new description and conflicts.
|
|
1197
|
+
- **Released** by `unlockFile`, or when the last descriptor of the description is closed, which a
|
|
1198
|
+
process exit (`SIGKILL` included) does.
|
|
1199
|
+
- **Not an `fcntl` lock.** On a local Linux filesystem `fcntl(2)` record locks (`F_SETLK`,
|
|
1200
|
+
`F_OFD_SETLK`) and `flock(2)` locks do not conflict with each other, so a peer that uses `fcntl` is
|
|
1201
|
+
not excluded.
|
|
1202
|
+
- Any other program taking `flock(2)` on the same file, util-linux `flock(1)` for example, excludes
|
|
1203
|
+
and is excluded by it.
|
|
1204
|
+
|
|
1205
|
+
It requires **Node.js 26 or later** on Linux x64 or arm64 with glibc, like `/systemd-runtime`:
|
|
1206
|
+
importing it on an older Node.js throws `FileLockError` `runtime_unsupported`, and the first call
|
|
1207
|
+
throws the same code on another platform or with `node:ffi` unavailable or denied (pass
|
|
1208
|
+
`--allow-ffi` under the permission model).
|
|
1209
|
+
|
|
1210
|
+
There is **no blocking lock**, on purpose. `node:ffi` has no off-thread call mode, so a blocking
|
|
1211
|
+
`flock(fd, LOCK_EX)` on the main thread stops the event loop for the whole wait: signal handlers,
|
|
1212
|
+
timers and `sd_notify` messages all stall. Moving it into a worker does not help: a worker blocked
|
|
1213
|
+
in `flock` cannot be terminated, and `process.exit()` waits until the lock is granted, which is
|
|
1214
|
+
never while another process holds it. A caller that must wait polls `tryLockFile` on a timer:
|
|
1215
|
+
|
|
1216
|
+
```typescript
|
|
1217
|
+
const lockWhenFree = async (fd: number, intervalMs = 100): Promise<void> => {
|
|
1218
|
+
while (!tryLockFile(fd)) await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1219
|
+
};
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1158
1222
|
### Starting a program in its own transient scope
|
|
1159
1223
|
|
|
1160
1224
|
A service that launches a long-lived program keeps that program in its own
|
|
@@ -1411,8 +1475,8 @@ console.log(template); // View generated systemd unit file
|
|
|
1411
1475
|
- **systemd 240 or later** for installing a definition with an `append` output (`standardOutput` or
|
|
1412
1476
|
`standardError`), refused the same way
|
|
1413
1477
|
- **Node.js**: Version 14.x or higher
|
|
1414
|
-
- **Node.js 26 or later** for the `@push.rocks/smartdaemon/systemd-runtime`
|
|
1415
|
-
x64 or arm64 with glibc
|
|
1478
|
+
- **Node.js 26 or later** for the `@push.rocks/smartdaemon/systemd-runtime` and
|
|
1479
|
+
`@push.rocks/smartdaemon/file-lock` subpaths (`node:ffi`), on Linux x64 or arm64 with glibc
|
|
1416
1480
|
- **Permissions**: Either root access or properly configured sudo
|
|
1417
1481
|
|
|
1418
1482
|
## ⚙️ Generated Systemd Service Files
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a file lock operation refused or failed:
|
|
3
|
+
*
|
|
4
|
+
* - `runtime_unsupported`: the lock cannot be called: Node.js older than 26, a platform other than
|
|
5
|
+
* Linux on x64 or arm64, a C library other than glibc (`libc.so.6`), `node:ffi` disabled
|
|
6
|
+
* (`--no-experimental-ffi`) or denied by the permission model (no `--allow-ffi`).
|
|
7
|
+
* - `descriptor_invalid`: a descriptor number that cannot exist (not an integer from 0 to
|
|
8
|
+
* `INT_MAX`); refused before any call.
|
|
9
|
+
* - `lock_failed`: `flock(LOCK_EX | LOCK_NB)` failed for a reason other than a conflicting lock
|
|
10
|
+
* (`errno` names why: `EBADF` for a descriptor that is not open, for example).
|
|
11
|
+
* - `unlock_failed`: `flock(LOCK_UN)` failed (`errno` names why).
|
|
12
|
+
*/
|
|
13
|
+
export type TFileLockErrorCode = 'runtime_unsupported' | 'descriptor_invalid' | 'lock_failed' | 'unlock_failed';
|
|
14
|
+
|
|
15
|
+
export interface IFileLockErrorOptions {
|
|
16
|
+
/** The C library's `errno` after the failed call, when a call failed. */
|
|
17
|
+
readonly errno?: number;
|
|
18
|
+
readonly cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FileLockError extends Error {
|
|
22
|
+
/** The C library's `errno` after the failed call, or null when no call failed. */
|
|
23
|
+
public readonly errno: number | null;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
public readonly code: TFileLockErrorCode,
|
|
27
|
+
messageArg: string,
|
|
28
|
+
optionsArg: IFileLockErrorOptions = {},
|
|
29
|
+
) {
|
|
30
|
+
super(
|
|
31
|
+
`${messageArg}${optionsArg.errno === undefined ? '' : ` (errno ${optionsArg.errno})`}`,
|
|
32
|
+
optionsArg.cause === undefined ? undefined : { cause: optionsArg.cause },
|
|
33
|
+
);
|
|
34
|
+
this.name = 'FileLockError';
|
|
35
|
+
this.errno = optionsArg.errno ?? null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -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
|
-
/**
|
|
8
|
-
|
|
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
|
|
12
|
-
|
|
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
|
|
77
|
-
* `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
119
|
+
error,
|
|
99
120
|
);
|
|
100
121
|
}
|
|
101
122
|
loaded = Object.freeze({
|