plugin-hostd runs your LV2 and CLAP audio plugins, each in a worker process of its own, and puts back any worker that dies. A plugin that crashes takes down its own worker and nothing else. It is for anyone who runs plugins live on Linux, on a mixing console, a stage rig or a Raspberry Pi, and cannot let one bad plugin stop the show.
- A crash stays small: the plugin's worker is respawned with its settings put back.
- Nothing to change in your controller: it speaks mod-host's socket protocol, same ports, same replies.
- LV2 (through mod-host) and CLAP (through omx-clap-host) side by side.
- Audio never passes through the daemon; every worker is a JACK client, as in mod-host.
- A plugin that keeps crashing is singled out and given up on without touching its neighbours.
Fedora:
sudo dnf config-manager addrepo --from-repofile=https://freemixer.github.io/rpm/freemixer.repo
sudo dnf install plugin-hostd omx-clap-host
Debian bookworm and trixie and Raspberry Pi OS (amd64 and arm64):
curl -fsSL https://freemixer.github.io/deb/freemixer.asc | sudo tee /usr/share/keyrings/freemixer.asc >/dev/null
echo "deb [signed-by=/usr/share/keyrings/freemixer.asc] https://freemixer.github.io/deb/debian/$(. /etc/os-release; echo $VERSION_CODENAME) ./" | sudo tee /etc/apt/sources.list.d/freemixer.list
sudo apt update && sudo apt install plugin-hostd omx-clap-host
The LV2 worker is mod-host, which Debian does not package; without it CLAP plugins still work.
What the continuous integration runs on every push:
- The daemon against stand-in workers that die on cue: placement, forwarding, the ledger, replay, attribution and the bound on a respawn storm. No JACK and no plugin are needed for this.
- The generated protocol files, README tables and man page checked against
protocol.h, and every guard broken on purpose to see its test go red. - The daemon with a real CLAP worker (omx-clap-host) over PipeWire's JACK and over JACK2's
jackd, including meters reaching the controller. - RPMs (Fedora 44) and DEBs (bookworm and trixie) built and installed in a clean environment.
The LV2 worker (mod-host) is not run in CI; those steps stay in the Makefile for a machine that has mod-host and an LV2 bundle.
Start the daemon, with pin checking off for a first try (it says when both ports accept):
echo 'require_pins 0' > plugin-hostd.conf
plugin-hostd -n -p 5555 -c plugin-hostd.conf
In another terminal, add an LV2 plugin, set its gain and ask where it runs
(messages end in a NUL byte, which printf adds):
send() { printf '%s\0' "$1" | nc -w3 127.0.0.1 5555 | tr '\0' '\n'; }
send "add http://plugin.org.uk/swh-plugins/amp 0" # resp 0
send "param_set 0 gain 6" # resp 0
send "param_get 0 gain" # resp 0 6.0000
send "worker_list" # resp 1 w1:<pid>:lv2:up:own:0
The plugin now runs as JACK client effect_0 in its own process. Link it in your graph as
you would a mod-host plugin. man plugin-hostd has the options; the full protocol is below.
Using it from your own host (Zynthian, a Raspberry Pi, any Debian): docs/own-host.md, nine commands from nothing to a plugin running.
Building from source: see BUILDING.md. More about FreeMixer at https://freemixer.github.io.
The verbs the daemon adds, the placement syntax, the error codes, the feedback events and the
settings are declared once in include/plugin-hostd/protocol.h; the tables here, the man page's
key list and protocol/plugin-hostd.json are generated from it. A C program includes the installed
header (pkg-config --cflags plugin-hostd, package plugin-hostd-devel or plugin-hostd-dev); anything
else reads /usr/share/plugin-hostd/protocol.json. mod-host's own protocol, which the LV2 workers speak
(its command and feedback templates, its parse refusals and its reply codes), is installed beside it as
/usr/share/plugin-hostd/mod-host.json, written from the mod-host headers the daemon is built against.
plugin-hostd -n -p <port> [-f <port>] [-c plugin-hostd.conf]
It prints plugin-hostd ready! once both ports accept.
No worker exists until the first add. quit or SIGTERM stops every worker and the daemon;
after a SIGKILL the workers notice the parent gone (PR_SET_PDEATHSIG) and exit.
The settings live in a file, -c, $PLUGIN_HOSTD_CONF or
~/.config/plugin-hostd.conf, one key value per line:
| key | default | meaning |
|---|---|---|
mod_host |
mod-host |
worker for lv2 (a name on PATH or a path) |
clap_host |
omx-clap-host |
worker for clap |
lv2_worker |
mod-host |
who runs an LV2 plugin: mod-host (the mod_host program), omx-clap-host (the clap_host program, through its LV2 adapter, a plugin it cannot run refused with a code) or auto (the adapter first, mod-host for what it refuses); lv2_worker_set overrides it for one plugin |
lv2_path |
$LV2_PATH, else /usr/lib64/lv2:/usr/lib/lv2:/usr/local/lib/lv2 |
where bundles are searched |
state_root |
$XDG_RUNTIME_DIR, else /tmp |
the daemon's checkpoints and worker logs |
pin_mode |
first-use |
strict or first-use, while require_pins is 1: first-use records the pin of a plugin on its first add, strict admits only a plugin a controller or a pin_file pinned |
pin_store |
`` | the file first-use pins are kept in; empty is $XDG_STATE_HOME/plugin-hostd/pins, else $HOME/.local/state/plugin-hostd/pins |
pin_file |
`` | a file of pin lines loaded at start, as plugin-qualify --all --pins writes it; empty is none |
ready_timeout_ms |
5000 ms | a worker that does not accept in this long is a failed spawn |
rpc_timeout_ms |
5000 ms | a worker that does not answer in this long is killed |
backoff_base_ms |
250 ms | respawn backoff, doubling per death in the window |
backoff_max_ms |
5000 ms | the most a respawn backoff grows to |
storm_deaths |
5 deaths | deaths inside the window that end respawning of a placement |
storm_window_ms |
60000 ms | the window storm_deaths are counted in |
suspect_window_ms |
500 ms | a worker that dies this soon after a reply drops the verb it answered; 0 turns it off |
checkpoint_ms |
5000 ms | a quiet interval with a changed ledger writes a checkpoint |
idle_ms |
25 ms | the period of the idle tick: reap, respawn, checkpoint |
pool_max |
8 instances | instances in a pool before a sibling opens |
require_pins |
1 | 1: add admits only a plugin that has a pin (see pin_mode), its files hashed before any worker sees it, and a plugin of a format whose pin holds only in a world of its own gets a worker of its own, its world the plugin's files (a default that resolves to a pool is placed own); 0: no pin is checked |
| name | value | meaning |
|---|---|---|
default_command_port |
5555 port | the command port without -p; the feedback port is the next one, unless -n is given |
connect_retry_ms |
5000 ms | how long the idle tick asks again for a connect a respawned worker could not make yet |
line_max |
4096 bytes | the protocol socket's buffer: the longest pin_set line the daemon takes |
The placement of an add is own | pool:<name> | default. own is a worker for that instance alone; pool:<name> is the pool's worker, opened on first use and capped by pool_config, and a full pool opens <name>#2, <name>#3, and so on; default is the policy of policy_set. A pool name matches ^[A-Za-z0-9_-]{1,32}$.
<uri> is lv2:<uri> or a bare URI for an LV2 plugin, clap:<path>#<id>
for a CLAP one. The instance number is the controller's and reaches the
worker unchanged. A worker holds one format. An own LV2 worker sees one
bundle: LV2_PATH is a directory of the daemon's holding a link to the bundle
whose manifest.ttl names the URI.
A token after the instance that is not a placement is the fork's optional
jack client name, passed on. An instance that crashed in a pool is placed
own whatever the hint until quarantine_clear.
| verb | arguments | reply | meaning |
|---|---|---|---|
add |
<uri> <instance> [own | pool:<name> | default] [client_name] |
resp <instance> |
mod-host's add with a placement; a token after the instance that is not a placement is the jack client name |
worker_list |
none | resp <n> <worker>:<pid>:<format>:<state>:<place>:<i>,<i>,... |
the workers, one record each |
instance_info |
<instance> |
resp 0 <worker> <pid> <state> <crashes> <quarantined> |
where an instance lives and what it has cost |
supervisor_reset |
[<worker> | all] |
resp 0 |
re-arm the storm bound and the backoff of a given-up placement |
quarantine_clear |
<instance> | all |
resp 0 |
let a quarantined instance go back where its placement says |
policy_set |
<format | *> own | pool:<name> |
resp 0 |
the placement of an add that says default |
pool_config |
<name> <max_instances> |
resp 0 |
the instances a pool holds before a sibling opens |
worker_env |
<format | *> <cpu-list|-> <nice|-> |
resp 0 |
the cpu list and the nice value of the workers of a format, at spawn and on the ones running |
pin_set |
<uri> <path>=<sha256>[,<path>=<sha256>...] <scheme>:<sha256> [verdict=<verdict>] |
resp 0 |
the pin of one plugin: the SHA-256 of every file the host loads for it, a path relative to the bundle (the directory of a single-file format's file), its layout fingerprint and, when it is known, the verdict of its qualification; it replaces an earlier pin of the uri. The pin store and a pin_file hold the same line, with or without the verb |
pin_clear |
<uri> | all |
resp 0 |
forget the pin of one plugin, or of every one |
lv2_worker_set |
<uri> <mod-host | omx-clap-host | auto | -> |
resp 0 |
the worker of one LV2 plugin, for its adds from now on: it overrides the lv2_worker setting, - takes the override back; -902 for a word that is none of these. Like policy_set it is the controller's, not in the ledger, gone with the daemon |
pin_accept |
<uri> |
resp 0 |
pin_mode first-use only: take the plugin's files as they are now as its pin, after a pin_changed event; the layout is learned again by the next add and the verdict is dropped; -508 when the plugin has no pin, -509 when its files cannot be collected, -902 under pin_mode strict |
pin_layout |
<instance> |
resp 0 <scheme>:<sha256> |
sent by the daemon to a worker of a format that checks layouts, after it accepted the add of a plugin whose layout pin is still unknown: the layout the worker found after init, which the daemon records in the pin |
pin_expect |
<instance> <scheme>:<sha256> |
resp 0 |
sent by the daemon to a worker of a format that checks layouts, just before the instance's add: the layout pin that add checks after init and before activate; any other reply than resp 0 refuses the add with that code, and the add is never forwarded |
A <state> is up (accepting commands), starting (spawned, not yet accepting), backoff (dead, waiting to be respawned), given-up (the storm bound is spent for its placement).
A record's place holds a colon (pool:p), so a worker_list record is read from both ends: four fields on the
left, the instance list on the right, the place in between.
What a host knows of a strip and of a plugin's controls. For a CLAP instance the daemon routes them like mod-host's
instance verbs and passes the worker's reply through; the worker answers them from the plugin (clap.track-info,
clap.remote-controls, clap.params), except param_info: omx-clap-host 0.8.0 answers it -511 for every parameter
(and -103 for a symbol that is no parameter), because it holds no checked unit and scale for any of them. -511 is
the stable answer to read as "no contract; fall back to the layout from omx-clap-scan"; the daemon passes it through
and drops nothing. For an LV2 instance the daemon answers them itself, never forwarding them, so
mod-host learns none of them: track_info is kept and answered resp 0, an LV2 plugin has no remote pages, and
param_info answers -511 until the daemon reads the bundle's port data. When a CLAP plugin changes its pages, its
worker writes remote_pages_changed <instance> on its feedback port and the daemon relays it.
Notes are played the same way, by a controller that is not a jack client (a web console, a test, a MIDI surface on the
network): note_input says whether an instance takes notes, and note_on, note_off and all_notes_off reach its
worker unchanged once the daemon has checked their words. A worker answers them from the plugin's note input. mod-host
takes no notes on its socket, so for an LV2 instance on mod-host the daemon answers all four -514, and the plugin is
played through its jack MIDI input with connect. omx-clap-host does not answer them yet (FreeMixer/omx-clap-core#122).
preset_list and preset_get list a plugin's presets the way remote_pages and remote_page_get list its pages; the
key preset_get gives is the one preset_load takes. mod-host cannot list presets, so for an LV2 instance on mod-host
the daemon answers both -515. omx-clap-host does not list them yet (FreeMixer/omx-clap-core#95).
A plugin with a window or a surface of its own tells its host when the user takes hold of one of its knobs and lets go,
moves values by itself, and suggests a remote page. Its worker writes param_gesture, mod-host's param_set and
remote_page_suggest on its feedback port, and the daemon relays them as written, in order (events below). If the
worker dies, or the instance is removed, while a gesture is held, the daemon ends the gesture itself, so no control
stays held at the controller. omx-clap-host writes these lines from FreeMixer/omx-clap-core#87 on.
| verb | arguments | reply | meaning |
|---|---|---|---|
track_info |
<instance> <name> <#RRGGBB | -> [bus | return | master] |
resp 0 |
the strip's name ("" for none), colour and kind, absent for an input channel; a worker whose plugin format has a track-info extension passes them on; any other word is -902 |
remote_pages |
<instance> |
resp <count> |
the plugin's remote-control pages, from clap.remote-controls; 0 for an instance of a format whose worker has none |
remote_page_get |
<instance> <page> |
resp 0 <page_id> <section> <page_name> <s1> <s2> <s3> <s4> <s5> <s6> <s7> <s8> |
one page, 0 to count - 1: each slot the symbol param_set takes, - for an empty one; -902 for a page that is not one |
param_info |
<instance> <symbol> |
resp 0 <unit> <scale> <min> <max> <default> <step> <stable_symbol> |
what a parameter's value means: its unit, its scale, and the symbol it keeps across hosts and versions; -103 for a symbol that is not a parameter, the plugin's own bypass and :bypass included; -511 for a parameter whose unit and scale the worker holds no checked contract for |
preset_list |
<instance> |
resp <count> |
the plugin's presets, the ones its preset discovery or its bundle lists; (-515) for an instance of a worker that cannot list them |
preset_get |
<instance> <index> |
resp 0 <load_key> <name> |
one preset, 0 to count - 1: the key preset_load takes for it, and its name; -902 for an index that is not one |
note_input |
<instance> |
resp 0 <port | -> |
whether the instance takes notes: the name of its note input on its worker's jack client, - for a plugin that takes none; (-514) for an instance of a worker that cannot say |
note_on |
<instance> <channel> <key> <velocity> |
resp 0 |
play a key: channel 0 to 15, key 0 to 127, velocity 1 to 127, a MIDI note on at the start of the worker's next cycle; (-513) for a plugin that takes no notes, (-514) for an instance of a worker that takes none on its socket, -902 for a word out of its range |
note_off |
<instance> <channel> <key> <velocity> |
resp 0 |
release a key, velocity 0 to 127, a MIDI note off; answered as note_on is |
all_notes_off |
<instance> |
resp 0 |
release every key of every channel, MIDI's all notes off (controller 123) on each; answered as note_on is |
The ledger keeps the latest track_info of an instance and replays it after the add; the others are never replayed. A <scale> is linear (the value is linear in position), log (the value is geometric in position; both bounds are positive), stepped (the whole numbers from min to max, step 1). A string word is at most 255 bytes of UTF-8 with no control character.
Error codes the daemon adds to mod-host's:
| code | name | meaning |
|---|---|---|
-501 |
PHD_ERR_PLACEMENT_INVALID |
placement invalid: not a placement, or not valid here, as a pool: for a plugin of a format whose pin holds only in a world of the plugin's own, while require_pins is on |
-502 |
PHD_ERR_NO_BACKEND |
no worker program for the scheme |
-503 |
PHD_ERR_WORKER_SPAWN |
the worker is not up: the spawn failed, or it is in backoff; ask again after instance_restored |
-504 |
PHD_ERR_REPLAY |
the name of a refused replay, never a reply: a replayed add that its pin or the worker refuses is announced by the instance_replay_refused event with the refusal's own code, and written to stderr with this one and the step that refused it |
-505 |
PHD_ERR_GAVE_UP |
the storm bound is spent for that placement |
-506 |
PHD_ERR_NO_SUCH_WORKER |
no such worker |
-507 |
PHD_ERR_VERB_DROPPED |
the worker died on this very command and the daemon dropped it: it is not replayed, and the instance_verb_dropped event names it; or it died on, or did not answer in rpc_timeout_ms, the pin_expect of an add, which is then never forwarded |
-508 |
PHD_ERR_PIN_ABSENT |
pin_mode strict and the plugin has no pin, or its layout pin is in a scheme this version does not know: add is refused and no worker sees it |
-509 |
PHD_ERR_PIN_BINARY_MISMATCH |
a pinned file is missing or its SHA-256 differs, or the plugin's manifest names a file the pin does not hold: add is refused and no worker sees it |
-510 |
PHD_ERR_PIN_LAYOUT_MISMATCH |
the worker found the parameter layout after init differs from the layout pin: the instance is destroyed before activate |
-511 |
PHD_ERR_NO_PARAM_CONTRACT |
the host holds no checked unit and scale for this parameter: fall back to qualification |
-512 |
PHD_ERR_LV2_ADAPTER_REFUSED |
the lv2-clap worker's LV2 adapter cannot run this plugin (MIDI it does not feed, a required feature it lacks, a layout it does not take): the worker answered the add -102, the lv2_adapter_refused event carries the hosting.* code of that reply, and under lv2_worker auto the plugin is run by the lv2 row instead and this code is never answered; a plugin no bundle of the worker's world holds is no refusal: the worker's -101 is answered |
-513 |
PHD_ERR_NO_NOTE_INPUT |
the instance's plugin takes no notes (an effect): its worker answers note_on, note_off and all_notes_off with this code |
-514 |
PHD_ERR_NOTES_BY_JACK |
the instance's worker takes no notes on its socket (mod-host): the daemon answers the note verbs and note_input with this code, and the plugin is played through its jack MIDI input with connect |
-515 |
PHD_ERR_NO_PRESET_LIST |
the instance's worker cannot list the plugin's presets (mod-host): the daemon answers preset_list and preset_get with this code; preset_load still takes a preset the controller knows |
A worker's own refusal is returned as it said it. An add that killed its worker answers mod-host's -102.
worker_env offers no real-time priority: a worker's audio thread is set by the jack client library (under a SCHED_FIFO 60
driver it ran at 55, unasked), and a FIFO priority given to the whole process puts the plugin's own threads above
it, where a busy thread starves the graph.
Like mod-host, the daemon serves one controller at a time, and remove of an instance it does not hold answers
resp 0. With -n and no -f there is no feedback port (mod-host's own rule), and a controller need not open
one; without -n, or with -f, the daemon waits for the controller to open both. A command that finds its
worker gone answers PHD_ERR_WORKER_SPAWN: the worker is not up, ask again after instance_restored. A command that
kills its worker answers PHD_ERR_VERB_DROPPED.
Events on the feedback port, one NUL-terminated line each:
| event | fields | meaning |
|---|---|---|
worker_died |
<worker> <pid> <exit:N | signal:N> <instance>,... |
a worker died; the instances it held, or - for none |
instance_verb_dropped |
<instance> <the command as sent> |
the command a worker died on, dropped from the ledger, or the pin_expect of an add it died on |
instance_verb_dropped |
<instance> suspect:<ms> <the command as sent> |
the last verb a worker answered when it died inside suspect_window_ms, the age of the reply |
worker_backoff |
<worker> <ms> |
the respawn waits this long |
worker_respawned |
<worker> <pid> <replayed_count> <ms> |
a worker is back and its ledger replayed |
instance_restored |
<instance> <worker> |
an instance is back in a worker |
instance_replay_refused |
<instance> <code> |
a replayed add was refused, by its pin or by the worker, the refusal's own: no worker holds the instance until the next replay |
instance_quarantined |
<instance> <worker> |
the culprit of a pool death, placed own until cleared |
supervisor_gave_up |
<worker> <deaths> <window_ms> |
the storm bound is spent; respawning ends |
pin_changed |
<uri> files | layout |
an add or a replay of a plugin was refused because its files, or the layout the worker found, differ from its pin; the pin is as it was until pin_accept or pin_set |
lv2_adapter_refused |
<uri> <reason> auto | omx-clap-host |
the LV2 adapter of the lv2-clap worker refused a plugin: is the hosting.* code after -102 on the worker's reply to that add (- when it named none); the last word is the lv2_worker in force, auto for a plugin the lv2 row runs from now on, omx-clap-host for an add answered (-512) |
remote_pages_changed |
<instance> |
relayed from a worker: the plugin changed its remote pages; read them again |
remote_page_suggest |
<instance> <page_id> |
relayed from a worker: the plugin suggests the remote page of that id (a page_id of remote_page_get); a surface may follow it |
param_gesture |
<instance> <symbol> begin | end |
relayed from a worker: the user took hold of a control of the plugin's own, or let it go; a controller writes no automation of that parameter in between. The values the plugin moves meanwhile, or at any time by itself, are mod-host's param_set feedback lines, relayed as written. A gesture whose worker died, or whose instance was removed, before its end is ended by the daemon, before the worker_died event |
What a worker reports on its feedback port (the output_set of a monitored output, the param_set of a parameter the
plugin moved, data_finish, the log lines) reaches the controller on the daemon's feedback port, among the events
above:
Every worker is started as <program> -n -p <command port> -f <feedback port>.
- every line a worker writes on its feedback port goes out on the daemon's feedback port as the worker wrote it: output_set, param_set, data_finish and the rest of mod-host's feedback, and a line the daemon does not know.
- the instance number in a relayed line is already the controller's: add hands it to the worker unchanged, so nothing in a line is rewritten.
- a worker's lines go out in the order it wrote them, and what a worker wrote before it died goes out before its worker_died event; an instance lives in one worker at a time, so its lines keep their order across a respawn; the lines of different workers interleave.
- monitor_output is in the ledger, one line per output, and param_monitor as it was sent; a respawn replays them after the add, so a respawned instance reports the same outputs, each once, starting again from its first value.
- output_data_ready names no instance and goes to every worker; each worker's data_finish is relayed.
- with no feedback port, or no controller on it, a line is dropped, as mod-host drops it; the daemon reads every worker's feedback all the time, so no worker waits on it.
A synced delay or an arpeggiator comes back at the right tempo after a crash. What the daemon keeps of the commands every worker shares:
- the tempo, the metre and the transport a controller sets (mod-host's set_bpm, set_bpb, transport and transport_sync) are kept, the latest line of each as it was sent, in the order they were last sent.
- a worker is handed them before anything else: a respawned one before its first add, and a worker started later, of a new placement, as well; a worker's refusal of one is written to stderr and is no failure.
- a line whose words are not that command's grammar is passed on and not kept.
plugin-hostd-probe run loads a plugin in a child process under limits and writes down what happened, as facts only: no
verdict. The child is clap-qualify today; the runner never loads plugin code itself.
plugin-hostd-probe run --out probe.json --log probe.stderr.log /usr/lib64/clap/plate.clap org.example.plate
The runner kills the whole process tree of the child at its wall clock, and again when the child is done, so a process a
plugin started in a session of its own does not outlive the run. It applies these limits on every Linux: the wall clock,
the CPU time, the data size, the open files, the file size, no core dump, no real-time priority, no new privileges, the
child dying with the runner, and the cpus the child may use (--cpus). A limit the set asks for and this version does not
apply (the number of tasks, namespaces, Landlock, seccomp) is listed in the document under limits.unavailable with its
code, and --require tasks makes the run refuse instead.
A crash, a hang and a timeout are outcomes in the document, never a missing document: the steps the child finished before
dying stay in it. The exit status says whether there is one: 0 a complete run, 1 a document with some step that ended
otherwise, 2 no document, 3 no such plugin. The document is described by probe-output.schema.json, generated from the
codes in protocol.h, and installed with the protocol.
Why: a plugin is checked before the console trusts it. We measure one build of it for crashes, real-time safety and its parameters. The pin makes sure the bytes that load are the bytes that were checked, so an update or a swapped file cannot slip in unchecked. The layout pin also protects the controls: presets, surfaces and the REST rows are mapped to the plugin's parameters, and a changed layout would send values to the wrong ones. It also leaves a record you can audit: the exact plugin build that processed the audio.
With require_pins on, the default, add admits only a plugin that has a pin: the SHA-256 of
every file the host loads for it, and its parameter layout fingerprint. Before any worker is spawned or sees the
add, the daemon hashes each pinned file over one open descriptor: for a CLAP the files beside the .clap that
clap:<path>#<id> names, the .clap itself among them; for an LV2 the files of the bundle whose manifest.ttl names
the URI, the manifest among them, and every file the manifest names with lv2:binary or rdfs:seeAlso for the plugin
must be held by the pin. No pin, or a layout pin in a scheme this version does not know, answers PHD_ERR_PIN_ABSENT;
a file missing, changed or not held answers PHD_ERR_PIN_BINARY_MISMATCH. For a CLAP the daemon then sends the worker
pin_expect <instance> <layout pin> and the add unchanged, and the worker answers PHD_ERR_PIN_LAYOUT_MISMATCH
when the layout after init is not the pinned one; a worker that refuses pin_expect gets no add, and its refusal
is the reply, and one that dies on it or does not answer it is dropped with PHD_ERR_VERB_DROPPED, the
instance_verb_dropped event naming the pin_expect. An LV2 worker gets no pin verb: its layout is its bundle's TTL,
which the hash covers, in a world of that one bundle; so a pinned LV2 plugin gets a worker of its own, a default that
resolves to a pool is placed own and an explicit pool:<name> answers PHD_ERR_PLACEMENT_INVALID.
Where the pin comes from is pin_mode. strict takes it from pin_set or from a pin_file loaded at start, and a
plugin with no pin is refused. first-use, the default, records it: the first add of a plugin with no pin hashes its
files, keeps the pin in the pin store ($XDG_STATE_HOME/plugin-hostd/pins) and places the instance own; the layout is
asked of the worker after the add (pin_layout) and recorded too. A host that pins by itself sets pin_mode strict.
The store, a pin_file and pin_set carry one line format, pin_set <uri> <files> <layout> [verdict=<word>] with the
verb optional in a file. In first-use only a pin with verdict=in-process, which plugin-qualify writes, lets a plugin
share a worker. A plugin that changed since it was pinned is refused (-509, or -510 from the worker), the
pin_changed <uri> files|layout event names what changed, and pin_accept <uri> takes the new pin.
What cannot be pinned:
- a path holding ',', '=' or whitespace cannot be pinned: they are the separators of pin_set's words, and pin_set refuses it as a token outside its grammar.
- a pin_set line is at most line_max, 4096 bytes, the protocol socket's buffer; a longer one is refused as outside the grammar.
- a manifest that uses @base, or names a file of the plugin by a percent-encoded IRI, makes add answer PHD_ERR_PIN_BINARY_MISMATCH (-509): the daemon does not resolve either, so it cannot know the file the host would load.
Pins are the controller's policy, like policy_set: not in the ledger, gone with the daemon, set again by a controller
that reconnects. A replayed add is checked again, so a file swapped while a worker was down is refused on respawn:
the daemon emits instance_replay_refused <instance> <code> with the refusal's own code (the pin's, or the worker's
when it refuses the replayed pin_expect or add), writes PHD_ERR_REPLAY, the step and that code to stderr, and
the worker does not hold that instance until a later replay passes. The hashing and the layout serialisation are include/plugin-hostd/pin.h, installed beside the
protocol header for the hosts that check a layout.
- answer mod-host's protocol on
-n -p <port> -f <port>, the feedback port opened by the daemon right after the command port;
- print a line ending
ready!on stdout once its socket accepts;
- write, for
state_save <dir>, files whose names begineffect_<instance>and read them back onstate_load <dir>, skipping an instance with no file.
mod-host and omx-clap-host do.
The daemon holds its plugin formats in one table, src/formats.c, and nothing else in the daemon, the protocol or the
pins names a format. A row says how a uri reaches the format (prefix, strip_prefix, fallback), which program serves
it (worker), where its plugins are searched and how a worker's world is narrowed to one plugin (search, world_env,
narrow_world), which files a pin holds (pin_collect), whether the worker checks the layout (layout_check), whether
a pinned plugin must run in a worker of its own (pinned_own_only) and who answers the instance verbs
(native_instance_verbs, else param_info). Adding a format is a row and its worker; formats_register refuses a row
with a capability missing and says which. a test registers a third format in the test and takes it through
add, param_set, pin_set and remove.
lv2_worker chooses the program that runs LV2 plugins: mod-host (the default, the mod_host program), omx-clap-host
(the clap_host program, through its LV2 adapter) or auto (the adapter first, mod-host for a plugin the adapter
refuses). lv2_worker_set <uri> <mod-host|omx-clap-host|auto|-> overrides it for one plugin from then on, and - takes
the override back. The two programs are two rows of the table, lv2 and lv2-clap, so policy_set, worker_env and
worker_list name them like any format. The adapter row differs from the mod-host row in these ways, each held by a test:
| capability | lv2 (mod-host) |
lv2-clap (omx-clap-host) |
|---|---|---|
worker |
mod_host |
clap_host, given the uri with its lv2: prefix |
native_instance_verbs |
no: the daemon answers track_info, remote_pages, remote_page_get and param_info |
yes: the worker answers them from the plugin, and track_info is replayed to it after a respawn |
layout_check |
no | no: an LV2 pin holds the layout of the plugin's ports, not of the CLAP parameters the adapter presents, so pin_expect would refuse every pin plugin-qualify wrote |
world_scope |
one bundle, narrowed: LV2_PATH holds a link to the plugin's bundle |
the same: the adapter opens bundles through the LV2 world |
pin_pool |
no: a pinned plugin is placed own |
the same, for the same reason |
An adapter handles a subset of LV2. When it refuses a plugin, the worker answers the add resp -102 <hosting code>
(for instance hosting.features.midi-in-fed-empty, hosting.features.missing, hosting.topology.wider-than-strip or
hosting.no-realisation). The add under omx-clap-host then answers -512 (LV2_ADAPTER_REFUSED) and the
lv2_adapter_refused event carries that code, read from the reply to that add and never from the worker's log, which a
pool worker shares with other plugins (- when the reply named none). Under auto the add is made again on the lv2
row, the plugin stays there, the event is sent once, and the controller never sees -512. The daemon remembers the refusal until
the adapter has a reason to take the plugin again: lv2_worker_set of that uri (to any word, - included) forgets it, and
so does a change of pins (pin_set, pin_accept, pin_clear) for every plugin; the next add under auto asks the
adapter once more.
A plugin that no bundle of the worker's world holds is not a refusal: the worker answers resp -101 with no code. Under
omx-clap-host the add answers that -101, with no event. Under auto the same add is made on the lv2 row, so mod-host
looks for the plugin; nothing is remembered, and the adapter is asked again on the next add. Under auto an
omx-clap-host that is not installed is not an error either: the add goes to the lv2 row.