diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362aca8..b68f0cb 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 @@ -335,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: | @@ -350,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 @@ -379,11 +402,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 ./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 + 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..a08a0e9 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,23 @@ 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. 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 +it at the same version. + Tests ----- @@ -59,9 +76,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 79f35e9..0641046 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,14 +119,15 @@ 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 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 @@ -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 $@) @@ -236,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) @@ -288,7 +312,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 +321,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 +385,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/README.md b/README.md index f381228..12f86e3 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,15 @@ Building from source: see [BUILDING.md](BUILDING.md). More about FreeMixer at = 3.3.0", + "omx-clap-core": ">= 0.22.0" + }, + "PLUGIN_HOSTD_REPLY_CODES": { + "kind": "codes", + "since": "0.12.0", + "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": { + "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" + }, + "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" + }, + "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" + }, + "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" + }, + "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": "HOST_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": "HOST_REPLY_CODES.instanceNonExists" + }, + { + "ref": "HOST_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": "HOST_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": "HOST_REPLY_CODES.instanceNonExists" + }, + { + "ref": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_REPLY_CODES.pinAbsent" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.pinBinaryMismatch" + }, + { + "ref": "PLUGIN_HOSTD_REPLY_CODES.pinRefused" + }, + { + "ref": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_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": "HOST_REPLY_CODES.instanceNonExists" + }, + { + "ref": "HOST_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": "HOST_REPLY_CODES.instanceNonExists" + }, + { + "ref": "HOST_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." + } + } + } +} diff --git a/debian/control b/debian/control index 12e3157..38b9130 100644 --- a/debian/control +++ b/debian/control @@ -4,8 +4,10 @@ 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-core-contract (>= 0.22.0~) , omx-clap-host-tests (>= 0.21.0~) , + omx-contract (>= 3.3.0~) , pkgconf, python3 Standards-Version: 4.6.2 @@ -14,7 +16,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 +46,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: 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 + 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/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..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(); @@ -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..99a017f --- /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.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.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.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.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.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.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.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.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.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.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" +#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.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.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.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.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.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.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.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.host.instance-non-exists, reply.host.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..58525a4 --- /dev/null +++ b/include/plugin-hostd/plugin_hostd_vocabulary.h @@ -0,0 +1,40 @@ +// 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_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_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_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_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_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..9f4fc1b 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 @@ -252,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) @@ -317,24 +327,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 +357,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 +415,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 +427,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 +483,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 +511,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 +584,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 +806,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 +1010,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 +1022,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/packaging/plugin-hostd.spec b/packaging/plugin-hostd.spec index acc50a7..43efa0a 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,13 @@ 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 +# 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} # the workers: a format whose worker is not installed refuses its adds (-503), the other format is unaffected Recommends: mod-host @@ -47,6 +53,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 +62,21 @@ 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 +# 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 +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 +104,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 +132,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 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/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/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..4ec9863 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', @@ -732,6 +732,26 @@ ("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": -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", + "", "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 +821,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 +961,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 +984,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_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/tests/verbs_declared.py b/tests/verbs_declared.py new file mode 100644 index 0000000..a9fa726 --- /dev/null +++ b/tests/verbs_declared.py @@ -0,0 +1,433 @@ +# 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); +# - 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 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 + +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 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["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"]][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"], + 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() + + +def main(): + with open(OWN) as f: + own_docs = [json.load(f)] + docs, source = package("omx-clap-core") + 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)]))) + + 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()) 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)