Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ install_man:

# No tracked file points into a private repository, issue, branch or path (tools/private-refs-check.sh, with its
# --self-test, which turns each kind of pointer red in each kind of file).
.PHONY: private-refs
.PHONY: private-refs check-verb-table
private-refs:
tools/private-refs-check.sh
tools/private-refs-check.sh --self-test
Expand All @@ -160,7 +160,7 @@ clean:

# the daemon against workers that are not plugin hosts at all (tests/fake-host speaks the same protocol and can be
# made to die on cue): placement, forwarding, ledger, replay, attribution, the storm bound; no jack, no plugin
test: test-pin test-transforms test-format-coverage test-describe test-relay test-alloc test-format test-daemon check-generated check-schema test-probe-schema test-vocabulary test-probe test-contract test-consumer test-perturbation check-readme check-own-host-page check-no-pins check-ci-guards test-mod-host-json test-socket
test: test-pin test-transforms test-format-coverage test-describe test-relay test-alloc test-format test-daemon check-generated check-schema test-probe-schema test-vocabulary test-probe test-contract test-consumer test-perturbation check-readme check-own-host-page check-no-pins check-ci-guards test-mod-host-json test-socket check-verb-table
MOD_HOST_DIR=$(MOD_HOST_DIR) python3 tests/verbs_contract.py

# the same without the verb table, which is read from a mod-host checkout's README
Expand Down Expand Up @@ -322,6 +322,11 @@ check-ci-guards:
test-mod-host-json:
python3 tests/mod_host_json.py $(MOD_HOST_INCLUDE)

# the host verb table has a row for every verb a controller sends the daemon, no more and no fewer, and requires of every
# profile the verbs the daemon answers itself
check-verb-table:
python3 tests/verb_table.py

# the JSON against its schema, for which the jsonschema module is needed
check-schema:
python3 tests/protocol_schema.py protocol/plugin-hostd.json protocol/plugin-hostd.schema.json
Expand Down
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ The settings live in a file, `-c`, `$PLUGIN_HOSTD_CONF` or
| `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 |
| `line_words_max` | 64 words | the words of one command line the daemon reads, the verb included; a word past them is not read |
| `chain_members_min` | 1 instances | the fewest members of a chain: a chain of one is valid, so one isolated plugin is a chain like any other |
<!-- END GENERATED protocol:constants -->

Placement
Expand Down Expand Up @@ -165,7 +167,7 @@ Verbs the daemon adds
| `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, -516 when its pins_dir pin carries verdict=refused, -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 |
| `chain` | `<chain> <instance> <instance> [<instance> ...]` | `resp 0` | the instances, in this order, are one chain, run in one worker of place chain:<chain>: each member's state is saved, it is removed from the worker it was in and replayed in the chain's. A chain already declared takes the new order, and a member left out of it goes back to a worker of its own. A worker that runs chains in one client (clap, lv2-clap) is sent this line, with the members it holds, after the adds of its replay; one that does not (mod-host) never is, and a refusal of it is written to stderr and is no failure. Every instance verb still names one member by its own number. A member that crashes the worker is quarantined to a worker of its own and the chain goes on without it; quarantine_clear puts it back. -518 for an instance no add made, -519 for one named twice, -520 for one in another chain, -521 for members of more than one format, -501 for a plugin that may not share a worker while require_pins is on, -902 for a chain or an instance that is not a number, or fewer than two instances |
| `chain` | `<chain> <instance> [<instance> ...]` | `resp 0` | the instances, in this order, are one chain, run in one worker of place chain:<chain>: each member's state is saved, it is removed from the worker it was in and replayed in the chain's. A chain already declared takes the new order, and a member left out of it goes back to a worker of its own. A worker that runs chains in one client (clap, lv2-clap) is sent this line, with the members it holds, after the adds of its replay; one that does not (mod-host) never is, and a refusal of it is written to stderr and is no failure. Every instance verb still names one member by its own number. A member that crashes the worker is quarantined to a worker of its own and the chain goes on without it; quarantine_clear puts it back. -518 for an instance no add made, -519 for one named twice, -520 for one in another chain, -521 for members of more than one format, -501 for a plugin that may not share a worker while require_pins is on (every LV2 plugin, a chain of one as well, until a chain's worker is given the union of its members' bundles), -902 for a chain or an instance that is not a number, or fewer than chain_members_min (1) instances |
| `chain_clear` | `<chain>` | `resp 0` | forget the chain: each member goes to a worker of its own; -522 for a chain no chain declared |

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).
Expand Down Expand Up @@ -220,6 +222,85 @@ stays held at the controller. omx-clap-host writes these lines from FreeMixer/om
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.
<!-- END GENERATED protocol:instance-verbs -->

