preprintd
Printer swarm-worker daemon implementation for PreConnect.
Overview
This tiny worker is just a TcpStream under the hood, constantly listening for jobs and claiming one if open. It works by constantly listening for incoming data from the api.preconnect.app endpoint (which uses Mercure under the hood for streaming real-time data), and then initiating the claiming procedure.
Compiling
Requires Rust (2024 edition or later) to be installed.
Run the traditional release command:
You can also directly install the preprintd binary globally using cargo:
[!NOTE] The release binary is optimized for the smallest-possible size, although you can change this behavior by disabling the optimizations specified in the
[profile.release]section of Cargo.toml.
Prebuilt Binaries
See the GitHub Releases for a prebuilt binary for either Windows, Linux (built via CI workers running Ubuntu), or macOS. The Linux builds are done using the x86_64-unknown-linux-musl target in Rust (see musl.cc).
Daemon Usage
Create a new systemd service which you can enable later:
Write this INI configuration in your preprintd.service file. Make sure to replace the following fields/values:
- Under
Environment=:
WORKER_KEY: Your worker key credential (from the PreConnect API).DEF_HOST: The default printer host to use in case the API cannot provide one.DEF_QUEUE: The default queue name to send printable data to.- (Optional)
ALIAS: The name which determines the program's identity on the system and in TCP requests.
- Replace
/usr/bin/preprintdwith the appropriate path to the daemon binary.
[!WARNING] Since
preprintddoes not require access to user-specific paths, theUserfield under[Service]could be virtually any value depending on your environment.
Enable and start it once you're done:
# now check status:
To check the logs in real-time, run:
Inhibitor Locks (Linux-only)
You may pass in the --inhibit flag with the execution command in order to acquire an inhibitor file descriptorr for the duration of the program. This will prevent your Linux machine from sleeping (since sleeping disrupts the TCP connections that happen when running preprintd) and keep the program stable.
[!NOTE] By default the file descriptor is derived for the
org.freedesktop.login1service, which may or may not inhibit GUI-based suspension on some Linux distributions entirely. For example, the power menu of GNOME of Ubuntu does not block manual suspension, even if there is an entry clearly visible viasystemd-inhibit --list. This was tested on Ubuntu 22.04 LTS.
Code Inspection
When you're going through the code, you'll see these:
- The standard LPR/LPD sequence (except the code doing HTTP requests via reqwest's blocking API and every other code surrounding/using this logic).
- Lots of
LazyLockusage. Although this is not optimal for a program that's supposed to be tiny, we've kept this pattern to reuse as much data as physically possible without hardcoding and messing up.
More specific parts of the codebase that you may be more curious about are described below:
Mercure SSE Connection Protocol
preprintd streams real-time job notifications from the Mercure Hub (/.well-known/mercure).
- Endpoint:
https://api.preconnect.app/.well-known/mercure?topic=https%3A%2F%2Fpreconnect.app%2Fprinter - Authorization:
Bearer <subscriber-jwt>- The subscriber JWT is created by signing
{"mercure":{"subscribe":["https://preconnect.app/printer"]}}with HMAC-SHA256 usingWORKER_KEY.
- The subscriber JWT is created by signing
- Replay Support: On reconnect, pass the
Last-Event-IDheader containing the lastid:value received from the stream to receive any missed jobs.
Windows Inconsistencies
Although most of the instructions above are primarily made for Linux (and can be migrated over to Unix/macOS), some built-in features are not available on the Windows operating system by default. For example, the STATE_DIRECTORY environment variable set via systemd during runtime never shows up there. Moreover, some Windows-specific features might be missing from this implementation entirely, for which it is encouraged that you give the Reference Implementation a try.
Identifying Workers
While claiming a job, each worker identifies itself with an X-Worker-Ident header which has a pattern of <UUID>;<ARCH>;<IDENTITY_TYPE> (e.g. 03780793-e7af-49c1-b55d-92ff57be8c6e;aarch64-apple-darwin;static).
Visible from the pattern mentioned above, the worker identity can be broken down into three parts:
<UUID>: A randomly-generated UUID (v4) string literal, which is used to give the worker a unique identity to be correlated with.<ARCH>: The architecture of the compiled binary of the worker.<IDENTITY_TYPE>: Another string literal representing whether the identity is static (when it successfully retrieves a previous identity or creates a new one under$STATE_DIRECTORY/.identand then retrieves it), or dynamic (due to issues withstd::fsoperations or just the$STATE_DIRECTORYpath being unavailable).
Reference Implementation
See: https://github.com/sabbirba/preconnect/blob/main/printer.py (courtesy: @sabbirba)
License
Licensed under the GNU General Public License v3.