@primafuture/systemd-tasks 1.0.0 → 1.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/CHANGELOG.md +40 -0
- package/README.md +141 -8
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/finalizer.mjs +448 -0
- package/dist/finalizer.mjs.map +1 -0
- package/dist/guardian.mjs +212 -0
- package/dist/guardian.mjs.map +1 -0
- package/dist/index.cjs +9 -13287
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -316
- package/dist/index.d.ts +1 -316
- package/dist/index.mjs +2 -13304
- package/dist/index.mjs.map +1 -1
- package/dist/observed.cjs +35 -0
- package/dist/observed.cjs.map +1 -0
- package/dist/observed.d.cts +1 -0
- package/dist/observed.d.ts +1 -0
- package/dist/observed.mjs +8 -0
- package/dist/observed.mjs.map +1 -0
- package/dist/runtime.cjs +14382 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +487 -0
- package/docs/implementation-decisions.md +137 -0
- package/package.json +27 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
- Add observed `finalizeAfterCompletion(reference, { afterRelease, timeoutMs })`:
|
|
6
|
+
an opt-in transient systemd-owned finalizer survives caller detach and death,
|
|
7
|
+
confirms payload/guardian cleanup, releases existing retention, and executes
|
|
8
|
+
one bounded native after-release command with finite stdin.
|
|
9
|
+
- Bind repeated registrations to the exact reference and callback, preserve
|
|
10
|
+
truthful registration outcomes, and collect the finalizer itself after exit.
|
|
11
|
+
- Cover post-payload reads with the same finalization deadline and honor native
|
|
12
|
+
callback stop grace through manager-owned cgroup termination.
|
|
13
|
+
- Keep default retention, existing intent/reference formats, manual lifecycle,
|
|
14
|
+
and output ownership unchanged.
|
|
15
|
+
|
|
16
|
+
## 1.1.0 — 2026-09-17 (local candidate)
|
|
17
|
+
|
|
18
|
+
- Add the `@primafuture/systemd-tasks/observed` entrypoint with immutable
|
|
19
|
+
JSON-safe intents, finite manager-owned stdin, live binary stdout/stderr, and
|
|
20
|
+
separate payload/guardian lifecycle evidence.
|
|
21
|
+
- Create payload, retention, and `Type=notify` guardian services in one atomic
|
|
22
|
+
transaction. Both process services require successful retention initialization;
|
|
23
|
+
payload stop propagates to a drain-aware guardian even when exec was cancelled.
|
|
24
|
+
Fail closed when the guardian dies, while allowing it to drain output after
|
|
25
|
+
payload exit, stop, timeout, client detach, or client death.
|
|
26
|
+
- Add a versioned bounded guardian protocol, sequential asynchronous callback
|
|
27
|
+
backpressure, explicit local detach, and stable `OBSERVATION_FAILED` errors.
|
|
28
|
+
Reconnect, replay, and stream adoption remain deliberately unsupported.
|
|
29
|
+
- Generalize the existing D-Bus adapter to an ordered auxiliary-unit topology and
|
|
30
|
+
share transport ownership, mutation serialization, notification routing,
|
|
31
|
+
cleanup, diagnostics, and `TaskError` identity across root/observed APIs and
|
|
32
|
+
mixed ESM/CommonJS loading.
|
|
33
|
+
- Upgrade the external native transport to the exact published
|
|
34
|
+
`@primafuture/socket-fdx@1.1.0` release and use its owned `socketPair()` and
|
|
35
|
+
`adoptSocket()` primitives.
|
|
36
|
+
- Extend deterministic, real-systemd, installed ESM/CommonJS, and TypeScript
|
|
37
|
+
consumer tests, including the real 512 KiB stdin boundary, partial socket
|
|
38
|
+
allocation rollback, pre-readiness detach, cross-client pre-exec stop, final
|
|
39
|
+
output under slow-callback backpressure during explicit stop, 32 MiB per-channel draining, and guardian
|
|
40
|
+
fail-closed behavior. Detach also settles independently of an already-pending
|
|
41
|
+
application callback. No npm publish was performed.
|
|
42
|
+
|
|
3
43
|
## 1.0.0 — 2026-09-07
|
|
4
44
|
|
|
5
45
|
- Add explicit preparation, single-attempt submission, lookup, inspection, waiting,
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Run one-shot Linux processes owned by the **systemd user manager**, with literal
|
|
|
4
4
|
arguments, binary file-descriptor I/O, manager-owned timeouts, and recoverable
|
|
5
5
|
results after the Node client exits or crashes.
|
|
6
6
|
|
|
7
|
-
Version 1.
|
|
7
|
+
Version 1.2.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
|
|
8
8
|
and cgroup v2. The verified environment
|
|
9
9
|
is Node 24.13.0, systemd 257.9-0ubuntu2.4, Linux 6.17.0-14-generic, x86-64.
|
|
10
10
|
Other versions satisfying those minimums are not independently verified.
|
|
@@ -15,11 +15,12 @@ Other versions satisfying those minimums are not independently verified.
|
|
|
15
15
|
npm install @primafuture/systemd-tasks
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
The native dependency `@primafuture/socket-fdx@1.1.0` is installed under the npm
|
|
19
|
+
alias `usocket` and supplies both the D-Bus adapter and explicitly owned Unix
|
|
20
|
+
socket-pair primitives. It is pinned to the exact published release and ships a
|
|
21
|
+
prebuilt native binary, so ordinary installation needs no compiler, Python, install
|
|
22
|
+
scripts, or consumer override. Musl Linux and other architectures remain unsupported.
|
|
23
|
+
The JavaScript D-Bus stack
|
|
23
24
|
(`dbus-next@0.10.2` and its dependencies) is bundled; licenses are recorded in
|
|
24
25
|
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
25
26
|
|
|
@@ -29,6 +30,9 @@ that the retention executable resolves to a regular executable file before openi
|
|
|
29
30
|
the bus, then verifies the manager environment. It fails explicitly; the library does not provision a manager,
|
|
30
31
|
enable linger, request elevated privileges, or fall back to child_process.
|
|
31
32
|
Importing either the ESM or CommonJS entrypoint does not connect or start a process.
|
|
33
|
+
Observed tasks additionally require `/usr/bin/systemd-notify`, `/usr/bin/systemctl`,
|
|
34
|
+
and the packaged guardian bundle; `connectObserved()` verifies them before opening
|
|
35
|
+
the bus.
|
|
32
36
|
|
|
33
37
|
## Basic execution
|
|
34
38
|
|
|
@@ -70,6 +74,133 @@ environment that are absent from this map, using a snapshot immediately before
|
|
|
70
74
|
submission. Systemd may add execution metadata such as `INVOCATION_ID`. Concurrent
|
|
71
75
|
external changes to manager configuration are outside this guarantee.
|
|
72
76
|
|
|
77
|
+
## Observed execution
|
|
78
|
+
|
|
79
|
+
The separate `@primafuture/systemd-tasks/observed` entrypoint adds live binary
|
|
80
|
+
stdout/stderr delivery without wrapping the payload process:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import * as observed from '@primafuture/systemd-tasks/observed';
|
|
84
|
+
|
|
85
|
+
const intent = observed.prepareObserved({
|
|
86
|
+
executable: '/usr/bin/sort',
|
|
87
|
+
argv: [],
|
|
88
|
+
cwd: '/tmp',
|
|
89
|
+
env: {},
|
|
90
|
+
runtimeLimitMs: 5000,
|
|
91
|
+
stopGraceMs: 500,
|
|
92
|
+
stdin: 'second\nfirst\n',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const client = await observed.connectObserved();
|
|
96
|
+
try {
|
|
97
|
+
const started = await client.startObserved(intent, {
|
|
98
|
+
async onOutput(event) {
|
|
99
|
+
// Awaiting here applies bounded backpressure to both output channels.
|
|
100
|
+
await consume(event.stream, event.data);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
await started.observation.completion;
|
|
104
|
+
const completed = await client.wait(intent, { timeoutMs: 10000 });
|
|
105
|
+
await client.release(completed.reference);
|
|
106
|
+
} finally {
|
|
107
|
+
await client.disconnect();
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`stdin` is mandatory. A string is snapshotted as UTF-8, a `Uint8Array` is copied,
|
|
112
|
+
an empty value remains an explicit empty data input, and `null` selects `/dev/null`.
|
|
113
|
+
The fixed limit is 512 KiB, chosen to remain below systemd's manager-state line
|
|
114
|
+
serialization boundary after base64 expansion. The canonical base64 snapshot is
|
|
115
|
+
part of the serialized intent and its fingerprint. Systemd owns `StandardInputData` after submission and
|
|
116
|
+
may retain those bytes in manager state until release, so treat the intent and
|
|
117
|
+
manager access as sensitive when stdin contains secrets.
|
|
118
|
+
|
|
119
|
+
Each observed task consists of payload, retention, and `Type=notify` guardian
|
|
120
|
+
services created by one `StartTransientUnit` transaction. The payload is ordered
|
|
121
|
+
after and `BindsTo` the guardian; both process services require successful
|
|
122
|
+
retention initialization. `startObserved()` returns only after manager
|
|
123
|
+
acceptance and guardian readiness, with a fixed five-second readiness budget. A
|
|
124
|
+
failure after manager acceptance reports `mutationOutcome: 'accepted'`; it is not
|
|
125
|
+
permission to retry.
|
|
126
|
+
|
|
127
|
+
Output callbacks are sequential and their returned promises provide real bounded
|
|
128
|
+
backpressure. Callback failure rejects `observation.completion` with
|
|
129
|
+
`OBSERVATION_FAILED` and detaches locally; it does not stop the payload. `detach()`
|
|
130
|
+
and `disconnect()` likewise close only the local observer. The guardian continues
|
|
131
|
+
to drain and discard both channels so the payload does not receive `EPIPE` merely
|
|
132
|
+
because the client left. There is deliberately no reconnect, replay, or adoption
|
|
133
|
+
of another client's live stream. Use ordinary durable file I/O instead when output
|
|
134
|
+
must survive observation loss.
|
|
135
|
+
|
|
136
|
+
An already-invoked callback is application code and cannot be cancelled. Explicit
|
|
137
|
+
detach closes the socket and settles `completion` as `detached` without waiting for
|
|
138
|
+
that callback promise; no later callback is invoked, and a later settlement of the
|
|
139
|
+
detached callback cannot change the observation outcome or block `disconnect()`.
|
|
140
|
+
|
|
141
|
+
The guardian emits a private versioned binary protocol with separate stdout and
|
|
142
|
+
stderr frames. Payload bytes and callback causes are never included in diagnostics.
|
|
143
|
+
`wait()` reports payload and guardian results independently and reaches `completed`
|
|
144
|
+
only after both services are terminal with confirmed empty cgroups. `stop()` targets
|
|
145
|
+
the payload; systemd propagates that stop to the guardian in reverse dependency
|
|
146
|
+
order, so it drains bytes already written before completing. This also closes a
|
|
147
|
+
guardian whose payload start was cancelled before exec. `release()` refuses until
|
|
148
|
+
both process services are clean, then removes retention.
|
|
149
|
+
|
|
150
|
+
### Automatic finalization of a background task
|
|
151
|
+
|
|
152
|
+
Observed clients can explicitly hand completion to the user manager:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const found = await client.lookup(intent);
|
|
156
|
+
if (found.status === 'found' && found.snapshot.identity === 'complete') {
|
|
157
|
+
await client.finalizeAfterCompletion(found.reference, {
|
|
158
|
+
timeoutMs: 30000,
|
|
159
|
+
afterRelease: {
|
|
160
|
+
executable: '/absolute/path/to/node',
|
|
161
|
+
argv: ['/absolute/path/to/application.mjs', '--finish-owned-record'],
|
|
162
|
+
cwd: '/absolute/application/directory',
|
|
163
|
+
env: {},
|
|
164
|
+
runtimeLimitMs: 10000,
|
|
165
|
+
stopGraceMs: 1000,
|
|
166
|
+
stdin: JSON.stringify({ generation: 'application-owned-generation' }),
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Register after persisting the exact reference and before advertising the background
|
|
173
|
+
task as ready. A separate transient finalizer survives observer detach, CLI exit,
|
|
174
|
+
and death of the original client. It does not stop the original task. It waits for
|
|
175
|
+
payload termination and confirmed payload/guardian cleanup, uses the existing
|
|
176
|
+
`release()` checks, and then executes the finite native command once, without a
|
|
177
|
+
shell, inherited environment, or output replay. The command runs in the finalizer's
|
|
178
|
+
own systemd control group; its output is discarded. The finalizer is collected
|
|
179
|
+
after it exits, including failure. Safe error metadata remains in the journal.
|
|
180
|
+
|
|
181
|
+
`timeoutMs` is one shared deadline for lookup, cleanup, release, and the
|
|
182
|
+
after-release command once the finalizer confirms payload termination. It does
|
|
183
|
+
not bound the task's active lifetime or restart for each phase. Callback runtime
|
|
184
|
+
is also limited by its non-null `runtimeLimitMs`, when supplied. On timeout the
|
|
185
|
+
finalizer exits; systemd stops its remaining command processes with `SIGTERM`,
|
|
186
|
+
allows `stopGraceMs`, and escalates to `SIGKILL` only if necessary. Stop grace is
|
|
187
|
+
a separate bounded cleanup interval, not additional execution time. The callback
|
|
188
|
+
may not be a resident service. A timed-out read or release never starts the callback.
|
|
189
|
+
|
|
190
|
+
The receipt says `accepted` or `already-registered`, not `completed`. An identical
|
|
191
|
+
registration while the finalizer exists does not create another callback. A
|
|
192
|
+
different callback for the same task is a conflict. An unknown registration outcome
|
|
193
|
+
must be reconciled before proceeding; never blindly submit a replacement task.
|
|
194
|
+
Failed cleanup does not execute the callback. Callback failure does not restore
|
|
195
|
+
released task results or automatically retry the callback.
|
|
196
|
+
|
|
197
|
+
This is deliberately opt-in and does not change the default retention policy.
|
|
198
|
+
After handing finalization off, do not rely on a late `wait()` retaining an exit
|
|
199
|
+
result: release can win that race. Applications should confirm their own cleanup
|
|
200
|
+
effect plus absence of the exact task rather than fabricate an exit status. The
|
|
201
|
+
callback must condition its own changes on its persisted generation. Recovery is
|
|
202
|
+
still required for a client death before registration and for manager restart.
|
|
203
|
+
|
|
73
204
|
## Lifecycle and recovery
|
|
74
205
|
|
|
75
206
|
| Operation | Contract |
|
|
@@ -83,6 +214,7 @@ external changes to manager configuration are outside this guarantee.
|
|
|
83
214
|
| `stop(reference, options?)` | Request manager stop; `wait: true` additionally waits for cleanup. |
|
|
84
215
|
| `release(reference, options?)` | Refuse live tasks, then discard owned result retention. Never deletes logs. |
|
|
85
216
|
| `disconnect()` | Release local socket/listeners only; tasks keep running. Idempotent. |
|
|
217
|
+
| Observed `finalizeAfterCompletion(reference, options)` | Register exact manager-owned cleanup followed by one finite native command. Opt-in, not a completion receipt. |
|
|
86
218
|
|
|
87
219
|
Save the **intent before calling start**, then save an observed reference and final
|
|
88
220
|
result as they become available. Intents contain the command and environment,
|
|
@@ -206,7 +338,7 @@ service manipulation can invalidate evidence. The library does not enable linger
|
|
|
206
338
|
recreate missing tasks, implement a daemon, or promise survival when the manager
|
|
207
339
|
itself stops. Connection failure is distinct from missing units.
|
|
208
340
|
|
|
209
|
-
The only advanced properties allowed in 1.
|
|
341
|
+
The only advanced properties allowed in 1.x are `CPUWeight` and `IOWeight`, each
|
|
210
342
|
with signature `'t'` and an integer value from 1 to 10000. Duplicates, unknown
|
|
211
343
|
properties, and lifecycle overrides are rejected before mutation. Kernel/controller
|
|
212
344
|
configuration determines the practical scheduling effect of those weights.
|
|
@@ -228,7 +360,8 @@ npm run check
|
|
|
228
360
|
`check` runs strict typechecking, unit tests, real systemd integration tests, dual
|
|
229
361
|
ESM/CJS build, and tarball runtime/type smoke tests. `prepublishOnly` invokes the
|
|
230
362
|
same chain. Integration tests fail explicitly when prerequisites are missing.
|
|
231
|
-
Tests create unique identities and clean only their recorded task/retention pairs
|
|
363
|
+
Tests create unique identities and clean only their recorded task/retention pairs,
|
|
364
|
+
plus the exact guardian identity for observed tests.
|
|
232
365
|
They do not restart the user manager or change host configuration.
|
|
233
366
|
|
|
234
367
|
Runnable repository examples after `npm run build`:
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -25,7 +25,7 @@ This file is generated by `scripts/licenses.ts` from the distributed source map.
|
|
|
25
25
|
|
|
26
26
|
## External native dependency
|
|
27
27
|
|
|
28
|
-
`@primafuture/socket-fdx` 1.
|
|
28
|
+
`@primafuture/socket-fdx` 1.1.0 (ISC) is installed separately under the npm alias `usocket`. It ships its native binary, license, and third-party notices, has no runtime dependencies, and is not embedded in this JavaScript bundle. The direct pinned alias applies in consumer installations without root overrides.
|
|
29
29
|
|
|
30
30
|
## @nornagon/put 0.0.8
|
|
31
31
|
|