@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.systemdunit.d.ts +58 -2
- package/dist_ts/classes.systemdunit.js +87 -6
- package/dist_ts/smartdaemon.plugins.d.ts +2 -1
- package/dist_ts/smartdaemon.plugins.js +3 -2
- package/dist_ts/systemd.managerversion.d.ts +2 -0
- package/dist_ts/systemd.managerversion.js +3 -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 +11 -8
- package/readme.md +109 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.systemdunit.ts +120 -4
- package/ts/smartdaemon.plugins.ts +2 -1
- package/ts/systemd.managerversion.ts +3 -0
- 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
|
@@ -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,10 +41,8 @@
|
|
|
37
41
|
},
|
|
38
42
|
"homepage": "https://code.foss.global/push.rocks/smartdaemon",
|
|
39
43
|
"devDependencies": {
|
|
40
|
-
"@git.zone/
|
|
41
|
-
"@
|
|
42
|
-
"@git.zone/tstest": "^8.0.0",
|
|
43
|
-
"@types/node": "^26.6.4"
|
|
44
|
+
"@git.zone/tsrepo": "^2.1.0",
|
|
45
|
+
"@types/node": "^26.6.6"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"@push.rocks/lik": "^6.4.4",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"files": [
|
|
53
55
|
"ts/**/*",
|
|
54
56
|
"ts_systemdruntime/**/*",
|
|
57
|
+
"ts_filelock/**/*",
|
|
55
58
|
"ts_web/**/*",
|
|
56
59
|
"dist/**/*",
|
|
57
60
|
"dist_*/**/*",
|
|
@@ -68,9 +71,9 @@
|
|
|
68
71
|
"last 1 chrome versions"
|
|
69
72
|
],
|
|
70
73
|
"scripts": {
|
|
71
|
-
"test": "
|
|
72
|
-
"build": "
|
|
74
|
+
"test": "tsrepo test test/ --verbose --timeout 60",
|
|
75
|
+
"build": "tsrepo build tsfolders --web",
|
|
73
76
|
"format": "gitzone format",
|
|
74
|
-
"buildDocs": "
|
|
77
|
+
"buildDocs": "tsrepo docs"
|
|
75
78
|
}
|
|
76
79
|
}
|
package/readme.md
CHANGED
|
@@ -293,6 +293,49 @@ store needs systemd 254 or later: the call reads the manager's version first and
|
|
|
293
293
|
manager with `SystemdUnitManagerTooOldError` (`manager_too_old`), an unreadable version with
|
|
294
294
|
`version_unknown` and an unanswered read with `command_failed`, each without running the clean.
|
|
295
295
|
|
|
296
|
+
`kill(signal?, options?)` ends a unit's processes without a stop job: it sends `SIGKILL` (the
|
|
297
|
+
default) or `SIGTERM` to every process of the unit at once, its main and control process and every
|
|
298
|
+
other process in its control group (`systemctl kill --signal=<signal> --kill-whom=all`), then reads
|
|
299
|
+
the unit every 100 ms until it is at rest or about to restart, for at most `timeoutMs` (integer
|
|
300
|
+
milliseconds from 1 to 600000, default 30000). Use it where a `stop()` would wait out the unit's
|
|
301
|
+
`TimeoutStopSec=`, such as a unit whose stop already failed or overran; it does not run `ExecStop=`,
|
|
302
|
+
does not cancel or await a pending job, and never starts, enables, unmasks or resets the unit. It
|
|
303
|
+
resolves a frozen `{ outcome, state }`, where `state` is the snapshot that decided the outcome (for
|
|
304
|
+
`timeout`, the last one read):
|
|
305
|
+
|
|
306
|
+
| `outcome` | Meaning |
|
|
307
|
+
| --- | --- |
|
|
308
|
+
| `'stopped'`, `'failed'` | `systemdUnitQuiescence()`'s `rest`: no main or control process and no control group. A unit whose main process is gone while its control group still holds processes resolves only once the group is gone. A unit already at rest is signalled all the same (nothing receives it) and resolves at the first read |
|
|
309
|
+
| `'auto-restart'` | SubState `auto-restart`, or `auto-restart-queued` (systemd 254), without a main or control process: the unit waits for its `Restart=` timer or its queued restart. Its control group is not read here: with `RestartMode=direct` the manager keeps the emptied group |
|
|
310
|
+
| `'restarted'` | the manager began a new invocation (`InvocationID`) before a read saw the unit at rest: a restart delay shorter than the poll interval, or another client's start |
|
|
311
|
+
| `'timeout'` | none of these within `timeoutMs`; a read begun before the deadline is still awaited, each bounded by `commandTimeoutMs` |
|
|
312
|
+
|
|
313
|
+
A unit whose `LoadState` is not `loaded` in the read before the signal (`not-found`, `masked`,
|
|
314
|
+
`bad-setting`, `error`, ...) is refused with `command_failed` before any signal: that read makes the
|
|
315
|
+
manager load the unit object, and the manager may signal an improperly loaded unit, so such a unit
|
|
316
|
+
would otherwise resolve `stopped`. A refused signal and a `systemctl kill` that exceeded
|
|
317
|
+
`commandTimeoutMs` reject with `command_failed` too; after a timed-out call the signal may have been
|
|
318
|
+
sent, so inspect the unit. Once the manager accepted the signal, a failed read rejects with
|
|
319
|
+
`signalled_uninspected` and that read's error as `cause`. `signalled_uninspected` is a new member of
|
|
320
|
+
`SystemdUnitError['code']`: a consumer with an exhaustive `never` switch over the code must add a
|
|
321
|
+
case for it. `kill()` needs systemd 252 or later, the first whose `systemctl` takes
|
|
322
|
+
`--kill-whom=`: it reads the manager's version first and refuses an older manager with
|
|
323
|
+
`SystemdUnitManagerTooOldError`, an unreadable version with `version_unknown` and an unanswered read
|
|
324
|
+
with `command_failed`, each before any signal. A signal other than `'SIGKILL'` or `'SIGTERM'` and an
|
|
325
|
+
option other than `timeoutMs` reject with `invalid_options` before any command runs.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const killed = await unit.kill('SIGKILL', { timeoutMs: 30_000 });
|
|
329
|
+
if (killed.outcome === 'timeout') {
|
|
330
|
+
console.log('still running', killed.state.activeState, killed.state.controlGroup);
|
|
331
|
+
} else if (killed.outcome !== 'restarted') {
|
|
332
|
+
await unit.start();
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
After an `auto-restart` outcome the start job stays queued until the unit's `RestartSec=` elapses,
|
|
337
|
+
so `start()` waits for it; a long `RestartSec=` can exceed `commandTimeoutMs`.
|
|
338
|
+
|
|
296
339
|
`inspectConfiguration()` reports the selected `fragmentPath`, opaque `dropInPaths`
|
|
297
340
|
list and `needsReload` flag. `reloadConfiguration()` explicitly reloads the manager's
|
|
298
341
|
unit definitions globally and returns a fresh configuration snapshot. Neither
|
|
@@ -1112,6 +1155,70 @@ verifies the flag on every passed descriptor, as `sd_listen_fds(3)` does.
|
|
|
1112
1155
|
caller's user manager (`systemd-run --user`), and skips with the reason without Node.js 26 or a
|
|
1113
1156
|
user manager.
|
|
1114
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
|
+
|
|
1115
1222
|
### Starting a program in its own transient scope
|
|
1116
1223
|
|
|
1117
1224
|
A service that launches a long-lived program keeps that program in its own
|
|
@@ -1368,8 +1475,8 @@ console.log(template); // View generated systemd unit file
|
|
|
1368
1475
|
- **systemd 240 or later** for installing a definition with an `append` output (`standardOutput` or
|
|
1369
1476
|
`standardError`), refused the same way
|
|
1370
1477
|
- **Node.js**: Version 14.x or higher
|
|
1371
|
-
- **Node.js 26 or later** for the `@push.rocks/smartdaemon/systemd-runtime`
|
|
1372
|
-
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
|
|
1373
1480
|
- **Permissions**: Either root access or properly configured sudo
|
|
1374
1481
|
|
|
1375
1482
|
## ⚙️ Generated Systemd Service Files
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ISystemdManagerVersion,
|
|
13
13
|
fileDescriptorStoreCleanMajor,
|
|
14
14
|
fileDescriptorStorePreserveMajor,
|
|
15
|
+
killWhomMajor,
|
|
15
16
|
managerVersionArguments,
|
|
16
17
|
parseManagerVersion,
|
|
17
18
|
} from './systemd.managerversion.js';
|
|
@@ -127,6 +128,49 @@ export interface ISystemdUnitJobOptions {
|
|
|
127
128
|
wait?: boolean;
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/** The signals `SystemdUnit.kill()` sends. */
|
|
132
|
+
export type TSystemdUnitKillSignal = 'SIGKILL' | 'SIGTERM';
|
|
133
|
+
|
|
134
|
+
/** `SystemdUnit.kill()` options. */
|
|
135
|
+
export interface ISystemdUnitKillOptions {
|
|
136
|
+
/**
|
|
137
|
+
* How long to wait, after the manager accepted the signal, for the unit to come to rest: integer
|
|
138
|
+
* milliseconds from 1 to 600000. Defaults to 30000. A read begun before the deadline is still
|
|
139
|
+
* awaited; each read is bounded by `commandTimeoutMs`.
|
|
140
|
+
*/
|
|
141
|
+
timeoutMs?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* How the unit stood when `SystemdUnit.kill()` stopped waiting. `stopped` and `failed` are
|
|
146
|
+
* `systemdUnitQuiescence()`'s `rest`: no main or control process and no control group.
|
|
147
|
+
* `auto-restart`: SubState `auto-restart` or `auto-restart-queued` (systemd 254) without a main or
|
|
148
|
+
* control process, waiting for its `Restart=` timer or its queued restart job. `restarted`: the
|
|
149
|
+
* manager started a new invocation of the unit (a restart delay shorter than the poll interval, or
|
|
150
|
+
* another client's start) before a read saw it at rest. `timeout`: none of these within `timeoutMs`.
|
|
151
|
+
*/
|
|
152
|
+
export type TSystemdUnitKillOutcome = 'stopped' | 'failed' | 'auto-restart' | 'restarted' | 'timeout';
|
|
153
|
+
|
|
154
|
+
export interface ISystemdUnitKillResult {
|
|
155
|
+
readonly outcome: TSystemdUnitKillOutcome;
|
|
156
|
+
/** The snapshot that decided `outcome`; for `timeout`, the last one read before the deadline. */
|
|
157
|
+
readonly state: ISystemdUnitState;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Interval between the snapshots `kill()` reads while it waits. */
|
|
161
|
+
const killPollIntervalMs = 100;
|
|
162
|
+
|
|
163
|
+
/** Reads the outcome a kill has reached from one snapshot, or null while it has reached none. */
|
|
164
|
+
const killOutcome = (state: ISystemdUnitState, newInvocation: boolean): TSystemdUnitKillOutcome | null => {
|
|
165
|
+
const { rest } = systemdUnitQuiescence(state);
|
|
166
|
+
if (rest !== null) return rest;
|
|
167
|
+
// In auto-restart the manager may keep the unit's (empty) control group: with RestartMode=direct
|
|
168
|
+
// the unit never passes the inactive state that releases it, so ControlGroup is not read here.
|
|
169
|
+
if (state.mainPid === 0 && state.controlPid === 0 &&
|
|
170
|
+
(state.subState === 'auto-restart' || state.subState === 'auto-restart-queued')) return 'auto-restart';
|
|
171
|
+
return newInvocation ? 'restarted' : null;
|
|
172
|
+
};
|
|
173
|
+
|
|
130
174
|
export interface ISystemdUnitConfigurationState {
|
|
131
175
|
readonly unitName: string;
|
|
132
176
|
readonly fragmentPath: string;
|
|
@@ -232,12 +276,15 @@ export class SystemdUnitError extends Error {
|
|
|
232
276
|
* `version_unknown`: the manager answered, but its `Version` property is not one this package
|
|
233
277
|
* reads, so a call that needs a minimum manager version refuses.
|
|
234
278
|
* `manager_too_old`: always a `SystemdUnitManagerTooOldError`, naming both versions.
|
|
279
|
+
* `signalled_uninspected`: only `kill()`. The manager accepted the signal, and a snapshot read
|
|
280
|
+
* while waiting failed; `cause` is that inspection's error.
|
|
235
281
|
*/
|
|
236
282
|
public readonly code:
|
|
237
283
|
| 'invalid_options'
|
|
238
284
|
| 'command_failed'
|
|
239
285
|
| 'invalid_state'
|
|
240
286
|
| 'released_uninspected'
|
|
287
|
+
| 'signalled_uninspected'
|
|
241
288
|
| 'version_unknown'
|
|
242
289
|
| 'manager_too_old',
|
|
243
290
|
options?: ErrorOptions,
|
|
@@ -480,10 +527,18 @@ export class SystemdUnit {
|
|
|
480
527
|
|
|
481
528
|
/** Snapshot only. Callers own application-specific admission and restart policy. */
|
|
482
529
|
public async inspect(): Promise<ISystemdUnitState> {
|
|
530
|
+
return (await this.readState(properties)).state;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Reads one snapshot with `keys`: the snapshot properties, optionally followed by `InvocationID`,
|
|
535
|
+
* which is empty for a unit never started and otherwise 32 lowercase hex digits.
|
|
536
|
+
*/
|
|
537
|
+
private async readState(keys: readonly string[]): Promise<{ state: ISystemdUnitState; invocationId: string }> {
|
|
483
538
|
const stdout = await this.command([
|
|
484
539
|
'show',
|
|
485
540
|
this.unitName,
|
|
486
|
-
`--property=${
|
|
541
|
+
`--property=${keys.join(',')}`,
|
|
487
542
|
'--all',
|
|
488
543
|
]);
|
|
489
544
|
const fields = new Map<string, string>();
|
|
@@ -491,13 +546,14 @@ export class SystemdUnit {
|
|
|
491
546
|
for (const line of (stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout).split('\n')) {
|
|
492
547
|
const separator = line.indexOf('=');
|
|
493
548
|
const key = line.slice(0, separator);
|
|
494
|
-
if (separator < 1 || !
|
|
549
|
+
if (separator < 1 || !keys.includes(key) || fields.has(key)) {
|
|
495
550
|
throw new SystemdUnitError('invalid_state');
|
|
496
551
|
}
|
|
497
552
|
fields.set(key, line.slice(separator + 1));
|
|
498
553
|
}
|
|
499
554
|
if (
|
|
500
|
-
fields.size !==
|
|
555
|
+
fields.size !== keys.length || fields.get('Id') !== this.unitName ||
|
|
556
|
+
(keys.includes('InvocationID') && !/^(?:[0-9a-f]{32})?$/.test(fields.get('InvocationID') ?? '')) ||
|
|
501
557
|
['LoadState', 'ActiveState', 'SubState'].some((key) => !fields.get(key)) ||
|
|
502
558
|
!/^[a-z][a-z-]{0,31}$/.test(fields.get('Result')!) ||
|
|
503
559
|
['MainPID', 'ControlPID', 'NFileDescriptorStore'].some((key) =>
|
|
@@ -506,7 +562,7 @@ export class SystemdUnit {
|
|
|
506
562
|
// systemd prints the store's unsigned 32-bit count in decimal.
|
|
507
563
|
Number(fields.get('NFileDescriptorStore')) > 4294967295
|
|
508
564
|
) throw new SystemdUnitError('invalid_state');
|
|
509
|
-
|
|
565
|
+
const state: ISystemdUnitState = Object.freeze({
|
|
510
566
|
unitName: this.unitName,
|
|
511
567
|
loadState: fields.get('LoadState')!,
|
|
512
568
|
activeState: fields.get('ActiveState')!,
|
|
@@ -521,6 +577,7 @@ export class SystemdUnit {
|
|
|
521
577
|
result: fields.get('Result')!,
|
|
522
578
|
fileDescriptorStoreCount: Number(fields.get('NFileDescriptorStore')),
|
|
523
579
|
});
|
|
580
|
+
return { state, invocationId: fields.get('InvocationID') ?? '' };
|
|
524
581
|
}
|
|
525
582
|
|
|
526
583
|
/**
|
|
@@ -585,6 +642,65 @@ export class SystemdUnit {
|
|
|
585
642
|
return this.inspect();
|
|
586
643
|
}
|
|
587
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Sends `signal` (default `SIGKILL`) to every process of the unit at once, its main and control
|
|
647
|
+
* process and every other process in its control group (`systemctl kill --signal=<signal>
|
|
648
|
+
* --kill-whom=all`), then waits, bounded by `timeoutMs`, until the unit is at rest or about to
|
|
649
|
+
* restart, and resolves with the outcome and the snapshot that decided it. It does not queue a
|
|
650
|
+
* stop job and does not wait out `TimeoutStopSec=`; a pending job is neither cancelled nor
|
|
651
|
+
* awaited. It never starts, enables, unmasks or resets the unit.
|
|
652
|
+
*
|
|
653
|
+
* A unit already at rest is signalled all the same (nothing receives it) and resolves at the first
|
|
654
|
+
* read. A unit whose main process is gone while its control group still holds processes resolves
|
|
655
|
+
* only once the group is gone. A unit whose `LoadState` in the read before the signal is not
|
|
656
|
+
* `loaded` (`not-found`, `masked`, `bad-setting`, `error`, ...) is refused with `command_failed`
|
|
657
|
+
* before any signal, as are a refused signal and a `systemctl` call that exceeded
|
|
658
|
+
* `commandTimeoutMs`; after a timed-out call the signal may have been sent, so inspect the unit. Once the manager accepted the signal, a failed read
|
|
659
|
+
* rejects with `signalled_uninspected` and that read's error as `cause`.
|
|
660
|
+
*
|
|
661
|
+
* Needs systemd 252 or later, the first whose `systemctl` takes `--kill-whom=`: the manager's
|
|
662
|
+
* version is read first, and an older manager is refused with `SystemdUnitManagerTooOldError`, an
|
|
663
|
+
* unreadable version with `version_unknown` and an unanswered read with `command_failed`, each
|
|
664
|
+
* before any signal. A malformed signal or option rejects with `invalid_options` before any command.
|
|
665
|
+
*/
|
|
666
|
+
public async kill(
|
|
667
|
+
signalArg: TSystemdUnitKillSignal = 'SIGKILL',
|
|
668
|
+
optionsArg?: ISystemdUnitKillOptions,
|
|
669
|
+
): Promise<ISystemdUnitKillResult> {
|
|
670
|
+
if (
|
|
671
|
+
(signalArg !== 'SIGKILL' && signalArg !== 'SIGTERM') ||
|
|
672
|
+
(optionsArg !== undefined && (
|
|
673
|
+
typeof optionsArg !== 'object' || optionsArg === null ||
|
|
674
|
+
Object.keys(optionsArg).some((key) => key !== 'timeoutMs') ||
|
|
675
|
+
(optionsArg.timeoutMs !== undefined && (!Number.isSafeInteger(optionsArg.timeoutMs) ||
|
|
676
|
+
optionsArg.timeoutMs < 1 || optionsArg.timeoutMs > 600000))
|
|
677
|
+
))
|
|
678
|
+
) throw new SystemdUnitError('invalid_options');
|
|
679
|
+
const timeoutMs = optionsArg?.timeoutMs ?? 30000;
|
|
680
|
+
await this.requireManagerVersion(killWhomMajor);
|
|
681
|
+
const killKeys = [...properties, 'InvocationID'];
|
|
682
|
+
// The invocation before the signal, so a restart the wait could not observe at rest is reported.
|
|
683
|
+
const { state: before, invocationId } = await this.readState(killKeys);
|
|
684
|
+
// That read makes the manager load the unit object, and the manager may signal a unit it could
|
|
685
|
+
// not load properly; such a unit has no processes of its own to end, so it is refused here.
|
|
686
|
+
if (before.loadState !== 'loaded') throw new SystemdUnitError('command_failed');
|
|
687
|
+
await this.command(['kill', `--signal=${signalArg}`, '--kill-whom=all', this.unitName]);
|
|
688
|
+
const deadline = performance.now() + timeoutMs;
|
|
689
|
+
for (;;) {
|
|
690
|
+
let read: { state: ISystemdUnitState; invocationId: string };
|
|
691
|
+
try {
|
|
692
|
+
read = await this.readState(killKeys);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
throw new SystemdUnitError('signalled_uninspected', { cause: error });
|
|
695
|
+
}
|
|
696
|
+
const outcome = killOutcome(read.state, read.invocationId !== invocationId);
|
|
697
|
+
if (outcome !== null) return Object.freeze({ outcome, state: read.state });
|
|
698
|
+
const remaining = deadline - performance.now();
|
|
699
|
+
if (remaining <= 0) return Object.freeze({ outcome: 'timeout', state: read.state });
|
|
700
|
+
await plugins.timers.setTimeout(Math.min(killPollIntervalMs, remaining));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
588
704
|
/** Reports the manager's selected definition without reading or changing its contents. */
|
|
589
705
|
public async inspectConfiguration(): Promise<ISystemdUnitConfigurationState> {
|
|
590
706
|
const keys = ['Id', 'FragmentPath', 'DropInPaths', 'NeedDaemonReload'];
|
|
@@ -6,9 +6,10 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
6
6
|
import * as crypto from 'node:crypto';
|
|
7
7
|
import * as util from 'node:util';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
|
+
import * as timers from 'node:timers/promises';
|
|
9
10
|
export type { Stats } from 'node:fs';
|
|
10
11
|
|
|
11
|
-
export { path, childProcess, fs, fsConstants, crypto, util, os };
|
|
12
|
+
export { path, childProcess, fs, fsConstants, crypto, util, os, timers };
|
|
12
13
|
|
|
13
14
|
// @pushrocks scope
|
|
14
15
|
import * as lik from '@push.rocks/lik';
|
|
@@ -19,6 +19,9 @@ export const outputAppendMajor = 240;
|
|
|
19
19
|
/** Internal: the systemd major whose `systemctl clean` first releases a file descriptor store (`--what=fdstore`). */
|
|
20
20
|
export const fileDescriptorStoreCleanMajor = 254;
|
|
21
21
|
|
|
22
|
+
/** Internal: the systemd major whose `systemctl kill` first takes `--kill-whom=` (formerly `--kill-who=`). */
|
|
23
|
+
export const killWhomMajor = 252;
|
|
24
|
+
|
|
22
25
|
/**
|
|
23
26
|
* Internal: the systemd major whose `systemd-run` first takes `--expand-environment` and reports a
|
|
24
27
|
* started scope as `runInTransientScope()` reads it.
|
|
@@ -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
|
+
}
|