From e81e7899314a298e91b8d54859d005f806f1053a Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:04:15 +0100 Subject: [PATCH 1/9] Declare the daemon's own verbs and reply codes in its contract directory data/verbs.json carries PLUGIN_HOSTD_VERBS, the verbs plugin-hostd adds to mod-host's protocol and answers itself (worker_list, instance_info, supervisor_reset, quarantine_clear, policy_set, pool_config, worker_env, pin_set, pin_clear, pin_accept, lv2_worker_set, chain, chain_clear, chain_get, descriptor_get, panel_get, preset_list, preset_get) and pin_layout, which it sends a worker, with the format, the reply, the refusals and what each host owes each one. PLUGIN_HOSTD_REPLY_CODES carries the numbers PHD_ERRORS answers, -501 to -531, and the two mod-host numbers the daemon answers for its own verbs, -902 and -101. omx-contract 3.3.0 validates the package installed beside its own: 121 items in 92 files from 2 packages, valid; fmt --check is clean. --- data/verbs.json | 537 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 data/verbs.json diff --git a/data/verbs.json b/data/verbs.json new file mode 100644 index 0000000..bca0c5a --- /dev/null +++ b/data/verbs.json @@ -0,0 +1,537 @@ +{ + "$schema": "../schema/omx-contract.schema.json", + "requires": { + "omx-contract-schema": ">= 3.3.0" + }, + "PLUGIN_HOSTD_REPLY_CODES": { + "kind": "codes", + "since": "0.12.0", + "doc": "The refusals plugin-hostd answers by number (include/plugin-hostd/protocol.h PHD_ERRORS), and the two of mod-host's numbers it answers for its own verbs.", + "codes": { + "invalidOperation": { + "code": "reply.plugin-hostd.invalid-operation", + "resp": -902, + "doc": "mod-host's ERR_INVALID_OPERATION, answered by the daemon for a line outside the grammar of one of its own verbs" + }, + "instanceUnknown": { + "code": "reply.plugin-hostd.instance-unknown", + "resp": -101, + "doc": "mod-host's ERR_INSTANCE_NON_EXISTS, answered by the daemon for an instance no add made" + }, + "placementInvalid": { + "code": "reply.plugin-hostd.placement-invalid", + "resp": -501, + "doc": "PHD_ERR_PLACEMENT_INVALID: not a placement, or not valid here" + }, + "noBackend": { + "code": "reply.plugin-hostd.no-backend", + "resp": -502, + "doc": "PHD_ERR_NO_BACKEND: no worker program for the scheme" + }, + "workerSpawn": { + "code": "reply.plugin-hostd.worker-spawn", + "resp": -503, + "doc": "PHD_ERR_WORKER_SPAWN: the worker is not up" + }, + "replay": { + "code": "reply.plugin-hostd.replay", + "resp": -504, + "doc": "PHD_ERR_REPLAY: the name of a refused replay on stderr, never a reply" + }, + "gaveUp": { + "code": "reply.plugin-hostd.gave-up", + "resp": -505, + "doc": "PHD_ERR_GAVE_UP: the storm bound is spent for that placement" + }, + "noSuchWorker": { + "code": "reply.plugin-hostd.no-such-worker", + "resp": -506, + "doc": "PHD_ERR_NO_SUCH_WORKER: no such worker" + }, + "verbDropped": { + "code": "reply.plugin-hostd.verb-dropped", + "resp": -507, + "doc": "PHD_ERR_VERB_DROPPED: the worker died on this very command and the daemon dropped it" + }, + "pinAbsent": { + "code": "reply.plugin-hostd.pin-absent", + "resp": -508, + "doc": "PHD_ERR_PIN_ABSENT: the plugin has no pin, or its layout pin is in a scheme this version does not know" + }, + "pinBinaryMismatch": { + "code": "reply.plugin-hostd.pin-binary-mismatch", + "resp": -509, + "doc": "PHD_ERR_PIN_BINARY_MISMATCH: a pinned file is missing or its SHA-256 differs" + }, + "pinLayoutMismatch": { + "code": "reply.plugin-hostd.pin-layout-mismatch", + "resp": -510, + "doc": "PHD_ERR_PIN_LAYOUT_MISMATCH: the parameter layout after init differs from the layout pin" + }, + "noParamContract": { + "code": "reply.plugin-hostd.no-param-contract", + "resp": -511, + "doc": "PHD_ERR_NO_PARAM_CONTRACT: the host holds no checked unit and scale for this parameter" + }, + "lv2AdapterRefused": { + "code": "reply.plugin-hostd.lv2-adapter-refused", + "resp": -512, + "doc": "PHD_ERR_LV2_ADAPTER_REFUSED: the lv2-clap worker's LV2 adapter cannot run this plugin" + }, + "noNoteInput": { + "code": "reply.plugin-hostd.no-note-input", + "resp": -513, + "doc": "PHD_ERR_NO_NOTE_INPUT: the instance's plugin takes no notes" + }, + "notesByJack": { + "code": "reply.plugin-hostd.notes-by-jack", + "resp": -514, + "doc": "PHD_ERR_NOTES_BY_JACK: the instance's worker takes no notes on its socket" + }, + "noPresetList": { + "code": "reply.plugin-hostd.no-preset-list", + "resp": -515, + "doc": "PHD_ERR_NO_PRESET_LIST: the instance's worker cannot list the plugin's presets" + }, + "pinRefused": { + "code": "reply.plugin-hostd.pin-refused", + "resp": -516, + "doc": "PHD_ERR_PIN_REFUSED: the plugin's pin carries verdict=refused" + }, + "noteQueueFull": { + "code": "reply.plugin-hostd.note-queue-full", + "resp": -517, + "doc": "PHD_ERR_NOTE_QUEUE_FULL: the instance's worker has no room for this note before its next block" + }, + "chainUnknownInstance": { + "code": "reply.plugin-hostd.chain-unknown-instance", + "resp": -518, + "doc": "PHD_ERR_CHAIN_UNKNOWN_INSTANCE: a member is an instance no add made" + }, + "chainDuplicate": { + "code": "reply.plugin-hostd.chain-duplicate", + "resp": -519, + "doc": "PHD_ERR_CHAIN_DUPLICATE: an instance is named twice in one chain" + }, + "chainInstanceBusy": { + "code": "reply.plugin-hostd.chain-instance-busy", + "resp": -520, + "doc": "PHD_ERR_CHAIN_INSTANCE_BUSY: a member is already in another chain" + }, + "chainMixedFormat": { + "code": "reply.plugin-hostd.chain-mixed-format", + "resp": -521, + "doc": "PHD_ERR_CHAIN_MIXED_FORMAT: no one worker program runs every member" + }, + "noSuchChain": { + "code": "reply.plugin-hostd.no-such-chain", + "resp": -522, + "doc": "PHD_ERR_NO_SUCH_CHAIN: a chain no chain line declared" + }, + "presetNotOffered": { + "code": "reply.plugin-hostd.preset-not-offered", + "resp": -523, + "doc": "PHD_ERR_PRESET_NOT_OFFERED: a stable name the plugin does not offer" + }, + "presetInvalid": { + "code": "reply.plugin-hostd.preset-invalid", + "resp": -524, + "doc": "PHD_ERR_PRESET_INVALID: a preset file or path that is not the preset format" + }, + "presetNeedsState": { + "code": "reply.plugin-hostd.preset-needs-state", + "resp": -525, + "doc": "PHD_ERR_PRESET_NEEDS_STATE: a preset file that carries state for a worker that cannot take state" + }, + "presetPluginChanged": { + "code": "reply.plugin-hostd.preset-plugin-changed", + "resp": -526, + "doc": "PHD_ERR_PRESET_PLUGIN_CHANGED: the preset file's plugin id, version or fingerprint is not the instance's" + }, + "loadRefused": { + "code": "reply.plugin-hostd.load-refused", + "resp": -527, + "doc": "PHD_ERR_LOAD_REFUSED: the plugin refused the preset or the state its file carries" + }, + "dirNotWritable": { + "code": "reply.plugin-hostd.dir-not-writable", + "resp": -528, + "doc": "PHD_ERR_DIR_NOT_WRITABLE: descriptor_dir or qualification_dir cannot be made or written" + }, + "descriptorAbsent": { + "code": "reply.plugin-hostd.descriptor-absent", + "resp": -529, + "doc": "PHD_ERR_DESCRIPTOR_ABSENT: no descriptor (panel) for the plugin's binary as it is now" + }, + "descriptorPending": { + "code": "reply.plugin-hostd.descriptor-pending", + "resp": -530, + "doc": "PHD_ERR_DESCRIPTOR_PENDING: the scan its add started has not written the descriptor (panel) yet" + }, + "descriptorScanFailed": { + "code": "reply.plugin-hostd.descriptor-scan-failed", + "resp": -531, + "doc": "PHD_ERR_DESCRIPTOR_SCAN_FAILED: the plugin's last scan wrote no descriptor (panel)" + } + } + }, + "PLUGIN_HOSTD_VERBS": { + "kind": "verbs", + "since": "0.12.0", + "doc": "The verbs plugin-hostd adds to mod-host's protocol and answers itself, and the ones it sends a worker (README.md, Verbs the daemon adds).", + "verbs": { + "workerList": { + "verb": "worker_list", + "format": "worker_list", + "reply": "resp :::::,,...", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The workers, one record each." + }, + "instanceInfo": { + "verb": "instance_info", + "format": "instance_info %i", + "reply": "resp 0 ", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Where an instance lives and what it has cost." + }, + "supervisorReset": { + "verb": "supervisor_reset", + "format": "supervisor_reset ...", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchWorker" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Re-arm the storm bound and the backoff of a given-up placement: [ | all]." + }, + "quarantineClear": { + "verb": "quarantine_clear", + "format": "quarantine_clear %s", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Let a quarantined instance go back where its placement says: | all." + }, + "policySet": { + "verb": "policy_set", + "format": "policy_set %s %s", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The placement of an add that says default: own | pool:." + }, + "poolConfig": { + "verb": "pool_config", + "format": "pool_config %s %i", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The instances a pool holds before a sibling opens: ." + }, + "workerEnv": { + "verb": "worker_env", + "format": "worker_env %s %s %s", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The cpu list and the nice value of the workers of a format, at spawn and on the ones running: ." + }, + "pinSet": { + "verb": "pin_set", + "format": "pin_set %s %s %s ...", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The pin of one plugin: =[,=...] : [verdict=]." + }, + "pinClear": { + "verb": "pin_clear", + "format": "pin_clear %s", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Forget the pin of one plugin, or of every one: | all." + }, + "pinAccept": { + "verb": "pin_accept", + "format": "pin_accept %s", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.pinAbsent" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.pinBinaryMismatch" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Under pin_mode first-use, take the plugin's files as they are now as its pin: ." + }, + "pinLayout": { + "verb": "pin_layout", + "format": "pin_layout %i", + "reply": "resp 0 :", + "implementedBy": { + "omx-clap-host": "optional", + "mod-host": "unsupported" + }, + "doc": "Sent by the daemon to a worker that checks layouts, after it accepted an add whose layout pin is unknown: the layout the worker found after init." + }, + "lv2WorkerSet": { + "verb": "lv2_worker_set", + "format": "lv2_worker_set %s %s", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The worker of one LV2 plugin, for its adds from now on: ." + }, + "chain": { + "verb": "chain", + "format": "chain %i %i ...", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.chainUnknownInstance" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.chainDuplicate" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.chainInstanceBusy" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.chainMixedFormat" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required", + "omx-clap-host": "required", + "mod-host": "unsupported" + }, + "doc": "The instances, in this order, are one chain run in one worker: [ ...]; a worker that runs chains in one client is sent the line with the members it holds." + }, + "chainClear": { + "verb": "chain_clear", + "format": "chain_clear %i", + "reply": "resp 0", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchChain" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Forget the chain: each member goes to a worker of its own." + }, + "chainGet": { + "verb": "chain_get", + "format": "chain_get %i", + "reply": "resp 0 ,... ,...", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchChain" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "Read a chain back: its worker, its members in the declared order, and the members its worker runs now." + }, + "descriptorGet": { + "verb": "descriptor_get", + "format": "descriptor_get %s", + "reply": "resp 0 \"\"", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.dirNotWritable" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorAbsent" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorPending" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorScanFailed" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The scanned descriptor of the plugin, answered from descriptor_dir: ." + }, + "panelGet": { + "verb": "panel_get", + "format": "panel_get %s", + "reply": "resp 0 \"\"", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.dirNotWritable" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorAbsent" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorPending" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorScanFailed" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "required" + }, + "doc": "The panel the plugin publishes, answered from descriptor_dir: ." + }, + "presetList": { + "verb": "preset_list", + "format": "preset_list %i", + "reply": "resp ", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.noPresetList" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "optional", + "omx-clap-host": "optional", + "mod-host": "unsupported" + }, + "doc": "The plugin's presets, the ones its preset discovery or its bundle lists." + }, + "presetGet": { + "verb": "preset_get", + "format": "preset_get %i %i", + "reply": "resp 0 ", + "codes": [ + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.noPresetList" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + } + ], + "implementedBy": { + "plugin-hostd": "optional", + "omx-clap-host": "optional", + "mod-host": "unsupported" + }, + "doc": "One preset, 0 to count - 1: the key preset_load takes for it, and its name." + } + } + } +} From 22a08e4968f8618bcbaaa68f26231607cd2efbce Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:12:42 +0100 Subject: [PATCH 2/9] Read the daemon's own verbs from the rendered header, not from protocol.h omx-contract renders data/verbs.json into include/plugin-hostd/plugin_hostd_host_verbs.h (OMX_VERB_, _FMT, _REPLY, _CODES_INIT and the table of what each host owes each verb) and plugin_hostd_vocabulary.h (the reply codes as strings). make render-verbs writes them from the package as it installs, beside the installed contract directory and OMX_CONTRACT_PATH; make check-rendered fails on any drift. make install puts both beside protocol.h, which includes the verbs header. The PHD_VERB_* defines of the daemon's own verbs are gone: every source, the fake host, the generator and the fuzz harnesses read OMX_VERB_*, and the PHD_VERBS and PHD_INSTANCE_VERBS rows take their word and their reply from the render. protocol.h keeps add (mod-host's, with a placement), pin_expect, the instance verbs a worker answers and the note verbs, which omx-clap-core declares, and the error numbers, the settings, the events and the prose of the tables. The generated README, man page and protocol/plugin-hostd.json do not change. tests/verbs_contract.py reads the rendered words too: no C string of src/, fake_host.c or protocol-gen.c starts with one, nor one of fuzz/ or protocol.h. tests/perturbation.py renames a verb in a scratch data/verbs.json: check-rendered and check-generated go red, and after render-verbs and gen the rebuilt daemon answers instance_where and not instance_info. --- Makefile | 30 ++- fuzz/fuzz_command.c | 2 +- fuzz/recorder.c | 28 +-- fuzz/sabotage/command-arity-broadcast.patch | 2 +- fuzz/sabotage/command-arity.patch | 4 +- .../plugin-hostd/plugin_hostd_host_verbs.h | 211 ++++++++++++++++++ .../plugin-hostd/plugin_hostd_vocabulary.h | 48 ++++ include/plugin-hostd/protocol.h | 162 +++++++------- src/formats.h | 2 +- src/pins.c | 6 +- src/supervisor.c | 6 +- src/verbs.c | 32 +-- tests/fake_host.c | 16 +- tests/perturbation.py | 44 +++- tests/sabotage.py | 12 +- tests/verbs_contract.py | 52 +++-- tools/protocol-gen.c | 8 +- 17 files changed, 485 insertions(+), 180 deletions(-) create mode 100644 include/plugin-hostd/plugin_hostd_host_verbs.h create mode 100644 include/plugin-hostd/plugin_hostd_vocabulary.h diff --git a/Makefile b/Makefile index 79f35e9..7344b45 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,10 @@ SRC = src/main.c src/conf.c src/proc.c src/supervisor.c src/verbs.c src/pins.c s src/descriptors.c src/describe.c src/transforms.c src/json.c OBJ = $(SRC:.c=.o) +# the C names of the daemon's verbs and reply codes, rendered from data/verbs.json (render-verbs below); protocol.h +# includes them +RENDERED = include/plugin-hostd/plugin_hostd_host_verbs.h include/plugin-hostd/plugin_hostd_vocabulary.h + # default build all: $(PROG) $(PROBE) @@ -69,7 +73,7 @@ $(PROG): $(OBJ) $(PROTOCOL_LIB) # the probe runner: it loads no plugin and links nothing of the daemon or of mod-host # describe reads the installed table of a format by default (docs/formats.md) PROBE_SRC = src/probe.c src/json.c src/describe.c src/transforms.c -$(PROBE): $(PROBE_SRC) src/json.h src/describe.h src/transforms.h include/plugin-hostd/protocol.h include/plugin-hostd/pin.h +$(PROBE): $(PROBE_SRC) src/json.h src/describe.h src/transforms.h include/plugin-hostd/protocol.h $(RENDERED) include/plugin-hostd/pin.h $(CC) -Iinclude -Isrc $(CONTRACT_CFLAGS) $(CFLAGS) -D_GNU_SOURCE -DPROBE_VERSION='"$(VERSION)"' \ -DPROBE_CONTRACTDIR='"$(CONTRACTDIR)"' -DPROBE_CONTRACT_MAJOR='"$(CONTRACT_SCHEMA_MAJOR)"' -o $@ $(PROBE_SRC) -lm @@ -79,7 +83,7 @@ $(PROTOCOL_LIB): endif # the declared protocol is compiled into every object -$(OBJ): include/plugin-hostd/protocol.h include/plugin-hostd/pin.h +$(OBJ): include/plugin-hostd/protocol.h $(RENDERED) include/plugin-hostd/pin.h src/descriptors.o: src/descriptors.h src/describe.h src/json.h src/formats.h src/supervisor.h @@ -115,6 +119,7 @@ install_protocol: install -d $(DESTDIR)$(DATADIR)/plugin-hostd/pins.d install -d $(DESTDIR)$(INCLUDEDIR)/plugin-hostd install -m 644 include/plugin-hostd/protocol.h $(DESTDIR)$(INCLUDEDIR)/plugin-hostd/protocol.h + install -m 644 $(RENDERED) $(DESTDIR)$(INCLUDEDIR)/plugin-hostd/ install -m 644 include/plugin-hostd/pin.h $(DESTDIR)$(INCLUDEDIR)/plugin-hostd/pin.h install -d $(DESTDIR)$(DATADIR)/pkgconfig sed -e 's,@PREFIX@,$(PREFIX),g' -e 's,@VERSION@,$(VERSION),g' protocol/plugin-hostd.pc.in > $(DESTDIR)$(DATADIR)/pkgconfig/plugin-hostd.pc @@ -133,6 +138,19 @@ install-contract: @test -n "$(INSTALL_CONTRACT_DIR)" || { echo "install-contract: no install-contract-dir.sh: install omx-contract-devel, or set OMX_CONTRACT_DIR to a checkout" >&2; exit 1; } sh $(INSTALL_CONTRACT_DIR) $(PROG) $(VERSION) $(CONTRACT_SCHEMA_MAJOR) data $(DESTDIR)$(CONTRACTDIR) data +# the daemon's verbs and reply codes (data/verbs.json), rendered by omx-contract as the package installs them, beside +# the packages of OMX_CONTRACT_PATH and of the installed contract directory: the C names every source and protocol.h +# read (OMX_VERB_, its _FMT, _REPLY and _CODES_INIT). `make render-verbs` writes them, `make check-rendered` +# fails on any drift. +OMX_CONTRACT ?= omx-contract +.PHONY: render-verbs check-rendered +render-verbs check-rendered: + @test -n "$(INSTALL_CONTRACT_DIR)" || { echo "$@: no install-contract-dir.sh: install omx-contract-devel, or set OMX_CONTRACT_DIR to a checkout" >&2; exit 1; } + @set -e; tmp=$$(mktemp -d); trap 'rm -rf $$tmp' EXIT; \ + sh $(INSTALL_CONTRACT_DIR) $(PROG) $(VERSION) $(CONTRACT_SCHEMA_MAJOR) data $$tmp data >/dev/null; \ + OMX_CONTRACT_PATH=$$tmp$(if $(OMX_CONTRACT_PATH),:$(OMX_CONTRACT_PATH)):$(CONTRACTDIR) \ + $(OMX_CONTRACT) render --target c --package $(PROG) --installed --out include/plugin-hostd $(if $(filter check-rendered,$@),--check) + # the tables cover their format (tests/format_coverage.py, docs/formats.md#the-coverage-test): every term of # omx-contract's installed TERM files and every row with a from, once per format, each field one row; then its sabotage # arms, each red naming its field. CONTRACT_SCHEMA_MAJOR= reads the TERM files of another major @@ -178,7 +196,7 @@ FUZZ_INCS := -Iinclude -Isrc -Ifuzz $(PROTOCOL_CFLAGS) # reply harness includes src/relay.c itself FUZZ_SRC_command := src/verbs.c fuzz/recorder.c FUZZ_SRC_reply := -FUZZ_DEPS := fuzz/fuzz.h fuzz/syntax.h fuzz/recorder.h src/relay.c src/verbs.h include/plugin-hostd/protocol.h +FUZZ_DEPS := fuzz/fuzz.h fuzz/syntax.h fuzz/recorder.h src/relay.c src/verbs.h include/plugin-hostd/protocol.h $(RENDERED) $(FUZZ_BUILD)/fuzz_%: fuzz/fuzz_%.c $(FUZZ_DEPS) $(FUZZ_SRC_command) @mkdir -p $(dir $@) @@ -288,7 +306,7 @@ $(TRANSFORMS_TEST): tests/transforms_test.c src/transforms.c src/transforms.h test-relay: $(RELAY_TEST) ./$(RELAY_TEST) -$(RELAY_TEST): tests/relay_test.c src/relay.c src/relay.h include/plugin-hostd/protocol.h +$(RELAY_TEST): tests/relay_test.c src/relay.c src/relay.h include/plugin-hostd/protocol.h $(RENDERED) $(CC) -Iinclude $(CFLAGS) -Werror -Drealloc=relay_test_realloc -c -o tests/relay-test-relay.o src/relay.c $(CC) -Isrc $(CFLAGS) -Werror -o $@ tests/relay_test.c tests/relay-test-relay.o -pthread # the relay's gesture table and lv2.c's refusal and pick tables when memory runs out: each is built with its realloc @@ -297,7 +315,7 @@ test-alloc: $(GESTURE_ALLOC_TEST) $(LV2_ALLOC_TEST) ./$(GESTURE_ALLOC_TEST) ./$(LV2_ALLOC_TEST) -$(GESTURE_ALLOC_TEST): tests/gesture_alloc_test.c src/relay.c src/relay.h include/plugin-hostd/protocol.h +$(GESTURE_ALLOC_TEST): tests/gesture_alloc_test.c src/relay.c src/relay.h include/plugin-hostd/protocol.h $(RENDERED) $(CC) -Iinclude -Isrc $(CFLAGS) -Werror -Drealloc=alloc_test_realloc -o $@ tests/gesture_alloc_test.c -pthread $(LV2_ALLOC_TEST): tests/lv2_alloc_test.c src/lv2.c src/formats.h @@ -361,7 +379,7 @@ tests/jack_levels: tests/jack_levels.c GEN = tools/protocol-gen GENERATED = README.md doc/plugin-hostd.1 protocol/plugin-hostd.json protocol/mod-host.json protocol/probe-output.schema.json -$(GEN): tools/protocol-gen.c include/plugin-hostd/protocol.h +$(GEN): tools/protocol-gen.c include/plugin-hostd/protocol.h $(RENDERED) $(CC) -Wall -Wextra -Werror -std=gnu99 -Iinclude $(CONTRACT_CFLAGS) -o $@ tools/protocol-gen.c gen: $(GEN) diff --git a/fuzz/fuzz_command.c b/fuzz/fuzz_command.c index c13c039..9248eaf 100644 --- a/fuzz/fuzz_command.c +++ b/fuzz/fuzz_command.c @@ -264,7 +264,7 @@ __attribute__((noinline)) static void check_reply(const struct words *w) * grammar is mod-host's, so each worker's mod-host refuses them */ static int passed_on(const char *verb) { - return !strcmp(verb, PHD_VERB_PIN_LAYOUT) || !strcmp(verb, PHD_VERB_PIN_EXPECT) || verb_is(verb, EFFECT_CONNECT) || + return !strcmp(verb, OMX_VERB_PIN_LAYOUT) || !strcmp(verb, PHD_VERB_PIN_EXPECT) || verb_is(verb, EFFECT_CONNECT) || verb_is(verb, EFFECT_DISCONNECT); } diff --git a/fuzz/recorder.c b/fuzz/recorder.c index 0538662..20008ff 100644 --- a/fuzz/recorder.c +++ b/fuzz/recorder.c @@ -389,7 +389,7 @@ char *sup_instance_verb(const char *line, int ledger) char *sup_worker_list(void) { - return answer(record(REC_WORKER_LIST), daemon_shape(PHD_VERB_WORKER_LIST), 0); + return answer(record(REC_WORKER_LIST), daemon_shape(OMX_VERB_WORKER_LIST), 0); } char *sup_instance_info(int id) @@ -397,7 +397,7 @@ char *sup_instance_info(int id) struct rec_call *c = record(REC_INSTANCE_INFO); arg_i(c, id); - return answer(c, daemon_shape(PHD_VERB_INSTANCE_INFO), 0); + return answer(c, daemon_shape(OMX_VERB_INSTANCE_INFO), 0); } char *sup_reset(const char *which) @@ -405,7 +405,7 @@ char *sup_reset(const char *which) struct rec_call *c = record(REC_RESET); arg_s(c, which); - return answer(c, daemon_shape(PHD_VERB_SUPERVISOR_RESET), 1); + return answer(c, daemon_shape(OMX_VERB_SUPERVISOR_RESET), 1); } char *sup_quarantine_clear(const char *which) @@ -413,7 +413,7 @@ char *sup_quarantine_clear(const char *which) struct rec_call *c = record(REC_QUARANTINE_CLEAR); arg_s(c, which); - return answer(c, daemon_shape(PHD_VERB_QUARANTINE_CLEAR), 1); + return answer(c, daemon_shape(OMX_VERB_QUARANTINE_CLEAR), 1); } char *sup_policy_set(const char *format, const char *place) @@ -422,7 +422,7 @@ char *sup_policy_set(const char *format, const char *place) arg_s(c, format); arg_s(c, place); - return answer(c, daemon_shape(PHD_VERB_POLICY_SET), 1); + return answer(c, daemon_shape(OMX_VERB_POLICY_SET), 1); } char *sup_pin_accept(const char *uri) @@ -430,7 +430,7 @@ char *sup_pin_accept(const char *uri) struct rec_call *c = record(REC_PIN_ACCEPT); arg_s(c, uri); - return answer(c, daemon_shape(PHD_VERB_PIN_ACCEPT), 1); + return answer(c, daemon_shape(OMX_VERB_PIN_ACCEPT), 1); } char *sup_pool_config(const char *name, int max) @@ -439,7 +439,7 @@ char *sup_pool_config(const char *name, int max) arg_s(c, name); arg_i(c, max); - return answer(c, daemon_shape(PHD_VERB_POOL_CONFIG), 1); + return answer(c, daemon_shape(OMX_VERB_POOL_CONFIG), 1); } char *sup_lv2_worker_set(const char *uri, const char *worker) @@ -448,7 +448,7 @@ char *sup_lv2_worker_set(const char *uri, const char *worker) arg_s(c, uri); arg_s(c, worker); - return answer(c, daemon_shape(PHD_VERB_LV2_WORKER_SET), 1); + return answer(c, daemon_shape(OMX_VERB_LV2_WORKER_SET), 1); } char *sup_worker_env(const char *format, const char *cpus, const char *nice) @@ -458,7 +458,7 @@ char *sup_worker_env(const char *format, const char *cpus, const char *nice) arg_s(c, format); arg_s(c, cpus); arg_s(c, nice); - return answer(c, daemon_shape(PHD_VERB_WORKER_ENV), 1); + return answer(c, daemon_shape(OMX_VERB_WORKER_ENV), 1); } char *sup_chain(int id, const int *ids, int n) @@ -475,7 +475,7 @@ char *sup_chain(int id, const int *ids, int n) o += (size_t)sprintf(list + o, k ? " %d" : "%d", ids[k]); arg_s(c, list); free(list); - return answer(c, daemon_shape(PHD_VERB_CHAIN), 1); + return answer(c, daemon_shape(OMX_VERB_CHAIN), 1); } char *sup_chain_clear(int id) @@ -483,7 +483,7 @@ char *sup_chain_clear(int id) struct rec_call *c = record(REC_CHAIN_CLEAR); arg_i(c, id); - return answer(c, daemon_shape(PHD_VERB_CHAIN_CLEAR), 1); + return answer(c, daemon_shape(OMX_VERB_CHAIN_CLEAR), 1); } char *sup_chain_get(int id) @@ -491,7 +491,7 @@ char *sup_chain_get(int id) struct rec_call *c = record(REC_CHAIN_GET); arg_i(c, id); - return answer(c, daemon_shape(PHD_VERB_CHAIN_GET), 1); + return answer(c, daemon_shape(OMX_VERB_CHAIN_GET), 1); } /* a checkpoint of what a preset or patch changed: it writes down state, it changes none */ @@ -556,7 +556,7 @@ char *descriptors_get(const char *uri) struct rec_call *c = record(REC_DESCRIPTORS_GET); arg_s(c, uri); - return answer(c, daemon_shape(PHD_VERB_DESCRIPTOR_GET), 0); + return answer(c, daemon_shape(OMX_VERB_DESCRIPTOR_GET), 0); } char *descriptors_panel_get(const char *uri) @@ -564,7 +564,7 @@ char *descriptors_panel_get(const char *uri) struct rec_call *c = record(REC_DESCRIPTORS_PANEL_GET); arg_s(c, uri); - return answer(c, daemon_shape(PHD_VERB_PANEL_GET), 0); + return answer(c, daemon_shape(OMX_VERB_PANEL_GET), 0); } /* the adapter is asked again about what it refused: a change */ diff --git a/fuzz/sabotage/command-arity-broadcast.patch b/fuzz/sabotage/command-arity-broadcast.patch index cabfdfe..06e26f7 100644 --- a/fuzz/sabotage/command-arity-broadcast.patch +++ b/fuzz/sabotage/command-arity-broadcast.patch @@ -7,7 +7,7 @@ index 3b7feba..95bb8ec 100644 +++ b/src/verbs.c @@ -149,7 +149,7 @@ static char *handle(char *line, char **tok, int ntok) return ntok == 2 ? descriptors_get(tok[1]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_PANEL_GET)) + if (!strcmp(verb, OMX_VERB_PANEL_GET)) return ntok == 2 ? descriptors_panel_get(tok[1]) : sup_resp(ERR_INVALID_OPERATION); - if (verb_is(verb, EFFECT_REMOVE)) + if (verb_is(verb, EFFECT_REMOVE) && ntok == 2) diff --git a/fuzz/sabotage/command-arity.patch b/fuzz/sabotage/command-arity.patch index 222d2e2..b26cf42 100644 --- a/fuzz/sabotage/command-arity.patch +++ b/fuzz/sabotage/command-arity.patch @@ -7,8 +7,8 @@ index 78daab7..e940605 100644 +++ b/src/verbs.c @@ -171,5 +171,5 @@ static char *handle(char *line, char **tok, int ntok) return ntok == 3 ? sup_lv2_worker_set(tok[1], tok[2]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_POOL_CONFIG)) + if (!strcmp(verb, OMX_VERB_POOL_CONFIG)) - return ntok == 3 ? sup_pool_config(tok[1], atoi(tok[2])) : sup_resp(ERR_INVALID_OPERATION); + return ntok >= 3 ? sup_pool_config(tok[1], atoi(tok[2])) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_WORKER_ENV)) + if (!strcmp(verb, OMX_VERB_WORKER_ENV)) return ntok == 4 ? sup_worker_env(tok[1], tok[2], tok[3]) : sup_resp(ERR_INVALID_OPERATION); diff --git a/include/plugin-hostd/plugin_hostd_host_verbs.h b/include/plugin-hostd/plugin_hostd_host_verbs.h new file mode 100644 index 0000000..7b838e2 --- /dev/null +++ b/include/plugin-hostd/plugin_hostd_host_verbs.h @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Pau Aliagas +#ifndef PLUGIN_HOSTD_HOST_VERBS_H +#define PLUGIN_HOSTD_HOST_VERBS_H +/* + * GENERATED: DO NOT EDIT BY HAND. + * Produced by `omx-contract render --target c` from plugin-hostd's files. + * The host verbs (docs/contract-dir.md): a verb is OMX_VERB_, its word, OMX_VERB__FMT, the line + * protocol_add_command takes, and OMX_VERB__REPLY; a verb that answers refusals carries their resp numbers + * in OMX_VERB__CODES_INIT. plugin_hostd_host_verbs() is the table of what each host owes each verb. + */ + +#include + +#ifndef OMX_HOST_VERB_TYPES_H +#define OMX_HOST_VERB_TYPES_H +/* What a host owes a verb: required, answered as its grammar says; optional, answered or refused, never misread; + * unsupported, refused on purpose. */ +enum omx_host_verb_level { + OMX_HOST_VERB_UNSUPPORTED = 0, + OMX_HOST_VERB_OPTIONAL = 1, + OMX_HOST_VERB_REQUIRED = 2, +}; +/* The hosts of VERB_HOSTS, a column each of a verb's level, in this order. */ +#define OMX_HOST_VERB_HOST_PLUGIN_HOSTD 0u +#define OMX_HOST_VERB_HOST_OMX_CLAP_HOST 1u +#define OMX_HOST_VERB_HOST_MOD_HOST 2u +#define OMX_HOST_VERB_HOST_COUNT 3u +#define OMX_HOST_VERB_HOSTS_INIT { "plugin-hostd", "omx-clap-host", "mod-host" } +struct omx_host_verb { + const char *verb; + const char *format; + const char *reply; + enum omx_host_verb_level level[OMX_HOST_VERB_HOST_COUNT]; +}; +#endif /* OMX_HOST_VERB_TYPES_H */ + +/* PLUGIN_HOSTD_VERBS.workerList (data/verbs.json): The workers, one record each. */ +#define OMX_VERB_WORKER_LIST "worker_list" +#define OMX_VERB_WORKER_LIST_FMT "worker_list" +#define OMX_VERB_WORKER_LIST_REPLY "resp :::::,,..." +#define OMX_VERB_WORKER_LIST_CODES_COUNT 1u +#define OMX_VERB_WORKER_LIST_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.instanceInfo (data/verbs.json): Where an instance lives and what it has cost. */ +#define OMX_VERB_INSTANCE_INFO "instance_info" +#define OMX_VERB_INSTANCE_INFO_FMT "instance_info %i" +#define OMX_VERB_INSTANCE_INFO_REPLY "resp 0 " +#define OMX_VERB_INSTANCE_INFO_CODES_COUNT 2u +#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -101, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.supervisorReset (data/verbs.json): Re-arm the storm bound and the backoff of a given-up placement: [ | all]. */ +#define OMX_VERB_SUPERVISOR_RESET "supervisor_reset" +#define OMX_VERB_SUPERVISOR_RESET_FMT "supervisor_reset ..." +#define OMX_VERB_SUPERVISOR_RESET_REPLY "resp 0" +#define OMX_VERB_SUPERVISOR_RESET_CODES_COUNT 2u +#define OMX_VERB_SUPERVISOR_RESET_CODES_INIT { -506, -902 } /* reply.plugin-hostd.no-such-worker, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.quarantineClear (data/verbs.json): Let a quarantined instance go back where its placement says: | all. */ +#define OMX_VERB_QUARANTINE_CLEAR "quarantine_clear" +#define OMX_VERB_QUARANTINE_CLEAR_FMT "quarantine_clear %s" +#define OMX_VERB_QUARANTINE_CLEAR_REPLY "resp 0" +#define OMX_VERB_QUARANTINE_CLEAR_CODES_COUNT 2u +#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -101, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.policySet (data/verbs.json): The placement of an add that says default: own | pool:. */ +#define OMX_VERB_POLICY_SET "policy_set" +#define OMX_VERB_POLICY_SET_FMT "policy_set %s %s" +#define OMX_VERB_POLICY_SET_REPLY "resp 0" +#define OMX_VERB_POLICY_SET_CODES_COUNT 2u +#define OMX_VERB_POLICY_SET_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.poolConfig (data/verbs.json): The instances a pool holds before a sibling opens: . */ +#define OMX_VERB_POOL_CONFIG "pool_config" +#define OMX_VERB_POOL_CONFIG_FMT "pool_config %s %i" +#define OMX_VERB_POOL_CONFIG_REPLY "resp 0" +#define OMX_VERB_POOL_CONFIG_CODES_COUNT 2u +#define OMX_VERB_POOL_CONFIG_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.workerEnv (data/verbs.json): The cpu list and the nice value of the workers of a format, at spawn and on the ones running: . */ +#define OMX_VERB_WORKER_ENV "worker_env" +#define OMX_VERB_WORKER_ENV_FMT "worker_env %s %s %s" +#define OMX_VERB_WORKER_ENV_REPLY "resp 0" +#define OMX_VERB_WORKER_ENV_CODES_COUNT 1u +#define OMX_VERB_WORKER_ENV_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.pinSet (data/verbs.json): The pin of one plugin: =[,=...] : [verdict=]. */ +#define OMX_VERB_PIN_SET "pin_set" +#define OMX_VERB_PIN_SET_FMT "pin_set %s %s %s ..." +#define OMX_VERB_PIN_SET_REPLY "resp 0" +#define OMX_VERB_PIN_SET_CODES_COUNT 2u +#define OMX_VERB_PIN_SET_CODES_INIT { -516, -902 } /* reply.plugin-hostd.pin-refused, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.pinClear (data/verbs.json): Forget the pin of one plugin, or of every one: | all. */ +#define OMX_VERB_PIN_CLEAR "pin_clear" +#define OMX_VERB_PIN_CLEAR_FMT "pin_clear %s" +#define OMX_VERB_PIN_CLEAR_REPLY "resp 0" +#define OMX_VERB_PIN_CLEAR_CODES_COUNT 1u +#define OMX_VERB_PIN_CLEAR_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.pinAccept (data/verbs.json): Under pin_mode first-use, take the plugin's files as they are now as its pin: . */ +#define OMX_VERB_PIN_ACCEPT "pin_accept" +#define OMX_VERB_PIN_ACCEPT_FMT "pin_accept %s" +#define OMX_VERB_PIN_ACCEPT_REPLY "resp 0" +#define OMX_VERB_PIN_ACCEPT_CODES_COUNT 4u +#define OMX_VERB_PIN_ACCEPT_CODES_INIT { -508, -509, -516, -902 } /* reply.plugin-hostd.pin-absent, reply.plugin-hostd.pin-binary-mismatch, reply.plugin-hostd.pin-refused, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.pinLayout (data/verbs.json): Sent by the daemon to a worker that checks layouts, after it accepted an add whose layout pin is unknown: the layout the worker found after init. */ +#define OMX_VERB_PIN_LAYOUT "pin_layout" +#define OMX_VERB_PIN_LAYOUT_FMT "pin_layout %i" +#define OMX_VERB_PIN_LAYOUT_REPLY "resp 0 :" + +/* PLUGIN_HOSTD_VERBS.lv2WorkerSet (data/verbs.json): The worker of one LV2 plugin, for its adds from now on: . */ +#define OMX_VERB_LV2_WORKER_SET "lv2_worker_set" +#define OMX_VERB_LV2_WORKER_SET_FMT "lv2_worker_set %s %s" +#define OMX_VERB_LV2_WORKER_SET_REPLY "resp 0" +#define OMX_VERB_LV2_WORKER_SET_CODES_COUNT 1u +#define OMX_VERB_LV2_WORKER_SET_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.chain (data/verbs.json): The instances, in this order, are one chain run in one worker: [ ...]; a worker that runs chains in one client is sent the line with the members it holds. */ +#define OMX_VERB_CHAIN "chain" +#define OMX_VERB_CHAIN_FMT "chain %i %i ..." +#define OMX_VERB_CHAIN_REPLY "resp 0" +#define OMX_VERB_CHAIN_CODES_COUNT 6u +#define OMX_VERB_CHAIN_CODES_INIT { -518, -519, -520, -521, -501, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.chain-mixed-format, reply.plugin-hostd.placement-invalid, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.chainClear (data/verbs.json): Forget the chain: each member goes to a worker of its own. */ +#define OMX_VERB_CHAIN_CLEAR "chain_clear" +#define OMX_VERB_CHAIN_CLEAR_FMT "chain_clear %i" +#define OMX_VERB_CHAIN_CLEAR_REPLY "resp 0" +#define OMX_VERB_CHAIN_CLEAR_CODES_COUNT 2u +#define OMX_VERB_CHAIN_CLEAR_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.chainGet (data/verbs.json): Read a chain back: its worker, its members in the declared order, and the members its worker runs now. */ +#define OMX_VERB_CHAIN_GET "chain_get" +#define OMX_VERB_CHAIN_GET_FMT "chain_get %i" +#define OMX_VERB_CHAIN_GET_REPLY "resp 0 ,... ,..." +#define OMX_VERB_CHAIN_GET_CODES_COUNT 2u +#define OMX_VERB_CHAIN_GET_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.descriptorGet (data/verbs.json): The scanned descriptor of the plugin, answered from descriptor_dir: . */ +#define OMX_VERB_DESCRIPTOR_GET "descriptor_get" +#define OMX_VERB_DESCRIPTOR_GET_FMT "descriptor_get %s" +#define OMX_VERB_DESCRIPTOR_GET_REPLY "resp 0 \"\"" +#define OMX_VERB_DESCRIPTOR_GET_CODES_COUNT 5u +#define OMX_VERB_DESCRIPTOR_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.panelGet (data/verbs.json): The panel the plugin publishes, answered from descriptor_dir: . */ +#define OMX_VERB_PANEL_GET "panel_get" +#define OMX_VERB_PANEL_GET_FMT "panel_get %s" +#define OMX_VERB_PANEL_GET_REPLY "resp 0 \"\"" +#define OMX_VERB_PANEL_GET_CODES_COUNT 5u +#define OMX_VERB_PANEL_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.presetList (data/verbs.json): The plugin's presets, the ones its preset discovery or its bundle lists. */ +#define OMX_VERB_PRESET_LIST "preset_list" +#define OMX_VERB_PRESET_LIST_FMT "preset_list %i" +#define OMX_VERB_PRESET_LIST_REPLY "resp " +#define OMX_VERB_PRESET_LIST_CODES_COUNT 3u +#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -101, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ + +/* PLUGIN_HOSTD_VERBS.presetGet (data/verbs.json): One preset, 0 to count - 1: the key preset_load takes for it, and its name. */ +#define OMX_VERB_PRESET_GET "preset_get" +#define OMX_VERB_PRESET_GET_FMT "preset_get %i %i" +#define OMX_VERB_PRESET_GET_REPLY "resp 0 " +#define OMX_VERB_PRESET_GET_CODES_COUNT 3u +#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -101, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ + +#define PLUGIN_HOSTD_HOST_VERB_COUNT 19u + +/* Every verb, in the order of its items, with what each host owes it. */ +static inline const struct omx_host_verb *plugin_hostd_host_verbs(void) { + static const struct omx_host_verb table[PLUGIN_HOSTD_HOST_VERB_COUNT] = { + { OMX_VERB_WORKER_LIST, OMX_VERB_WORKER_LIST_FMT, OMX_VERB_WORKER_LIST_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_INSTANCE_INFO, OMX_VERB_INSTANCE_INFO_FMT, OMX_VERB_INSTANCE_INFO_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_SUPERVISOR_RESET, OMX_VERB_SUPERVISOR_RESET_FMT, OMX_VERB_SUPERVISOR_RESET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_QUARANTINE_CLEAR, OMX_VERB_QUARANTINE_CLEAR_FMT, OMX_VERB_QUARANTINE_CLEAR_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_POLICY_SET, OMX_VERB_POLICY_SET_FMT, OMX_VERB_POLICY_SET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_POOL_CONFIG, OMX_VERB_POOL_CONFIG_FMT, OMX_VERB_POOL_CONFIG_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_WORKER_ENV, OMX_VERB_WORKER_ENV_FMT, OMX_VERB_WORKER_ENV_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PIN_SET, OMX_VERB_PIN_SET_FMT, OMX_VERB_PIN_SET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PIN_CLEAR, OMX_VERB_PIN_CLEAR_FMT, OMX_VERB_PIN_CLEAR_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PIN_ACCEPT, OMX_VERB_PIN_ACCEPT_FMT, OMX_VERB_PIN_ACCEPT_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PIN_LAYOUT, OMX_VERB_PIN_LAYOUT_FMT, OMX_VERB_PIN_LAYOUT_REPLY, { OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_OPTIONAL, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_LV2_WORKER_SET, OMX_VERB_LV2_WORKER_SET_FMT, OMX_VERB_LV2_WORKER_SET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_CHAIN, OMX_VERB_CHAIN_FMT, OMX_VERB_CHAIN_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_CHAIN_CLEAR, OMX_VERB_CHAIN_CLEAR_FMT, OMX_VERB_CHAIN_CLEAR_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_CHAIN_GET, OMX_VERB_CHAIN_GET_FMT, OMX_VERB_CHAIN_GET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_DESCRIPTOR_GET, OMX_VERB_DESCRIPTOR_GET_FMT, OMX_VERB_DESCRIPTOR_GET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PANEL_GET, OMX_VERB_PANEL_GET_FMT, OMX_VERB_PANEL_GET_REPLY, { OMX_HOST_VERB_REQUIRED, OMX_HOST_VERB_UNSUPPORTED, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PRESET_LIST, OMX_VERB_PRESET_LIST_FMT, OMX_VERB_PRESET_LIST_REPLY, { OMX_HOST_VERB_OPTIONAL, OMX_HOST_VERB_OPTIONAL, OMX_HOST_VERB_UNSUPPORTED } }, + { OMX_VERB_PRESET_GET, OMX_VERB_PRESET_GET_FMT, OMX_VERB_PRESET_GET_REPLY, { OMX_HOST_VERB_OPTIONAL, OMX_HOST_VERB_OPTIONAL, OMX_HOST_VERB_UNSUPPORTED } }, + }; + return table; +} + +/* The verb whose word is `verb`, or NULL. */ +static inline const struct omx_host_verb *plugin_hostd_host_verb_find(const char *verb) { + const struct omx_host_verb *t = plugin_hostd_host_verbs(); + for (unsigned i = 0; i < PLUGIN_HOSTD_HOST_VERB_COUNT; i++) + if (!strcmp(t[i].verb, verb)) return &t[i]; + return NULL; +} + +/* What the host at column `host` (an OMX_HOST_VERB_HOST_* index) owes `verb`: unsupported for a verb not here. */ +static inline enum omx_host_verb_level plugin_hostd_host_verb_level(const char *verb, unsigned host) { + const struct omx_host_verb *v = plugin_hostd_host_verb_find(verb); + return v && host < OMX_HOST_VERB_HOST_COUNT ? v->level[host] : OMX_HOST_VERB_UNSUPPORTED; +} + +#endif /* PLUGIN_HOSTD_HOST_VERBS_H */ diff --git a/include/plugin-hostd/plugin_hostd_vocabulary.h b/include/plugin-hostd/plugin_hostd_vocabulary.h new file mode 100644 index 0000000..c31d8c1 --- /dev/null +++ b/include/plugin-hostd/plugin_hostd_vocabulary.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Pau Aliagas +#ifndef PLUGIN_HOSTD_VOCABULARY_H +#define PLUGIN_HOSTD_VOCABULARY_H +/* + * GENERATED — DO NOT EDIT BY HAND. + * Produced by `omx-contract render --target c` from FreeMixer/plugin-hostd's data/: the vocabulary + * (docs/data-layout.md). A code is OMX_ and the code in upper snake case; a word is OMX__, each words + * item with its _COUNT and an _INIT of its words in their order. A consumer writes and compares these, never a + * literal of its own. + */ + +/* PLUGIN_HOSTD_REPLY_CODES (data/verbs.json) */ +#define OMX_REPLY_PLUGIN_HOSTD_INVALID_OPERATION "reply.plugin-hostd.invalid-operation" ///< mod-host's ERR_INVALID_OPERATION, answered by the daemon for a line outside the grammar of one of its own verbs +#define OMX_REPLY_PLUGIN_HOSTD_INSTANCE_UNKNOWN "reply.plugin-hostd.instance-unknown" ///< mod-host's ERR_INSTANCE_NON_EXISTS, answered by the daemon for an instance no add made +#define OMX_REPLY_PLUGIN_HOSTD_PLACEMENT_INVALID "reply.plugin-hostd.placement-invalid" ///< PHD_ERR_PLACEMENT_INVALID: not a placement, or not valid here +#define OMX_REPLY_PLUGIN_HOSTD_NO_BACKEND "reply.plugin-hostd.no-backend" ///< PHD_ERR_NO_BACKEND: no worker program for the scheme +#define OMX_REPLY_PLUGIN_HOSTD_WORKER_SPAWN "reply.plugin-hostd.worker-spawn" ///< PHD_ERR_WORKER_SPAWN: the worker is not up +#define OMX_REPLY_PLUGIN_HOSTD_REPLAY "reply.plugin-hostd.replay" ///< PHD_ERR_REPLAY: the name of a refused replay on stderr, never a reply +#define OMX_REPLY_PLUGIN_HOSTD_GAVE_UP "reply.plugin-hostd.gave-up" ///< PHD_ERR_GAVE_UP: the storm bound is spent for that placement +#define OMX_REPLY_PLUGIN_HOSTD_NO_SUCH_WORKER "reply.plugin-hostd.no-such-worker" ///< PHD_ERR_NO_SUCH_WORKER: no such worker +#define OMX_REPLY_PLUGIN_HOSTD_VERB_DROPPED "reply.plugin-hostd.verb-dropped" ///< PHD_ERR_VERB_DROPPED: the worker died on this very command and the daemon dropped it +#define OMX_REPLY_PLUGIN_HOSTD_PIN_ABSENT "reply.plugin-hostd.pin-absent" ///< PHD_ERR_PIN_ABSENT: the plugin has no pin, or its layout pin is in a scheme this version does not know +#define OMX_REPLY_PLUGIN_HOSTD_PIN_BINARY_MISMATCH "reply.plugin-hostd.pin-binary-mismatch" ///< PHD_ERR_PIN_BINARY_MISMATCH: a pinned file is missing or its SHA-256 differs +#define OMX_REPLY_PLUGIN_HOSTD_PIN_LAYOUT_MISMATCH "reply.plugin-hostd.pin-layout-mismatch" ///< PHD_ERR_PIN_LAYOUT_MISMATCH: the parameter layout after init differs from the layout pin +#define OMX_REPLY_PLUGIN_HOSTD_NO_PARAM_CONTRACT "reply.plugin-hostd.no-param-contract" ///< PHD_ERR_NO_PARAM_CONTRACT: the host holds no checked unit and scale for this parameter +#define OMX_REPLY_PLUGIN_HOSTD_LV2_ADAPTER_REFUSED "reply.plugin-hostd.lv2-adapter-refused" ///< PHD_ERR_LV2_ADAPTER_REFUSED: the lv2-clap worker's LV2 adapter cannot run this plugin +#define OMX_REPLY_PLUGIN_HOSTD_NO_NOTE_INPUT "reply.plugin-hostd.no-note-input" ///< PHD_ERR_NO_NOTE_INPUT: the instance's plugin takes no notes +#define OMX_REPLY_PLUGIN_HOSTD_NOTES_BY_JACK "reply.plugin-hostd.notes-by-jack" ///< PHD_ERR_NOTES_BY_JACK: the instance's worker takes no notes on its socket +#define OMX_REPLY_PLUGIN_HOSTD_NO_PRESET_LIST "reply.plugin-hostd.no-preset-list" ///< PHD_ERR_NO_PRESET_LIST: the instance's worker cannot list the plugin's presets +#define OMX_REPLY_PLUGIN_HOSTD_PIN_REFUSED "reply.plugin-hostd.pin-refused" ///< PHD_ERR_PIN_REFUSED: the plugin's pin carries verdict=refused +#define OMX_REPLY_PLUGIN_HOSTD_NOTE_QUEUE_FULL "reply.plugin-hostd.note-queue-full" ///< PHD_ERR_NOTE_QUEUE_FULL: the instance's worker has no room for this note before its next block +#define OMX_REPLY_PLUGIN_HOSTD_CHAIN_UNKNOWN_INSTANCE "reply.plugin-hostd.chain-unknown-instance" ///< PHD_ERR_CHAIN_UNKNOWN_INSTANCE: a member is an instance no add made +#define OMX_REPLY_PLUGIN_HOSTD_CHAIN_DUPLICATE "reply.plugin-hostd.chain-duplicate" ///< PHD_ERR_CHAIN_DUPLICATE: an instance is named twice in one chain +#define OMX_REPLY_PLUGIN_HOSTD_CHAIN_INSTANCE_BUSY "reply.plugin-hostd.chain-instance-busy" ///< PHD_ERR_CHAIN_INSTANCE_BUSY: a member is already in another chain +#define OMX_REPLY_PLUGIN_HOSTD_CHAIN_MIXED_FORMAT "reply.plugin-hostd.chain-mixed-format" ///< PHD_ERR_CHAIN_MIXED_FORMAT: no one worker program runs every member +#define OMX_REPLY_PLUGIN_HOSTD_NO_SUCH_CHAIN "reply.plugin-hostd.no-such-chain" ///< PHD_ERR_NO_SUCH_CHAIN: a chain no chain line declared +#define OMX_REPLY_PLUGIN_HOSTD_PRESET_NOT_OFFERED "reply.plugin-hostd.preset-not-offered" ///< PHD_ERR_PRESET_NOT_OFFERED: a stable name the plugin does not offer +#define OMX_REPLY_PLUGIN_HOSTD_PRESET_INVALID "reply.plugin-hostd.preset-invalid" ///< PHD_ERR_PRESET_INVALID: a preset file or path that is not the preset format +#define OMX_REPLY_PLUGIN_HOSTD_PRESET_NEEDS_STATE "reply.plugin-hostd.preset-needs-state" ///< PHD_ERR_PRESET_NEEDS_STATE: a preset file that carries state for a worker that cannot take state +#define OMX_REPLY_PLUGIN_HOSTD_PRESET_PLUGIN_CHANGED "reply.plugin-hostd.preset-plugin-changed" ///< PHD_ERR_PRESET_PLUGIN_CHANGED: the preset file's plugin id, version or fingerprint is not the instance's +#define OMX_REPLY_PLUGIN_HOSTD_LOAD_REFUSED "reply.plugin-hostd.load-refused" ///< PHD_ERR_LOAD_REFUSED: the plugin refused the preset or the state its file carries +#define OMX_REPLY_PLUGIN_HOSTD_DIR_NOT_WRITABLE "reply.plugin-hostd.dir-not-writable" ///< PHD_ERR_DIR_NOT_WRITABLE: descriptor_dir or qualification_dir cannot be made or written +#define OMX_REPLY_PLUGIN_HOSTD_DESCRIPTOR_ABSENT "reply.plugin-hostd.descriptor-absent" ///< PHD_ERR_DESCRIPTOR_ABSENT: no descriptor (panel) for the plugin's binary as it is now +#define OMX_REPLY_PLUGIN_HOSTD_DESCRIPTOR_PENDING "reply.plugin-hostd.descriptor-pending" ///< PHD_ERR_DESCRIPTOR_PENDING: the scan its add started has not written the descriptor (panel) yet +#define OMX_REPLY_PLUGIN_HOSTD_DESCRIPTOR_SCAN_FAILED "reply.plugin-hostd.descriptor-scan-failed" ///< PHD_ERR_DESCRIPTOR_SCAN_FAILED: the plugin's last scan wrote no descriptor (panel) + +#endif /* PLUGIN_HOSTD_VOCABULARY_H */ diff --git a/include/plugin-hostd/protocol.h b/include/plugin-hostd/protocol.h index bcbe4b0..178b107 100644 --- a/include/plugin-hostd/protocol.h +++ b/include/plugin-hostd/protocol.h @@ -21,13 +21,20 @@ /* The protocol surface plugin-hostd owns, declared once. The daemon compiles against this header; the tables of * README.md, the key list of the man page and protocol/plugin-hostd.json are generated from it by tools/protocol-gen.c * (make gen; make check-generated fails on drift). A consumer includes this header and reads nothing else, or reads the - * JSON when it is not C. The mod-host protocol itself, which the daemon speaks unchanged, is not declared here. */ + * JSON when it is not C. The mod-host protocol itself, which the daemon speaks unchanged, is not declared here. + * + * The daemon's own verbs are declared in its contract directory (data/verbs.json, docs/contract-dir.md of omx-contract) + * and rendered into plugin_hostd_host_verbs.h beside this header: a verb is OMX_VERB_, its line OMX_VERB__FMT, + * its reply OMX_VERB__REPLY and its refusals OMX_VERB__CODES_INIT. The tables below take the words and the + * replies from there and add the prose of the README. */ #ifndef PLUGIN_HOSTD_PROTOCOL_H #define PLUGIN_HOSTD_PROTOCOL_H #include +#include "plugin_hostd_host_verbs.h" + /* bumped when a name, a code or the shape of a verb or an event changes incompatibly */ #define PLUGIN_HOSTD_PROTOCOL_VERSION 1 @@ -317,24 +324,24 @@ static inline int phd_pool_name_valid(const char *name) "and " PHD_VERB_NOTE_INPUT " with this code, and the plugin is played through its jack MIDI input " \ "with connect") \ X(NO_PRESET_LIST, "the instance's worker cannot list the plugin's presets (mod-host): the daemon answers " \ - PHD_VERB_PRESET_LIST " and " PHD_VERB_PRESET_GET " with this code; preset_load still takes a preset " \ + OMX_VERB_PRESET_LIST " and " OMX_VERB_PRESET_GET " with this code; preset_load still takes a preset " \ "the controller knows") \ X(PIN_REFUSED, "the plugin's pin carries " PHD_PIN_VERDICT_KEY PHD_PIN_VERDICT_REFUSED ": its files are the ones that " \ "failed qualification, so add is refused and no worker sees it, and a replayed add is refused the " \ "same way; files that differ from the pin answer " PHD_STR(PHD_ERR_PIN_BINARY_MISMATCH) " first. A pins_dir " \ - "pin of that verdict is never overridden: " PHD_VERB_PIN_SET " and " PHD_VERB_PIN_ACCEPT " of its plugin " \ + "pin of that verdict is never overridden: " OMX_VERB_PIN_SET " and " OMX_VERB_PIN_ACCEPT " of its plugin " \ "answer this code") \ X(NOTE_QUEUE_FULL, "the instance's worker has no room for this " PHD_VERB_NOTE_ON ", " PHD_VERB_NOTE_OFF " or " \ PHD_VERB_ALL_NOTES_OFF " before its next block: the line was not taken and nothing of it is kept; the " \ "controller may send it again") \ - X(CHAIN_UNKNOWN_INSTANCE, "a member of " PHD_VERB_CHAIN " is an instance no add made: the chain is not declared") \ - X(CHAIN_DUPLICATE, "an instance is named twice in one " PHD_VERB_CHAIN ": the chain is not declared") \ - X(CHAIN_INSTANCE_BUSY, "a member of " PHD_VERB_CHAIN " is already in another chain: the chain is not declared; " \ - PHD_VERB_CHAIN_CLEAR " of the other chain frees it") \ - X(CHAIN_MIXED_FORMAT, "the members of " PHD_VERB_CHAIN " are not all run by one worker program, or are of two " \ + X(CHAIN_UNKNOWN_INSTANCE, "a member of " OMX_VERB_CHAIN " is an instance no add made: the chain is not declared") \ + X(CHAIN_DUPLICATE, "an instance is named twice in one " OMX_VERB_CHAIN ": the chain is not declared") \ + X(CHAIN_INSTANCE_BUSY, "a member of " OMX_VERB_CHAIN " is already in another chain: the chain is not declared; " \ + OMX_VERB_CHAIN_CLEAR " of the other chain frees it") \ + X(CHAIN_MIXED_FORMAT, "the members of " OMX_VERB_CHAIN " are not all run by one worker program, or are of two " \ "rows of one program and a row does not run chains in one client, so no one worker runs " \ "them: the chain is not declared") \ - X(NO_SUCH_CHAIN, PHD_VERB_CHAIN_CLEAR " or " PHD_VERB_CHAIN_GET " of a chain no " PHD_VERB_CHAIN " declared") \ + X(NO_SUCH_CHAIN, OMX_VERB_CHAIN_CLEAR " or " OMX_VERB_CHAIN_GET " of a chain no " OMX_VERB_CHAIN " declared") \ X(PRESET_NOT_OFFERED, "the worker: preset_load or preset_show of a stable name the plugin does not offer, or of a " \ "plugin with no preset system and no state") \ X(PRESET_INVALID, "preset_save with a file path that is not absolute or with no name, or preset_load or " \ @@ -347,39 +354,24 @@ static inline int phd_pool_name_valid(const char *name) X(LOAD_REFUSED, "the worker: the plugin refused the preset or the state its file carries") \ X(DIR_NOT_WRITABLE, "descriptor_dir or qualification_dir cannot be made or written: the daemon does not start, and " \ "says which key and which path on stderr with this code; a directory that turns read-only " \ - "later makes the scan of an add write nothing, and " PHD_VERB_DESCRIPTOR_GET " and " \ - PHD_VERB_PANEL_GET " of that plugin answer this code") \ - X(DESCRIPTOR_ABSENT, PHD_VERB_DESCRIPTOR_GET " (" PHD_VERB_PANEL_GET ") of a plugin that has no descriptor (panel) " \ + "later makes the scan of an add write nothing, and " OMX_VERB_DESCRIPTOR_GET " and " \ + OMX_VERB_PANEL_GET " of that plugin answer this code") \ + X(DESCRIPTOR_ABSENT, OMX_VERB_DESCRIPTOR_GET " (" OMX_VERB_PANEL_GET ") of a plugin that has no descriptor (panel) " \ "in descriptor_dir for its binary as it is now, and none being written: no add since it " \ "changed, or a binary that cannot be found") \ - X(DESCRIPTOR_PENDING, PHD_VERB_DESCRIPTOR_GET " (" PHD_VERB_PANEL_GET ") of a plugin whose scan, started by its " \ + X(DESCRIPTOR_PENDING, OMX_VERB_DESCRIPTOR_GET " (" OMX_VERB_PANEL_GET ") of a plugin whose scan, started by its " \ "add, has not written its descriptor (panel) yet: ask again") \ - X(DESCRIPTOR_SCAN_FAILED, PHD_VERB_DESCRIPTOR_GET " (" PHD_VERB_PANEL_GET ") of a plugin whose last scan wrote no " \ + X(DESCRIPTOR_SCAN_FAILED, OMX_VERB_DESCRIPTOR_GET " (" OMX_VERB_PANEL_GET ") of a plugin whose last scan wrote no " \ "descriptor (panel): the scanner refused the plugin, made no dump or no panel, or is not " \ "installed; the add it ran for stands, and stderr and the scan's log say why. The next add " \ "scans again") /* ---------------------------------------------------------------- verbs */ +/* mod-host's add, which the daemon extends with a placement; every verb of its own is OMX_VERB_ of + * plugin_hostd_host_verbs.h */ #define PHD_VERB_ADD "add" -#define PHD_VERB_WORKER_LIST "worker_list" -#define PHD_VERB_INSTANCE_INFO "instance_info" -#define PHD_VERB_SUPERVISOR_RESET "supervisor_reset" -#define PHD_VERB_QUARANTINE_CLEAR "quarantine_clear" -#define PHD_VERB_POLICY_SET "policy_set" -#define PHD_VERB_POOL_CONFIG "pool_config" -#define PHD_VERB_WORKER_ENV "worker_env" -#define PHD_VERB_PIN_SET "pin_set" -#define PHD_VERB_PIN_CLEAR "pin_clear" #define PHD_VERB_PIN_EXPECT "pin_expect" -#define PHD_VERB_PIN_ACCEPT "pin_accept" -#define PHD_VERB_PIN_LAYOUT "pin_layout" -#define PHD_VERB_LV2_WORKER_SET "lv2_worker_set" -#define PHD_VERB_CHAIN "chain" -#define PHD_VERB_CHAIN_CLEAR "chain_clear" -#define PHD_VERB_CHAIN_GET "chain_get" -#define PHD_VERB_DESCRIPTOR_GET "descriptor_get" -#define PHD_VERB_PANEL_GET "panel_get" /* the words of a pin: =, joined by ',', and : */ #define PHD_PIN_FILE_SEPARATOR "," @@ -420,9 +412,9 @@ static inline int phd_pool_name_valid(const char *name) /* what cannot be pinned, each a refusal and never a partial pin: X(id, meaning) */ #define PHD_PIN_LIMITS(X) \ X(PATH, "a path holding '" PHD_PIN_FILE_SEPARATOR "', '" PHD_PIN_DIGEST_SEPARATOR "' or whitespace cannot be pinned: " \ - "they are the separators of " PHD_VERB_PIN_SET "'s words, and " PHD_VERB_PIN_SET " refuses it as a token " \ + "they are the separators of " OMX_VERB_PIN_SET "'s words, and " OMX_VERB_PIN_SET " refuses it as a token " \ "outside its grammar") \ - X(LINE, "a " PHD_VERB_PIN_SET " line is at most line_max, " PHD_STR(PHD_LINE_MAX) " bytes, the protocol socket's " \ + X(LINE, "a " OMX_VERB_PIN_SET " line is at most line_max, " PHD_STR(PHD_LINE_MAX) " bytes, the protocol socket's " \ "buffer; a longer one is refused as outside the grammar") \ X(MANIFEST, "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 " PHD_STR(PHD_ERR_PIN_BINARY_MISMATCH) ": the daemon does not resolve " \ @@ -432,54 +424,53 @@ static inline int phd_pool_name_valid(const char *name) #define PHD_VERBS(X) \ X(ADD, PHD_VERB_ADD, " [" PHD_PLACEMENT_SYNTAX "] [client_name]", "resp ", \ "mod-host's add with a placement; a token after the instance that is not a placement is the jack client name") \ - X(WORKER_LIST, PHD_VERB_WORKER_LIST, "", \ - "resp :::::,,...", "the workers, one record each") \ - X(INSTANCE_INFO, PHD_VERB_INSTANCE_INFO, "", "resp 0 ", \ + X(WORKER_LIST, OMX_VERB_WORKER_LIST, "", OMX_VERB_WORKER_LIST_REPLY, "the workers, one record each") \ + X(INSTANCE_INFO, OMX_VERB_INSTANCE_INFO, "", OMX_VERB_INSTANCE_INFO_REPLY, \ "where an instance lives and what it has cost") \ - X(SUPERVISOR_RESET, PHD_VERB_SUPERVISOR_RESET, "[ | " PHD_WORD_ALL "]", "resp 0", \ + X(SUPERVISOR_RESET, OMX_VERB_SUPERVISOR_RESET, "[ | " PHD_WORD_ALL "]", OMX_VERB_SUPERVISOR_RESET_REPLY, \ "re-arm the storm bound and the backoff of a given-up placement") \ - X(QUARANTINE_CLEAR, PHD_VERB_QUARANTINE_CLEAR, " | " PHD_WORD_ALL, "resp 0", \ + X(QUARANTINE_CLEAR, OMX_VERB_QUARANTINE_CLEAR, " | " PHD_WORD_ALL, OMX_VERB_QUARANTINE_CLEAR_REPLY, \ "let a quarantined instance go back where its placement says") \ - X(POLICY_SET, PHD_VERB_POLICY_SET, PHD_FORMAT_ARGUMENT " " \ - PHD_PLACE_OWN " | " PHD_PLACE_POOL_PREFIX "", "resp 0", "the placement of an add that says default") \ - X(POOL_CONFIG, PHD_VERB_POOL_CONFIG, " ", "resp 0", \ + X(POLICY_SET, OMX_VERB_POLICY_SET, PHD_FORMAT_ARGUMENT " " \ + PHD_PLACE_OWN " | " PHD_PLACE_POOL_PREFIX "", OMX_VERB_POLICY_SET_REPLY, "the placement of an add that says default") \ + X(POOL_CONFIG, OMX_VERB_POOL_CONFIG, " ", OMX_VERB_POOL_CONFIG_REPLY, \ "the instances a pool holds before a sibling opens") \ - X(WORKER_ENV, PHD_VERB_WORKER_ENV, PHD_FORMAT_ARGUMENT " ", "resp 0", \ + X(WORKER_ENV, OMX_VERB_WORKER_ENV, PHD_FORMAT_ARGUMENT " ", OMX_VERB_WORKER_ENV_REPLY, \ "the cpu list and the nice value of the workers of a format, at spawn and on the ones running") \ - X(PIN_SET, PHD_VERB_PIN_SET, " " PHD_PIN_DIGEST_SEPARATOR "[" PHD_PIN_FILE_SEPARATOR "" \ + X(PIN_SET, OMX_VERB_PIN_SET, " " PHD_PIN_DIGEST_SEPARATOR "[" PHD_PIN_FILE_SEPARATOR "" \ PHD_PIN_DIGEST_SEPARATOR "...] " PHD_PIN_SCHEME_SEPARATOR " [" PHD_PIN_VERDICT_KEY \ - "]", "resp 0", \ + "]", OMX_VERB_PIN_SET_REPLY, \ "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; -516 for a plugin whose pins_dir pin carries " PHD_PIN_VERDICT_KEY \ PHD_PIN_VERDICT_REFUSED ", which nothing overrides. The pin store, a pin_file and a pins_dir file hold the same " \ "line, with or without the verb") \ - X(PIN_CLEAR, PHD_VERB_PIN_CLEAR, " | " PHD_WORD_ALL, "resp 0", "forget the pin of one plugin, or of every one; a pins_dir " \ + X(PIN_CLEAR, OMX_VERB_PIN_CLEAR, " | " PHD_WORD_ALL, OMX_VERB_PIN_CLEAR_REPLY, "forget the pin of one plugin, or of every one; a pins_dir " \ "pin is read-only and holds again") \ - X(LV2_WORKER_SET, PHD_VERB_LV2_WORKER_SET, " <" PHD_LV2_WORKER_MOD_HOST " | " PHD_LV2_WORKER_CLAP_HOST " | " \ - PHD_LV2_WORKER_AUTO " | " PHD_WORD_NONE ">", "resp 0", \ + X(LV2_WORKER_SET, OMX_VERB_LV2_WORKER_SET, " <" PHD_LV2_WORKER_MOD_HOST " | " PHD_LV2_WORKER_CLAP_HOST " | " \ + PHD_LV2_WORKER_AUTO " | " PHD_WORD_NONE ">", OMX_VERB_LV2_WORKER_SET_REPLY, \ "the worker of one LV2 plugin, for its adds from now on: it overrides the lv2_worker setting, " PHD_WORD_NONE " 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") \ - X(PIN_ACCEPT, PHD_VERB_PIN_ACCEPT, "", "resp 0", \ + X(PIN_ACCEPT, OMX_VERB_PIN_ACCEPT, "", OMX_VERB_PIN_ACCEPT_REPLY, \ "pin_mode " PHD_PIN_MODE_FIRST_USE " only: take the plugin's files as they are now as its pin, after a " \ PHD_EVENT_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 " \ PHD_PIN_VERDICT_KEY PHD_PIN_VERDICT_REFUSED ", -902 under pin_mode strict") \ - X(PIN_LAYOUT, PHD_VERB_PIN_LAYOUT, "", "resp 0 " PHD_PIN_SCHEME_SEPARATOR "", \ + X(PIN_LAYOUT, OMX_VERB_PIN_LAYOUT, "", OMX_VERB_PIN_LAYOUT_REPLY, \ "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") \ X(PIN_EXPECT, PHD_VERB_PIN_EXPECT, " " PHD_PIN_SCHEME_SEPARATOR "", "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") \ - X(CHAIN, PHD_VERB_CHAIN, " [ ...]", "resp 0", \ + X(CHAIN, OMX_VERB_CHAIN, " [ ...]", OMX_VERB_CHAIN_REPLY, \ "the instances, in this order, are one chain, run in one worker of place " PHD_PLACE_CHAIN_PREFIX ": 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; while its worker " \ "is up that worker is kept: a new member joins it (its add, the order, its state, its ledger, its connections) " \ - "and no other member is restarted, unless the new member's format narrows the worker's world, which forms the " \ - "chain again. Members of " PHD_FORMAT_CLAP " and " PHD_FORMAT_LV2_CLAP " form one chain. A worker that runs " \ + "and no other member is restarted, unless the new member's format narrows the worker's world, which forms " \ + "the chain again. Members of " PHD_FORMAT_CLAP " and " PHD_FORMAT_LV2_CLAP " form one chain. A worker that runs " \ "chains in one client (" PHD_FORMAT_CLAP ", " PHD_FORMAT_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 " \ @@ -489,21 +480,21 @@ static inline int phd_pool_name_valid(const char *name) "whose first-use pin is not in-process; an LV2 member whose bundle is not found, since a chain's worker sees " \ "a world narrowed to the union of its members' bundles), -902 for a chain " \ "or an instance that is not a number, or fewer than chain_members_min (" PHD_STR(PHD_CHAIN_MEMBERS_MIN) ") instances") \ - X(CHAIN_CLEAR, PHD_VERB_CHAIN_CLEAR, "", "resp 0", \ - "forget the chain: each member goes to a worker of its own; -522 for a chain no " PHD_VERB_CHAIN " declared") \ - X(CHAIN_GET, PHD_VERB_CHAIN_GET, "", "resp 0 ,... ,...", \ + X(CHAIN_CLEAR, OMX_VERB_CHAIN_CLEAR, "", OMX_VERB_CHAIN_CLEAR_REPLY, \ + "forget the chain: each member goes to a worker of its own; -522 for a chain no " OMX_VERB_CHAIN " declared") \ + X(CHAIN_GET, OMX_VERB_CHAIN_GET, "", OMX_VERB_CHAIN_GET_REPLY, \ "read a chain back: its worker (" PHD_WORD_NONE " when it has none), its members in the declared order, and the " \ "members its worker runs now in their order (" PHD_WORD_NONE " for none); a quarantined member is declared and " \ - "not running; -522 for a chain no " PHD_VERB_CHAIN " declared, -902 for a chain that is not a number") \ - X(DESCRIPTOR_GET, PHD_VERB_DESCRIPTOR_GET, "", "resp 0 \"\"", \ + "not running; -522 for a chain no " OMX_VERB_CHAIN " declared, -902 for a chain that is not a number") \ + X(DESCRIPTOR_GET, OMX_VERB_DESCRIPTOR_GET, "", OMX_VERB_DESCRIPTOR_GET_REPLY, \ "the scanned descriptor of the plugin, answered from descriptor_dir: the file named for its binary as it is now " \ "(" PHD_DESCRIPTOR_NAME_SYNTAX "), written by the scan the plugin's add ran. -529 when there is none, -530 while " \ "its scan runs, -531 when its last scan wrote nothing, -528 when the directory could not be written") \ - X(PANEL_GET, PHD_VERB_PANEL_GET, "", "resp 0 \"\"", \ + X(PANEL_GET, OMX_VERB_PANEL_GET, "", OMX_VERB_PANEL_GET_REPLY, \ "the panel the plugin publishes (org.openmixer.panel-description/1), answered from descriptor_dir: the file " \ PHD_PANEL_NAME_SYNTAX ", named as its descriptor, which the same scan writes once the descriptor is in place, " \ "{\"plugin\": {\"format\", \"id\"}, \"panel\": }; an LV2 plugin's panel is null. The codes are " PHD_VERB_DESCRIPTOR_GET "'s: -529 " \ + "null when it serves none>}; an LV2 plugin's panel is null. The codes are " OMX_VERB_DESCRIPTOR_GET "'s: -529 " \ "when there is none, -530 while its scan runs, -531 when its last scan wrote nothing, -528 when the directory " \ "could not be written") @@ -517,16 +508,13 @@ static inline int phd_pool_name_valid(const char *name) #define PHD_VERB_REMOTE_PAGES "remote_pages" #define PHD_VERB_REMOTE_PAGE_GET "remote_page_get" #define PHD_VERB_PARAM_INFO "param_info" -#define PHD_VERB_PRESET_LIST "preset_list" -#define PHD_VERB_PRESET_GET "preset_get" -/* the same verbs as a host registers them with protocol_add_command */ +/* the same verbs as a host registers them with protocol_add_command; preset_list and preset_get are the daemon's, + * OMX_VERB_PRESET_LIST and OMX_VERB_PRESET_GET */ #define PHD_VERB_TRACK_INFO_FMT PHD_VERB_TRACK_INFO " %i %s %s ..." #define PHD_VERB_REMOTE_PAGES_FMT PHD_VERB_REMOTE_PAGES " %i" #define PHD_VERB_REMOTE_PAGE_GET_FMT PHD_VERB_REMOTE_PAGE_GET " %i %i" #define PHD_VERB_PARAM_INFO_FMT PHD_VERB_PARAM_INFO " %i %s" -#define PHD_VERB_PRESET_LIST_FMT PHD_VERB_PRESET_LIST " %i" -#define PHD_VERB_PRESET_GET_FMT PHD_VERB_PRESET_GET " %i %i" /* Notes, played on an instance by a controller that is not a jack client: the worker delivers each one as a MIDI * message at the start of its next cycle, on the plugin's note input. Nothing of them is kept: a note is an act, not a @@ -593,10 +581,10 @@ static inline int phd_pool_name_valid(const char *name) "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") \ - X(PRESET_LIST, PHD_VERB_PRESET_LIST, "", "resp ", NONE, \ + X(PRESET_LIST, OMX_VERB_PRESET_LIST, "", OMX_VERB_PRESET_LIST_REPLY, NONE, \ "the plugin's presets, the ones its preset discovery or its bundle lists; " PHD_STR(PHD_ERR_NO_PRESET_LIST) \ " for an instance of a worker that cannot list them") \ - X(PRESET_GET, PHD_VERB_PRESET_GET, " ", "resp 0 ", NONE, \ + X(PRESET_GET, OMX_VERB_PRESET_GET, " ", OMX_VERB_PRESET_GET_REPLY, NONE, \ "one preset, 0 to count - 1: the key preset_load takes for it, and its name; -902 for an index that is not one") \ X(NOTE_INPUT, PHD_VERB_NOTE_INPUT, "", "resp 0 ", NONE, \ "whether the instance takes notes: the name of its note input on its worker's jack client, " PHD_NO_NOTE_INPUT \ @@ -815,28 +803,28 @@ static inline int phd_is_note_verb(const char *verb) X(PHD_VERB_REMOTE_PAGES, MOD_HOST, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ X(PHD_VERB_REMOTE_PAGE_GET, MOD_HOST, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ X(PHD_VERB_PARAM_INFO, MOD_HOST, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ - X(PHD_VERB_PRESET_LIST, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ - X(PHD_VERB_PRESET_GET, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ + X(OMX_VERB_PRESET_LIST, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ + X(OMX_VERB_PRESET_GET, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ X(PHD_VERB_NOTE_INPUT, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ X(PHD_VERB_NOTE_ON, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ X(PHD_VERB_NOTE_OFF, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ X(PHD_VERB_ALL_NOTES_OFF, DAEMON, OPTIONAL, OPTIONAL, OPTIONAL, OPTIONAL) \ - X(PHD_VERB_WORKER_LIST, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_INSTANCE_INFO, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_SUPERVISOR_RESET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_QUARANTINE_CLEAR, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_POLICY_SET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_POOL_CONFIG, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_WORKER_ENV, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_PIN_SET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_PIN_CLEAR, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_PIN_ACCEPT, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_LV2_WORKER_SET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_CHAIN, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_CHAIN_CLEAR, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_CHAIN_GET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_DESCRIPTOR_GET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ - X(PHD_VERB_PANEL_GET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) + X(OMX_VERB_WORKER_LIST, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_INSTANCE_INFO, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_SUPERVISOR_RESET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_QUARANTINE_CLEAR, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_POLICY_SET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_POOL_CONFIG, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_WORKER_ENV, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_PIN_SET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_PIN_CLEAR, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_PIN_ACCEPT, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_LV2_WORKER_SET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_CHAIN, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_CHAIN_CLEAR, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_CHAIN_GET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_DESCRIPTOR_GET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \ + X(OMX_VERB_PANEL_GET, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) /* ---------------------------------------------------------------- host settings */ @@ -1019,7 +1007,7 @@ static inline int phd_is_note_verb(const char *verb) X(lv2_worker, word, PHD_DEFAULT_LV2_WORKER, "", "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, each such plugin in a worker of its own unless its add names a placement); " \ - "lv2_worker_set overrides it for one plugin") \ + OMX_VERB_LV2_WORKER_SET " overrides it for one plugin") \ X(lv2_path, pathlist, PHD_DEFAULT_LV2_PATH, "LV2_PATH", "where bundles are searched") \ X(state_root, path, PHD_DEFAULT_STATE_ROOT, "XDG_RUNTIME_DIR", "the daemon's checkpoints and worker logs") \ X(pin_mode, word, PHD_DEFAULT_PIN_MODE, "", "strict or first-use, while require_pins is 1: first-use records the pin " \ @@ -1031,7 +1019,7 @@ static inline int phd_is_note_verb(const char *verb) "in name order, under the store, the pin_file and pin_set, except that a pin of verdict=refused there always holds; " \ "empty is none") \ X(descriptor_dir, path, "", "OMX_DESCRIPTOR_DIR", "the scanned descriptors, one file per plugin binary (" \ - PHD_DESCRIPTOR_NAME_SYNTAX ", the hash the binary's SHA-256), written by the scan of an add and read by " PHD_VERB_DESCRIPTOR_GET "; " \ + PHD_DESCRIPTOR_NAME_SYNTAX ", the hash the binary's SHA-256), written by the scan of an add and read by " OMX_VERB_DESCRIPTOR_GET "; " \ PHD_DIR_DEFAULT_TEXT(PHD_DIR_DESCRIPTORS_SUBDIR) "; its variable is for tests only") \ X(qualification_dir, path, "", "OMX_QUALIFICATION_DIR", "plugin-qualify's qualification records (verdict, cost, " \ "version tested), kept apart from the descriptors; " PHD_DIR_DEFAULT_TEXT(PHD_DIR_QUALIFICATION_SUBDIR) \ diff --git a/src/formats.h b/src/formats.h index 941b6db..9ea22f5 100644 --- a/src/formats.h +++ b/src/formats.h @@ -59,7 +59,7 @@ typedef struct FORMAT_T { int pinned_own_only; /* a pinned plugin runs in a worker of its own: the pin holds in no shared world */ int native_instance_verbs; /* the worker answers PHD_INSTANCE_VERBS; 0: the daemon does */ - int native_chain; /* the worker takes PHD_VERB_CHAIN and runs a chain's members in order in one client */ + int native_chain; /* the worker takes OMX_VERB_CHAIN and runs a chain's members in order in one client */ char *(*param_info)(int id, const char *symbol); /* the daemon's param_info, needed when it answers */ } format_t; diff --git a/src/pins.c b/src/pins.c index 87fe4c4..85cc54a 100644 --- a/src/pins.c +++ b/src/pins.c @@ -263,7 +263,7 @@ static void line_write(FILE *file, const pin_t *p) { int n; - fprintf(file, "%s %s ", PHD_VERB_PIN_SET, p->uri); + fprintf(file, "%s %s ", OMX_VERB_PIN_SET, p->uri); for (n = 0; n < p->nfiles; n++) fprintf(file, "%s%s" PHD_PIN_DIGEST_SEPARATOR "%s", n ? PHD_PIN_FILE_SEPARATOR : "", p->path[n], p->digest[n]); fprintf(file, " %s", p->layout); @@ -332,7 +332,7 @@ static int held_names(const char *line, const char *uri) snprintf(copy, sizeof(copy), "%s", line); t = strtok_r(copy, " \t", &save); - if (t && !strcmp(t, PHD_VERB_PIN_SET)) + if (t && !strcmp(t, OMX_VERB_PIN_SET)) t = strtok_r(NULL, " \t", &save); return t && !strcmp(bare(t), uri); } @@ -440,7 +440,7 @@ static int line_read(char *line, layer_t layer, const char *path, int lineno) tok[ntok++] = t; if (ntok == 0 || tok[0][0] == '#') return SUCCESS; - if (!strcmp(tok[0], PHD_VERB_PIN_SET)) + if (!strcmp(tok[0], OMX_VERB_PIN_SET)) first = 1; if (ntok - first != 3 && ntok - first != 4) return ERR_INVALID_OPERATION; diff --git a/src/supervisor.c b/src/supervisor.c index 1469def..a029d8c 100644 --- a/src/supervisor.c +++ b/src/supervisor.c @@ -962,7 +962,7 @@ static int replay_pin(worker_t *w, instance_t *i) /* "chain ...": the members a chain's worker holds and replayed, in order; 0 when it holds none */ static int chain_line(worker_t *w, char *line, size_t size) { - size_t used = snprintf(line, size, PHD_VERB_CHAIN " %d", w->chain->id); + size_t used = snprintf(line, size, OMX_VERB_CHAIN " %d", w->chain->id); int n, count = 0; for (n = 0; n < w->ninst && used + 12 < size; n++) @@ -1281,7 +1281,7 @@ static void learn_layout(worker_t *w, int id, const char *uri) char line[64], *reply = NULL, *save = NULL, *word; int rc; - snprintf(line, sizeof(line), PHD_VERB_PIN_LAYOUT " %d", id); + snprintf(line, sizeof(line), OMX_VERB_PIN_LAYOUT " %d", id); rc = proc_rpc(&g_conf, w->pid, w->fd, line, &reply); if (rc == RPC_OK && resp_code(reply) >= 0) { @@ -1819,7 +1819,7 @@ char *sup_instance_verb(const char *line, int ledger) reply = count == 3 ? format_at(i->fmt)->param_info(id, word[2]) : sup_resp(ERR_INVALID_OPERATION); else if (phd_is_note_verb(word[0])) reply = sup_resp(PHD_ERR_NOTES_BY_JACK); - else if (!strcmp(word[0], PHD_VERB_PRESET_LIST) || !strcmp(word[0], PHD_VERB_PRESET_GET)) + else if (!strcmp(word[0], OMX_VERB_PRESET_LIST) || !strcmp(word[0], OMX_VERB_PRESET_GET)) reply = sup_resp(PHD_ERR_NO_PRESET_LIST); else reply = sup_resp(ERR_INVALID_OPERATION); diff --git a/src/verbs.c b/src/verbs.c index 4a929c7..4dc630a 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -145,9 +145,9 @@ static char *handle(char *line, char **tok, int ntok) descriptors_add(tok[1]); return reply_text; } - if (!strcmp(verb, PHD_VERB_DESCRIPTOR_GET)) + if (!strcmp(verb, OMX_VERB_DESCRIPTOR_GET)) return ntok == 2 ? descriptors_get(tok[1]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_PANEL_GET)) + if (!strcmp(verb, OMX_VERB_PANEL_GET)) return ntok == 2 ? descriptors_panel_get(tok[1]) : sup_resp(ERR_INVALID_OPERATION); if (verb_is(verb, EFFECT_REMOVE)) { @@ -155,45 +155,45 @@ static char *handle(char *line, char **tok, int ntok) return sup_resp(ERR_INVALID_OPERATION); return atoi(tok[1]) == -1 ? sup_remove_all() : sup_remove(atoi(tok[1])); } - if (!strcmp(verb, PHD_VERB_WORKER_LIST)) + if (!strcmp(verb, OMX_VERB_WORKER_LIST)) return ntok == 1 ? sup_worker_list() : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_INSTANCE_INFO)) + if (!strcmp(verb, OMX_VERB_INSTANCE_INFO)) return ntok == 2 ? sup_instance_info(atoi(tok[1])) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_SUPERVISOR_RESET)) + if (!strcmp(verb, OMX_VERB_SUPERVISOR_RESET)) { if (ntok > 2) return sup_resp(ERR_INVALID_OPERATION); return sup_reset(ntok > 1 ? tok[1] : PHD_WORD_ALL); } - if (!strcmp(verb, PHD_VERB_QUARANTINE_CLEAR)) + if (!strcmp(verb, OMX_VERB_QUARANTINE_CLEAR)) return ntok == 2 ? sup_quarantine_clear(tok[1]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_POLICY_SET)) + if (!strcmp(verb, OMX_VERB_POLICY_SET)) return ntok == 3 ? sup_policy_set(tok[1], tok[2]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_LV2_WORKER_SET)) + if (!strcmp(verb, OMX_VERB_LV2_WORKER_SET)) return ntok == 3 ? sup_lv2_worker_set(tok[1], tok[2]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_POOL_CONFIG)) + if (!strcmp(verb, OMX_VERB_POOL_CONFIG)) return ntok == 3 ? sup_pool_config(tok[1], atoi(tok[2])) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_WORKER_ENV)) + if (!strcmp(verb, OMX_VERB_WORKER_ENV)) return ntok == 4 ? sup_worker_env(tok[1], tok[2], tok[3]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_CHAIN)) + if (!strcmp(verb, OMX_VERB_CHAIN)) return chain_words(tok, ntok); - if (!strcmp(verb, PHD_VERB_CHAIN_CLEAR)) + if (!strcmp(verb, OMX_VERB_CHAIN_CLEAR)) { int id; return ntok == 2 && number(tok[1], &id) ? sup_chain_clear(id) : sup_resp(ERR_INVALID_OPERATION); } - if (!strcmp(verb, PHD_VERB_CHAIN_GET)) + if (!strcmp(verb, OMX_VERB_CHAIN_GET)) { int id; return ntok == 2 && number(tok[1], &id) ? sup_chain_get(id) : sup_resp(ERR_INVALID_OPERATION); } - if (!strcmp(verb, PHD_VERB_PIN_SET)) + if (!strcmp(verb, OMX_VERB_PIN_SET)) return sup_resp(pin_set_words(line, tok, ntok)); - if (!strcmp(verb, PHD_VERB_PIN_ACCEPT)) + if (!strcmp(verb, OMX_VERB_PIN_ACCEPT)) return ntok == 2 ? sup_pin_accept(tok[1]) : sup_resp(ERR_INVALID_OPERATION); - if (!strcmp(verb, PHD_VERB_PIN_CLEAR)) + if (!strcmp(verb, OMX_VERB_PIN_CLEAR)) { int rc = ntok == 2 ? pins_clear(tok[1]) : ERR_INVALID_OPERATION; diff --git a/tests/fake_host.c b/tests/fake_host.c index e336f55..2c53187 100644 --- a/tests/fake_host.c +++ b/tests/fake_host.c @@ -273,9 +273,9 @@ static void receive(msg_t *msg) snprintf(g_expect[n], sizeof(g_expect[n]), "%s", tok[2]); answer(fd, "resp 0"); } - else if (verb_is(verb, PHD_VERB_PIN_LAYOUT) && getenv("FAKE_NO_PIN_LAYOUT")) + else if (verb_is(verb, OMX_VERB_PIN_LAYOUT) && getenv("FAKE_NO_PIN_LAYOUT")) answer(fd, "resp -902"); - else if (verb_is(verb, PHD_VERB_PIN_LAYOUT) && ntok == 2 && n >= 0 && n < MAX_INST && g_inst[n].exists) + else if (verb_is(verb, OMX_VERB_PIN_LAYOUT) && ntok == 2 && n >= 0 && n < MAX_INST && g_inst[n].exists) { char mine[PHD_SHA256_HEX_LEN + 64]; @@ -347,9 +347,9 @@ static void receive(msg_t *msg) g_out[k--] = g_out[--g_nout]; answer(fd, "resp 0"); } - else if (!strcmp(verb, PHD_VERB_CHAIN) && getenv("FAKE_REFUSE_CHAIN")) + else if (!strcmp(verb, OMX_VERB_CHAIN) && getenv("FAKE_REFUSE_CHAIN")) answer(fd, "resp -902"); - else if (!strcmp(verb, PHD_VERB_CHAIN)) + else if (!strcmp(verb, OMX_VERB_CHAIN)) { int held = ntok >= 3; @@ -503,13 +503,13 @@ static void receive(msg_t *msg) answer(fd, "resp 0 8 \"\" \"Two\" 2 - - - - - - -"); else if (!strcmp(verb, PHD_VERB_REMOTE_PAGE_GET) && ntok == 3) answer(fd, "resp -902"); - else if (!strcmp(verb, PHD_VERB_PRESET_LIST) && ntok == 2) + else if (!strcmp(verb, OMX_VERB_PRESET_LIST) && ntok == 2) answer(fd, "resp 2"); - else if (!strcmp(verb, PHD_VERB_PRESET_GET) && ntok == 3 && !strcmp(tok[2], "0")) + else if (!strcmp(verb, OMX_VERB_PRESET_GET) && ntok == 3 && !strcmp(tok[2], "0")) answer(fd, "resp 0 \"file:///presets/warm.clap-preset\" \"Warm\""); - else if (!strcmp(verb, PHD_VERB_PRESET_GET) && ntok == 3 && !strcmp(tok[2], "1")) + else if (!strcmp(verb, OMX_VERB_PRESET_GET) && ntok == 3 && !strcmp(tok[2], "1")) answer(fd, "resp 0 urn:fake:bright \"Bright \\\"B\\\"\""); - else if (!strcmp(verb, PHD_VERB_PRESET_GET) && ntok == 3) + else if (!strcmp(verb, OMX_VERB_PRESET_GET) && ntok == 3) answer(fd, "resp -902"); else if (!strcmp(verb, PHD_VERB_PARAM_INFO) && ntok == 3) answer(fd, !strcmp(tok[2], "0") ? "resp 0 ms linear 1 2000 350 1 time_ms" : !strcmp(tok[2], "1") ? "resp -511" : "resp -103"); diff --git a/tests/perturbation.py b/tests/perturbation.py index cfe8a25..cd42856 100644 --- a/tests/perturbation.py +++ b/tests/perturbation.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later -# The declaration is the one copy: change values in a scratch copy of include/plugin-hostd/protocol.h, regenerate, +# The declaration is the one copy: change values in a scratch copy of include/plugin-hostd/protocol.h, and a verb in +# one of data/verbs.json, the daemon's verbs that omx-contract renders, render and regenerate, import sys as _s; _s.stdout.reconfigure(line_buffering=True) # rebuild, and the daemon's behaviour and the docs follow, with no other copy of an old value left in src/, the README # or the man page. The same behaviour test runs on the tree as it is, so it reads every value from the declared JSON and @@ -105,7 +106,10 @@ def make(tree, *args): ("an error code", r"(#define PHD_ERR_GAVE_UP +)\(-505\)", r"\g<1>(-595)", r"\b505\b"), ("the pool name limit", r"(#define PHD_POOL_NAME_MAX )32\b", r"\g<1>5", None), ("the readiness line", r'(#define PHD_READY_LINE )"plugin-hostd ready!"', r'\g<1>"plugin-hostd is up!"', r"plugin.hostd ready!"), - ("a verb", r'(#define PHD_VERB_INSTANCE_INFO +)"instance_info"', r'\g<1>"instance_where"', r"\binstance_info\b"), +] +# a verb of the daemon is declared in data/verbs.json and rendered: (what, pattern, replacement, old) on that file +DATA_EDITS = [ + ("a verb", r'"instance_info( %i)?"', lambda m: '"instance_where%s"' % (m.group(1) or ""), r"\binstance_info\b"), ] # the verb the daemon has after the edit, and the one it has before, which the perturbed daemon must not have VERB_AFTER, VERB_BEFORE = "instance_where", "instance_info" @@ -170,36 +174,54 @@ def main(): try: tree = os.path.join(work, "t") os.mkdir(tree) - for name in ("include", "src", "tools", "protocol", "doc", "packaging", "tests", "Makefile", "README.md"): + for name in ("data", "include", "src", "tools", "protocol", "doc", "packaging", "tests", "Makefile", "README.md"): src = os.path.join(ROOT, name) (shutil.copytree if os.path.isdir(src) else shutil.copy)(src, os.path.join(tree, name)) header = os.path.join(tree, "include", "plugin-hostd", "protocol.h") text = open(header).read() + declaration = os.path.join(tree, "data", "verbs.json") + verbs = open(declaration).read() # positive control: the greps find the old values where they are declared and documented - for what, pattern, _, old in EDITS: + for what, pattern, _, old in EDITS + DATA_EDITS: if old: found = grep(old, os.path.join(tree, "include"), os.path.join(tree, "README.md"), os.path.join(tree, "doc")) assert found, "control: the grep for the old value of %s finds nothing where it is declared" % what # and the generated files are current before anything is touched - r = make(tree, "check-generated") - assert r.returncode == 0, "the scratch copy is not current before the edit: " + r.stdout + r.stderr + for target in ("check-generated", "check-rendered"): + r = make(tree, target) + assert r.returncode == 0, "the scratch copy is not current before the edit: " + r.stdout + r.stderr for what, pattern, repl, _ in EDITS: text, n = re.subn(pattern, repl, text) assert n == 1, "the edit of %s matched %d times" % (what, n) open(header, "w").write(text) - - # the guard: a header changed and not regenerated is drift + for what, pattern, repl, _ in DATA_EDITS: + verbs, n = re.subn(pattern, repl, verbs) + assert n == 2, "the edit of %s matched %d times, not its word and its format" % (what, n) + open(declaration, "w").write(verbs) + # the render names a verb's macro after its word, so a renamed verb renames its C name, which every reader of it + # follows; a reader of the old name does not compile + for base, _, files in os.walk(tree): + for f in files: + if f.endswith((".c", ".h")) and not f.startswith("plugin_hostd_"): + path = os.path.join(base, f) + old = open(path).read() + if "OMX_VERB_INSTANCE_INFO" in old: + open(path, "w").write(old.replace("OMX_VERB_INSTANCE_INFO", "OMX_VERB_INSTANCE_WHERE")) + + # the guards: a header changed and not regenerated is drift, and so is a declaration not rendered r = make(tree, "check-generated") assert r.returncode != 0, "check-generated passed over a changed declaration" - print("ok check-generated fails on a changed declaration that was not regenerated") + r = make(tree, "check-rendered") + assert r.returncode != 0, "check-rendered passed over a changed verbs declaration" + print("ok check-generated and check-rendered fail on a changed declaration that was not regenerated") - for target in ("gen", "check-generated", "plugin-hostd"): + for target in ("render-verbs", "gen", "check-generated", "check-rendered", "plugin-hostd"): r = make(tree, target) assert r.returncode == 0, "make %s in the scratch copy: %s%s" % (target, r.stdout, r.stderr) - for what, _, _, old in EDITS: + for what, _, _, old in EDITS + DATA_EDITS: if old: left = grep(old, os.path.join(tree, "src"), os.path.join(tree, "README.md"), os.path.join(tree, "doc")) assert not left, "a copy of the old value of %s remains:\n%s" % (what, "\n".join(left)) diff --git a/tests/sabotage.py b/tests/sabotage.py index 225ffc1..2ce05e3 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -415,8 +415,8 @@ " else if (!begin && n < s->nheld)\n", " else if (0)\n", "a_gesture_the_value_it_moved_and_a_suggested_page_reach_the_controller_unchanged_and_in_order"), ("an LV2 instance's preset list goes to mod-host", "src/supervisor.c", - " else if (!strcmp(word[0], PHD_VERB_PRESET_LIST) || !strcmp(word[0], PHD_VERB_PRESET_GET))\n reply = sup_resp(PHD_ERR_NO_PRESET_LIST);", - " else if (!strcmp(word[0], PHD_VERB_PRESET_LIST) || !strcmp(word[0], PHD_VERB_PRESET_GET))\n reply = sup_call(id, line);", + " else if (!strcmp(word[0], OMX_VERB_PRESET_LIST) || !strcmp(word[0], OMX_VERB_PRESET_GET))\n reply = sup_resp(PHD_ERR_NO_PRESET_LIST);", + " else if (!strcmp(word[0], OMX_VERB_PRESET_LIST) || !strcmp(word[0], OMX_VERB_PRESET_GET))\n reply = sup_call(id, line);", "presets_are_listed_by_a_clap_worker_and_an_lv2_instance_on_mod_host_answers_no_list"), ("pin_accept leaves the adapter's refusals in place", "src/supervisor.c", " if (rc == SUCCESS)\n lv2_refusals_forget(NULL);\n return sup_resp(rc);", " return sup_resp(rc);", "pin_accept_makes_the_adapter_be_asked_again"), @@ -650,12 +650,12 @@ ("a row of the host verb table is dropped", "include/plugin-hostd/protocol.h", ' X("cpu_load", MOD_UI, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \\\n', "", "verb table", "verbtable"), ("a verb is added to the daemon with no row in the host verb table", "include/plugin-hostd/protocol.h", - ' X(CHAIN_CLEAR, PHD_VERB_CHAIN_CLEAR, "", "resp 0", \\\n', + ' X(CHAIN_CLEAR, OMX_VERB_CHAIN_CLEAR, "", OMX_VERB_CHAIN_CLEAR_REPLY, \\\n', ' X(SABOTAGE, "verb_table_sabotage", "", "resp 0", "a verb with no row") \\\n' - ' X(CHAIN_CLEAR, PHD_VERB_CHAIN_CLEAR, "", "resp 0", \\\n', "verb table", "verbtable"), + ' X(CHAIN_CLEAR, OMX_VERB_CHAIN_CLEAR, "", OMX_VERB_CHAIN_CLEAR_REPLY, \\\n', "verb table", "verbtable"), ("a verb the daemon answers itself is absent from a profile", "include/plugin-hostd/protocol.h", - " X(PHD_VERB_CHAIN, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \\\n", - " X(PHD_VERB_CHAIN, DAEMON, REQUIRED, REQUIRED, ABSENT, REQUIRED) \\\n", "verb table", "verbtable"), + " X(OMX_VERB_CHAIN, DAEMON, REQUIRED, REQUIRED, REQUIRED, REQUIRED) \\\n", + " X(OMX_VERB_CHAIN, DAEMON, REQUIRED, REQUIRED, ABSENT, REQUIRED) \\\n", "verb table", "verbtable"), ("a verb mod-host never had is required of a profile", "include/plugin-hostd/protocol.h", ' X("set_midi_program_change_pedalboard_bank_channel", MOD_UI, ABSENT, ABSENT, ABSENT, ABSENT) \\\n', ' X("set_midi_program_change_pedalboard_bank_channel", MOD_UI, ABSENT, REQUIRED, ABSENT, ABSENT) \\\n', diff --git a/tests/verbs_contract.py b/tests/verbs_contract.py index 2f1d284..efd9b1b 100644 --- a/tests/verbs_contract.py +++ b/tests/verbs_contract.py @@ -2,8 +2,10 @@ # The daemon routes by instance the commands whose first argument is an instance number. That list is mod-host's # grammar, not ours: the " " lines of mod-host's README are the commands of verbs.c's table, no # more and no fewer, and each one's format in mod-host.h takes a number first. The type alone does not make an -# instance verb: "transport %i" and "monitor_midi_program %i" take a number that is not one. And no verb is spelled in the C sources: a verb of mod-host.h or of -# include/plugin-hostd/protocol.h is read from its declaration, so a C string that starts with one is a copy. +# instance verb: "transport %i" and "monitor_midi_program %i" take a number that is not one. And no verb is spelled in the C sources: a verb of mod-host.h, of +# include/plugin-hostd/protocol.h or of the daemon's declaration (data/verbs.json, rendered into +# include/plugin-hostd/plugin_hostd_host_verbs.h) is read from its declaration, so a C string that starts with one, its +# word or its format, is a copy; protocol.h spells none of the rendered ones. import os, re, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -13,7 +15,10 @@ MOD_HOST_DIR = os.environ.get("MOD_HOST_DIR", os.path.join(ROOT, "..", "mod-host")) # the C sources built against the declarations: the daemon, and the worker the tests run it with SOURCES = [os.path.join("src", f) for f in sorted(os.listdir(os.path.join(ROOT, "src"))) if f.endswith((".c", ".h"))] -SOURCES.append(os.path.join("tests", "fake_host.c")) +SOURCES += [os.path.join("tests", "fake_host.c"), os.path.join("tools", "protocol-gen.c")] +# the fuzz harnesses type mod-host's words on purpose, as a controller does; the daemon's own they read rendered +RENDERED_ONLY = [os.path.join("fuzz", f) for f in sorted(os.listdir(os.path.join(ROOT, "fuzz"))) if f.endswith((".c", ".h"))] +RENDERED_ONLY.append(os.path.join("include", "plugin-hostd", "protocol.h")) failed = 0 @@ -45,18 +50,29 @@ def spelled(text, verbs): commands = modhost.commands(include) with open(os.path.join(ROOT, "include", "plugin-hostd", "protocol.h")) as f: own = set(re.findall(r'^#define\s+PHD_(?:VERB|EVENT)_\w+\s+"(\w+)"', f.read(), re.M)) -verbs = set(commands.values()) | own -if len(commands) < 40 or len(own) < 15: - fail("found only %d commands in %s and %d verbs and events in protocol.h" % (len(commands), include, len(own))) - -# positive control: the scan finds a verb where one is spelled -if not spelled('x = strcmp(verb, "%s");' % commands["EFFECT_BYPASS"], verbs): - fail("the scan does not find a spelled verb") +# the daemon's own verbs, declared in data/verbs.json and rendered: the word and the format of each are in the +# rendered header and nowhere else, protocol.h included +with open(os.path.join(ROOT, "include", "plugin-hostd", "plugin_hostd_host_verbs.h")) as f: + rendered = set(re.findall(r'^#define\s+OMX_VERB_[A-Z0-9_]+\s+"(\w+)"$', f.read(), re.M)) +verbs = set(commands.values()) | own | rendered +if len(commands) < 40 or len(own) < 15 or len(rendered) < 15: + fail("found only %d commands in %s, %d verbs and events in protocol.h and %d rendered verbs" + % (len(commands), include, len(own), len(rendered))) + +# positive control: the scan finds a verb where one is spelled, a rendered verb's word and its format alike +for planted in ('x = strcmp(verb, "%s");' % commands["EFFECT_BYPASS"], 'x = strcmp(verb, "chain_get");', + 'protocol_add_command("chain_get %i", cb);'): + if not spelled(planted, verbs): + fail("the scan does not find a spelled verb in %s" % planted) for path in SOURCES: with open(os.path.join(ROOT, path)) as f: for n, s in spelled(f.read(), verbs): fail('%s:%d spells "%s": read it from its declaration' % (path, n, s)) +for path in RENDERED_ONLY: + with open(os.path.join(ROOT, path)) as f: + for n, s in spelled(f.read(), rendered): + fail('%s:%d spells "%s": read it from plugin_hostd_host_verbs.h' % (path, n, s)) with open(os.path.join(ROOT, "src", "verbs.c")) as f: table = f.read() @@ -83,15 +99,15 @@ def spelled(text, verbs): dispatch = f.read() dispatch = dispatch[dispatch.index("static char *handle("):] own_verbs = 0 -for macro, row in re.findall(r"X\(\w+, (PHD_VERB_\w+),(.*?)(?=\n X\(|\Z)", block, re.S): +for macro, row in re.findall(r"X\(\w+, ((?:PHD|OMX)_VERB_\w+),(.*?)(?=\n X\(|\Z)", block, re.S): if "sent by the daemon" in row: continue own_verbs += 1 if macro not in dispatch: fail("%s is a verb of PHD_VERBS that verbs.c's handle() never answers" % macro) -if own_verbs < 10 or "PHD_VERB_CHAIN" not in block: +if own_verbs < 10 or "OMX_VERB_CHAIN" not in block: fail("found only %d controller verbs in PHD_VERBS, or no chain among them" % own_verbs) -if "PHD_VERB_CHAIN" in table or re.search(r"\bchain\b", " ".join(commands.values())): +if "OMX_VERB_CHAIN" in table or re.search(r"\bchain\b", " ".join(commands.values())): fail("chain is routed by instance, or mod-host.h has a command of that name") readme = os.path.join(MOD_HOST_DIR, "README.md") @@ -106,8 +122,10 @@ def spelled(text, verbs): header = f.read() rows = header[header.index("#define PHD_INSTANCE_VERBS(X)"):] rows = rows[:rows.index("\n\n")] - by_macro = dict(re.findall(r'^#define\s+(PHD_(?:VERB|EVENT)_\w+)\s+"(\w+)"', header, re.M)) - instance_info = {by_macro[m] for m in re.findall(r"X\(\w+, (PHD_VERB_\w+),", rows)} + with open(os.path.join(ROOT, "include", "plugin-hostd", "plugin_hostd_host_verbs.h")) as f: + header += f.read() + by_macro = dict(re.findall(r'^#define\s+((?:PHD_(?:VERB|EVENT)|OMX_VERB)_\w+)\s+"(\w+)"$', header, re.M)) + instance_info = {by_macro[m] for m in re.findall(r"X\(\w+, ((?:PHD|OMX)_VERB_\w+),", rows)} events = {v for m, v in by_macro.items() if m.startswith("PHD_EVENT_")} with open(os.path.join(ROOT, "src", "verbs.c")) as f: if "PHD_INSTANCE_VERBS(X)" not in f.read(): @@ -129,5 +147,5 @@ def spelled(text, verbs): if failed: sys.exit(1) -print("ok verbs contract: %d commands of mod-host.h and %d verbs and events of protocol.h, none spelled in %d sources" - % (len(commands), len(own), len(SOURCES))) +print("ok verbs contract: %d commands of mod-host.h, %d verbs and events of protocol.h and %d rendered verbs, none " + "spelled in %d sources" % (len(commands), len(own), len(rendered), len(SOURCES) + len(RENDERED_ONLY))) diff --git a/tools/protocol-gen.c b/tools/protocol-gen.c index 7ab4f82..e6c3acd 100644 --- a/tools/protocol-gen.c +++ b/tools/protocol-gen.c @@ -377,8 +377,8 @@ static void region_placement(void) printf("The placement of an `add` is `%s`. `%s` is a worker for that instance alone; `%s` is the pool's worker, " "opened on first use and capped by `%s`, and a full pool opens `%s2`, `%s3`, and so on; `%s` is " "the policy of `%s`. A pool name matches `%s`.\n", - PHD_PLACEMENT_SYNTAX, PHD_PLACE_OWN, PHD_PLACE_POOL_PREFIX, PHD_VERB_POOL_CONFIG, PHD_POOL_SIBLING_SEPARATOR, - PHD_POOL_SIBLING_SEPARATOR, PHD_PLACE_DEFAULT, PHD_VERB_POLICY_SET, PHD_POOL_NAME_PATTERN); + PHD_PLACEMENT_SYNTAX, PHD_PLACE_OWN, PHD_PLACE_POOL_PREFIX, OMX_VERB_POOL_CONFIG, PHD_POOL_SIBLING_SEPARATOR, + PHD_POOL_SIBLING_SEPARATOR, PHD_PLACE_DEFAULT, OMX_VERB_POLICY_SET, PHD_POOL_NAME_PATTERN); } static void region_verbs(void) @@ -548,7 +548,7 @@ static void region_man_dirs(void) printf(".\n.PP\nThe answered\n.B add\nof a plugin whose files have no descriptor yet runs\n.I probe\n.B describe\n" "in a child of its own and files its stdout as\n.IR \""); roff_escape(PHD_DESCRIPTOR_NAME_SYNTAX); - printf("\" ,\nwritten once; the add never waits for it.\n.B " PHD_VERB_DESCRIPTOR_GET "\n.I uri\n" + printf("\" ,\nwritten once; the add never waits for it.\n.B " OMX_VERB_DESCRIPTOR_GET "\n.I uri\n" "answers the file's path, or "); snprintf(buffer, sizeof(buffer), "%d (none), %d (the scan runs), %d (the scan wrote nothing)", PHD_ERR_DESCRIPTOR_ABSENT, PHD_ERR_DESCRIPTOR_PENDING, PHD_ERR_DESCRIPTOR_SCAN_FAILED); @@ -556,7 +556,7 @@ static void region_man_dirs(void) printf(".\nOnce the descriptor is in place the same scan runs\n.I probe\n.B panel\nand files the plugin's panel as\n" ".IR \""); roff_escape(PHD_PANEL_NAME_SYNTAX); - printf("\" ,\nunder the descriptor's name;\n.B " PHD_VERB_PANEL_GET "\n.I uri\nanswers its path, with the same codes.\n"); + printf("\" ,\nunder the descriptor's name;\n.B " OMX_VERB_PANEL_GET "\n.I uri\nanswers its path, with the same codes.\n"); } static void region_man_ready(void) From dffca5d380568c86b26121e417c66fc068d58fdf Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:16:25 +0100 Subject: [PATCH 3/9] Ship the contract directory's files in a noarch plugin-hostd-contract package The files under share/omx-contract/contract.d/3/plugin-hostd (MANIFEST.json, formats/clap.json, formats/lv2.json and now verbs.json) are read at run time, so they leave plugin-hostd-devel and plugin-hostd-dev for a new noarch package, plugin-hostd-contract on Fedora and Debian alike (Architecture: all), at the same paths. plugin-hostd and plugin-hostd-devel (plugin-hostd-dev) require it at the same version; the Debian package Breaks and Replaces plugin-hostd-dev (<< ${source:Version}), which shipped them before. The development package gains the two rendered headers. The floor of omx-contract-devel (libomx-contract-dev) is 3.3.0, the first schema with the verbs kind, and the check stage needs the omx-contract command for make check-rendered, which %check and dh_auto_test now run. The CI file-list checks follow: the RPM job lists the contract package and the Requires of the other two, the clean root installs it, and the DEB smoke checks its files, its architecture, its Breaks and Replaces and the Depends on it. A local rpmbuild -ba (fc44, check stage on) writes plugin-hostd-contract-0.11.0-1.fc44.noarch.rpm with the four files and the licence, and tests/consumer.c compiles against plugin-hostd-devel and plugin-hostd-contract installed in a clean fedora:44. --- .github/workflows/ci.yml | 35 ++++++++++++++++++++++++---- BUILDING.md | 19 ++++++++++++--- Makefile | 6 ++--- debian/control | 21 ++++++++++++++--- debian/plugin-hostd-contract.install | 1 + debian/plugin-hostd-dev.install | 3 ++- debian/rules | 4 ++-- docs/formats.md | 3 ++- packaging/plugin-hostd.spec | 32 +++++++++++++++++++++---- 9 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 debian/plugin-hostd-contract.install diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362aca8..922af3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,22 +217,34 @@ jobs: rpm -qp --requires "$rpm" | tee requires.txt grep -q 'libmod-host-protocol.so.0' requires.txt grep -qx 'mod-host-protocol(.*) >= 0.10.6.f14a230-11.1' requires.txt + grep -qx "plugin-hostd-contract = $(rpm -qp --qf '%{VERSION}-%{RELEASE}' "$rpm")" requires.txt rpm -qp --recommends "$rpm" | tee recommends.txt grep -q '^mod-host' recommends.txt grep -q '^omx-clap-host' recommends.txt done - # the development package: the header a consumer includes, its pkg-config file, the schema of the JSON + # the development package: the headers a consumer includes, its pkg-config file, the schema of the JSON for rpm in rpms/plugin-hostd-devel-[0-9]*.noarch.rpm; do rpm -qlp "$rpm" rpm -qp --qf '[%{FILEMODES:perms} %{FILENAMES}\n]' "$rpm" | grep -v '^d' | cut -d' ' -f2 | sort > files.txt printf '%s\n' /usr/include/plugin-hostd/protocol.h /usr/include/plugin-hostd/pin.h /usr/share/pkgconfig/plugin-hostd.pc \ + /usr/include/plugin-hostd/plugin_hostd_host_verbs.h /usr/include/plugin-hostd/plugin_hostd_vocabulary.h \ /usr/share/plugin-hostd/protocol.schema.json /usr/share/plugin-hostd/probe-output.schema.json /usr/share/plugin-hostd/preset.schema.json \ - /usr/share/omx-contract/contract.d/3/plugin-hostd/MANIFEST.json \ + /usr/share/licenses/plugin-hostd-devel/COPYING | sort > expected.txt + diff -u expected.txt files.txt + rpm -qp --requires "$rpm" | grep -qx "plugin-hostd-contract = $(rpm -qp --qf '%{VERSION}-%{RELEASE}' "$rpm")" + done + # the contract package: the contract directory's files, which readers read at run time, noarch + for rpm in rpms/plugin-hostd-contract-[0-9]*.noarch.rpm; do + rpm -qlp "$rpm" + rpm -qp --qf '[%{FILEMODES:perms} %{FILENAMES}\n]' "$rpm" | grep -v '^d' | cut -d' ' -f2 | sort > files.txt + printf '%s\n' /usr/share/omx-contract/contract.d/3/plugin-hostd/MANIFEST.json \ /usr/share/omx-contract/contract.d/3/plugin-hostd/formats/clap.json \ /usr/share/omx-contract/contract.d/3/plugin-hostd/formats/lv2.json \ - /usr/share/licenses/plugin-hostd-devel/COPYING | sort > expected.txt + /usr/share/omx-contract/contract.d/3/plugin-hostd/verbs.json \ + /usr/share/licenses/plugin-hostd-contract/COPYING | sort > expected.txt diff -u expected.txt files.txt done + ls rpms/plugin-hostd-contract-[0-9]*.noarch.rpm # the tests package: the fake worker, where plugin-qualify's pins-file test finds it for rpm in rpms/plugin-hostd-tests-[0-9]*.${{ matrix.arch }}.rpm; do rpm -qlp "$rpm" @@ -257,6 +269,7 @@ jobs: mod-host-rpms/mod-host-[0-9]*.rpm mod-host-rpms/mod-host-protocol-[0-9]*.rpm rpms/plugin-hostd-[0-9]*.${{ matrix.arch }}.rpm + rpms/plugin-hostd-contract-[0-9]*.noarch.rpm rpms/plugin-hostd-tests-[0-9]*.${{ matrix.arch }}.rpm packages: python3 @@ -379,11 +392,25 @@ jobs: run: | set -euo pipefail apt-get update -qq - apt-get install -y --no-install-recommends ./debs/plugin-hostd_*.deb ./debs/plugin-hostd-dev_*.deb ./debs/plugin-hostd-tests_*.deb ./dep-debs/libmod-host-protocol0_*.deb python3 + apt-get install -y --no-install-recommends ./debs/plugin-hostd_*.deb ./debs/plugin-hostd-contract_*.deb ./debs/plugin-hostd-dev_*.deb ./debs/plugin-hostd-tests_*.deb ./dep-debs/libmod-host-protocol0_*.deb python3 dpkg -L plugin-hostd-dev | tee dev.txt grep -qx /usr/include/plugin-hostd/protocol.h dev.txt + grep -qx /usr/include/plugin-hostd/plugin_hostd_host_verbs.h dev.txt + grep -qx /usr/include/plugin-hostd/plugin_hostd_vocabulary.h dev.txt grep -qx /usr/include/plugin-hostd/pin.h dev.txt grep -qx /usr/share/pkgconfig/plugin-hostd.pc dev.txt + ! grep -q /usr/share/omx-contract/ dev.txt + # the contract directory's files are the noarch contract package's, which the daemon and the headers depend on + test "$(dpkg-deb -f debs/plugin-hostd-contract_*.deb Architecture)" = all + dpkg -L plugin-hostd-contract | tee contract.txt + for f in MANIFEST.json formats/clap.json formats/lv2.json verbs.json; do + grep -qx "/usr/share/omx-contract/contract.d/3/plugin-hostd/$f" contract.txt + done + dpkg-deb -f debs/plugin-hostd-contract_*.deb Breaks Replaces | tee breaks.txt + test "$(grep -c '^\(Breaks\|Replaces\): plugin-hostd-dev (<< ' breaks.txt)" = 2 + for p in plugin-hostd plugin-hostd-dev; do + dpkg-deb -f debs/${p}_*.deb Depends | grep -q 'plugin-hostd-contract (= ' + done dpkg -L plugin-hostd | grep -qx /usr/share/plugin-hostd/protocol.json dpkg -L plugin-hostd | grep -qx /usr/share/plugin-hostd/mod-host.json dpkg -L plugin-hostd-tests | tee tests.txt diff --git a/BUILDING.md b/BUILDING.md index b07c4b2..8f933b3 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -10,7 +10,7 @@ missing. The daemon needs libc and that library and nothing else. The probe runner and the protocol generator also read omx-contract's C headers, `` and ``, from -`pkg-config omx-contract` (`omx-contract-devel`, `libomx-contract-dev`, 2.18.0 or +`pkg-config omx-contract` (`omx-contract-devel`, `libomx-contract-dev`, 3.3.0 or later; `CONTRACT_CFLAGS=-I` for one that is not installed). The runner's default rates are the contract's `ORACLE_FLOOR_RATES`, and the generator refuses to build or run when the protocol's parameter scales are not the contract's @@ -46,6 +46,19 @@ The tables in README.md marked `BEGIN GENERATED protocol:...`, the key list of t headers the build compiles against (`pkg-config --cflags mod-host-protocol`, or `MOD_HOST_DIR/src`), by `tools/mod-host-json.py`; `make test-mod-host-json` checks that it follows them. +The daemon's verbs +------------------ + +The verbs the daemon adds to mod-host's protocol, and the codes they answer, are declared in `data/verbs.json`, a +`verbs` item of omx-contract's contract directory (schema 3.3.0 or later). `omx-contract render` writes them into +`include/plugin-hostd/plugin_hostd_host_verbs.h` and `plugin_hostd_vocabulary.h`, which are committed and which +`protocol.h` includes: `make render-verbs` rewrites them and `make check-rendered` fails on any drift. Both run the +`omx-contract` command (package `omx-contract`); the build itself does not. + +`make install-contract` installs `data/` as the package `plugin-hostd` of the contract directory. The noarch package +`plugin-hostd-contract` carries those files, and `plugin-hostd` and `plugin-hostd-devel` (`plugin-hostd-dev`) require +it at the same version. + Tests ----- @@ -59,9 +72,9 @@ ledger, replay to the byte, attribution, quarantine, the storm bound, The verbs are read from their declarations, never spelled: mod-host's from the command formats of `mod-host.h`, the daemon's own from -`include/plugin-hostd/protocol.h`. `tests/verbs_contract.py` fails on a C string +`data/verbs.json` through the rendered header. `tests/verbs_contract.py` fails on a C string that starts with one, and `tests/perturbation.py` renames a verb in a scratch -copy of each header and requires the rebuilt daemon to answer the new name and +copy of `data/verbs.json` and of `mod-host.h` and requires the rebuilt daemon to answer the new name and not the old. make test-jack OMX_CLAP_HOST= [MOD_HOST= LV2_DIR= LV2_URI= LV2_BUNDLE= LV2_PARAM=] diff --git a/Makefile b/Makefile index 7344b45..5094a9b 100644 --- a/Makefile +++ b/Makefile @@ -125,9 +125,9 @@ install_protocol: sed -e 's,@PREFIX@,$(PREFIX),g' -e 's,@VERSION@,$(VERSION),g' protocol/plugin-hostd.pc.in > $(DESTDIR)$(DATADIR)/pkgconfig/plugin-hostd.pc chmod 644 $(DESTDIR)$(DATADIR)/pkgconfig/plugin-hostd.pc -# the format tables this tree owns (data/formats/, docs/formats.md), installed as the package plugin-hostd into -# omx-contract's contract directory, //plugin-hostd/formats/{clap,lv2}.json, with a -# MANIFEST.json naming this version and the tree they came from; plugin-hostd-devel ships them. contractdir and the +# the format tables (data/formats/, docs/formats.md) and the verbs (data/verbs.json) this tree owns, installed as the +# package plugin-hostd into omx-contract's contract directory, //plugin-hostd/, with a +# MANIFEST.json naming this version and the tree they came from; plugin-hostd-contract ships them. contractdir and the # install script are omx-contract-devel's; a checkout of omx-contract (OMX_CONTRACT_DIR) stands in for the script when # it is not installed. CONTRACT_SCHEMA_MAJOR = 3 diff --git a/debian/control b/debian/control index 12e3157..99e93a6 100644 --- a/debian/control +++ b/debian/control @@ -4,8 +4,9 @@ Priority: optional Maintainer: Pau Aliagas Build-Depends: debhelper-compat (= 13), libmod-host-protocol-dev (>= 0.1.0-5), - libomx-contract-dev (>= 3.0.0~), + libomx-contract-dev (>= 3.3.0~), omx-clap-host-tests (>= 0.21.0~) , + omx-contract (>= 3.3.0~) , pkgconf, python3 Standards-Version: 4.6.2 @@ -14,7 +15,8 @@ Rules-Requires-Root: no Package: plugin-hostd Architecture: any -Depends: libmod-host-protocol0 (>= 0.1.0-6~fmx), ${shlibs:Depends}, ${misc:Depends} +Depends: libmod-host-protocol0 (>= 0.1.0-6~fmx), plugin-hostd-contract (= ${source:Version}), ${shlibs:Depends}, + ${misc:Depends} Recommends: omx-clap-host (>= 0.21.0) Suggests: mod-host Description: run LV2 and CLAP plugins so that one crash cannot stop the show @@ -43,10 +45,23 @@ Description: stand-in plugin worker to test a program that drives plugin-hostd Package: plugin-hostd-dev Section: libdevel Architecture: all -Depends: ${misc:Depends} +Depends: plugin-hostd-contract (= ${source:Version}), ${misc:Depends} Description: C header to drive plugin-hostd from your own controller The C header that declares everything a controller needs to drive plugin-hostd: the verbs it adds to mod-host's protocol, error codes, feedback events and settings. Include it instead of copying the values, and pkg-config plugin-hostd gives the flags. The same declaration as JSON, for tools not written in C, ships in the plugin-hostd package. + +Package: plugin-hostd-contract +Architecture: all +Depends: ${misc:Depends} +Breaks: plugin-hostd-dev (<< ${source:Version}) +Replaces: plugin-hostd-dev (<< ${source:Version}) +Description: plugin-hostd's format tables and verbs, read at run time + plugin-hostd's package of omx-contract's contract directory: the format + tables a scan maps a CLAP or LV2 plugin through (formats/clap.json, + formats/lv2.json) and the verbs the daemon adds to mod-host's protocol, with + their replies, their refusals and what each host owes them (verbs.json), + under /usr/share/omx-contract/contract.d/3/plugin-hostd. A program that reads + them at run time depends on this package, never on the development one. diff --git a/debian/plugin-hostd-contract.install b/debian/plugin-hostd-contract.install new file mode 100644 index 0000000..9b2ff3f --- /dev/null +++ b/debian/plugin-hostd-contract.install @@ -0,0 +1 @@ +usr/share/omx-contract/contract.d/3/plugin-hostd diff --git a/debian/plugin-hostd-dev.install b/debian/plugin-hostd-dev.install index 6f26223..df1330b 100644 --- a/debian/plugin-hostd-dev.install +++ b/debian/plugin-hostd-dev.install @@ -1,7 +1,8 @@ usr/include/plugin-hostd/protocol.h +usr/include/plugin-hostd/plugin_hostd_host_verbs.h +usr/include/plugin-hostd/plugin_hostd_vocabulary.h usr/include/plugin-hostd/pin.h usr/share/pkgconfig/plugin-hostd.pc usr/share/plugin-hostd/protocol.schema.json usr/share/plugin-hostd/probe-output.schema.json usr/share/plugin-hostd/preset.schema.json -usr/share/omx-contract/contract.d/3/plugin-hostd diff --git a/debian/rules b/debian/rules index afee6b9..6176628 100755 --- a/debian/rules +++ b/debian/rules @@ -11,11 +11,11 @@ override_dh_auto_build: override_dh_auto_install: dh_auto_install -- PREFIX=/usr - # the format tables, as the package plugin-hostd of omx-contract's contract directory (docs/formats.md) + # the format tables and the verbs, as the package plugin-hostd of omx-contract's contract directory (docs/formats.md) $(MAKE) install-contract DESTDIR=$(CURDIR)/debian/tmp PREFIX=/usr # the verb table is checked against mod-host's README, which a build does not have override_dh_auto_test: ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) - $(MAKE) test-daemon check-generated + $(MAKE) test-daemon check-generated check-rendered endif diff --git a/docs/formats.md b/docs/formats.md index f30a609..0b624f3 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -19,7 +19,8 @@ is the proof. `make install-contract` installs both tables, with omx-contract-devel's `install-contract-dir.sh`, as the package `plugin-hostd` of the contract directory: `/3/plugin-hostd/formats/{clap,lv2}.json`, with its -`MANIFEST.json`. plugin-hostd-devel ships them. +`MANIFEST.json`, beside the daemon's verbs (`verbs.json`). The noarch package plugin-hostd-contract +(`plugin-hostd-contract` on Debian too) ships them, and a program that reads them at run time requires it. A table's `rows` are keyed by the source field, and each row has exactly one class: diff --git a/packaging/plugin-hostd.spec b/packaging/plugin-hostd.spec index acc50a7..bf6d2bd 100644 --- a/packaging/plugin-hostd.spec +++ b/packaging/plugin-hostd.spec @@ -17,9 +17,9 @@ BuildRequires: python3 # release 11 on, and protocol/mod-host.json is generated from them; a floor only BuildRequires: mod-host-protocol-devel >= 0.10.6.f14a230-11 # the probe runner's default rates, the protocol's parameter scales and the probe output's shared words are read from -# omx-contract's C headers, and the format tables are items of its schema 3 (3.0.0 is the first with the formatMap -# kind, the extension bag and plugin.doc); a floor only -BuildRequires: omx-contract-devel >= 3.0.0 +# omx-contract's C headers, the format tables are items of its schema 3, and the daemon's verbs are a verbs item +# (3.3.0 is the first with the verbs kind); a floor only +BuildRequires: omx-contract-devel >= 3.3.0 # a command of exactly the socket's buffer size, or several commands in one read, needs mod-audio/mod-host#113: # FreeMixer's build carrying it (11.1.fmx) or audinux's once it carries it (12); a floor only Requires: mod-host-protocol%{?_isa} >= 0.10.6.f14a230-11.1 @@ -30,7 +30,11 @@ Requires: mod-host-protocol%{?_isa} >= 0.10.6.f14a230-11.1 # ships fault-14.clap and jack_identity) %if %{with check} BuildRequires: omx-clap-host-tests >= 0.21.0 +# the reader that holds the committed render of the daemon's verbs to data/verbs.json (make check-rendered) +BuildRequires: omx-contract >= 3.3.0 %endif +# the format tables and the verbs, which readers read at run time +Requires: %{name}-contract = %{version}-%{release} # the workers: a format whose worker is not installed refuses its adds (-503), the other format is unaffected Recommends: mod-host @@ -47,6 +51,7 @@ latency: every worker is a JACK client. %package devel Summary: Talk to plugin-hostd from your own controller without retyping its protocol BuildArch: noarch +Requires: %{name}-contract = %{version}-%{release} %description devel The C header that declares everything a controller needs to drive plugin-hostd: @@ -55,6 +60,18 @@ settings. Include it instead of copying the values, and pkg-config plugin-hostd gives the flags. The same declaration as JSON, for tools not written in C, ships in the plugin-hostd package. +%package contract +Summary: plugin-hostd's format tables and verbs, for programs that read them at run time +BuildArch: noarch + +%description contract +plugin-hostd's package of omx-contract's contract directory: the format tables +a scan maps a CLAP or LV2 plugin through (formats/clap.json, formats/lv2.json) +and the verbs the daemon adds to mod-host's protocol, with their replies, +their refusals and what each host owes them (verbs.json), under +share/omx-contract/contract.d/3/plugin-hostd. A program that reads them at run +time requires this package, never the development one. + %package tests Summary: A stand-in plugin worker to test a program that drives plugin-hostd Requires: %{name}%{?_isa} = %{version}-%{release} @@ -82,13 +99,13 @@ sed -i 's,LDFLAGS += -s,LDFLAGS +=,g' Makefile %install %make_install PREFIX=%{_prefix} -# the format tables, as the package plugin-hostd of omx-contract's contract directory (docs/formats.md) +# the format tables and the verbs, as the package plugin-hostd of omx-contract's contract directory (docs/formats.md) %make_build install-contract DESTDIR=%{buildroot} PREFIX=%{_prefix} # the verb table is checked against mod-host's README, which a build does not have %check %if %{with check} -make test-daemon check-generated +make test-daemon check-generated check-rendered %endif %files @@ -110,11 +127,16 @@ make test-daemon check-generated %files devel %license COPYING %{_includedir}/plugin-hostd/protocol.h +%{_includedir}/plugin-hostd/plugin_hostd_host_verbs.h +%{_includedir}/plugin-hostd/plugin_hostd_vocabulary.h %{_includedir}/plugin-hostd/pin.h %{_datadir}/pkgconfig/plugin-hostd.pc %{_datadir}/plugin-hostd/protocol.schema.json %{_datadir}/plugin-hostd/probe-output.schema.json %{_datadir}/plugin-hostd/preset.schema.json + +%files contract +%license COPYING %{_datadir}/omx-contract/contract.d/3/plugin-hostd/ %changelog From b216ad75a79b2c3386fb35d570af0046c63ca10f Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:33:23 +0100 Subject: [PATCH 4/9] The instance-unknown refusal is -3, the number the daemon answers mod-host's ERR_INSTANCE_NON_EXISTS is -3 (host-errors.h); -101 is ERR_LV2_INVALID_URI. On the daemon's socket instance_info 99, quarantine_clear 99, preset_list 99 and preset_get 99 0 answer resp -3, so the declaration said a number no reply carries. tests/verbs_declared.py, the next commit, found it: four refused lines red against -101, green against -3. The render follows: OMX_VERB_INSTANCE_INFO_CODES_INIT is { -3, -902 }. --- data/verbs.json | 2 +- include/plugin-hostd/plugin_hostd_host_verbs.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/verbs.json b/data/verbs.json index bca0c5a..00e3ef7 100644 --- a/data/verbs.json +++ b/data/verbs.json @@ -15,7 +15,7 @@ }, "instanceUnknown": { "code": "reply.plugin-hostd.instance-unknown", - "resp": -101, + "resp": -3, "doc": "mod-host's ERR_INSTANCE_NON_EXISTS, answered by the daemon for an instance no add made" }, "placementInvalid": { diff --git a/include/plugin-hostd/plugin_hostd_host_verbs.h b/include/plugin-hostd/plugin_hostd_host_verbs.h index 7b838e2..6ba19ea 100644 --- a/include/plugin-hostd/plugin_hostd_host_verbs.h +++ b/include/plugin-hostd/plugin_hostd_host_verbs.h @@ -47,7 +47,7 @@ struct omx_host_verb { #define OMX_VERB_INSTANCE_INFO_FMT "instance_info %i" #define OMX_VERB_INSTANCE_INFO_REPLY "resp 0 " #define OMX_VERB_INSTANCE_INFO_CODES_COUNT 2u -#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -101, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -3, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ /* PLUGIN_HOSTD_VERBS.supervisorReset (data/verbs.json): Re-arm the storm bound and the backoff of a given-up placement: [ | all]. */ #define OMX_VERB_SUPERVISOR_RESET "supervisor_reset" @@ -61,7 +61,7 @@ struct omx_host_verb { #define OMX_VERB_QUARANTINE_CLEAR_FMT "quarantine_clear %s" #define OMX_VERB_QUARANTINE_CLEAR_REPLY "resp 0" #define OMX_VERB_QUARANTINE_CLEAR_CODES_COUNT 2u -#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -101, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -3, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ /* PLUGIN_HOSTD_VERBS.policySet (data/verbs.json): The placement of an add that says default: own | pool:. */ #define OMX_VERB_POLICY_SET "policy_set" @@ -157,14 +157,14 @@ struct omx_host_verb { #define OMX_VERB_PRESET_LIST_FMT "preset_list %i" #define OMX_VERB_PRESET_LIST_REPLY "resp " #define OMX_VERB_PRESET_LIST_CODES_COUNT 3u -#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -101, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ /* PLUGIN_HOSTD_VERBS.presetGet (data/verbs.json): One preset, 0 to count - 1: the key preset_load takes for it, and its name. */ #define OMX_VERB_PRESET_GET "preset_get" #define OMX_VERB_PRESET_GET_FMT "preset_get %i %i" #define OMX_VERB_PRESET_GET_REPLY "resp 0 " #define OMX_VERB_PRESET_GET_CODES_COUNT 3u -#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -101, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ #define PLUGIN_HOSTD_HOST_VERB_COUNT 19u From 508ff733cb8b45b70e7d99e856392ec1526b24a4 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:33:31 +0100 Subject: [PATCH 5/9] Hold the declared verbs to the daemon on its socket tests/verbs_declared.py (make test-verbs-declared, part of make test) reads data/verbs.json and omx-clap-core's declarations, from its package in the first directory of OMX_CONTRACT_PATH that holds one or else the installed contract directory, and drives the daemon in front of tests/fake-host: - each of the 18 verbs plugin-hostd owes, and the shared ones it owes, answers a line its declared format takes with its declared reply (an optional one may refuse with a code it declares), and a malformed line and the lines its meaning refuses with codes it declares; - the words of the binary the daemon answers without relaying them to its worker (29 of 205) are the declared verbs and mod-host's commands, no more; - a verb both hosts owe answers one line in one shape from the daemon and from the fake worker run on ports of its own; - protocol.h's formats of the shared verbs are omx-clap-core's. Without omx-clap-core's package the run prints PENDING, takes the shared verbs protocol.h names in their place and names that source. tests/sabotage.py breaks it five ways, each red and then restored: a declared format (chain_get %i %i), a declared code (-101 for -3), a deleted handler (chain_get), an undeclared verb the daemon answers itself, and a declared verb typed by hand in src/verbs.c (tests/verbs_contract.py). The scan fixture moves from tests/daemon_test.py to tests/scan_fixture.py, which both tests read. --- Makefile | 8 +- tests/daemon_test.py | 109 +---------- tests/sabotage.py | 42 ++++ tests/scan_fixture.py | 110 +++++++++++ tests/verbs_declared.py | 416 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 581 insertions(+), 104 deletions(-) create mode 100644 tests/scan_fixture.py create mode 100644 tests/verbs_declared.py diff --git a/Makefile b/Makefile index 5094a9b..0641046 100644 --- a/Makefile +++ b/Makefile @@ -254,13 +254,19 @@ 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 check-verb-table check-preset-schema test-install-schemas +test: test-pin test-transforms test-format-coverage test-describe test-relay test-alloc test-format test-daemon test-verbs-declared check-rendered 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 check-preset-schema test-install-schemas 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 test-daemon: $(PROG) $(FAKE) PLUGIN_HOSTD=./$(PROG) FAKE_HOST=./$(FAKE) python3 tests/daemon_test.py +# the declared verbs on the daemon's socket (tests/verbs_declared.py): each answers its declared reply and codes, the +# daemon answers no undeclared word itself, and a verb both hosts owe answers in one shape from both; omx-clap-core's +# declarations come from OMX_CONTRACT_PATH or the installed contract directory +test-verbs-declared: $(PROG) $(PROBE) $(FAKE) + PLUGIN_HOSTD=./$(PROG) PROBE=./$(PROBE) FAKE_HOST=./$(FAKE) python3 tests/verbs_declared.py + # panel_get on the wire against the installed omx-clap-scan (omx-clap-core 0.20.0 or later) and real plugins: one that # publishes a panel (omx-plugins-clap's omx-delay) and one that publishes none (omx-clap-host-tests' fake compressor) test-panel-wire: $(PROG) $(PROBE) $(FAKE) diff --git a/tests/daemon_test.py b/tests/daemon_test.py index 98f80dd..8ce7ab4 100644 --- a/tests/daemon_test.py +++ b/tests/daemon_test.py @@ -1,11 +1,13 @@ # SPDX-License-Identifier: GPL-3.0-or-later # plugin-hostd against workers that are not plugin hosts (tests/fake-host). No jack, no plugin: the daemon's own # behaviour, over the wire, the way a controller sees it. tests/jack_e2e.sh runs the real workers. -import hashlib, json, os, shutil, signal, subprocess, sys, tempfile, time +import json, os, shutil, signal, subprocess, sys, tempfile, time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import harness +import scan_fixture from harness import Daemon, Fail, alive, check, resp, run_tests, wait_for +from scan_fixture import CLAP_ID, FIELDS, LV2_COMPRESSOR, MANIFEST, PROBE, ROOT, descriptor_name, panel_name, sha, write EXE = os.path.abspath(os.environ.get("PLUGIN_HOSTD", "./plugin-hostd")) FAKE = os.path.abspath(os.environ.get("FAKE_HOST", "tests/fake-host")) @@ -1270,22 +1272,9 @@ def a_tempo_the_worker_refuses_is_kept_written_and_the_worker_still_comes_back() # ---------------------------------------------------------------- pins (one-contract §10, supervisor §5.8) -MANIFEST = """@prefix lv2: . -@prefix rdfs: . - -<%s> - a lv2:Plugin ; - lv2:binary ; - rdfs:seeAlso . -""" LV2_URI = "urn:test:pinned" -def sha(path): - with open(path, "rb") as f: - return hashlib.sha256(f.read()).hexdigest() - - def fake_layout(default=None): """the layout pin of the fake's one parameter, as the fake computes it through pin.h""" env = dict(os.environ) @@ -1294,11 +1283,6 @@ def fake_layout(default=None): return subprocess.check_output([FAKE, "-L"], env=env, text=True).strip() -def write(path, data): - with open(path, "wb") as f: - f.write(data) - - class Plugins: """a CLAP file and an LV2 bundle on disk, and the log of every command a fake worker received""" @@ -2974,100 +2958,19 @@ def under_require_pins_a_chain_of_a_clap_and_an_lv2_member_sees_the_lv2_members_ # ---------------------------------------------------------------- descriptors -ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -PROBE = os.path.abspath(os.environ.get("PROBE", os.path.join(ROOT, "plugin-hostd-probe"))) -FIELDS = os.path.join(ROOT, "tests", "fixtures", "fields") -CLAP_ID = "org.freemixer.plugin-qualify.fixture.controls" -LV2_COMPRESSOR = "urn:example:compressor" -SCANNER = """#!/bin/sh -# a stand-in for the probe: the real describe, over the recorded dump of the target's format, and the real panel over -# the recorded scan of the CLAP fixture; the panel stage logs to $SCAN_LOG.panel -if [ "$1" = panel ]; then - echo "$2" >> "$SCAN_LOG.panel" - if [ -n "$PANEL_EXIT" ]; then exit "$PANEL_EXIT"; fi - exec %(probe)s panel --json-from %(fields)s/clap-panel-scan.json "$2" -fi -echo "$2" >> "$SCAN_LOG" -if [ -n "$SCAN_HOLD" ]; then while [ ! -e "$SCAN_HOLD" ]; do sleep 0.02; done; fi -if [ -n "$SCAN_EXIT" ]; then exit "$SCAN_EXIT"; fi -case "$2" in - clap:*) exec %(probe)s describe --table %(tables)s/clap.json --fields-from %(fields)s/clap-controls.tsv "$2" ;; - *) exec %(probe)s describe --table %(tables)s/lv2.json --fields-from %(fields)s/lv2-compressor.tsv "$2" ;; -esac -""" +def Scan(): + """tests/scan_fixture.py's files and scanner, in front of the daemon under test""" + return scan_fixture.Scan(daemon) def config_meaning(key): return [c for c in harness.DECLARED["config"] if c["key"] == key][0] -def safe(text): - """protocol.h's PHD_DESCRIPTOR_NAME_SYNTAX, plugin-qualify's rule: every character outside [A-Za-z0-9._-] as _""" - return "".join(c if c.isascii() and (c.isalnum() or c in "._-") else "_" for c in text) - - -def descriptor_name(root, plugin_id, version, binary): - """@-.json""" - return os.path.join(root, "%s@%s-%s.json" % (safe(plugin_id), safe(version), sha(binary)[:12])) - - -class Scan: - """a CLAP file, an LV2 bundle of the compressor, and a scanner that logs every target it is run for""" - - def __init__(self): - self.dir = tempfile.mkdtemp(prefix="plugin-hostd-scan.") - self.clap = os.path.join(self.dir, "clap", "controls.clap") - os.makedirs(os.path.dirname(self.clap)) - write(self.clap, b"a clap build") - self.clap_uri = "clap:%s#%s" % (self.clap, CLAP_ID) - self.lv2_dir = os.path.join(self.dir, "lv2") - self.bundle = os.path.join(self.lv2_dir, "compressor.lv2") - os.makedirs(self.bundle) - write(os.path.join(self.bundle, "manifest.ttl"), (MANIFEST % LV2_COMPRESSOR).encode()) - write(os.path.join(self.bundle, "pinned.so"), b"lv2 binary") - write(os.path.join(self.bundle, "pinned.ttl"), b"lv2:port [ lv2:index 0 ] .") - self.scanner = os.path.join(self.dir, "scanner") - write(self.scanner, (SCANNER % {"probe": PROBE, "tables": os.path.join(ROOT, "data", "formats"), - "fields": FIELDS}).encode()) - os.chmod(self.scanner, 0o755) - self.log = os.path.join(self.dir, "scans.log") - self.descriptors = os.path.join(self.dir, "descriptors") - self.qualification = os.path.join(self.dir, "qualification") - - def daemon(self, conf=None, env=None): - c = {"probe": self.scanner, "lv2_path": self.lv2_dir, "descriptor_dir": self.descriptors, - "qualification_dir": self.qualification} - c.update(conf or {}) - e = {"SCAN_LOG": self.log} - e.update(env or {}) - return daemon(conf=c, env=e) - - def scans(self, stage=""): - if not os.path.exists(self.log + stage): - return [] - with open(self.log + stage) as f: - return [l.strip() for l in f if l.strip()] - - def clap_name(self, root=None): - return descriptor_name(root or self.descriptors, CLAP_ID, "1", self.clap) - - def lv2_name(self): - # the compressor states no version; its binary is the file its manifest names with lv2:binary - return descriptor_name(self.descriptors, LV2_COMPRESSOR, "unversioned", os.path.join(self.bundle, "pinned.so")) - - def close(self): - shutil.rmtree(self.dir, ignore_errors=True) - - def answered(d, uri, path): return d.send("descriptor_get %s" % uri) == 'resp 0 "%s"' % path -def panel_name(descriptor): - """protocol.h's PHD_PANEL_NAME_SYNTAX: the descriptor's name under panels/ of descriptor_dir""" - return os.path.join(os.path.dirname(descriptor), "panels", os.path.basename(descriptor)) - - def panel_answered(d, uri, path): return d.send("panel_get %s" % uri) == 'resp 0 "%s"' % path diff --git a/tests/sabotage.py b/tests/sabotage.py index 2ce05e3..8488dac 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -732,6 +732,21 @@ ("the preset schema takes params+state with no state body", "protocol/preset.schema.json", '"then": {"required": ["params", "state"]}', '"then": {"required": ["params"]}', "params+state with the state body dropped is refused", "presetschema"), + # the declared verbs (data/verbs.json) against the daemon on its socket, and the sources against the declaration + ("a declared format is not the grammar the daemon answers", "data/verbs.json", + '"format": "chain_get %i",', '"format": "chain_get %i %i",', "verbs_declared", "verbs"), + ("a declared code is not the one the daemon answers", "data/verbs.json", + '"resp": -3,', '"resp": -101,', "verbs_declared", "verbs"), + ("the handler of a declared verb is deleted", "src/verbs.c", + " if (!strcmp(verb, OMX_VERB_CHAIN_GET))\n {\n int id;\n\n" + " return ntok == 2 && number(tok[1], &id) ? sup_chain_get(id) : sup_resp(ERR_INVALID_OPERATION);\n }\n", + "", "verbs_declared", "verbs"), + ("the daemon answers a verb no declaration holds", "src/verbs.c", + " if (!strcmp(verb, OMX_VERB_WORKER_LIST))\n", + ' if (!strcmp(verb, "verb_sabotage"))\n return sup_resp(SUCCESS);\n' + " if (!strcmp(verb, OMX_VERB_WORKER_LIST))\n", "verbs_declared", "verbs"), + ("a declared verb is typed by hand in the daemon", "src/verbs.c", + "if (!strcmp(verb, OMX_VERB_CHAIN_GET))", 'if (!strcmp(verb, "chain_get"))', "verbs_contract", "verbs"), ] @@ -801,7 +816,23 @@ def run_alloc_test(tree): return p.returncode, p.stdout + p.stderr + ("ok alloc_test\n" if p.returncode == 0 else "") +def run_verbs_test(tree, name): + """tests/verbs_declared.py against the tree's daemon and declaration ("ok verbs_declared"), or the tree's + tests/verbs_contract.py over its sources ("ok verbs_contract")""" + if name == "verbs_contract": + p = subprocess.run([sys.executable, os.path.join(tree, "tests", "verbs_contract.py")], capture_output=True, + text=True, timeout=60, env=dict(os.environ, MOD_HOST_DIR=MOD_HOST_DIR)) + else: + env = dict(os.environ, PLUGIN_HOSTD=os.path.join(tree, "plugin-hostd"), FAKE_HOST=FAKE, + PROBE=os.path.join(tree, "plugin-hostd-probe"), PLUGIN_HOSTD_VERBS=os.path.join(tree, "data", "verbs.json")) + p = subprocess.run([sys.executable, os.path.join(tree, "tests", "verbs_declared.py")], env=env, capture_output=True, + text=True, timeout=300) + return p.returncode, p.stdout + p.stderr + ("ok %s\n" % name if p.returncode == 0 else "") + + def run_test(tree, name, jack=False): + if jack == "verbs": + return run_verbs_test(tree, name) if jack == "consumer": return run_consumer_test(tree) if jack == "alloc": @@ -925,6 +956,13 @@ def run_test(tree, name, jack=False): shutil.copy(os.path.join(ROOT, f), os.path.join(tree, f)) elif jack == "ciguards": shutil.copytree(os.path.join(ROOT, ".github", "workflows"), os.path.join(tree, ".github", "workflows")) + elif jack == "verbs": + # the declaration, what renders and builds from it, and the tests that read it + for d in ("data", "src", "include", "tools", "protocol", "tests", "packaging", "fuzz"): + shutil.copytree(os.path.join(ROOT, d), os.path.join(tree, d), ignore=shutil.ignore_patterns("*.o")) + shutil.copy(os.path.join(ROOT, "Makefile"), tree) + build(tree) + build_probe(tree) else: shutil.copytree(os.path.join(ROOT, "src"), os.path.join(tree, "src")) shutil.copytree(os.path.join(ROOT, "include"), os.path.join(tree, "include")) @@ -941,6 +979,10 @@ def run_test(tree, name, jack=False): failed += 1 continue open(os.path.join(tree, path), "w").write(src.replace(text, repl)) + if path.startswith("data/"): + # a changed declaration is rendered, as make render-verbs does, before the daemon is built from it + subprocess.run(["make", "-s", "MOD_HOST_DIR=" + MOD_HOST_DIR, "render-verbs"], cwd=tree, check=True, + stdout=subprocess.DEVNULL) if jack == "probe": build_probe(tree) elif jack not in ("readme", "nopins", "ciguards", "format", "relay", "contract", "consumer", "probeschema", "presetschema", "vocabulary", "alloc", diff --git a/tests/scan_fixture.py b/tests/scan_fixture.py new file mode 100644 index 0000000..25df8e6 --- /dev/null +++ b/tests/scan_fixture.py @@ -0,0 +1,110 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# What the scan of an add runs over, for the tests that drive a daemon: a CLAP file and an LV2 bundle of the compressor +# on disk, and a scanner that runs the real describe and panel over the recorded field dumps and logs every target it +# is run for. tests/daemon_test.py and tests/verbs_declared.py both read it. +import hashlib, os, shutil, tempfile + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROBE = os.path.abspath(os.environ.get("PROBE", os.path.join(ROOT, "plugin-hostd-probe"))) +FIELDS = os.path.join(ROOT, "tests", "fixtures", "fields") +CLAP_ID = "org.freemixer.plugin-qualify.fixture.controls" +LV2_COMPRESSOR = "urn:example:compressor" +SCANNER = """#!/bin/sh +# a stand-in for the probe: the real describe, over the recorded dump of the target's format, and the real panel over +# the recorded scan of the CLAP fixture; the panel stage logs to $SCAN_LOG.panel +if [ "$1" = panel ]; then + echo "$2" >> "$SCAN_LOG.panel" + if [ -n "$PANEL_EXIT" ]; then exit "$PANEL_EXIT"; fi + exec %(probe)s panel --json-from %(fields)s/clap-panel-scan.json "$2" +fi +echo "$2" >> "$SCAN_LOG" +if [ -n "$SCAN_HOLD" ]; then while [ ! -e "$SCAN_HOLD" ]; do sleep 0.02; done; fi +if [ -n "$SCAN_EXIT" ]; then exit "$SCAN_EXIT"; fi +case "$2" in + clap:*) exec %(probe)s describe --table %(tables)s/clap.json --fields-from %(fields)s/clap-controls.tsv "$2" ;; + *) exec %(probe)s describe --table %(tables)s/lv2.json --fields-from %(fields)s/lv2-compressor.tsv "$2" ;; +esac +""" +MANIFEST = """@prefix lv2: . +@prefix rdfs: . + +<%s> + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . +""" + + +def sha(path): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def write(path, data): + with open(path, "wb") as f: + f.write(data) + + +def safe(text): + """protocol.h's PHD_DESCRIPTOR_NAME_SYNTAX, plugin-qualify's rule: every character outside [A-Za-z0-9._-] as _""" + return "".join(c if c.isascii() and (c.isalnum() or c in "._-") else "_" for c in text) + + +def descriptor_name(root, plugin_id, version, binary): + """@-.json""" + return os.path.join(root, "%s@%s-%s.json" % (safe(plugin_id), safe(version), sha(binary)[:12])) + + +class Scan: + """a CLAP file, an LV2 bundle of the compressor, and a scanner that logs every target it is run for; daemon makes + the daemon under test from its settings and its environment""" + + def __init__(self, daemon): + self.make_daemon = daemon + self.dir = tempfile.mkdtemp(prefix="plugin-hostd-scan.") + self.clap = os.path.join(self.dir, "clap", "controls.clap") + os.makedirs(os.path.dirname(self.clap)) + write(self.clap, b"a clap build") + self.clap_uri = "clap:%s#%s" % (self.clap, CLAP_ID) + self.lv2_dir = os.path.join(self.dir, "lv2") + self.bundle = os.path.join(self.lv2_dir, "compressor.lv2") + os.makedirs(self.bundle) + write(os.path.join(self.bundle, "manifest.ttl"), (MANIFEST % LV2_COMPRESSOR).encode()) + write(os.path.join(self.bundle, "pinned.so"), b"lv2 binary") + write(os.path.join(self.bundle, "pinned.ttl"), b"lv2:port [ lv2:index 0 ] .") + self.scanner = os.path.join(self.dir, "scanner") + write(self.scanner, (SCANNER % {"probe": PROBE, "tables": os.path.join(ROOT, "data", "formats"), + "fields": FIELDS}).encode()) + os.chmod(self.scanner, 0o755) + self.log = os.path.join(self.dir, "scans.log") + self.descriptors = os.path.join(self.dir, "descriptors") + self.qualification = os.path.join(self.dir, "qualification") + + def daemon(self, conf=None, env=None): + c = {"probe": self.scanner, "lv2_path": self.lv2_dir, "descriptor_dir": self.descriptors, + "qualification_dir": self.qualification} + c.update(conf or {}) + e = {"SCAN_LOG": self.log} + e.update(env or {}) + return self.make_daemon(conf=c, env=e) + + def scans(self, stage=""): + if not os.path.exists(self.log + stage): + return [] + with open(self.log + stage) as f: + return [l.strip() for l in f if l.strip()] + + def clap_name(self, root=None): + return descriptor_name(root or self.descriptors, CLAP_ID, "1", self.clap) + + def lv2_name(self): + # the compressor states no version; its binary is the file its manifest names with lv2:binary + return descriptor_name(self.descriptors, LV2_COMPRESSOR, "unversioned", os.path.join(self.bundle, "pinned.so")) + + def close(self): + shutil.rmtree(self.dir, ignore_errors=True) + + +def panel_name(descriptor): + """protocol.h's PHD_PANEL_NAME_SYNTAX: the descriptor's name under panels/ of descriptor_dir""" + return os.path.join(os.path.dirname(descriptor), "panels", os.path.basename(descriptor)) diff --git a/tests/verbs_declared.py b/tests/verbs_declared.py new file mode 100644 index 0000000..9b22f76 --- /dev/null +++ b/tests/verbs_declared.py @@ -0,0 +1,416 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# The verbs plugin-hostd declares (data/verbs.json) and the ones omx-clap-core declares for both hosts, held to the +# daemon on its socket: +# - every verb the declarations give plugin-hostd as required or optional answers a well-formed line, one that its +# declared format takes, with its declared reply (an optional one may refuse with a code it declares), and a +# malformed line with a code it declares; +# - the verbs the daemon answers itself are the declared ones and mod-host's: a word of the binary that the daemon +# answers without relaying it to its worker, and that no declaration and no mod-host command holds, is red; +# - a verb both hosts owe answers the same line in the same shape from the daemon and from a worker that speaks for +# omx-clap-host (tests/fake-host, the worker every test of this tree runs). +# omx-clap-core's declarations are read from its package in the contract directory: the first of OMX_CONTRACT_PATH +# (directories that stand where contractdir stands) that holds 3/omx-clap-core, else pkg-config's contractdir. Without +# one the run says PENDING, takes the verbs protocol.h still names for both hosts in their place and fails nothing for +# them; it names which source it read either way. +# Usage: verbs_declared.py (PLUGIN_HOSTD, FAKE_HOST, PROTOCOL from the environment, or the tree's own). +import json, os, re, socket, subprocess, sys, tempfile, time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import harness +import scan_fixture +from harness import Daemon, Fail, check, wait_for + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +EXE = os.path.abspath(os.environ.get("PLUGIN_HOSTD", os.path.join(ROOT, "plugin-hostd"))) +FAKE = os.path.abspath(os.environ.get("FAKE_HOST", os.path.join(ROOT, "tests", "fake-host"))) +OWN = os.environ.get("PLUGIN_HOSTD_VERBS", os.path.join(ROOT, "data", "verbs.json")) +MAJOR = "3" +HOSTD, CLAP_HOST = "plugin-hostd", "omx-clap-host" +failures = [] + + +def fail(what): + failures.append(what) + print("FAIL " + what) + + +# ---------------------------------------------------------------- the declarations + +def verbs_of(docs): + """{word: {format, reply, codes: [resp], implementedBy}} of every verbs item of docs, each code resolved over the + codes items of docs""" + codes = {} + for doc in docs: + for name, item in doc.items(): + if isinstance(item, dict) and item.get("kind") == "codes": + for key, c in item["codes"].items(): + codes["%s.%s" % (name, key)] = c.get("resp") + out = {} + for doc in docs: + for item in doc.values(): + if isinstance(item, dict) and item.get("kind") == "verbs": + for v in item["verbs"].values(): + out[v["verb"]] = dict(v, codes=[codes[c["ref"]] for c in v.get("codes", [])]) + return out + + +def contract_dirs(): + dirs = [d for d in os.environ.get("OMX_CONTRACT_PATH", "").split(":") if d] + p = subprocess.run([os.environ.get("PKG_CONFIG", "pkg-config"), "--variable=contractdir", "omx-contract"], + capture_output=True, text=True) + if p.returncode == 0 and p.stdout.strip(): + dirs.append(p.stdout.strip()) + return dirs + + +def package(name): + """the files of a package of the contract directory, and where they were read, or (None, None)""" + for d in contract_dirs(): + base = os.path.join(d, MAJOR, name) + manifest = os.path.join(base, "MANIFEST.json") + if os.path.exists(manifest): + with open(manifest) as f: + m = json.load(f) + docs = [] + for rel in m["files"]: + with open(os.path.join(base, rel)) as f: + docs.append(json.load(f)) + return docs, "%s %s from %s" % (name, m["version"], base) + return None, None + + +def owes(verb, host): + return verb["implementedBy"].get(host) in ("required", "optional") + + +# ---------------------------------------------------------------- a reply and its declared shape + +WORD = re.compile(r'"(?:[^"\\]|\\.)*"|\S+') + + +def words(line): + return WORD.findall(line) + + +def token_rx(token): + if "..." in token: + return r"\S+" + if token.startswith('"') and token.endswith('"'): + return r'"(?:[^"\\]|\\.)*"' + parts = re.split(r"(<[^>]+>)", token) + return "".join(r'(?:"(?:[^"\\]|\\.)*"|[^\s"]+?)' if p.startswith("<") else re.escape(p) for p in parts if p) + + +def shaped(reply, declared): + """whether reply is declared's shape: its literal words, one word per placeholder; a count in second place + (resp or resp ) is a whole number and the words after it are that many records""" + got, want = words(reply), declared.split() + if len(got) < 2 or got[0] != want[0]: + return False + if want[1].startswith("<"): + if not re.fullmatch(r"[0-9]+", got[1]): + return False + rest = want[2:] + want = want[:2] + rest * int(got[1]) if rest else want + if len(got) != len(want): + return False + return all(re.fullmatch(token_rx(w), g) for g, w in zip(got[1:], want[1:])) + + +def takes(fmt, line): + """whether the declared format takes the line: the verb, then a word per conversion, %i a whole number, and more + words only where it ends in ...""" + want, got = fmt.split(), words(line) + if got[0] != want[0]: + return False + open_end = want[-1] == "..." + conv = [w for w in want[1:] if w != "..."] + if len(got) - 1 < len(conv) or (not open_end and len(got) - 1 != len(conv)): + return False + return all(c != "%i" or re.fullmatch(r"-?[0-9]+", g) for c, g in zip(conv, got[1:])) + + +def code(reply): + m = re.fullmatch(r"resp (-[0-9]+)", reply) + return int(m.group(1)) if m else None + + +# ---------------------------------------------------------------- the lines + +# For each verb plugin-hostd owes, in the order they run: the arguments of a well-formed line, of a malformed one, and +# of lines the verb refuses by its meaning, each of which must answer a code the verb declares. {uri} is the CLAP +# fixture's uri (an add made 1 and 2 of it, and its scan wrote its descriptor and its panel; their first-use pins are +# recorded), 3 is an LV2 instance on mod-host's row, {layout} the fake's layout pin, {sha} the CLAP file's SHA-256. +# pin_accept takes the first-use pin before pin_set replaces it with one of verdict in-process, which lets the members +# share the chain's worker; chain_get and chain_clear read and forget what chain declared; pin_clear runs last. +LINES = { + "pin_accept": ("{uri}", "", ()), + "pin_set": ("{uri} controls.clap={sha} {layout} verdict=in-process", "{uri}", ()), + "worker_list": ("", "x", ()), + "instance_info": ("1", "1 x", ("99",)), + "supervisor_reset": ("all", "all x", ("w99",)), + "quarantine_clear": ("all", "", ("99",)), + "policy_set": ("clap own", "clap", ("clap pool:",)), + "pool_config": ("p 4", "p x", ()), + "worker_env": ("clap - -", "clap", ()), + "lv2_worker_set": ("urn:example:other auto", "urn:example:other", ()), + "descriptor_get": ("{uri}", "{uri} x", ("clap:/nowhere.clap#x",)), + "panel_get": ("{uri}", "", ("clap:/nowhere.clap#x",)), + "preset_list": ("1", "", ("99", "3")), + "preset_get": ("1 0", "1", ("99 0", "3 0")), + "track_info": ('1 "Lead" #112233', "1", ("99 x -",)), + "remote_pages": ("1", "", ("99",)), + "remote_page_get": ("1 0", "1", ("99 0",)), + "param_info": ("1 0", "1", ("99 x",)), + "note_input": ("1", "", ("99",)), + "note_on": ("1 0 60 100", "1 0 60", ("99 0 60 100",)), + "note_off": ("1 0 60 0", "1 0 60", ("99 0 60 0",)), + "all_notes_off": ("1", "", ("99",)), + "chain": ("7 1 2", "7", ("8 99", "8 1 1")), + "chain_get": ("7", "x", ("9",)), + "chain_clear": ("7", "x", ("7",)), + "pin_clear": ("all", "", ()), +} +ORDER = list(LINES) + + +def line(verb, args, ctx): + return (verb + " " + args.format(**ctx)).strip() + + +def setup(s): + """a daemon in front of the fake worker under pin_mode first-use, with two CLAP instances whose scan is done and + an LV2 one; the first-use pins of the CLAP adds are recorded, so pin_accept takes them""" + lay = subprocess.check_output([FAKE, "-L"], text=True).strip() + d = s.daemon(conf={"require_pins": 1, "pin_mode": "first-use"}) + for n in (1, 2): + d.expect("add %s %d" % (s.clap_uri, n), "resp %d" % n) + d.expect("add lv2:%s 3" % scan_fixture.LV2_COMPRESSOR, "resp 3") + wait_for(lambda: d.send("panel_get %s" % s.clap_uri).startswith("resp 0 "), "the scan's descriptor and panel") + return d, {"uri": s.clap_uri, "layout": lay, "sha": scan_fixture.sha(s.clap)} + + +def check_verbs(declared): + s = scan_fixture.Scan(lambda **kw: Daemon(EXE, FAKE, **kw)) + d = None + try: + d, ctx = setup(s) + owed = [v for v in declared if owes(declared[v], HOSTD)] + missing = [v for v in owed if v not in LINES] + if missing: + fail("no well-formed line for %s, which the declarations give plugin-hostd" % missing) + for verb in sorted((v for v in owed if v in LINES), key=ORDER.index): + decl = declared[verb] + good, bad = (line(verb, a, ctx) for a in LINES[verb][:2]) + if not takes(decl["format"], good): + fail("%s: the well-formed line %r is not what its declared format %r takes" % (verb, good, decl["format"])) + continue + r = d.send(good) + optional = decl["implementedBy"][HOSTD] == "optional" + if shaped(r, decl["reply"]): + print("ok %-16s %-40s -> %s" % (verb, good[:40], r[:60])) + elif optional and code(r) in decl["codes"]: + print("ok %-16s %-40s -> %s (optional: a declared refusal)" % (verb, good[:40], r)) + else: + fail("%s: %r answers %r, not the declared %r%s" % (verb, good, r, decl["reply"], + " or one of %s" % decl["codes"] if optional else "")) + # a format that ends in ... takes any words past its conversions: the grammar past them is the verb's own + if not decl["format"].endswith("...") and takes(decl["format"], bad): + fail("%s: the malformed line %r is one its declared format %r takes" % (verb, bad, decl["format"])) + for kind, refused in [("malformed", bad)] + [("refused", line(verb, a, ctx)) for a in LINES[verb][2]]: + r = d.send(refused) + if code(r) is not None and code(r) in decl["codes"]: + print("ok %-16s %-40s -> %s (declared)" % (verb, refused[:40], r)) + else: + fail("%s: the %s %r answers %r, which is none of its declared codes %s" + % (verb, kind, refused, r, decl["codes"])) + except Fail as e: + fail(str(e)) + finally: + if d: + d.close() + s.close() + + +# ---------------------------------------------------------------- what the daemon answers itself + +def binary_words(exe): + """every lower-case word the binary holds as a C string: each word the daemon compares a verb with is one""" + with open(exe, "rb") as f: + data = f.read() + return sorted({w.decode() for w in re.findall(rb"(?<=\x00)([a-z][a-z0-9_]{2,40})(?=\x00)", data)}) + + +def answered_by_daemon(candidates): + """the words whose line the daemon answers without relaying it: a line of eleven words no verb takes, sent with one + worker up, that never reaches the worker""" + log = tempfile.mktemp(prefix="plugin-hostd-verbs-log.") + s = scan_fixture.Scan(lambda **kw: Daemon(EXE, FAKE, **kw)) + d = s.daemon(env={"FAKE_LOG": log}) + try: + d.expect("add %s 1" % s.clap_uri, "resp 1") + junk = " ~" * 11 + for w in candidates: + d.send(w + junk) + d.send("worker_list") + with open(log) as f: + relayed = {l.split(" ", 2)[1] for l in f if l.endswith(junk + "\n")} + return {w for w in candidates if w not in relayed} + finally: + d.close() + s.close() + if os.path.exists(log): + os.unlink(log) + + +def check_answered(declared): + with open(os.path.join(ROOT, "protocol", "mod-host.json")) as f: + modhost = {c["verb"] for c in json.load(f)["commands"]} + candidates = binary_words(EXE) + ours = {v for v in declared if owes(declared[v], HOSTD)} + unseen = ours - set(candidates) + if unseen: + fail("positive control: the scan of the binary does not see the declared %s" % sorted(unseen)) + answered = answered_by_daemon(candidates) + extra = answered - ours - modhost + if extra: + fail("the daemon answers %s itself, which no declaration and no mod-host command holds" % sorted(extra)) + relayed = ours - answered + if relayed: + fail("declared for plugin-hostd, but the daemon relays them and never answers them: %s" % sorted(relayed)) + if not extra and not relayed: + print("ok the daemon answers %d words of its %d itself: the %d declared for it and %d of mod-host's" + % (len(answered), len(candidates), len(ours), len(answered - ours))) + + +# ---------------------------------------------------------------- the same line to both hosts + +class Worker: + """tests/fake-host on ports of its own, as a daemon starts it; it answers once a controller holds both""" + + def __init__(self): + self.port, fb = harness.port_pair() + self.proc = subprocess.Popen([FAKE, "-p", str(self.port), "-f", str(fb)], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + end = time.monotonic() + 10 + while True: + try: + self.sock = socket.create_connection(("127.0.0.1", self.port), timeout=10) + self.fb = socket.create_connection(("127.0.0.1", fb), timeout=10) + break + except OSError: + if time.monotonic() > end: + raise Fail("the fake worker does not listen on %d and %d" % (self.port, fb)) + time.sleep(0.02) + + def send(self, msg): + self.sock.sendall(msg.encode() + b"\0") + buf = b"" + while not buf.endswith(b"\0"): + c = self.sock.recv(65536) + if not c: + raise Fail("the fake worker closed on " + msg) + buf += c + return buf[:-1].decode() + + def close(self): + self.proc.kill() + self.proc.wait() + + +def check_both(declared): + both = [v for v in declared if owes(declared[v], HOSTD) and owes(declared[v], CLAP_HOST) and v in LINES] + s = scan_fixture.Scan(lambda **kw: Daemon(EXE, FAKE, **kw)) + d = w = None + try: + d, ctx = setup(s) + # the members share a worker under a pin of verdict in-process, as in check_verbs + d.expect(line("pin_set", LINES["pin_set"][0], ctx), "resp 0") + w = Worker() + for n in (1, 2): + w.send("add %s %d" % (s.clap_uri, n)) + for verb in sorted(both, key=ORDER.index): + decl = declared[verb] + good = line(verb, LINES[verb][0], ctx) + a, b = d.send(good), w.send(good) + same = (shaped(a, decl["reply"]) and shaped(b, decl["reply"])) or (code(a) is not None and code(a) == code(b)) + if same: + print("ok %-16s the same shape from both hosts: %s | %s" % (verb, a[:40], b[:40])) + else: + fail("%s: %r answers %r from plugin-hostd and %r from the worker, not one declared shape" % (verb, good, a, b)) + except Fail as e: + fail(str(e)) + finally: + if w: + w.close() + if d: + d.close() + s.close() + + +# ---------------------------------------------------------------- the declarations as protocol.h still names them + +def pending_shared(): + """the verbs protocol.h names for both hosts, the instance verbs and pin_expect, in the shape of a declaration: + what omx-clap-core declares once its package is read""" + with open(os.path.join(ROOT, "protocol", "plugin-hostd.json")) as f: + proto = json.load(f) + out = {} + for v in proto["instance_verbs"]: + out[v["name"]] = {"verb": v["name"], "format": None, "reply": v["reply"], "codes": [], + "implementedBy": {HOSTD: "optional", CLAP_HOST: "optional"}} + return out + + +def main(): + with open(OWN) as f: + own = verbs_of([json.load(f)]) + docs, source = package("omx-clap-core") + if docs: + shared = verbs_of(docs) + print("ok the shared verbs are read from %s" % source) + dup = set(shared) & set(own) + if dup: + fail("declared by plugin-hostd and by omx-clap-core: %s" % sorted(dup)) + declared = dict(shared, **own) + # the words and the formats protocol.h still names for both hosts are omx-clap-core's declarations + with open(os.path.join(ROOT, "include", "plugin-hostd", "protocol.h")) as f: + header = f.read() + word = dict(re.findall(r'^#define\s+(PHD_VERB_\w+)\s+"(\w+)"', header, re.M)) + named = 0 + for macro, verb_macro, rest in re.findall(r'^#define\s+(PHD_VERB_\w+)_FMT\s+(PHD_VERB_\w+) "([^"]*)"', header, re.M): + verb = word[verb_macro] + if verb in shared: + named += 1 + if verb + rest != shared[verb]["format"]: + fail("protocol.h's %s_FMT is %r, omx-clap-core declares %r" % (macro, verb + rest, shared[verb]["format"])) + print("ok protocol.h names %d formats of the shared verbs, as omx-clap-core declares them" % named) + else: + shared = {k: v for k, v in pending_shared().items() if k not in own} + print("PENDING the shared verbs are protocol.h's (%s): no omx-clap-core package in OMX_CONTRACT_PATH or the " + "contract directory" % ", ".join(sorted(shared))) + declared = dict(own) + print("plugin-hostd declares %d verbs, %d of them owed by plugin-hostd" % + (len(own), len([v for v in own if owes(own[v], HOSTD)]))) + + check_verbs(declared) + check_answered(dict(shared, **declared)) + check_both(declared) + + # positive controls of the shape and the format: a reply or a line that is not the declared one is seen + for reply, decl, want in (("resp 0", "resp 0", True), ("resp 1", "resp 0", False), + ('resp 0 "/x"', 'resp 0 ""', True), ("resp 0 /x", 'resp 0 ""', False), + ("resp 2 a:1 b:2", "resp :

", True), ("resp 2 a:1", "resp :

", False), + ("resp 0 w4 1 1,2", "resp 0 ,... ,...", True)): + if shaped(reply, decl) != want: + fail("positive control: %r against %r is %s" % (reply, decl, not want)) + for fmt, l, want in (("chain %i %i ...", "chain 7 1 2", True), ("chain %i %i ...", "chain x 1", False), + ("chain_get %i", "chain_get 7 8", False), ("worker_list", "worker_list", True)): + if takes(fmt, l) != want: + fail("positive control: %r against %r is %s" % (l, fmt, not want)) + + print("verbs declared: %s" % ("FAILED (%d)" % len(failures) if failures else "ok")) + return 1 if failures else 0 + + +sys.exit(main()) From a52d85311d6360ffbf35ccdb684a567ac0c9a950 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:33:52 +0100 Subject: [PATCH 6/9] README: the daemon's verbs are declared in its contract directory The verbs the daemon adds now live in verbs.json of the package plugin-hostd-contract, rendered into plugin_hostd_host_verbs.h, which protocol.h includes; the rest of the protocol stays declared in protocol.h. --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f381228..1211ad6 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,13 @@ Building from source: see [BUILDING.md](BUILDING.md). More about FreeMixer at Date: Mon, 26 Oct 2026 13:54:01 +0100 Subject: [PATCH 7/9] One name per refusal number: the shared ones are omx-clap-core's reply.host.* -3, -508, -510, -511, -513, -514, -517 and -902 are answered by both hosts and declared by omx-clap-core's HOST_REPLY_CODES (omx-clap-core#354). data/verbs.json no longer names them a second time: it requires omx-clap-core >= 0.22.0 and its verbs cite HOST_REPLY_CODES.instanceNonExists, .invalidOperation and .pinAbsent. PLUGIN_HOSTD_REPLY_CODES keeps the 25 numbers of -501 to -531 no other package names. The rendered header follows (same resp numbers, reply.host.* names), and plugin_hostd_vocabulary.h drops the eight duplicate strings. tests/verbs_declared.py resolves a verb's codes over both packages, fails when omx-clap-core's package is unreadable (the declaration requires it), and holds one name per number: no resp named by two codes, and every PHD_ERR_ number of protocol.h named by one of the two packages (36 numbers, 31 of protocol.h's, 6 of those omx-clap-core's). The -3 sabotage arm moves to -522; a new arm names -902 again in plugin-hostd and goes red on that check. Validated against omx-clap-core#354 at a1fcd5e installed as 0.22.0 in a scratch directory named by OMX_CONTRACT_PATH: omx-contract 3.3.0 validate, 146 items in 111 files from 3 packages, valid; render --check byte-identical; links --check 0 problems. --- data/verbs.json | 91 ++++---------- .../plugin-hostd/plugin_hostd_host_verbs.h | 36 +++--- .../plugin-hostd/plugin_hostd_vocabulary.h | 8 -- include/plugin-hostd/protocol.h | 5 +- tests/sabotage.py | 7 +- tests/verbs_declared.py | 111 ++++++++++-------- 6 files changed, 118 insertions(+), 140 deletions(-) diff --git a/data/verbs.json b/data/verbs.json index 00e3ef7..40160ac 100644 --- a/data/verbs.json +++ b/data/verbs.json @@ -1,23 +1,14 @@ { "$schema": "../schema/omx-contract.schema.json", "requires": { - "omx-contract-schema": ">= 3.3.0" + "omx-contract-schema": ">= 3.3.0", + "omx-clap-core": ">= 0.22.0" }, "PLUGIN_HOSTD_REPLY_CODES": { "kind": "codes", "since": "0.12.0", - "doc": "The refusals plugin-hostd answers by number (include/plugin-hostd/protocol.h PHD_ERRORS), and the two of mod-host's numbers it answers for its own verbs.", + "doc": "The refusals only plugin-hostd answers by number (include/plugin-hostd/protocol.h PHD_ERRORS). The numbers it answers in common with omx-clap-host are omx-clap-core's HOST_REPLY_CODES.", "codes": { - "invalidOperation": { - "code": "reply.plugin-hostd.invalid-operation", - "resp": -902, - "doc": "mod-host's ERR_INVALID_OPERATION, answered by the daemon for a line outside the grammar of one of its own verbs" - }, - "instanceUnknown": { - "code": "reply.plugin-hostd.instance-unknown", - "resp": -3, - "doc": "mod-host's ERR_INSTANCE_NON_EXISTS, answered by the daemon for an instance no add made" - }, "placementInvalid": { "code": "reply.plugin-hostd.placement-invalid", "resp": -501, @@ -53,41 +44,16 @@ "resp": -507, "doc": "PHD_ERR_VERB_DROPPED: the worker died on this very command and the daemon dropped it" }, - "pinAbsent": { - "code": "reply.plugin-hostd.pin-absent", - "resp": -508, - "doc": "PHD_ERR_PIN_ABSENT: the plugin has no pin, or its layout pin is in a scheme this version does not know" - }, "pinBinaryMismatch": { "code": "reply.plugin-hostd.pin-binary-mismatch", "resp": -509, "doc": "PHD_ERR_PIN_BINARY_MISMATCH: a pinned file is missing or its SHA-256 differs" }, - "pinLayoutMismatch": { - "code": "reply.plugin-hostd.pin-layout-mismatch", - "resp": -510, - "doc": "PHD_ERR_PIN_LAYOUT_MISMATCH: the parameter layout after init differs from the layout pin" - }, - "noParamContract": { - "code": "reply.plugin-hostd.no-param-contract", - "resp": -511, - "doc": "PHD_ERR_NO_PARAM_CONTRACT: the host holds no checked unit and scale for this parameter" - }, "lv2AdapterRefused": { "code": "reply.plugin-hostd.lv2-adapter-refused", "resp": -512, "doc": "PHD_ERR_LV2_ADAPTER_REFUSED: the lv2-clap worker's LV2 adapter cannot run this plugin" }, - "noNoteInput": { - "code": "reply.plugin-hostd.no-note-input", - "resp": -513, - "doc": "PHD_ERR_NO_NOTE_INPUT: the instance's plugin takes no notes" - }, - "notesByJack": { - "code": "reply.plugin-hostd.notes-by-jack", - "resp": -514, - "doc": "PHD_ERR_NOTES_BY_JACK: the instance's worker takes no notes on its socket" - }, "noPresetList": { "code": "reply.plugin-hostd.no-preset-list", "resp": -515, @@ -98,11 +64,6 @@ "resp": -516, "doc": "PHD_ERR_PIN_REFUSED: the plugin's pin carries verdict=refused" }, - "noteQueueFull": { - "code": "reply.plugin-hostd.note-queue-full", - "resp": -517, - "doc": "PHD_ERR_NOTE_QUEUE_FULL: the instance's worker has no room for this note before its next block" - }, "chainUnknownInstance": { "code": "reply.plugin-hostd.chain-unknown-instance", "resp": -518, @@ -186,7 +147,7 @@ "reply": "resp :::::,,...", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -200,10 +161,10 @@ "reply": "resp 0 ", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + "ref": "HOST_REPLY_CODES.instanceNonExists" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -220,7 +181,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchWorker" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -234,10 +195,10 @@ "reply": "resp 0", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + "ref": "HOST_REPLY_CODES.instanceNonExists" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -254,7 +215,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -271,7 +232,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -285,7 +246,7 @@ "reply": "resp 0", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -302,7 +263,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -316,7 +277,7 @@ "reply": "resp 0", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -330,7 +291,7 @@ "reply": "resp 0", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.pinAbsent" + "ref": "HOST_REPLY_CODES.pinAbsent" }, { "ref": "PLUGIN_HOSTD_REPLY_CODES.pinBinaryMismatch" @@ -339,7 +300,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -363,7 +324,7 @@ "reply": "resp 0", "codes": [ { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -392,7 +353,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.placementInvalid" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -411,7 +372,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchChain" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -428,7 +389,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.noSuchChain" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -454,7 +415,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorScanFailed" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -480,7 +441,7 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.descriptorScanFailed" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -497,10 +458,10 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.noPresetList" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + "ref": "HOST_REPLY_CODES.instanceNonExists" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { @@ -519,10 +480,10 @@ "ref": "PLUGIN_HOSTD_REPLY_CODES.noPresetList" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.instanceUnknown" + "ref": "HOST_REPLY_CODES.instanceNonExists" }, { - "ref": "PLUGIN_HOSTD_REPLY_CODES.invalidOperation" + "ref": "HOST_REPLY_CODES.invalidOperation" } ], "implementedBy": { diff --git a/include/plugin-hostd/plugin_hostd_host_verbs.h b/include/plugin-hostd/plugin_hostd_host_verbs.h index 6ba19ea..99a017f 100644 --- a/include/plugin-hostd/plugin_hostd_host_verbs.h +++ b/include/plugin-hostd/plugin_hostd_host_verbs.h @@ -40,70 +40,70 @@ struct omx_host_verb { #define OMX_VERB_WORKER_LIST_FMT "worker_list" #define OMX_VERB_WORKER_LIST_REPLY "resp :::::,,..." #define OMX_VERB_WORKER_LIST_CODES_COUNT 1u -#define OMX_VERB_WORKER_LIST_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_WORKER_LIST_CODES_INIT { -902 } /* reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.instanceInfo (data/verbs.json): Where an instance lives and what it has cost. */ #define OMX_VERB_INSTANCE_INFO "instance_info" #define OMX_VERB_INSTANCE_INFO_FMT "instance_info %i" #define OMX_VERB_INSTANCE_INFO_REPLY "resp 0 " #define OMX_VERB_INSTANCE_INFO_CODES_COUNT 2u -#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -3, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_INSTANCE_INFO_CODES_INIT { -3, -902 } /* reply.host.instance-non-exists, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.supervisorReset (data/verbs.json): Re-arm the storm bound and the backoff of a given-up placement: [ | all]. */ #define OMX_VERB_SUPERVISOR_RESET "supervisor_reset" #define OMX_VERB_SUPERVISOR_RESET_FMT "supervisor_reset ..." #define OMX_VERB_SUPERVISOR_RESET_REPLY "resp 0" #define OMX_VERB_SUPERVISOR_RESET_CODES_COUNT 2u -#define OMX_VERB_SUPERVISOR_RESET_CODES_INIT { -506, -902 } /* reply.plugin-hostd.no-such-worker, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_SUPERVISOR_RESET_CODES_INIT { -506, -902 } /* reply.plugin-hostd.no-such-worker, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.quarantineClear (data/verbs.json): Let a quarantined instance go back where its placement says: | all. */ #define OMX_VERB_QUARANTINE_CLEAR "quarantine_clear" #define OMX_VERB_QUARANTINE_CLEAR_FMT "quarantine_clear %s" #define OMX_VERB_QUARANTINE_CLEAR_REPLY "resp 0" #define OMX_VERB_QUARANTINE_CLEAR_CODES_COUNT 2u -#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -3, -902 } /* reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_QUARANTINE_CLEAR_CODES_INIT { -3, -902 } /* reply.host.instance-non-exists, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.policySet (data/verbs.json): The placement of an add that says default: own | pool:. */ #define OMX_VERB_POLICY_SET "policy_set" #define OMX_VERB_POLICY_SET_FMT "policy_set %s %s" #define OMX_VERB_POLICY_SET_REPLY "resp 0" #define OMX_VERB_POLICY_SET_CODES_COUNT 2u -#define OMX_VERB_POLICY_SET_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_POLICY_SET_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.poolConfig (data/verbs.json): The instances a pool holds before a sibling opens: . */ #define OMX_VERB_POOL_CONFIG "pool_config" #define OMX_VERB_POOL_CONFIG_FMT "pool_config %s %i" #define OMX_VERB_POOL_CONFIG_REPLY "resp 0" #define OMX_VERB_POOL_CONFIG_CODES_COUNT 2u -#define OMX_VERB_POOL_CONFIG_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_POOL_CONFIG_CODES_INIT { -501, -902 } /* reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.workerEnv (data/verbs.json): The cpu list and the nice value of the workers of a format, at spawn and on the ones running: . */ #define OMX_VERB_WORKER_ENV "worker_env" #define OMX_VERB_WORKER_ENV_FMT "worker_env %s %s %s" #define OMX_VERB_WORKER_ENV_REPLY "resp 0" #define OMX_VERB_WORKER_ENV_CODES_COUNT 1u -#define OMX_VERB_WORKER_ENV_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_WORKER_ENV_CODES_INIT { -902 } /* reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinSet (data/verbs.json): The pin of one plugin: =[,=...] : [verdict=]. */ #define OMX_VERB_PIN_SET "pin_set" #define OMX_VERB_PIN_SET_FMT "pin_set %s %s %s ..." #define OMX_VERB_PIN_SET_REPLY "resp 0" #define OMX_VERB_PIN_SET_CODES_COUNT 2u -#define OMX_VERB_PIN_SET_CODES_INIT { -516, -902 } /* reply.plugin-hostd.pin-refused, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PIN_SET_CODES_INIT { -516, -902 } /* reply.plugin-hostd.pin-refused, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinClear (data/verbs.json): Forget the pin of one plugin, or of every one: | all. */ #define OMX_VERB_PIN_CLEAR "pin_clear" #define OMX_VERB_PIN_CLEAR_FMT "pin_clear %s" #define OMX_VERB_PIN_CLEAR_REPLY "resp 0" #define OMX_VERB_PIN_CLEAR_CODES_COUNT 1u -#define OMX_VERB_PIN_CLEAR_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PIN_CLEAR_CODES_INIT { -902 } /* reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinAccept (data/verbs.json): Under pin_mode first-use, take the plugin's files as they are now as its pin: . */ #define OMX_VERB_PIN_ACCEPT "pin_accept" #define OMX_VERB_PIN_ACCEPT_FMT "pin_accept %s" #define OMX_VERB_PIN_ACCEPT_REPLY "resp 0" #define OMX_VERB_PIN_ACCEPT_CODES_COUNT 4u -#define OMX_VERB_PIN_ACCEPT_CODES_INIT { -508, -509, -516, -902 } /* reply.plugin-hostd.pin-absent, reply.plugin-hostd.pin-binary-mismatch, reply.plugin-hostd.pin-refused, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PIN_ACCEPT_CODES_INIT { -508, -509, -516, -902 } /* reply.host.pin-absent, reply.plugin-hostd.pin-binary-mismatch, reply.plugin-hostd.pin-refused, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.pinLayout (data/verbs.json): Sent by the daemon to a worker that checks layouts, after it accepted an add whose layout pin is unknown: the layout the worker found after init. */ #define OMX_VERB_PIN_LAYOUT "pin_layout" @@ -115,56 +115,56 @@ struct omx_host_verb { #define OMX_VERB_LV2_WORKER_SET_FMT "lv2_worker_set %s %s" #define OMX_VERB_LV2_WORKER_SET_REPLY "resp 0" #define OMX_VERB_LV2_WORKER_SET_CODES_COUNT 1u -#define OMX_VERB_LV2_WORKER_SET_CODES_INIT { -902 } /* reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_LV2_WORKER_SET_CODES_INIT { -902 } /* reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.chain (data/verbs.json): The instances, in this order, are one chain run in one worker: [ ...]; a worker that runs chains in one client is sent the line with the members it holds. */ #define OMX_VERB_CHAIN "chain" #define OMX_VERB_CHAIN_FMT "chain %i %i ..." #define OMX_VERB_CHAIN_REPLY "resp 0" #define OMX_VERB_CHAIN_CODES_COUNT 6u -#define OMX_VERB_CHAIN_CODES_INIT { -518, -519, -520, -521, -501, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.chain-mixed-format, reply.plugin-hostd.placement-invalid, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_CHAIN_CODES_INIT { -518, -519, -520, -521, -501, -902 } /* reply.plugin-hostd.chain-unknown-instance, reply.plugin-hostd.chain-duplicate, reply.plugin-hostd.chain-instance-busy, reply.plugin-hostd.chain-mixed-format, reply.plugin-hostd.placement-invalid, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.chainClear (data/verbs.json): Forget the chain: each member goes to a worker of its own. */ #define OMX_VERB_CHAIN_CLEAR "chain_clear" #define OMX_VERB_CHAIN_CLEAR_FMT "chain_clear %i" #define OMX_VERB_CHAIN_CLEAR_REPLY "resp 0" #define OMX_VERB_CHAIN_CLEAR_CODES_COUNT 2u -#define OMX_VERB_CHAIN_CLEAR_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_CHAIN_CLEAR_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.chainGet (data/verbs.json): Read a chain back: its worker, its members in the declared order, and the members its worker runs now. */ #define OMX_VERB_CHAIN_GET "chain_get" #define OMX_VERB_CHAIN_GET_FMT "chain_get %i" #define OMX_VERB_CHAIN_GET_REPLY "resp 0 ,... ,..." #define OMX_VERB_CHAIN_GET_CODES_COUNT 2u -#define OMX_VERB_CHAIN_GET_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_CHAIN_GET_CODES_INIT { -522, -902 } /* reply.plugin-hostd.no-such-chain, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.descriptorGet (data/verbs.json): The scanned descriptor of the plugin, answered from descriptor_dir: . */ #define OMX_VERB_DESCRIPTOR_GET "descriptor_get" #define OMX_VERB_DESCRIPTOR_GET_FMT "descriptor_get %s" #define OMX_VERB_DESCRIPTOR_GET_REPLY "resp 0 \"\"" #define OMX_VERB_DESCRIPTOR_GET_CODES_COUNT 5u -#define OMX_VERB_DESCRIPTOR_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_DESCRIPTOR_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.panelGet (data/verbs.json): The panel the plugin publishes, answered from descriptor_dir: . */ #define OMX_VERB_PANEL_GET "panel_get" #define OMX_VERB_PANEL_GET_FMT "panel_get %s" #define OMX_VERB_PANEL_GET_REPLY "resp 0 \"\"" #define OMX_VERB_PANEL_GET_CODES_COUNT 5u -#define OMX_VERB_PANEL_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PANEL_GET_CODES_INIT { -528, -529, -530, -531, -902 } /* reply.plugin-hostd.dir-not-writable, reply.plugin-hostd.descriptor-absent, reply.plugin-hostd.descriptor-pending, reply.plugin-hostd.descriptor-scan-failed, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.presetList (data/verbs.json): The plugin's presets, the ones its preset discovery or its bundle lists. */ #define OMX_VERB_PRESET_LIST "preset_list" #define OMX_VERB_PRESET_LIST_FMT "preset_list %i" #define OMX_VERB_PRESET_LIST_REPLY "resp " #define OMX_VERB_PRESET_LIST_CODES_COUNT 3u -#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PRESET_LIST_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.host.instance-non-exists, reply.host.invalid-operation */ /* PLUGIN_HOSTD_VERBS.presetGet (data/verbs.json): One preset, 0 to count - 1: the key preset_load takes for it, and its name. */ #define OMX_VERB_PRESET_GET "preset_get" #define OMX_VERB_PRESET_GET_FMT "preset_get %i %i" #define OMX_VERB_PRESET_GET_REPLY "resp 0 " #define OMX_VERB_PRESET_GET_CODES_COUNT 3u -#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.plugin-hostd.instance-unknown, reply.plugin-hostd.invalid-operation */ +#define OMX_VERB_PRESET_GET_CODES_INIT { -515, -3, -902 } /* reply.plugin-hostd.no-preset-list, reply.host.instance-non-exists, reply.host.invalid-operation */ #define PLUGIN_HOSTD_HOST_VERB_COUNT 19u diff --git a/include/plugin-hostd/plugin_hostd_vocabulary.h b/include/plugin-hostd/plugin_hostd_vocabulary.h index c31d8c1..58525a4 100644 --- a/include/plugin-hostd/plugin_hostd_vocabulary.h +++ b/include/plugin-hostd/plugin_hostd_vocabulary.h @@ -11,8 +11,6 @@ */ /* PLUGIN_HOSTD_REPLY_CODES (data/verbs.json) */ -#define OMX_REPLY_PLUGIN_HOSTD_INVALID_OPERATION "reply.plugin-hostd.invalid-operation" ///< mod-host's ERR_INVALID_OPERATION, answered by the daemon for a line outside the grammar of one of its own verbs -#define OMX_REPLY_PLUGIN_HOSTD_INSTANCE_UNKNOWN "reply.plugin-hostd.instance-unknown" ///< mod-host's ERR_INSTANCE_NON_EXISTS, answered by the daemon for an instance no add made #define OMX_REPLY_PLUGIN_HOSTD_PLACEMENT_INVALID "reply.plugin-hostd.placement-invalid" ///< PHD_ERR_PLACEMENT_INVALID: not a placement, or not valid here #define OMX_REPLY_PLUGIN_HOSTD_NO_BACKEND "reply.plugin-hostd.no-backend" ///< PHD_ERR_NO_BACKEND: no worker program for the scheme #define OMX_REPLY_PLUGIN_HOSTD_WORKER_SPAWN "reply.plugin-hostd.worker-spawn" ///< PHD_ERR_WORKER_SPAWN: the worker is not up @@ -20,16 +18,10 @@ #define OMX_REPLY_PLUGIN_HOSTD_GAVE_UP "reply.plugin-hostd.gave-up" ///< PHD_ERR_GAVE_UP: the storm bound is spent for that placement #define OMX_REPLY_PLUGIN_HOSTD_NO_SUCH_WORKER "reply.plugin-hostd.no-such-worker" ///< PHD_ERR_NO_SUCH_WORKER: no such worker #define OMX_REPLY_PLUGIN_HOSTD_VERB_DROPPED "reply.plugin-hostd.verb-dropped" ///< PHD_ERR_VERB_DROPPED: the worker died on this very command and the daemon dropped it -#define OMX_REPLY_PLUGIN_HOSTD_PIN_ABSENT "reply.plugin-hostd.pin-absent" ///< PHD_ERR_PIN_ABSENT: the plugin has no pin, or its layout pin is in a scheme this version does not know #define OMX_REPLY_PLUGIN_HOSTD_PIN_BINARY_MISMATCH "reply.plugin-hostd.pin-binary-mismatch" ///< PHD_ERR_PIN_BINARY_MISMATCH: a pinned file is missing or its SHA-256 differs -#define OMX_REPLY_PLUGIN_HOSTD_PIN_LAYOUT_MISMATCH "reply.plugin-hostd.pin-layout-mismatch" ///< PHD_ERR_PIN_LAYOUT_MISMATCH: the parameter layout after init differs from the layout pin -#define OMX_REPLY_PLUGIN_HOSTD_NO_PARAM_CONTRACT "reply.plugin-hostd.no-param-contract" ///< PHD_ERR_NO_PARAM_CONTRACT: the host holds no checked unit and scale for this parameter #define OMX_REPLY_PLUGIN_HOSTD_LV2_ADAPTER_REFUSED "reply.plugin-hostd.lv2-adapter-refused" ///< PHD_ERR_LV2_ADAPTER_REFUSED: the lv2-clap worker's LV2 adapter cannot run this plugin -#define OMX_REPLY_PLUGIN_HOSTD_NO_NOTE_INPUT "reply.plugin-hostd.no-note-input" ///< PHD_ERR_NO_NOTE_INPUT: the instance's plugin takes no notes -#define OMX_REPLY_PLUGIN_HOSTD_NOTES_BY_JACK "reply.plugin-hostd.notes-by-jack" ///< PHD_ERR_NOTES_BY_JACK: the instance's worker takes no notes on its socket #define OMX_REPLY_PLUGIN_HOSTD_NO_PRESET_LIST "reply.plugin-hostd.no-preset-list" ///< PHD_ERR_NO_PRESET_LIST: the instance's worker cannot list the plugin's presets #define OMX_REPLY_PLUGIN_HOSTD_PIN_REFUSED "reply.plugin-hostd.pin-refused" ///< PHD_ERR_PIN_REFUSED: the plugin's pin carries verdict=refused -#define OMX_REPLY_PLUGIN_HOSTD_NOTE_QUEUE_FULL "reply.plugin-hostd.note-queue-full" ///< PHD_ERR_NOTE_QUEUE_FULL: the instance's worker has no room for this note before its next block #define OMX_REPLY_PLUGIN_HOSTD_CHAIN_UNKNOWN_INSTANCE "reply.plugin-hostd.chain-unknown-instance" ///< PHD_ERR_CHAIN_UNKNOWN_INSTANCE: a member is an instance no add made #define OMX_REPLY_PLUGIN_HOSTD_CHAIN_DUPLICATE "reply.plugin-hostd.chain-duplicate" ///< PHD_ERR_CHAIN_DUPLICATE: an instance is named twice in one chain #define OMX_REPLY_PLUGIN_HOSTD_CHAIN_INSTANCE_BUSY "reply.plugin-hostd.chain-instance-busy" ///< PHD_ERR_CHAIN_INSTANCE_BUSY: a member is already in another chain diff --git a/include/plugin-hostd/protocol.h b/include/plugin-hostd/protocol.h index 178b107..9f4fc1b 100644 --- a/include/plugin-hostd/protocol.h +++ b/include/plugin-hostd/protocol.h @@ -259,7 +259,10 @@ static inline int phd_pool_name_valid(const char *name) /* ---------------------------------------------------------------- error codes */ -/* the codes the supervisor adds to mod-host's, the -5xx hundred; a reply is "resp " */ +/* the codes the supervisor adds to mod-host's, the -5xx hundred; a reply is "resp ". Each number has one name in + * the contract: the ones omx-clap-host answers too are omx-clap-core's reply.host.* codes (its HOST_REPLY_CODES), the + * others plugin-hostd's reply.plugin-hostd.* (data/verbs.json PLUGIN_HOSTD_REPLY_CODES); tests/verbs_declared.py + * holds every number below to exactly one of them */ #define PHD_ERR_PLACEMENT_INVALID (-501) #define PHD_ERR_NO_BACKEND (-502) #define PHD_ERR_WORKER_SPAWN (-503) diff --git a/tests/sabotage.py b/tests/sabotage.py index 8488dac..4ec9863 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -736,7 +736,12 @@ ("a declared format is not the grammar the daemon answers", "data/verbs.json", '"format": "chain_get %i",', '"format": "chain_get %i %i",', "verbs_declared", "verbs"), ("a declared code is not the one the daemon answers", "data/verbs.json", - '"resp": -3,', '"resp": -101,', "verbs_declared", "verbs"), + '"resp": -522,', '"resp": -101,', "verbs_declared", "verbs"), + ("a number omx-clap-core names is named again by plugin-hostd", "data/verbs.json", + ' "codes": {\n "placementInvalid": {', + ' "codes": {\n "invalidOperation": {\n "code": "reply.plugin-hostd.invalid-operation",\n' + ' "resp": -902,\n "doc": "a second name for -902"\n },\n "placementInvalid": {', + "verbs_declared", "verbs"), ("the handler of a declared verb is deleted", "src/verbs.c", " if (!strcmp(verb, OMX_VERB_CHAIN_GET))\n {\n int id;\n\n" " return ntok == 2 && number(tok[1], &id) ? sup_chain_get(id) : sup_resp(ERR_INVALID_OPERATION);\n }\n", diff --git a/tests/verbs_declared.py b/tests/verbs_declared.py index 9b22f76..a9fa726 100644 --- a/tests/verbs_declared.py +++ b/tests/verbs_declared.py @@ -7,11 +7,12 @@ # - the verbs the daemon answers itself are the declared ones and mod-host's: a word of the binary that the daemon # answers without relaying it to its worker, and that no declaration and no mod-host command holds, is red; # - a verb both hosts owe answers the same line in the same shape from the daemon and from a worker that speaks for -# omx-clap-host (tests/fake-host, the worker every test of this tree runs). +# omx-clap-host (tests/fake-host, the worker every test of this tree runs); +# - one name per number: no refusal number is named by two codes, and each number protocol.h answers is named by a +# code of plugin-hostd or of omx-clap-core, whose HOST_REPLY_CODES name the numbers both hosts answer. # omx-clap-core's declarations are read from its package in the contract directory: the first of OMX_CONTRACT_PATH # (directories that stand where contractdir stands) that holds 3/omx-clap-core, else pkg-config's contractdir. Without -# one the run says PENDING, takes the verbs protocol.h still names for both hosts in their place and fails nothing for -# them; it names which source it read either way. +# one the run fails: data/verbs.json requires it. The run names which source it read. # Usage: verbs_declared.py (PLUGIN_HOSTD, FAKE_HOST, PROTOCOL from the environment, or the tree's own). import json, os, re, socket, subprocess, sys, tempfile, time @@ -36,24 +37,54 @@ def fail(what): # ---------------------------------------------------------------- the declarations -def verbs_of(docs): - """{word: {format, reply, codes: [resp], implementedBy}} of every verbs item of docs, each code resolved over the - codes items of docs""" +def codes_of(docs): + """{ITEM.name: (code, resp)} of every codes item of docs""" codes = {} for doc in docs: for name, item in doc.items(): if isinstance(item, dict) and item.get("kind") == "codes": for key, c in item["codes"].items(): - codes["%s.%s" % (name, key)] = c.get("resp") + codes["%s.%s" % (name, key)] = (c["code"], c.get("resp")) + return codes + + +def verbs_of(docs, refs): + """{word: {format, reply, codes: [resp], implementedBy}} of every verbs item of docs, each code resolved over the + codes items of refs""" + codes = codes_of(refs) out = {} for doc in docs: for item in doc.values(): if isinstance(item, dict) and item.get("kind") == "verbs": for v in item["verbs"].values(): - out[v["verb"]] = dict(v, codes=[codes[c["ref"]] for c in v.get("codes", [])]) + out[v["verb"]] = dict(v, codes=[codes[c["ref"]][1] for c in v.get("codes", [])]) return out +def check_one_name(own_docs, shared_docs): + """one name per number: no resp is named by two codes, of one package or of both, and each number protocol.h's + PHD_ERRORS answers is named by one code of the two packages; the lower package, omx-clap-core, owns the numbers + both hosts answer and plugin-hostd names only its own""" + named = {} + for pkg, docs in (("plugin-hostd", own_docs), ("omx-clap-core", shared_docs)): + for ref, (code, resp) in codes_of(docs).items(): + if resp is not None: + named.setdefault(resp, []).append("%s %s (%s)" % (pkg, code, ref)) + for resp, names in sorted(named.items()): + if len(names) > 1: + fail("%d has %d names: %s" % (resp, len(names), ", ".join(names))) + with open(os.path.join(ROOT, "include", "plugin-hostd", "protocol.h")) as f: + header = f.read() + errors = {int(n) for n in re.findall(r"^#define\s+PHD_ERR_\w+\s+\((-[0-9]+)\)", header, re.M)} + if len(errors) < 20: + fail("found only %d PHD_ERR_ numbers in protocol.h" % len(errors)) + unnamed = sorted(errors - set(named)) + if unnamed: + fail("protocol.h answers %s, which no code of plugin-hostd or omx-clap-core names" % unnamed) + print("ok one name per number: %d numbers, %d of protocol.h's, %d of them named by omx-clap-core" + % (len(named), len(errors), len([r for r in errors if any(n.startswith("omx-clap-core") for n in named.get(r, []))]))) + + def contract_dirs(): dirs = [d for d in os.environ.get("OMX_CONTRACT_PATH", "").split(":") if d] p = subprocess.run([os.environ.get("PKG_CONFIG", "pkg-config"), "--variable=contractdir", "omx-contract"], @@ -348,48 +379,34 @@ def check_both(declared): s.close() -# ---------------------------------------------------------------- the declarations as protocol.h still names them - -def pending_shared(): - """the verbs protocol.h names for both hosts, the instance verbs and pin_expect, in the shape of a declaration: - what omx-clap-core declares once its package is read""" - with open(os.path.join(ROOT, "protocol", "plugin-hostd.json")) as f: - proto = json.load(f) - out = {} - for v in proto["instance_verbs"]: - out[v["name"]] = {"verb": v["name"], "format": None, "reply": v["reply"], "codes": [], - "implementedBy": {HOSTD: "optional", CLAP_HOST: "optional"}} - return out - - def main(): with open(OWN) as f: - own = verbs_of([json.load(f)]) + own_docs = [json.load(f)] docs, source = package("omx-clap-core") - if docs: - shared = verbs_of(docs) - print("ok the shared verbs are read from %s" % source) - dup = set(shared) & set(own) - if dup: - fail("declared by plugin-hostd and by omx-clap-core: %s" % sorted(dup)) - declared = dict(shared, **own) - # the words and the formats protocol.h still names for both hosts are omx-clap-core's declarations - with open(os.path.join(ROOT, "include", "plugin-hostd", "protocol.h")) as f: - header = f.read() - word = dict(re.findall(r'^#define\s+(PHD_VERB_\w+)\s+"(\w+)"', header, re.M)) - named = 0 - for macro, verb_macro, rest in re.findall(r'^#define\s+(PHD_VERB_\w+)_FMT\s+(PHD_VERB_\w+) "([^"]*)"', header, re.M): - verb = word[verb_macro] - if verb in shared: - named += 1 - if verb + rest != shared[verb]["format"]: - fail("protocol.h's %s_FMT is %r, omx-clap-core declares %r" % (macro, verb + rest, shared[verb]["format"])) - print("ok protocol.h names %d formats of the shared verbs, as omx-clap-core declares them" % named) - else: - shared = {k: v for k, v in pending_shared().items() if k not in own} - print("PENDING the shared verbs are protocol.h's (%s): no omx-clap-core package in OMX_CONTRACT_PATH or the " - "contract directory" % ", ".join(sorted(shared))) - declared = dict(own) + if not docs: + fail("no omx-clap-core package in OMX_CONTRACT_PATH or the contract directory: data/verbs.json requires it " + "(%s), its verbs refuse with its HOST_REPLY_CODES" % own_docs[0]["requires"].get("omx-clap-core")) + return 1 + own = verbs_of(own_docs, own_docs + docs) + shared = verbs_of(docs, docs) + print("ok the shared verbs and codes are read from %s" % source) + dup = set(shared) & set(own) + if dup: + fail("declared by plugin-hostd and by omx-clap-core: %s" % sorted(dup)) + declared = dict(shared, **own) + check_one_name(own_docs, docs) + # the words and the formats protocol.h still names for both hosts are omx-clap-core's declarations + with open(os.path.join(ROOT, "include", "plugin-hostd", "protocol.h")) as f: + header = f.read() + word = dict(re.findall(r'^#define\s+(PHD_VERB_\w+)\s+"(\w+)"', header, re.M)) + named = 0 + for macro, verb_macro, rest in re.findall(r'^#define\s+(PHD_VERB_\w+)_FMT\s+(PHD_VERB_\w+) "([^"]*)"', header, re.M): + verb = word[verb_macro] + if verb in shared: + named += 1 + if verb + rest != shared[verb]["format"]: + fail("protocol.h's %s_FMT is %r, omx-clap-core declares %r" % (macro, verb + rest, shared[verb]["format"])) + print("ok protocol.h names %d formats of the shared verbs, as omx-clap-core declares them" % named) print("plugin-hostd declares %d verbs, %d of them owed by plugin-hostd" % (len(own), len([v for v in own if owes(own[v], HOSTD)]))) From 7db33d3ff5b4580ac01a7621b3231eb794a26965 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 13:54:04 +0100 Subject: [PATCH 8/9] plugin-hostd-contract depends on omx-clap-core-contract verbs.json refers to omx-clap-core's reply.host.* codes, so a reader of plugin-hostd's package reads omx-clap-core's too: plugin-hostd-contract requires omx-clap-core-contract >= 0.22.0 (deb: Depends), and the check stage build-requires it for check-rendered and test-verbs-declared (%if %{with check}, ; check-no-pins stays green). BUILDING.md and the README say which package names which number. --- BUILDING.md | 6 +++++- README.md | 4 +++- debian/control | 3 ++- packaging/plugin-hostd.spec | 5 +++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 8f933b3..a08a0e9 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -53,7 +53,11 @@ The verbs the daemon adds to mod-host's protocol, and the codes they answer, are `verbs` item of omx-contract's contract directory (schema 3.3.0 or later). `omx-contract render` writes them into `include/plugin-hostd/plugin_hostd_host_verbs.h` and `plugin_hostd_vocabulary.h`, which are committed and which `protocol.h` includes: `make render-verbs` rewrites them and `make check-rendered` fails on any drift. Both run the -`omx-contract` command (package `omx-contract`); the build itself does not. +`omx-contract` command (package `omx-contract`); the build itself does not. A refusal number omx-clap-host answers +too (mod-host's `-3` and `-902`, and the pin, parameter and note numbers) is named once, by omx-clap-core's +`reply.host.*` codes: `data/verbs.json` requires `omx-clap-core >= 0.22.0`, so both commands and +`make test-verbs-declared` read omx-clap-core's package of the contract directory (`omx-clap-core-contract`), or +the first directory of `OMX_CONTRACT_PATH` that holds one. `make install-contract` installs `data/` as the package `plugin-hostd` of the contract directory. The noarch package `plugin-hostd-contract` carries those files, and `plugin-hostd` and `plugin-hostd-devel` (`plugin-hostd-dev`) require diff --git a/README.md b/README.md index 1211ad6..12f86e3 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,9 @@ Protocol The verbs the daemon adds are declared once in omx-contract's contract directory, with their formats, their replies, their refusal codes and what each host owes them: `/usr/share/omx-contract/contract.d/3/plugin-hostd/verbs.json`, -package `plugin-hostd-contract`, beside the format tables. The placement syntax, the error codes, the feedback events +package `plugin-hostd-contract`, beside the format tables. A refusal number omx-clap-host answers too carries +omx-clap-core's `reply.host.*` code (package `omx-clap-core-contract`), the others plugin-hostd's +`reply.plugin-hostd.*`: one name per number. The placement syntax, the error codes, the feedback events and the settings are declared once in `include/plugin-hostd/protocol.h`, which includes the verbs as `plugin_hostd_host_verbs.h` renders them; the tables here, the man page's key list and `protocol/plugin-hostd.json` are generated from both. A C program includes the installed header (`pkg-config --cflags plugin-hostd`, package diff --git a/debian/control b/debian/control index 99e93a6..38b9130 100644 --- a/debian/control +++ b/debian/control @@ -5,6 +5,7 @@ Maintainer: Pau Aliagas Build-Depends: debhelper-compat (= 13), libmod-host-protocol-dev (>= 0.1.0-5), libomx-contract-dev (>= 3.3.0~), + omx-clap-core-contract (>= 0.22.0~) , omx-clap-host-tests (>= 0.21.0~) , omx-contract (>= 3.3.0~) , pkgconf, @@ -55,7 +56,7 @@ Description: C header to drive plugin-hostd from your own controller Package: plugin-hostd-contract Architecture: all -Depends: ${misc:Depends} +Depends: omx-clap-core-contract (>= 0.22.0~), ${misc:Depends} Breaks: plugin-hostd-dev (<< ${source:Version}) Replaces: plugin-hostd-dev (<< ${source:Version}) Description: plugin-hostd's format tables and verbs, read at run time diff --git a/packaging/plugin-hostd.spec b/packaging/plugin-hostd.spec index bf6d2bd..43efa0a 100644 --- a/packaging/plugin-hostd.spec +++ b/packaging/plugin-hostd.spec @@ -32,6 +32,8 @@ Requires: mod-host-protocol%{?_isa} >= 0.10.6.f14a230-11.1 BuildRequires: omx-clap-host-tests >= 0.21.0 # the reader that holds the committed render of the daemon's verbs to data/verbs.json (make check-rendered) BuildRequires: omx-contract >= 3.3.0 +# the shared verbs and the reply.host.* codes data/verbs.json refers to (make check-rendered, test-verbs-declared) +BuildRequires: omx-clap-core-contract >= 0.22.0 %endif # the format tables and the verbs, which readers read at run time Requires: %{name}-contract = %{version}-%{release} @@ -63,6 +65,9 @@ in the plugin-hostd package. %package contract Summary: plugin-hostd's format tables and verbs, for programs that read them at run time BuildArch: noarch +# verbs.json refuses with omx-clap-core's reply.host.* codes for the numbers both hosts answer: a reader of this +# package reads that one too; a floor only +Requires: omx-clap-core-contract >= 0.22.0 %description contract plugin-hostd's package of omx-contract's contract directory: the format tables From 6612e6ecbdcda081b3d1dd164b4896b6b439e199 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 26 Oct 2026 17:33:20 +0100 Subject: [PATCH 9/9] ci: the deb install job takes the channel's contract data packages; the fuzz mutant cites protocol.h:53 --- .github/workflows/ci.yml | 12 +++++++++++- fuzz/recorder.c | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 922af3c..b68f0cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -348,6 +348,14 @@ jobs: - uses: FreeMixer/.github/.github/actions/build-deb@v1 + # the data packages plugin-hostd-contract depends on are the channel's, not Debian's: the clean container + # installs them from here, like the protocol library + - name: Fetch the contract data packages of the channel + run: | + set -euo pipefail + mkdir -p dep-debs + (cd dep-debs && apt-get download omx-clap-core-contract omx-contract-data) + # what the smoke test runs the installed package against, built here for the architecture - name: Build the smoke test fixtures run: | @@ -363,6 +371,8 @@ jobs: path: | debs/*.deb dep-debs/libmod-host-protocol0_*.deb + dep-debs/omx-clap-core-contract_*.deb + dep-debs/omx-contract-data_*.deb smoke if-no-files-found: error retention-days: 1 @@ -392,7 +402,7 @@ jobs: run: | set -euo pipefail apt-get update -qq - apt-get install -y --no-install-recommends ./debs/plugin-hostd_*.deb ./debs/plugin-hostd-contract_*.deb ./debs/plugin-hostd-dev_*.deb ./debs/plugin-hostd-tests_*.deb ./dep-debs/libmod-host-protocol0_*.deb python3 + apt-get install -y --no-install-recommends ./debs/plugin-hostd_*.deb ./debs/plugin-hostd-contract_*.deb ./debs/plugin-hostd-dev_*.deb ./debs/plugin-hostd-tests_*.deb ./dep-debs/libmod-host-protocol0_*.deb ./dep-debs/omx-clap-core-contract_*.deb ./dep-debs/omx-contract-data_*.deb python3 dpkg -L plugin-hostd-dev | tee dev.txt grep -qx /usr/include/plugin-hostd/protocol.h dev.txt grep -qx /usr/include/plugin-hostd/plugin_hostd_host_verbs.h dev.txt diff --git a/fuzz/recorder.c b/fuzz/recorder.c index 20008ff..bd14e04 100644 --- a/fuzz/recorder.c +++ b/fuzz/recorder.c @@ -261,7 +261,7 @@ static char *answer(struct rec_call *c, const char *shape, int changes) if (changes) g_rec.state = mix_call(g_rec.state, c); /* the reply a verb declares fits one line of the protocol - * mutant: include/plugin-hostd/protocol.h:46 `4096` -> `4` + * mutant: include/plugin-hostd/protocol.h:53 `4096` -> `4` * input: mod-host-bypass */ if (syntax_instantiate(shape, buf, sizeof(buf)) < 0) abort();