npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@primafuture/systemd-tasks

v1.1.0

Published

One-shot process tasks owned by the systemd user manager.

Readme

@primafuture/systemd-tasks

Run one-shot Linux processes owned by the systemd user manager, with literal arguments, binary file-descriptor I/O, manager-owned timeouts, and recoverable results after the Node client exits or crashes.

Version 1.1.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+, and cgroup v2. The verified environment is Node 24.13.0, systemd 257.9-0ubuntu2.4, Linux 6.17.0-14-generic, x86-64. Other versions satisfying those minimums are not independently verified.

Installation

npm install @primafuture/systemd-tasks

The native dependency @primafuture/[email protected] is installed under the npm alias usocket and supplies both the D-Bus adapter and explicitly owned Unix socket-pair primitives. It is pinned to the exact published release and ships a prebuilt native binary, so ordinary installation needs no compiler, Python, install scripts, or consumer override. Musl Linux and other architectures remain unsupported. The JavaScript D-Bus stack ([email protected] and its dependencies) is bundled; licenses are recorded in THIRD_PARTY_NOTICES.md.

An existing local user bus at /run/user/<uid>/bus, a reachable systemd user manager, Unix FD negotiation, and /usr/bin/true are required. connect() checks that the retention executable resolves to a regular executable file before opening the bus, then verifies the manager environment. It fails explicitly; the library does not provision a manager, enable linger, request elevated privileges, or fall back to child_process. Importing either the ESM or CommonJS entrypoint does not connect or start a process. Observed tasks additionally require /usr/bin/systemd-notify, /usr/bin/systemctl, and the packaged guardian bundle; connectObserved() verifies them before opening the bus.

Basic execution

import * as tasks from '@primafuture/systemd-tasks';

const intent = tasks.prepare({
	executable: '/usr/bin/printf',
	argv: ['%s\n', 'Literal $HOME, spaces, and Unicode: žluťoučký'],
	cwd: '/tmp',
	env: {},
	runtimeLimitMs: 5000,
	stopGraceMs: 500,
});
const client = await tasks.connect();
try {
	await client.start(intent, {
		...tasks.nullIO,
		stdout: { type: 'file', path: '/tmp/task-output.log', mode: 'append' },
	});
	const completed = await client.wait(intent, { timeoutMs: 10000 });
	console.log(completed.result);
	if (completed.phase === 'completed') {
		// Persist any required result before discarding manager retention.
		await client.release(completed.reference);
	}
} finally {
	await client.disconnect();
}

executable and cwd must be absolute; no PATH lookup or shell parsing is performed. argv excludes argv[0]. Empty arguments, $, %, quotes, and Unicode remain literal. Every task must explicitly choose runtimeLimitMs (positive integer milliseconds or null) and stopGraceMs (positive integer milliseconds).

env is an explicit map. The library removes names from the manager's inherited environment that are absent from this map, using a snapshot immediately before submission. Systemd may add execution metadata such as INVOCATION_ID. Concurrent external changes to manager configuration are outside this guarantee.

Observed execution

The separate @primafuture/systemd-tasks/observed entrypoint adds live binary stdout/stderr delivery without wrapping the payload process:

import * as observed from '@primafuture/systemd-tasks/observed';

const intent = observed.prepareObserved({
	executable: '/usr/bin/sort',
	argv: [],
	cwd: '/tmp',
	env: {},
	runtimeLimitMs: 5000,
	stopGraceMs: 500,
	stdin: 'second\nfirst\n',
});

const client = await observed.connectObserved();
try {
	const started = await client.startObserved(intent, {
		async onOutput(event) {
			// Awaiting here applies bounded backpressure to both output channels.
			await consume(event.stream, event.data);
		},
	});
	await started.observation.completion;
	const completed = await client.wait(intent, { timeoutMs: 10000 });
	await client.release(completed.reference);
} finally {
	await client.disconnect();
}

stdin is mandatory. A string is snapshotted as UTF-8, a Uint8Array is copied, an empty value remains an explicit empty data input, and null selects /dev/null. The fixed limit is 512 KiB, chosen to remain below systemd's manager-state line serialization boundary after base64 expansion. The canonical base64 snapshot is part of the serialized intent and its fingerprint. Systemd owns StandardInputData after submission and may retain those bytes in manager state until release, so treat the intent and manager access as sensitive when stdin contains secrets.

Each observed task consists of payload, retention, and Type=notify guardian services created by one StartTransientUnit transaction. The payload is ordered after and BindsTo the guardian; both process services require successful retention initialization. startObserved() returns only after manager acceptance and guardian readiness, with a fixed five-second readiness budget. A failure after manager acceptance reports mutationOutcome: 'accepted'; it is not permission to retry.

