Appearance
Reference
Everything the daemon exposes, in tables.
Command line
muxen-blocswapThere are no options. The binary accepts no arguments and ignores anything passed to it — there is no --help, no --version and no verbosity control. The systemd unit invokes it bare.
Exit codes
| Code | Meaning |
|---|---|
| 0 | every termination |
The daemon has no failure exit. A configuration it cannot use — file missing, feature off, no device to monitor — is reported on standard output and the daemon enters its main loop anyway. It leaves that loop only on SIGINT or SIGTERM, and returns 0.
Signals are handled from the main loop, so one arriving while a muxen-uds command is running takes effect only once that command returns.
Environment
The binary reads no environment variable of its own. Three are inherited and matter:
| Variable | Effect |
|---|---|
PATH | muxen-uds is invoked by name through /bin/sh, so it must be on the path |
TMPDIR | where the temporary scan file is created |
Files
| Path | Access | Content |
|---|---|---|
/usr/bin/muxen-blocswap | — | the daemon |
/usr/lib/systemd/system/muxen-blocswap.service | — | the systemd unit |
/etc/muxen/deploy.json | read once, at startup | the boat's deployment file. Not shipped by this package |
/var/lib/muxen/deployed.json | read once, at startup | the UID last deployed at each deviceId. Written by muxen-uds deploy, never by this daemon |
$TMPDIR/blocswap-scan.XXXXXX | created and deleted every 30 s | the scan result, handed to muxen-uds and read back |
Both input paths are hard-coded. Neither can be moved.
The temporary file is created with g_file_open_tmp, so it lands in TMPDIR — and the unit sets PrivateTmp=true, so under systemd that is the service's own /tmp, invisible from a shell on the Brain.
Commands it runs
Verbatim, through /bin/sh:
| When | Command |
|---|---|
| every 30 s | muxen-uds uid --scan-to-json <tmpfile> >/dev/null |
| on a detected swap | muxen-uds --device-id <id> deploy |
| immediately after | muxen-uds --device-id <id> reset |
Notes that matter:
--device-idprecedes the subcommand. The order is not decorative.- The scan's standard output is discarded; its standard error is not, so
muxen-udsdiagnostics reach the journal. - Exit codes are not examined. The daemon distinguishes only "the command could not be started, or was killed by a signal" from anything else. A
muxen-udsinvocation that ran and reported failure is treated as a success. - The calls are synchronous. The daemon's main loop is blocked for the duration of each one.
The scan file
Written by muxen-uds, read back and parsed by the daemon. It is the daemon's whole view of the bus.
json
{
"devices": [
{ "deviceId": 65, "uid": "27005E000B504256" }
],
"duplicate": false
}| Key | Type | Use |
|---|---|---|
devices[] | array | one entry per unit answering the scan |
devices[].deviceId | integer | the address. Entries without it are skipped |
devices[].uid | string, 16 hex characters | the physical unit's serial number. Entries without it are skipped |
duplicate | boolean | true when two different units answer at the same address |
An entry whose deviceId matches no monitored device is ignored. A monitored device that appears in no entry is marked absent for that scan.
duplicate is the interlock: while it is true, nothing is deployed, no state advances and nothing is logged. A missing duplicate key is treated as true — the daemon locks rather than assume.
If the file cannot be read the daemon logs cmd: failed to read '<path>'; if it is not valid JSON, cmd: failed to parse the JSON. In both cases every monitored device stays marked absent for that scan.
/var/lib/muxen/deployed.json
Read once, at startup, to seed the expected UID of each monitored device. The daemon reads exactly one field per device — the uid of the entry whose deviceId matches — and nothing else.
A device with no entry there gets an empty expected UID, which matches no real unit, so the device is treated as a replacement and deployed on the daemon's second tick.
Configuration keys
Read from /etc/muxen/deploy.json. Everything not listed here is ignored by this daemon.
| Location | Key | Type | Meaning |
|---|---|---|---|
settings[] | FeatureBlocSwap | object with a value field | the feature switch. The daemon monitors nothing unless value is the string "1" |
devices[] | deviceId | integer | the address to watch. Required |
devices[].parameters[] | VirtualDevice | object with a value field | "1" excludes the device from monitoring |
Matching is by the literal name field, exactly. There is no default for FeatureBlocSwap: absent means off, and so does any value other than "1" — a JSON number 1 included.
The state machine
One instance per monitored device, advanced once per scan. States are internal and never named in the log.
| State | Entered | Device seen | Device not seen |
|---|---|---|---|
| settling | at startup, and after every deploy | → watching. No UID comparison | stays |
| watching | from settling | UID differs → deploy; UID same → nothing, absence counter cleared | counter++; at 3 → waiting, logs become OFFLINE |
| waiting | after 3 consecutive absences | UID same → watching, logs become ONLINE; UID differs → deploy | stays |
A deploy is the pair deploy + reset, after which the newly seen UID becomes the expected one and the device returns to settling.
The settling pass is why a cold swap takes two ticks to be found: the first tick only establishes that something is answering.
Journal messages
The complete set. The daemon emits nothing else.
| Message | When |
|---|---|
Auto deploy is not activated in the deployed configuration | startup, when the feature is off, the deployment file is unreadable, or no device qualifies |
Feature BlocSwap enabled | startup, when FeatureBlocSwap is "1" |
Monitoring device <id> (0x<id>) | startup, once per monitored device |
Loading deployed.json | startup, before the UID table |
<id> => <uid> | startup, once per monitored device. An empty right-hand side means no recorded UID |
device <id> has been changed | a different UID answered at that address. A deploy and a reset follow |
device <id> become OFFLINE | three consecutive scans found nothing at that address |
device <id> become ONLINE | the same unit returned after being offline |
cmd: failed to read '<path>' | the scan file could not be read back |
cmd: failed to parse the JSON | the scan file was not valid JSON |
There is no message for a deploy starting, succeeding or failing, and none for the duplicate interlock.
All of it goes to standard output, unflushed. Under systemd, standard output is not a terminal and is therefore fully buffered, so lines reach the journal in blocks rather than as they are written.
Constants
None of these is configurable.
| Constant | Value |
|---|---|
| Scan interval | 30 s |
| Absences before a device is declared offline | 3 |
| Expected feature value | the string "1" |
| Expected virtual-device value | the string "1" |
| UID length | 16 characters |
| Command buffer | 512 bytes, for both the scan and the deploy commands |
| Temporary file template | blocswap-scan.XXXXXX |
systemd
Unit muxen-blocswap.service:
| Directive | Value |
|---|---|
Description | Muxen Automatic Device Deploy |
PartOf | muxen-deploy.target |
WantedBy | muxen-deploy.target |
After | mosquitto.service |
User / Group | muxen |
ExecStart | /usr/bin/muxen-blocswap |
Restart | always, RestartSec=30 |
ConfigurationDirectory | muxen, mode 0755 |
StateDirectory | muxen, mode 0755 |
Hardening: NoNewPrivileges, LockPersonality, PrivateDevices, PrivateTmp, ProtectClock, ProtectControlGroups, ProtectHome, ProtectHostname, ProtectKernelLogs, ProtectKernelModules, ProtectKernelTunables, ProtectProc=invisible, ProtectSystem=full, RemoveIPC, RestrictNamespaces, RestrictNetworkInterfaces=lo, RestrictRealtime, RestrictSUIDSGID, SystemCallArchitectures=native. Twenty capabilities are removed from the bounding set, including CAP_NET_ADMIN, CAP_SYS_ADMIN, CAP_SYS_BOOT and CAP_SYS_PTRACE. The @clock, @cpu-emulation, @debug, @module, @mount, @obsolete, @privileged, @raw-io, @reboot, @resources and @swap system-call sets are denied with EPERM.
These restrictions apply to the muxen-uds processes the daemon starts as well, since they are children of the unit.
Restarting muxen-deploy.target restarts the daemon. That is the mechanism by which a change to the deployment file is picked up: the muxen-systemd package watches /etc/muxen/deploy.json and restarts the target when it changes.
Packaging
| Field | Value |
|---|---|
| Source / package | muxen-blocswap |
| Section / Priority | misc / optional |
| Architecture | any, Multi-Arch: foreign |
| Depends | muxen-uds, muxen-systemd (>= 1.2.0), plus ${shlibs:Depends} and ${misc:Depends} |
| Replaces / Breaks | muxen-auto-deploy |
| Trigger | activates muxen-restart-target |
| Licence | MIT |
| Homepage | https://www.muxen.fr |
muxen-auto-deploy is this program's original name; the two packages cannot be installed together.
The muxen-restart-target trigger is what makes the MUXEN services restart when the package is upgraded.
The package ships one binary and one unit file. No configuration file, no bash completion, no data files.
Building from source
sh
meson setup build
meson compile -C buildThe makefile wraps the common cases: make deb builds the Debian package, make dev an address-sanitiser build, make lint and make lint-check run clang-format, make mrproper cleans the tree and the vendored libraries.
Dependencies: GLib, json-c, and the MUXEN libcanmqtt and libmuxenfile, resolved as installed packages when present and as Meson subprojects otherwise.
Build options in force: default_library=static, warning_level=3, LTO with a reproducible debug-path mapping, and full RELRO with immediate binding.
libcanmqtt supplies main() and the GLib main loop; Bloc swap provides application_init() and application_exit() as the weak-symbol overrides. That is why the daemon has no argument parsing of its own — it never looks at argc or argv.
