@primafuture/systemd-tasks 1.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/CHANGELOG.md +31 -0
- package/LICENSE +15 -0
- package/README.md +250 -0
- package/THIRD_PARTY_NOTICES.md +1101 -0
- package/dist/index.cjs +13315 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +316 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.mjs +13311 -0
- package/dist/index.mjs.map +1 -0
- package/docs/implementation-decisions.md +323 -0
- package/package.json +63 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 — 2026-09-07
|
|
4
|
+
|
|
5
|
+
- Add explicit preparation, single-attempt submission, lookup, inspection, waiting,
|
|
6
|
+
stopping, release, and disconnect for systemd user-manager tasks.
|
|
7
|
+
- Preserve literal arguments, explicit environment entries, and binary Unix FD I/O
|
|
8
|
+
with defined borrowed/file ownership and append/truncate semantics.
|
|
9
|
+
- Retain results through client crashes using an auxiliary transient oneshot;
|
|
10
|
+
verify manager and invocation identities before lifecycle mutations.
|
|
11
|
+
- Separate acceptance, execution evidence, exit result, and descendant cleanup;
|
|
12
|
+
retain unknown mutation outcomes without automatic retries.
|
|
13
|
+
- Provide independent cancellable waiters, manager-owned escalation, redacted
|
|
14
|
+
correlated diagnostics, and a CPUWeight/IOWeight property allowlist.
|
|
15
|
+
- Ship ESM/CommonJS builds and declarations, native FD dependency licensing,
|
|
16
|
+
runnable recovery examples, and real plus deterministic lifecycle tests.
|
|
17
|
+
- Reject unavailable internal retention executables during connection preflight,
|
|
18
|
+
including missing paths, non-regular files, and missing execute permissions.
|
|
19
|
+
- Replace upstream usocket with the pinned `@primafuture/socket-fdx@1.0.0`
|
|
20
|
+
alias, shipping a prebuilt FD transport without consumer overrides or install
|
|
21
|
+
scripts. Require Node 24.13.0+ and Linux x64 with glibc 2.28+.
|
|
22
|
+
- Classify native platform, ABI, and missing-binary failures as unsupported
|
|
23
|
+
environment; exercise overlapping stop/release and repeated FD-bearing success
|
|
24
|
+
and rejection paths through real connections.
|
|
25
|
+
- Give release an independent configurable local deadline and cancellation,
|
|
26
|
+
preserving acknowledged cleanup mutations across observation timeouts and later
|
|
27
|
+
rejected or uncertain cleanup steps through explicit progress metadata.
|
|
28
|
+
- Isolate asynchronous diagnostic sink rejections without delaying task operations.
|
|
29
|
+
|
|
30
|
+
Verified on Node 24.13.0 and systemd 257.9 on Linux x86-64 with cgroup v2.
|
|
31
|
+
Retention does not survive manager shutdown or reboot.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PrimaFuture.cz s.r.o.
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# @primafuture/systemd-tasks
|
|
2
|
+
|
|
3
|
+
Run one-shot Linux processes owned by the **systemd user manager**, with literal
|
|
4
|
+
arguments, binary file-descriptor I/O, manager-owned timeouts, and recoverable
|
|
5
|
+
results after the Node client exits or crashes.
|
|
6
|
+
|
|
7
|
+
Version 1.0.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
|
|
8
|
+
and cgroup v2. The verified environment
|
|
9
|
+
is Node 24.13.0, systemd 257.9-0ubuntu2.4, Linux 6.17.0-14-generic, x86-64.
|
|
10
|
+
Other versions satisfying those minimums are not independently verified.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @primafuture/systemd-tasks
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The pinned native dependency `@primafuture/socket-fdx@1.0.0` is installed under
|
|
19
|
+
the npm alias `usocket` and supplies its compatible `USocket` adapter. It ships a
|
|
20
|
+
prebuilt native binary: ordinary installation needs no compiler, Python, or install
|
|
21
|
+
scripts. Consumers do not need `overrides`. The dependency is pinned to its stable release;
|
|
22
|
+
musl Linux and other architectures are outside its current support. The JavaScript D-Bus stack
|
|
23
|
+
(`dbus-next@0.10.2` and its dependencies) is bundled; licenses are recorded in
|
|
24
|
+
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
25
|
+
|
|
26
|
+
An existing local user bus at `/run/user/<uid>/bus`, a reachable systemd user
|
|
27
|
+
manager, Unix FD negotiation, and `/usr/bin/true` are required. `connect()` checks
|
|
28
|
+
that the retention executable resolves to a regular executable file before opening
|
|
29
|
+
the bus, then verifies the manager environment. It fails explicitly; the library does not provision a manager,
|
|
30
|
+
enable linger, request elevated privileges, or fall back to child_process.
|
|
31
|
+
Importing either the ESM or CommonJS entrypoint does not connect or start a process.
|
|
32
|
+
|
|
33
|
+
## Basic execution
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import * as tasks from '@primafuture/systemd-tasks';
|
|
37
|
+
|
|
38
|
+
const intent = tasks.prepare({
|
|
39
|
+
executable: '/usr/bin/printf',
|
|
40
|
+
argv: ['%s\n', 'Literal $HOME, spaces, and Unicode: žluťoučký'],
|
|
41
|
+
cwd: '/tmp',
|
|
42
|
+
env: {},
|
|
43
|
+
runtimeLimitMs: 5000,
|
|
44
|
+
stopGraceMs: 500,
|
|
45
|
+
});
|
|
46
|
+
const client = await tasks.connect();
|
|
47
|
+
try {
|
|
48
|
+
await client.start(intent, {
|
|
49
|
+
...tasks.nullIO,
|
|
50
|
+
stdout: { type: 'file', path: '/tmp/task-output.log', mode: 'append' },
|
|
51
|
+
});
|
|
52
|
+
const completed = await client.wait(intent, { timeoutMs: 10000 });
|
|
53
|
+
console.log(completed.result);
|
|
54
|
+
if (completed.phase === 'completed') {
|
|
55
|
+
// Persist any required result before discarding manager retention.
|
|
56
|
+
await client.release(completed.reference);
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
await client.disconnect();
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`executable` and `cwd` must be absolute; no PATH lookup or shell parsing is performed.
|
|
64
|
+
`argv` excludes argv[0]. Empty arguments, `$`, `%`, quotes, and Unicode remain
|
|
65
|
+
literal. Every task must explicitly choose `runtimeLimitMs` (positive integer
|
|
66
|
+
milliseconds or `null`) and `stopGraceMs` (positive integer milliseconds).
|
|
67
|
+
|
|
68
|
+
`env` is an explicit map. The library removes names from the manager's inherited
|
|
69
|
+
environment that are absent from this map, using a snapshot immediately before
|
|
70
|
+
submission. Systemd may add execution metadata such as `INVOCATION_ID`. Concurrent
|
|
71
|
+
external changes to manager configuration are outside this guarantee.
|
|
72
|
+
|
|
73
|
+
## Lifecycle and recovery
|
|
74
|
+
|
|
75
|
+
| Operation | Contract |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `prepare(spec)` | Pure local validation, random identity, frozen serializable intent. No I/O or execution. |
|
|
78
|
+
| `connect(options?)` | Explicit local connection, environment checks, immutable manager identity. |
|
|
79
|
+
| `start(intent, io)` | One creation attempt. A receipt means manager acceptance, not successful exec. |
|
|
80
|
+
| `lookup(intentOrReference)` | Observe existing work: `found`, `not-found`, or `incomplete`. Never starts anything. |
|
|
81
|
+
| `inspect(reference)` | Snapshot of the exact instance; missing evidence is an error. |
|
|
82
|
+
| `wait(intentOrReference, options?)` | Wait for terminal manager state and report cleanup evidence. |
|
|
83
|
+
| `stop(reference, options?)` | Request manager stop; `wait: true` additionally waits for cleanup. |
|
|
84
|
+
| `release(reference, options?)` | Refuse live tasks, then discard owned result retention. Never deletes logs. |
|
|
85
|
+
| `disconnect()` | Release local socket/listeners only; tasks keep running. Idempotent. |
|
|
86
|
+
|
|
87
|
+
Save the **intent before calling start**, then save an observed reference and final
|
|
88
|
+
result as they become available. Intents contain the command and environment,
|
|
89
|
+
possibly secrets. Use application-appropriate permissions and durable storage.
|
|
90
|
+
A prepared intent does not prove that submission ever occurred.
|
|
91
|
+
|
|
92
|
+
A second process can deserialize an intent, connect, and call `lookup` or `wait`.
|
|
93
|
+
References bind the task UUID, specification fingerprint, manager scope, UID,
|
|
94
|
+
machine/boot/bus identity, manager bus owner, and both service InvocationIDs.
|
|
95
|
+
Missing InvocationIDs are explicitly `null`; an incomplete reference cannot
|
|
96
|
+
silently adopt a later invocation. Waiting on an intent pins IDs as they appear.
|
|
97
|
+
PIDs are diagnostic information, never ownership identity.
|
|
98
|
+
|
|
99
|
+
`not-found` means that no owned record is currently observable in the reachable
|
|
100
|
+
manager. It does **not** prove that the command never executed. `incomplete` means
|
|
101
|
+
only the retention unit remains and the main result is unavailable. A found
|
|
102
|
+
snapshot can itself have incomplete invocation evidence, for example after
|
|
103
|
+
retention initialization prevented execution.
|
|
104
|
+
|
|
105
|
+
Start errors expose `TaskError.details.mutationOutcome`:
|
|
106
|
+
|
|
107
|
+
| Outcome | Recovery meaning |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `not-submitted` | No creation request was sent by this operation; file-opening effects may exist. |
|
|
110
|
+
| `rejected` | A manager error reply was received; inspect possible partial records. |
|
|
111
|
+
| `unknown` | Delivery or acceptance cannot be established. Reconnect and look up the saved identity. |
|
|
112
|
+
| `accepted` | Acceptance was confirmed; a later local stop wait can still fail or be cancelled. |
|
|
113
|
+
|
|
114
|
+
**Never automatically retry a start with an unknown outcome**, even when lookup
|
|
115
|
+
returns `not-found`. Local duplicate protection lasts only for that client; this
|
|
116
|
+
is not a durable exactly-once ledger. Creating another intent is a new execution
|
|
117
|
+
and requires an application decision. There is no automatic reconnect or retry.
|
|
118
|
+
|
|
119
|
+
References guard accidental collisions and stale instances. They are not a security
|
|
120
|
+
boundary against equally privileged processes. Systemd has no conditional
|
|
121
|
+
"stop only if InvocationID equals X" method; read/validate/mutate has a race if an
|
|
122
|
+
external actor replaces a unit between those steps. Do not modify library-owned
|
|
123
|
+
units concurrently through another controller.
|
|
124
|
+
|
|
125
|
+
## Results, timeouts, and cleanup
|
|
126
|
+
|
|
127
|
+
`TaskSnapshot.result` is a normal result for application failures, signals, and
|
|
128
|
+
execution failures. It contains `kind`, `exitCode`, `signal`, `execution`,
|
|
129
|
+
`systemdResult`, and `cleanup`. The snapshot also preserves raw exit, timestamp,
|
|
130
|
+
job, and cgroup evidence. A program returning 203 differs from systemd failing to
|
|
131
|
+
exec with status 203; classification uses retained `Type=exec` activation evidence.
|
|
132
|
+
|
|
133
|
+
`phase: 'completed'` requires terminal manager state, no pending job, and confirmed
|
|
134
|
+
cgroup cleanup. Main-process exit alone is insufficient. `wait()` can return a
|
|
135
|
+
terminal snapshot with `cleanup: 'incomplete'` or `'unknown'`; inspect that field
|
|
136
|
+
and do not treat it as successful completion or attempt release. Such a snapshot
|
|
137
|
+
remains in `phase: 'cleaning'`.
|
|
138
|
+
|
|
139
|
+
Runtime limits, SIGTERM, stop grace, SIGKILL escalation, and descendant cleanup are
|
|
140
|
+
owned by systemd. They continue after client death. `Result=timeout` can mean a
|
|
141
|
+
runtime deadline, stop escalation, or descendant cleanup escalation; the library
|
|
142
|
+
preserves it without claiming which occurred. Descendants remaining in the task
|
|
143
|
+
cgroup are covered, including ordinary setsid descendants. Privileged processes
|
|
144
|
+
that escape the cgroup are outside the guarantee.
|
|
145
|
+
|
|
146
|
+
`wait`/`stop` options `signal` and `timeoutMs` affect only that caller's local wait.
|
|
147
|
+
They do not stop a task, cancel an accepted stop, or cancel other waiters. Local
|
|
148
|
+
timeouts are positive integers up to 2147483647 ms. Stop cancellation while its
|
|
149
|
+
request is in flight reports unknown delivery; after an acknowledgment, the error
|
|
150
|
+
retains `mutationOutcome: 'accepted'`.
|
|
151
|
+
|
|
152
|
+
## Binary I/O and ownership
|
|
153
|
+
|
|
154
|
+
All three channels are explicit. `nullIO` provides EOF stdin and discarded output.
|
|
155
|
+
|
|
156
|
+
| Channel value | Ownership and behavior |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `{ type: 'null' }` | Selects systemd's null channel mode; the client opens no descriptor. |
|
|
159
|
+
| `{ type: 'file', path }` for stdin | Existing regular file, opened for reading before submission. |
|
|
160
|
+
| `{ type: 'file', path, mode: 'append' \| 'truncate' }` for output | Regular file; parent directory must exist. Creation/truncation is not transactional. |
|
|
161
|
+
| `{ type: 'fd', fd }` | Borrowed Unix FD; the caller must keep it open until start settles. Never closed by the library. |
|
|
162
|
+
|
|
163
|
+
The transport passes descriptors directly to systemd; the client never decodes or
|
|
164
|
+
buffers process output. File output survives client death. Successful submission
|
|
165
|
+
does not mean the task has finished using its manager-held descriptor.
|
|
166
|
+
|
|
167
|
+
Identical output paths and aliases of the same inode with matching modes share one
|
|
168
|
+
open file description and offset. Conflicting modes are rejected. Opens and alias
|
|
169
|
+
checks precede truncation; a later manager rejection or exec failure can still leave
|
|
170
|
+
created or truncated files. Truncating an input/output alias is rejected. Existing
|
|
171
|
+
symlinks are followed; this is not a sandbox for untrusted paths. Use borrowed FDs
|
|
172
|
+
when your application needs stricter opening policies or non-regular files.
|
|
173
|
+
|
|
174
|
+
Borrowed FDs share underlying offsets and flags with the application. Unix pipes
|
|
175
|
+
retain normal backpressure, EOF, and EPIPE behavior. Closing a reader can break task
|
|
176
|
+
output. Reconnecting does not reconstruct lost pipe bytes or create stream history.
|
|
177
|
+
The library does not own application streams and never waits for task-held pipe EOF
|
|
178
|
+
when disconnecting. Regular files are the durable-output option. Release leaves
|
|
179
|
+
those files untouched.
|
|
180
|
+
|
|
181
|
+
## Retention and operational limits
|
|
182
|
+
|
|
183
|
+
Each task uses two transient services: the actual `Type=exec` service and a short
|
|
184
|
+
`/usr/bin/true` oneshot with `RemainAfterExit=yes`. Retention becomes active before
|
|
185
|
+
the user command runs. It keeps the completed service referenced without a resident
|
|
186
|
+
helper process. Both records consume manager memory until explicit release; failed
|
|
187
|
+
records are reset during release. Garbage collection may follow asynchronously,
|
|
188
|
+
which is reflected by `ReleaseReceipt.collected`.
|
|
189
|
+
|
|
190
|
+
`release(reference, { timeoutMs, signal })` has its own total local deadline,
|
|
191
|
+
including time queued behind another mutation; it defaults to 5000 ms.
|
|
192
|
+
`connect({ requestTimeoutMs })` independently limits each individual D-Bus request,
|
|
193
|
+
not the sum of release's reads and mutations. Increasing the release deadline does
|
|
194
|
+
not relax the per-request deadline. Cancellation does not undo manager mutations.
|
|
195
|
+
Release errors include `acceptedCleanupMutations`, the number of acknowledged
|
|
196
|
+
reset/stop calls during that attempt. If earlier cleanup was accepted but a later
|
|
197
|
+
step is explicitly rejected or not submitted, `mutationOutcome` remains `accepted`:
|
|
198
|
+
the manager has already acknowledged part of cleanup. If a later step has unknown delivery,
|
|
199
|
+
the outcome is `unknown` and the positive count still preserves the earlier facts.
|
|
200
|
+
Neither acceptance nor the count proves completion of the whole release sequence.
|
|
201
|
+
Reconcile the reference before continuing cleanup.
|
|
202
|
+
|
|
203
|
+
Retention is not persistent storage across manager restart, logout that stops the
|
|
204
|
+
user manager, bus replacement, or reboot. Store final results yourself. External
|
|
205
|
+
service manipulation can invalidate evidence. The library does not enable linger,
|
|
206
|
+
recreate missing tasks, implement a daemon, or promise survival when the manager
|
|
207
|
+
itself stops. Connection failure is distinct from missing units.
|
|
208
|
+
|
|
209
|
+
The only advanced properties allowed in 1.0.0 are `CPUWeight` and `IOWeight`, each
|
|
210
|
+
with signature `'t'` and an integer value from 1 to 10000. Duplicates, unknown
|
|
211
|
+
properties, and lifecycle overrides are rejected before mutation. Kernel/controller
|
|
212
|
+
configuration determines the practical scheduling effect of those weights.
|
|
213
|
+
|
|
214
|
+
Optional `connect({ onDiagnostic })` receives structured events correlated by
|
|
215
|
+
`context.connection` and `context.task`. No argv, environment, output, or raw error
|
|
216
|
+
cause is logged implicitly. Synchronous sink exceptions and rejected promises or
|
|
217
|
+
thenables do not change task outcomes; asynchronous sinks are never awaited.
|
|
218
|
+
`TaskError.cause` is explicitly accessible and may contain sensitive upstream text;
|
|
219
|
+
default error inspection and JSON formatting are redacted.
|
|
220
|
+
|
|
221
|
+
## Development and verification
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
npm ci --prefix "$PWD"
|
|
225
|
+
npm run check
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`check` runs strict typechecking, unit tests, real systemd integration tests, dual
|
|
229
|
+
ESM/CJS build, and tarball runtime/type smoke tests. `prepublishOnly` invokes the
|
|
230
|
+
same chain. Integration tests fail explicitly when prerequisites are missing.
|
|
231
|
+
Tests create unique identities and clean only their recorded task/retention pairs.
|
|
232
|
+
They do not restart the user manager or change host configuration.
|
|
233
|
+
|
|
234
|
+
Runnable repository examples after `npm run build`:
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
npx tsx examples/basic.ts
|
|
238
|
+
npx tsx examples/durable-start.ts
|
|
239
|
+
# Use the printed record directory from the preceding command:
|
|
240
|
+
npx tsx examples/durable-reconnect.ts /absolute/path/to/record-directory
|
|
241
|
+
npx tsx examples/timeout-stop.ts
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The two durable examples are independent processes. The first fsyncs its intent
|
|
245
|
+
before submission and disconnects; the second fsyncs the result before release.
|
|
246
|
+
Generated records stay under `.artifacts/examples` and are not published.
|
|
247
|
+
|
|
248
|
+
See [implementation decisions and acceptance evidence](docs/implementation-decisions.md)
|
|
249
|
+
for the feasibility experiments, T01–T18 mapping, exact versions, simulated failure
|
|
250
|
+
boundaries, and unverified environments.
|