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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ mod-host-protocol` when it is installed (mod-host's `make install-lib`),
otherwise from `MOD_HOST_DIR`, a mod-host tree where the library is built if
missing. The daemon needs libc and that library and nothing else.

The probe runner and the protocol generator also read omx-contract's C header,
`<omxcontract/omx_contract_limits.h>`, from `pkg-config omx-contract`
(`omx-contract-devel`, `libomx-contract-dev`; `CONTRACT_CFLAGS=-I<dir>` 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 `PARAM_SCALES`. `make
test-contract` checks both, against the installed contract and against a copy
of its header with the values changed. Nothing is linked: the header holds
values only.

Building without omx-clap-core
------------------------------

Expand Down
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ endif
# protocol's flags (mod-host-protocol.pc names <includedir>/mod-host), or MOD_HOST_INCLUDE=<dir> on the command line
MOD_HOST_INCLUDE ?= $(patsubst -I%,%,$(firstword $(filter -I%,$(PROTOCOL_CFLAGS))))

# omx-contract's C header (omx-contract-devel, libomx-contract-dev): the probe runner's default rates and the protocol's
# parameter scales are read from it; CONTRACT_CFLAGS=-I<dir> for a header that is not installed
CONTRACT_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags omx-contract 2>/dev/null)

# CLAP headers for the test plugin: pkg-config when clap-devel is installed, CLAP_CFLAGS=-I<dir> otherwise
CLAP_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags clap 2>/dev/null)

Expand Down Expand Up @@ -60,7 +64,7 @@ $(PROG): $(OBJ) $(PROTOCOL_LIB)

# the probe runner: it loads no plugin and links nothing of the daemon or of mod-host
$(PROBE): src/probe.c include/plugin-hostd/protocol.h include/plugin-hostd/pin.h
$(CC) -Iinclude $(CFLAGS) -D_GNU_SOURCE -DPROBE_VERSION='"$(VERSION)"' -o $@ src/probe.c
$(CC) -Iinclude $(CONTRACT_CFLAGS) $(CFLAGS) -D_GNU_SOURCE -DPROBE_VERSION='"$(VERSION)"' -o $@ src/probe.c

ifneq ($(PROTOCOL_LIB),)
$(PROTOCOL_LIB):
Expand Down Expand Up @@ -126,7 +130,7 @@ clean:

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

# the same without the verb table, which is read from a mod-host checkout's README
Expand Down Expand Up @@ -227,7 +231,7 @@ 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
$(CC) -Wall -Wextra -Werror -std=gnu99 -Iinclude -o $@ tools/protocol-gen.c
$(CC) -Wall -Wextra -Werror -std=gnu99 -Iinclude $(CONTRACT_CFLAGS) -o $@ tools/protocol-gen.c

gen: $(GEN)
$(GEN) json > protocol/plugin-hostd.json.new && mv protocol/plugin-hostd.json.new protocol/plugin-hostd.json
Expand Down Expand Up @@ -283,6 +287,11 @@ tests/rt_interpose.so: tests/rt_interpose_stub.c
tests/probe-child: tests/probe_child.c
$(CC) $(CLAP_CFLAGS) $(CFLAGS) -Wall -Wextra -Werror -o $@ $< -ldl

# what is omx-contract's is read from it: the runner's default rates and the protocol's parameter scales follow a
# scratch copy of its header with other values
test-contract: $(PROBE)
PROBE=./$(PROBE) python3 tests/contract_derived.py

# the probe output: its three fixtures validate and five broken copies of the first are refused
test-probe-schema:
python3 tests/probe_schema.py
Expand Down
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Priority: optional
Maintainer: Pau Aliagas <linuxnow@gmail.com>
Build-Depends: debhelper-compat (= 13),
libmod-host-protocol-dev,
libomx-contract-dev (>= 2.16.0~),
omx-clap-host-tests (>= 0.9.0~) <!nocheck>,
pkgconf,
python3
Expand Down
3 changes: 2 additions & 1 deletion doc/plugin-hostd-probe.1
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ Nothing a plugin prints can enter it.
The program that loads the plugin. Default clap\-qualify, and /usr/libexec/plugin\-qualify/lv2_measure for an LV2.
.TP
.BI \-\-rates " R,R,..."
Sample rates to run at. Default 44100,48000,96000,192000.
Sample rates to run at. Default: omx\-contract's ORACLE_FLOOR_RATES, the rates every kernel oracle runs at, as the
runner's usage prints them.
.TP
.BI \-\-quantum " n"
Block size in frames. Default 512.
Expand Down
2 changes: 2 additions & 0 deletions packaging/plugin-hostd.spec
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ BuildRequires: make
BuildRequires: diffutils
BuildRequires: python3
BuildRequires: pkgconfig(mod-host-protocol)
# the probe runner's default rates and the protocol's parameter scales are read from omx-contract's C header; a floor only
BuildRequires: omx-contract-devel >= 2.16.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
Expand Down
20 changes: 17 additions & 3 deletions src/probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
#include <time.h>
#include <unistd.h>

#include <omxcontract/omx_contract_limits.h>
#include <plugin-hostd/pin.h>
#include <plugin-hostd/protocol.h>

Expand Down Expand Up @@ -1516,6 +1517,19 @@ static void put_lv2_measure(sb_t *b, const jnode_t *m, long rate, unsigned quant
sb_printf(b, "]}}");
}

/* the rates a run measures at when it is given none: omx-contract's ORACLE_FLOOR_RATES, every kernel oracle's floor */
static const unsigned g_floor_rates[] = OMX_ORACLE_FLOOR_RATES_INIT;

static const char *floor_rates(void)
{
static char text[16 * sizeof(g_floor_rates) / sizeof(g_floor_rates[0])];
size_t n, used = 0;

for (n = 0; n < sizeof(g_floor_rates) / sizeof(g_floor_rates[0]) && used < sizeof(text); n++)
used += snprintf(text + used, sizeof(text) - used, "%s%u", n ? "," : "", g_floor_rates[n]);
return text;
}

static const char *base_name(const char *path)
{
const char *s = strrchr(path, '/');
Expand All @@ -1525,7 +1539,7 @@ static const char *base_name(const char *path)

int main(int argc, char **argv)
{
options_t o = { .child = "clap-qualify", .rates = "44100,48000,96000,192000", .quantum = 512, .reason = "on-request",
options_t o = { .child = "clap-qualify", .rates = floor_rates(), .quantum = 512, .reason = "on-request",
.set = "default", .wall_s = 600, .cpu_s = 540, .files = 256, .data_bytes = 4LL << 30,
.file_bytes = 64LL << 20, .grace_ms = 300 };
int a = 1, pin_option = 0, exit_code = 0, i, rc;
Expand All @@ -1551,15 +1565,15 @@ int main(int argc, char **argv)
fprintf(stderr, "usage: " RUNNER_NAME " run [options] <plugin.clap> [<clap id>]\n"
" " RUNNER_NAME " run [options] lv2:<uri>\n"
" --child PROGRAM the program that loads the plugin (default clap-qualify; " PROBE_LV2_CHILD " for lv2:)\n"
" --rates R,R,... sample rates (default 44100,48000,96000,192000)\n"
" --rates R,R,... sample rates (default %s)\n"
" --quantum N block size in frames (default 512) --blocks N blocks per rate\n"
" --offer EXT,... host extensions to offer on top of the child's own\n"
" --out FILE the document, written atomically (default stdout)\n"
" --log FILE the child's stderr (default discarded)\n"
" --reason WHY add, rescan, binary-changed, on-request, auto-measure, reference, ci\n"
" --wall-s N --cpu-s N --data-bytes N --files N --file-bytes N --grace-ms N\n"
" --cpus LIST pin the child to these cpus (\"0-3,8\")\n"
" --require LIMIT refuse to run unless the limit is applied (repeatable)\n");
" --require LIMIT refuse to run unless the limit is applied (repeatable)\n", floor_rates());
return 2;
}
for (a = 2; a < argc; a++)
Expand Down
151 changes: 151 additions & 0 deletions tests/contract_derived.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# What plugin-hostd shares with omx-contract is read from omx-contract, never typed again here: the probe runner's
# default rates are ORACLE_FLOOR_RATES, and the parameter scales of the protocol are PARAM_SCALES, the same words in
# the same order. Checked twice: against the installed contract (its data and its C header), and against a scratch
# copy of the header with the values changed, where the runner must follow and the generator must refuse to build.
# Usage: contract_derived.py (run from the tree; CC, PKG_CONFIG and PROBE from the environment, or their defaults).
import json, os, re, shutil, subprocess, sys, tempfile

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CC = os.environ.get("CC", "cc")
PKG_CONFIG = os.environ.get("PKG_CONFIG", "pkg-config")
PROBE = os.path.abspath(os.environ.get("PROBE", os.path.join(ROOT, "plugin-hostd-probe")))
failures = 0


def check(cond, what):
global failures
if not cond:
failures += 1
print("FAIL contract: " + what)


def pkg(*args):
return subprocess.run([PKG_CONFIG] + list(args) + ["omx-contract"], capture_output=True, text=True, check=True).stdout.strip()


def contract_data(name):
"""an item of omx-contract's own data, as the installed package lays it out (contract.d/<major>/omx-contract)"""
base = pkg("--variable=contractdir")
for major in sorted(os.listdir(base), reverse=True):
for f in sorted(os.listdir(os.path.join(base, major, "omx-contract"))):
if f.endswith(".json") and f != "MANIFEST.json":
with open(os.path.join(base, major, "omx-contract", f)) as fh:
data = json.load(fh)
if name in data:
return data[name]
raise SystemExit("omx-contract declares no %s" % name)


def header():
for flag in pkg("--cflags").split() + ["-I/usr/include"]:
if flag.startswith("-I"):
path = os.path.join(flag[2:], "omxcontract", "omx_contract_limits.h")
if os.path.exists(path):
return path
raise SystemExit("omx-contract's C header is not installed")


def child_rates(probe, tmp):
"""the --rates the runner hands its child when it is given none"""
seen = os.path.join(tmp, "argv")
child = os.path.join(tmp, "child.sh")
with open(child, "w") as f:
f.write('#!/bin/sh\nprintf "%%s\\n" "$@" > %s\n' % seen)
os.chmod(child, 0o755)
plugin = os.path.join(tmp, "x.clap")
open(plugin, "w").close()
subprocess.run([probe, "run", "--child", child, "--wall-s", "5", "--out", os.path.join(tmp, "doc.json"), plugin],
capture_output=True, text=True, timeout=60)
args = open(seen).read().split("\n") if os.path.exists(seen) else []
return args[args.index("--rates") + 1] if "--rates" in args else None


def help_rates(probe):
p = subprocess.run([probe], capture_output=True, text=True, timeout=10)
m = re.search(r"--rates R,R,\.\.\.\s+sample rates \(default ([0-9,]+)\)", p.stderr)
return m.group(1) if m else None


def build_probe(include_first, out):
cmd = [CC, "-I" + include_first, "-I" + os.path.join(ROOT, "include")] + pkg("--cflags").split() + \
["-O2", "-std=gnu99", "-D_GNU_SOURCE", "-DPROBE_VERSION=\"0\"", "-o", out, os.path.join(ROOT, "src", "probe.c")]
return subprocess.run(cmd, capture_output=True, text=True)


def build_gen(include_first, out):
cmd = [CC, "-I" + include_first, "-I" + os.path.join(ROOT, "include")] + pkg("--cflags").split() + \
["-std=gnu99", "-o", out, os.path.join(ROOT, "tools", "protocol-gen.c")]
return subprocess.run(cmd, capture_output=True, text=True)


def gen_json(gen):
p = subprocess.run([gen, "json"], capture_output=True, text=True, timeout=30)
return p.returncode, p.stdout, p.stderr


def main():
floor = ",".join(str(r) for r in contract_data("ORACLE_FLOOR_RATES")["values"])
scales = contract_data("PARAM_SCALES")["ids"]
with open(os.path.join(ROOT, "protocol", "plugin-hostd.json")) as f:
declared = [s["name"] for s in json.load(f)["param_scales"]]
check(declared == scales, "the protocol's parameter scales %s are omx-contract's PARAM_SCALES %s" % (declared, scales))

tmp = tempfile.mkdtemp(prefix="plugin-hostd-contract.")
try:
check(child_rates(PROBE, tmp) == floor, "the runner's child gets omx-contract's ORACLE_FLOOR_RATES %s by default, "
"not %s" % (floor, child_rates(PROBE, tmp)))
check(help_rates(PROBE) == floor, "the runner's usage names the same default: %s" % help_rates(PROBE))

# a scratch contract whose floor is other rates: the runner built against it follows, with no edit of its own
scratch = os.path.join(tmp, "include")
os.makedirs(os.path.join(scratch, "omxcontract"))
text = open(header()).read()
moved = re.sub(r"(#define OMX_ORACLE_FLOOR_RATES_INIT )\{[^}]*\}", r"\1{ 32000, 88200 }", text)
check(moved != text, "positive control: the scratch header's floor was changed")
with open(os.path.join(scratch, "omxcontract", "omx_contract_limits.h"), "w") as f:
f.write(moved.replace("#define OMX_ORACLE_FLOOR_RATES_COUNT 4u", "#define OMX_ORACLE_FLOOR_RATES_COUNT 2u"))
built = build_probe(scratch, os.path.join(tmp, "probe"))
check(built.returncode == 0, "the runner builds against the scratch contract: %s" % built.stderr[-400:])
if built.returncode == 0:
check(child_rates(os.path.join(tmp, "probe"), tmp) == "32000,88200",
"a moved floor moves the runner's default: %s" % child_rates(os.path.join(tmp, "probe"), tmp))

# the generator builds against the contract as it is and writes its scales; it refuses a word of the
# protocol's own that is not the contract's, and a contract whose scales are in another order
built = build_gen(os.path.dirname(os.path.dirname(header())), os.path.join(tmp, "gen"))
check(built.returncode == 0, "positive control: the generator builds against the contract: %s" % built.stderr[-400:])
if built.returncode == 0:
rc, out, err = gen_json(os.path.join(tmp, "gen"))
check(rc == 0 and [x["name"] for x in json.loads(out)["param_scales"]] == scales,
"positive control: the generator writes the contract's scales: rc %d %s" % (rc, err[-200:]))
own = os.path.join(tmp, "own")
shutil.copytree(os.path.join(ROOT, "include"), own)
proto = os.path.join(own, "plugin-hostd", "protocol.h")
ptext = open(proto).read()
renamed = ptext.replace('X(LOG, "log", ', 'X(LOG, "logarithmic", ')
check(renamed != ptext, "positive control: the scratch protocol.h's word was changed")
with open(proto, "w") as f:
f.write(renamed)
built = build_gen(own, os.path.join(tmp, "gen-renamed"))
if built.returncode == 0:
rc, out, err = gen_json(os.path.join(tmp, "gen-renamed"))
check(rc != 0 and "PARAM_SCALES" in err and out == "",
"the generator writes nothing for a scale word that is not the contract's: rc %d %r" % (rc, err[-200:]))
else:
check(0, "the generator builds with a renamed word, to refuse it when it runs: %s" % built.stderr[-300:])
swapped = text.replace("OMX_PARAM_SCALES_LOG = 1", "OMX_PARAM_SCALES_LOG = 2").replace(
"OMX_PARAM_SCALES_STEPPED = 2", "OMX_PARAM_SCALES_STEPPED = 1")
check(swapped != text, "positive control: the scratch header's scales were swapped")
with open(os.path.join(scratch, "omxcontract", "omx_contract_limits.h"), "w") as f:
f.write(swapped)
built = build_gen(scratch, os.path.join(tmp, "gen-swapped"))
check(built.returncode != 0 and "PARAM_SCALES" in built.stderr,
"the generator refuses scales the contract orders otherwise: rc %d %s" % (built.returncode, built.stderr[-300:]))
finally:
shutil.rmtree(tmp, ignore_errors=True)
print("contract: %s" % ("FAILED (%d)" % failures if failures else "ok"))
return 1 if failures else 0


sys.exit(main())
Loading
Loading