Output callbacks are sequential and their returned promises provide real bounded backpressure. Callback failure rejects observation.completion with OBSERVATION_FAILED and detaches locally; it does not stop the payload. detach() and disconnect() likewise close only the local observer. The guardian continues to drain and discard both channels so the payload does not receive EPIPE merely because the client left. There is deliberately no reconnect, replay, or adoption of another client's live stream. Use ordinary durable file I/O instead when output must survive observation loss.

An already-invoked callback is application code and cannot be cancelled. Explicit detach closes the socket and settles completion as detached without waiting for that callback promise; no later callback is invoked, and a later settlement of the detached callback cannot change the observation outcome or block disconnect().

The guardian emits a private versioned binary protocol with separate stdout and stderr frames. Payload bytes and callback causes are never included in diagnostics. wait() reports payload and guardian results independently and reaches completed only after both services are terminal with confirmed empty cgroups. stop() targets the payload; systemd propagates that stop to the guardian in reverse dependency order, so it drains bytes already written before completing. This also closes a guardian whose payload start was cancelled before exec. release() refuses until both process services are clean, then removes retention.

Lifecycle and recovery

| Operation | Contract | | --- | --- | | prepare(spec) | Pure local validation, random identity, frozen serializable intent. No I/O or execution. | | connect(options?) | Explicit local connection, environment checks, immutable manager identity. | | start(intent, io) | One creation attempt. A receipt means manager acceptance, not successful exec. | | lookup(intentOrReference) | Observe existing work: found, not-found, or incomplete. Never starts anything. | | inspect(reference) | Snapshot of the exact instance; missing evidence is an error. | | wait(intentOrReference, options?) | Wait for terminal manager state and report cleanup evidence. | | stop(reference, options?) | Request manager stop; wait: true additionally waits for cleanup. | | release(reference, options?) | Refuse live tasks, then discard owned result retention. Never deletes logs. | | disconnect() | Release local socket/listeners only; tasks keep running. Idempotent. |

Save the intent before calling start, then save an observed reference and final result as they become available. Intents contain the command and environment, possibly secrets. Use application-appropriate permissions and durable storage. A prepared intent does not prove that submission ever occurred.

A second process can deserialize an intent, connect, and call lookup or wait. References bind the task UUID, specification fingerprint, manager scope, UID, machine/boot/bus identity, manager bus owner, and both service InvocationIDs. Missing InvocationIDs are explicitly null; an incomplete reference cannot silently adopt a later invocation. Waiting on an intent pins IDs as they appear. PIDs are diagnostic information, never ownership identity.

not-found means that no owned record is currently observable in the reachable manager. It does not prove that the command never executed. incomplete means only the retention unit remains and the main result is unavailable. A found snapshot can itself have incomplete invocation evidence, for example after retention initialization prevented execution.

Start errors expose TaskError.details.mutationOutcome:

| Outcome | Recovery meaning | | --- | --- | | not-submitted | No creation request was sent by this operation; file-opening effects may exist. | | rejected | A manager error reply was received; inspect possible partial records. | | unknown | Delivery or acceptance cannot be established. Reconnect and look up the saved identity. | | accepted | Acceptance was confirmed; a later local stop wait can still fail or be cancelled. |

Never automatically retry a start with an unknown outcome, even when lookup returns not-found. Local duplicate protection lasts only for that client; this is not a durable exactly-once ledger. Creating another intent is a new execution and requires an application decision. There is no automatic reconnect or retry.

References guard accidental collisions and stale instances. They are not a security boundary against equally privileged processes. Systemd has no conditional "stop only if InvocationID equals X" method; read/validate/mutate has a race if an external actor replaces a unit between those steps. Do not modify library-owned units concurrently through another controller.

Results, timeouts, and cleanup

TaskSnapshot.result is a normal result for application failures, signals, and execution failures. It contains kind, exitCode, signal, execution, systemdResult, and cleanup. The snapshot also preserves raw exit, timestamp, job, and cgroup evidence. A program returning 203 differs from systemd failing to exec with status 203; classification uses retained Type=exec activation evidence.

phase: 'completed' requires terminal manager state, no pending job, and confirmed cgroup cleanup. Main-process exit alone is insufficient. wait() can return a terminal snapshot with cleanup: 'incomplete' or 'unknown'; inspect that field and do not treat it as successful completion or attempt release. Such a snapshot remains in phase: 'cleaning'.

Runtime limits, SIGTERM, stop grace, SIGKILL escalation, and descendant cleanup are owned by systemd. They continue after client death. Result=timeout can mean a runtime deadline, stop escalation, or descendant cleanup escalation; the library preserves it without claiming which occurred. Descendants remaining in the task cgroup are covered, including ordinary setsid descendants. Privileged processes that escape the cgroup are outside the guarantee.