What each host profile owes every verb a controller sends: mod-host's commands, the two that Zynthian's mod-ui still
names, and the verbs the daemon adds. A verb has a row here before the daemon serves it.

<!-- BEGIN GENERATED protocol:host-verbs -->
| verb | sent by | lv2-worker | clap-worker | console | zynthian |
|---|---|---|---|---|---|
| `add` | mod-ui | required | required | required | required |
| `remove` | mod-ui | required | required | required | required |
| `bypass` | mod-ui | required | required | required | required |
| `connect` | mod-ui | required | required | required | required |
| `param_set` | mod-ui | required | required | required | required |
| `state_load` | mod-ui | required | required | required | required |
| `state_save` | mod-ui | required | required | required | required |
| `state_tmpdir` | mod-ui | required | required | required | required |
| `feature_enable` | mod-ui | required | required | required | required |
| `bundle_add` | mod-ui | required | required | required | required |
| `bundle_remove` | mod-ui | required | required | required | required |
| `preset_load` | mod-ui | required | required | required | required |
| `preset_save` | mod-ui | required | required | required | required |
| `preset_show` | mod-ui | required | required | required | required |
| `transport` | mod-ui | required | required | required | required |
| `transport_sync` | mod-ui | required | required | required | required |
| `cpu_load` | mod-ui | required | required | required | required |
| `midi_learn` | mod-ui | optional | required | optional | required |
| `midi_map` | mod-ui | optional | required | optional | required |
| `midi_unmap` | mod-ui | optional | required | optional | required |
| `cv_map` | mod-ui | optional | required | optional | required |
| `cv_unmap` | mod-ui | optional | required | optional | required |
| `monitor_midi_program` | mod-ui | optional | optional | optional | optional |
| `monitor_output` | mod-ui | optional | optional | optional | optional |
| `output_data_ready` | mod-ui | optional | optional | optional | optional |
| `patch_get` | mod-ui | optional | optional | optional | optional |
| `patch_set` | mod-ui | optional | optional | optional | optional |
| `cc_map` | mod-ui | absent | absent | absent | absent |
| `cc_value_set` | mod-ui | absent | absent | absent | absent |
| `cc_unmap` | mod-ui | absent | absent | absent | absent |
| `hmi_map` | mod-ui | absent | absent | absent | absent |
| `hmi_unmap` | mod-ui | absent | absent | absent | absent |
| `show_external_ui` | mod-ui | absent | absent | absent | absent |
| `set_midi_program_change_pedalboard_bank_channel` | mod-ui | absent | absent | absent | absent |
| `set_midi_program_change_pedalboard_snapshot_channel` | mod-ui | absent | absent | absent | absent |
| `disconnect` | mod-host | required | required | required | required |
| `set_bpm` | mod-host | required | required | required | required |
| `set_bpb` | mod-host | required | required | required | required |
| `quit` | mod-host | required | required | required | required |
| `param_get` | mod-host | optional | optional | optional | optional |
| `param_monitor` | mod-host | optional | optional | optional | optional |
| `licensee` | mod-host | optional | optional | optional | optional |
| `monitor` | mod-host | optional | optional | optional | optional |
| `load` | mod-host | optional | optional | optional | optional |
| `save` | mod-host | optional | optional | optional | optional |
| `help` | mod-host | optional | optional | optional | optional |
| `track_info` | mod-host | optional | optional | optional | optional |
| `remote_pages` | mod-host | optional | optional | optional | optional |
| `remote_page_get` | mod-host | optional | optional | optional | optional |
| `param_info` | mod-host | optional | optional | optional | optional |
| `preset_list` | daemon | optional | optional | optional | optional |
| `preset_get` | daemon | optional | optional | optional | optional |
| `note_input` | daemon | optional | optional | optional | optional |
| `note_on` | daemon | optional | optional | optional | optional |
| `note_off` | daemon | optional | optional | optional | optional |
| `all_notes_off` | daemon | optional | optional | optional | optional |
| `worker_list` | daemon | required | required | required | required |
| `instance_info` | daemon | required | required | required | required |
| `supervisor_reset` | daemon | required | required | required | required |
| `quarantine_clear` | daemon | required | required | required | required |
| `policy_set` | daemon | required | required | required | required |
| `pool_config` | daemon | required | required | required | required |
| `worker_env` | daemon | required | required | required | required |
| `pin_set` | daemon | required | required | required | required |
| `pin_clear` | daemon | required | required | required | required |
| `pin_accept` | daemon | required | required | required | required |
| `lv2_worker_set` | daemon | required | required | required | required |
| `chain` | daemon | required | required | required | required |
| `chain_clear` | daemon | required | required | required | required |