wait/stop options signal and timeoutMs affect only that caller's local wait. They do not stop a task, cancel an accepted stop, or cancel other waiters. Local timeouts are positive integers up to 2147483647 ms. Stop cancellation while its request is in flight reports unknown delivery; after an acknowledgment, the error retains mutationOutcome: 'accepted'.

Binary I/O and ownership

All three channels are explicit. nullIO provides EOF stdin and discarded output.

| Channel value | Ownership and behavior | | --- | --- | | { type: 'null' } | Selects systemd's null channel mode; the client opens no descriptor. | | { type: 'file', path } for stdin | Existing regular file, opened for reading before submission. | | { type: 'file', path, mode: 'append' \| 'truncate' } for output | Regular file; parent directory must exist. Creation/truncation is not transactional. | | { type: 'fd', fd } | Borrowed Unix FD; the caller must keep it open until start settles. Never closed by the library. |

The transport passes descriptors directly to systemd; the client never decodes or buffers process output. File output survives client death. Successful submission does not mean the task has finished using its manager-held descriptor.

Identical output paths and aliases of the same inode with matching modes share one open file description and offset. Conflicting modes are rejected. Opens and alias checks precede truncation; a later manager rejection or exec failure can still leave created or truncated files. Truncating an input/output alias is rejected. Existing symlinks are followed; this is not a sandbox for untrusted paths. Use borrowed FDs when your application needs stricter opening policies or non-regular files.

Borrowed FDs share underlying offsets and flags with the application. Unix pipes retain normal backpressure, EOF, and EPIPE behavior. Closing a reader can break task output. Reconnecting does not reconstruct lost pipe bytes or create stream history. The library does not own application streams and never waits for task-held pipe EOF when disconnecting. Regular files are the durable-output option. Release leaves those files untouched.

Retention and operational limits

Each task uses two transient services: the actual Type=exec service and a short /usr/bin/true oneshot with RemainAfterExit=yes. Retention becomes active before the user command runs. It keeps the completed service referenced without a resident helper process. Both records consume manager memory until explicit release; failed records are reset during release. Garbage collection may follow asynchronously, which is reflected by ReleaseReceipt.collected.

release(reference, { timeoutMs, signal }) has its own total local deadline, including time queued behind another mutation; it defaults to 5000 ms. connect({ requestTimeoutMs }) independently limits each individual D-Bus request, not the sum of release's reads and mutations. Increasing the release deadline does not relax the per-request deadline. Cancellation does not undo manager mutations. Release errors include acceptedCleanupMutations, the number of acknowledged reset/stop calls during that attempt. If earlier cleanup was accepted but a later step is explicitly rejected or not submitted, mutationOutcome remains accepted: the manager has already acknowledged part of cleanup. If a later step has unknown delivery, the outcome is unknown and the positive count still preserves the earlier facts. Neither acceptance nor the count proves completion of the whole release sequence. Reconcile the reference before continuing cleanup.

Retention is not persistent storage across manager restart, logout that stops the user manager, bus replacement, or reboot. Store final results yourself. External service manipulation can invalidate evidence. The library does not enable linger, recreate missing tasks, implement a daemon, or promise survival when the manager itself stops. Connection failure is distinct from missing units.

The only advanced properties allowed in 1.x are CPUWeight and IOWeight, each with signature 't' and an integer value from 1 to 10000. Duplicates, unknown properties, and lifecycle overrides are rejected before mutation. Kernel/controller configuration determines the practical scheduling effect of those weights.

Optional connect({ onDiagnostic }) receives structured events correlated by context.connection and context.task. No argv, environment, output, or raw error cause is logged implicitly. Synchronous sink exceptions and rejected promises or thenables do not change task outcomes; asynchronous sinks are never awaited. TaskError.cause is explicitly accessible and may contain sensitive upstream text; default error inspection and JSON formatting are redacted.

Development and verification

npm ci --prefix "$PWD"
npm run check

check runs strict typechecking, unit tests, real systemd integration tests, dual ESM/CJS build, and tarball runtime/type smoke tests. prepublishOnly invokes the same chain. Integration tests fail explicitly when prerequisites are missing. Tests create unique identities and clean only their recorded task/retention pairs, plus the exact guardian identity for observed tests. They do not restart the user manager or change host configuration.

Runnable repository examples after npm run build:

npx tsx examples/basic.ts
npx tsx examples/durable-start.ts
# Use the printed record directory from the preceding command:
npx tsx examples/durable-reconnect.ts /absolute/path/to/record-directory
npx tsx examples/timeout-stop.ts

The two durable examples are independent processes. The first fsyncs its intent before submission and disconnects; the second fsyncs the result before release. Generated records stay under .artifacts/examples and are not published.

See implementation decisions and acceptance evidence for the feasibility experiments, T01–T18 mapping, exact versions, simulated failure boundaries, and unverified environments.