The profiles: `lv2-worker`, an LV2 plugin's worker: mod-host, or omx-clap-host's LV2 adapter; `clap-worker`, a CLAP plugin's worker, omx-clap-host; `console`, the openmixer console as a controller of the daemon; `zynthian`, Zynthian's mod-ui and its plugind engine as a controller of the daemon. `required` is answered as its grammar says; `optional` is answered, or refused with -902, and never misread; `absent` is refused on purpose, and no client of the profile may need it.
<!-- END GENERATED protocol:host-verbs -->

Error codes the daemon adds to mod-host's:

<!-- BEGIN GENERATED protocol:errors -->
Expand Down
30 changes: 22 additions & 8 deletions docs/design/notes/2026-10-21-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ down. plugin-hostd exists to isolate crashes, so the member that crashed must le

## The verbs

chain <chain> <instance> <instance> [<instance> ...] resp 0
chain <chain> <instance> [<instance> ...] resp 0
chain_clear <chain> resp 0

`<chain>` is a number, as an instance is. `chain` declares the chain's members in order, and they must be instances an
Expand All @@ -34,7 +34,8 @@ A refusal changes nothing:
| `PLACEMENT_INVALID` | a member whose plugin may not share a worker while `require_pins` is on, as for a pool |
| `NO_SUCH_CHAIN` | `chain_clear` of a chain no `chain` declared |

A list of fewer than two instances, or a word that is not a number, is outside the grammar (-902).
A chain of one is valid: one isolated plugin is a chain like any other (`chain_members_min`, 1, in the declared
constants). A list of no instance, or a word that is not a number, is outside the grammar (-902).

## What the worker is told

Expand Down Expand Up @@ -67,12 +68,25 @@ chain, and the chain's worker is sent the order without it. A chain whose last m
What omx-clap-host does with the `chain` line, one client and one callback, is omx-clap-core#104. A `chain:` place
cannot be named by `add`: an instance joins a chain only through `chain`.

## Open question
## LV2 chains under require_pins: the union-narrowed world

While `require_pins` is on, a chain is refused with `PLACEMENT_INVALID` when a member's plugin may not share a worker,
by the rule a pool keeps. Every LV2 plugin falls under it, on the `lv2` row and on the `lv2-clap` row (omx-clap-host's
adapter), because an LV2 pin holds only in a world narrowed to the plugin's own bundle. So a chain of LV2 plugins runs
only with `require_pins 0`, and CLAP plugins chain as they pool (under `pin_mode first-use`, only those whose pin says
`verdict=in-process`). omx-clap-core#104's bench is eight LV2 plugins through the adapter. Should a chain's worker get a
world narrowed to the union of its members' bundles, so that each member's pin still holds in it? That needs the
format's `narrow_world` to take a list of uris.
adapter), because an LV2 pin holds only in a world narrowed to the plugin's own bundle, and a chain's worker is given
the whole `lv2_path`. A chain of one LV2 member is refused too: its worker is a chain's, not an own one, so its world
is not narrowed. So today a chain of LV2 plugins runs only with `require_pins 0`, and CLAP plugins chain as they pool
(under `pin_mode first-use`, only those whose pin says `verdict=in-process`).

Required (operator ruling 2026-10-21, a precondition of removing mod-host): a chain's worker gets a world narrowed to
the UNION of its members' bundles, so that each member's pin still holds in it. The format's `narrow_world` takes the
list of the members' uris and links each one's bundle into the worker's world; a member whose bundle is not found
leaves the chain unformed rather than widening the world. With it, a chain of LV2 members is accepted under
`require_pins`, and a chain of one gets the world an own worker gets.

What it must not do, as measured: in omx-clap-core's Stage 0 bench (eight LV2 plugins chained through the adapter), a
chain member in an `LV2_PATH` of a single bundle answered `resp -101` (no plugin of that uri). A world narrowed to one
member's bundle is that failure; the union is the fix.

Held by `under_require_pins_a_chain_of_lv2_members_is_refused_until_its_worker_gets_the_union_of_their_bundles`
(tests/daemon_test.py): the refusal, for a chain of one and of two, until the union world is built; that test then
turns into its proof (the chain forms, and each member's worker world holds exactly the members' bundles).
Loading
Loading