@openmixer/plugin-qualify 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -14
- package/bin/plugin-qualify-sync.mjs +57 -0
- package/bin/plugin-qualify.mjs +66 -15
- package/dist/adjust-cli.d.ts +83 -0
- package/dist/adjust-cli.js +214 -0
- package/dist/adjust-cli.js.map +1 -0
- package/dist/clap-loader.d.ts +3 -3
- package/dist/clap-loader.js +11 -11
- package/dist/clap-loader.js.map +1 -1
- package/dist/history-adjust.d.ts +77 -0
- package/dist/history-adjust.js +99 -0
- package/dist/history-adjust.js.map +1 -0
- package/dist/history-file.d.ts +73 -0
- package/dist/history-file.js +133 -0
- package/dist/history-file.js.map +1 -0
- package/dist/host-admission.d.ts +53 -0
- package/dist/host-admission.js +96 -0
- package/dist/host-admission.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lv2-loader.d.ts +16 -2
- package/dist/lv2-loader.js +168 -17
- package/dist/lv2-loader.js.map +1 -1
- package/dist/measurement/cli.d.ts +6 -4
- package/dist/measurement/cli.js +12 -19
- package/dist/measurement/cli.js.map +1 -1
- package/dist/pins-file.d.ts +8 -2
- package/dist/pins-file.js +17 -3
- package/dist/pins-file.js.map +1 -1
- package/dist/probe-judge.d.ts +11 -1
- package/dist/probe-judge.js +20 -1
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +10 -0
- package/dist/probe-output-fixtures.js +86 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +2 -0
- package/dist/probe-output.js +1 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +9 -3
- package/dist/probe-runner.js +36 -12
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +109 -3
- package/dist/qualification-record.js +79 -13
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-cli.d.ts +0 -1
- package/dist/qualify-cli.js +2 -4
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +8 -1
- package/dist/qualify-declarations.js +8 -1
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-options.js +1 -2
- package/dist/qualify-options.js.map +1 -1
- package/dist/qualify-sync.d.ts +215 -0
- package/dist/qualify-sync.js +794 -0
- package/dist/qualify-sync.js.map +1 -0
- package/dist/whole-file.d.ts +3 -0
- package/dist/whole-file.js +36 -0
- package/dist/whole-file.js.map +1 -0
- package/fixtures/admission/2026-10-26.json +19 -0
- package/fixtures/clap-scan/fake.json +24 -24
- package/package.json +5 -4
- package/src/adjust-cli.ts +262 -0
- package/src/clap-loader.ts +9 -13
- package/src/history-adjust.ts +156 -0
- package/src/history-file.ts +177 -0
- package/src/host-admission.ts +136 -0
- package/src/index.ts +6 -0
- package/src/lv2-loader.ts +150 -15
- package/src/measurement/cli.ts +11 -17
- package/src/pins-file.ts +17 -3
- package/src/probe-judge.ts +27 -1
- package/src/probe-output-fixtures.ts +86 -0
- package/src/probe-output.ts +3 -0
- package/src/probe-runner.ts +45 -14
- package/src/qualification-record.ts +174 -15
- package/src/qualify-cli.ts +2 -5
- package/src/qualify-declarations.ts +9 -1
- package/src/qualify-options.ts +1 -2
- package/src/qualify-sync.ts +889 -0
- package/src/whole-file.ts +35 -0
- package/tools/_declarations.py +3 -1
- package/tools/ai-trace-commit-check.sh +26 -14
- package/tools/ai-trace-patterns.json +10 -2
- package/tools/gen-hosting-codes.mjs +1 -1
- package/tools/gen-python-declarations.mjs +2 -0
- package/tools/install-roundtrip.sh +173 -0
- package/tools/mod-host-admit.mjs +74 -0
package/README.md
CHANGED
|
@@ -7,22 +7,20 @@ Licence: GPL-3.0-or-later (`LICENSE`).
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
plugin-qualify is published, signed, in the
|
|
10
|
+
plugin-qualify is published, signed, in the OpenMixer package channel, for Fedora 44 (x86_64, aarch64)
|
|
11
11
|
and for Debian bookworm and trixie, including Raspberry Pi OS (amd64, arm64).
|
|
12
12
|
|
|
13
13
|
Fedora:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
sudo dnf
|
|
16
|
+
sudo dnf install https://www.openmixer.org/rpm/openmixer-release.noarch.rpm
|
|
17
17
|
sudo dnf install plugin-qualify
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Debian and Raspberry Pi OS:
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
|
|
24
|
-
sudo curl -fsSL -o /etc/apt/keyrings/freemixer.asc https://freemixer.github.io/deb/freemixer.asc
|
|
25
|
-
echo "deb [signed-by=/etc/apt/keyrings/freemixer.asc] https://freemixer.github.io/deb/debian/$(. /etc/os-release && echo $VERSION_CODENAME) ./" | sudo tee /etc/apt/sources.list.d/freemixer.list
|
|
23
|
+
curl -fsSLO https://www.openmixer.org/deb/openmixer-release.deb && sudo apt install ./openmixer-release.deb
|
|
26
24
|
sudo apt update
|
|
27
25
|
sudo apt install plugin-qualify
|
|
28
26
|
```
|
|
@@ -59,13 +57,12 @@ them. The commands below are written as `plugin-qualify`; from a checkout run th
|
|
|
59
57
|
```
|
|
60
58
|
plugin-qualify urn:example:my-plugin # one plugin, by URI
|
|
61
59
|
plugin-qualify ./my-plugin.lv2 # every plugin in a bundle
|
|
62
|
-
plugin-qualify --all
|
|
60
|
+
plugin-qualify --all --out ./qualification # every LV2 bundle on LV2_PATH and every CLAP on the search path, one record each
|
|
63
61
|
plugin-qualify --help # every option and its default
|
|
64
62
|
```
|
|
65
63
|
|
|
66
64
|
`--format json` prints the verdicts as JSON; `--out <dir>` writes the full measurement document.
|
|
67
|
-
`--isolate` measures each plugin in a fresh worker so a crash or hang takes only that plugin
|
|
68
|
-
`--no-host-sweep` measures offline only, without mod-host. The `Containerfile` builds an image
|
|
65
|
+
`--isolate` measures each plugin in a fresh worker so a crash or hang takes only that plugin. The `Containerfile` builds an image
|
|
69
66
|
with lilv, mod-host and the ASan mod-host, if you do not want to install them yourself.
|
|
70
67
|
|
|
71
68
|
A named LV2 plugin is measured the same way as a CLAP one: through the probe runner, then judged from the probe
|
|
@@ -151,12 +148,54 @@ declares (CLAP `voice-info`, basis `declared`), or, when it declares none, the c
|
|
|
151
148
|
(basis `probe`; LV2 declares none). Each rate's figure says how many notes it held and why (`voices: { held,
|
|
152
149
|
basis, capacity }`). An instrument with no count to hold is not timed (`voices-unknown`).
|
|
153
150
|
|
|
151
|
+
An LV2 plugin's record holds the same two figures, under the same field names, in `measurements.grid[]`:
|
|
152
|
+
`latency { declaredFrames, measuredFrames }` and `cost`. `lv2_measure` takes them at the run's rate and block, the
|
|
153
|
+
way the CLAP prober does: the declared figure is what the plugin reports on its latency port
|
|
154
|
+
(`lv2:designation lv2:latency`, or the older `lv2:reportsLatency`; 0 when it has none), the measured one is where an
|
|
155
|
+
impulse at frame 0 lands (the first frame at or above 1e-3 of the response's own peak), or, for an instrument, where a
|
|
156
|
+
struck A4 sounds. A plugin that has no latency port is measured, never read as 0. An instrument's cost holds
|
|
157
|
+
`OMX_PROBE_NOTES` keys down (the LV2 counterpart of `--probe-notes`). A figure the run could not take (a response that
|
|
158
|
+
stays silent, an instrument with no count to hold) is left out of the grid point and named in
|
|
159
|
+
`measurements.unmeasured` with its hosting code (`hosting.latency.unmeasured`,
|
|
160
|
+
`hosting.cost.unmeasured-at-rate-and-quantum`).
|
|
161
|
+
|
|
154
162
|
|
|
155
163
|
The prober also scans: `OMX_CLAP_SCAN=native/build/clap-qualify` makes it the scanner, and its scan
|
|
156
164
|
keeps what a console needs to draw a third-party plugin's controls: an enum parameter's labels (as
|
|
157
165
|
the plugin's `value_to_text` renders them), each parameter's module, and the plugin's
|
|
158
166
|
remote-control pages.
|
|
159
167
|
|
|
168
|
+
## Adjusting a verdict by what the plugin did on this machine
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
plugin-qualify adjust --history ~/.local/state/plugin-hostd/history --qualification-dir ./qualification --host-profile omx-clap-host
|
|
172
|
+
plugin-qualify adjust --history history --qualification-dir ./qualification --host-class aarch64/4/2400 --pins pins.txt
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
plugin-hostd keeps a running history of every plugin binary on every host class it ran on (windows run alone and in a
|
|
176
|
+
group, CPU, xrun windows, crashes, restarts, quarantines). `adjust` reads that file directly, so it runs without a daemon,
|
|
177
|
+
and writes a second verdict beside the measured one in each qualification record: a `history` block per host class of the
|
|
178
|
+
file, holding the line it looked for (the plugin's plugin-hostd uri and the digest of the first file of its pin), whether
|
|
179
|
+
the history has it, the windows it counts, and for each `--host-profile` the measured pin verdict, the adjusted one, the
|
|
180
|
+
move (`demote`, `promote` or `none`) and a reason code. The measured `verdicts` and the measurement are never changed: the
|
|
181
|
+
same plugin on another host class has another history and another adjusted verdict.
|
|
182
|
+
|
|
183
|
+
| move | when | reason |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `in-process` to `isolated` | a crash was counted within 30 days, on this host class | `history.crashed` |
|
|
186
|
+
| `in-process` to `isolated` | from 30 grouped windows, the share of xrun windows grouped exceeds the share alone by 5 points | `history.xruns-grouped` |
|
|
187
|
+
| `isolated` to `refused` | three or more solo crashes, the latest within 30 days | `history.crashed-alone` |
|
|
188
|
+
| `isolated` to `in-process` | every reason against the plugin is a `hosting.cost.*` one, 360 windows run alone (six hours), no crash or restart ever counted, no xruns raised when grouped, `cpu_p95_solo` under the host profile's ceiling | `history.steady` |
|
|
189
|
+
| none | no line for this binary on this class | `history.no-record` |
|
|
190
|
+
| none | a line, and no rule moves the verdict | `history.no-change` |
|
|
191
|
+
| none | the line would promote, but the verdict is a fact about the plugin: a real-time-safety, feature, stability, latency or topology reason, an `unknown`, or a `refused` | `history.not-liftable`, with `blockedBy` |
|
|
192
|
+
|
|
193
|
+
History never lifts a real-time-safety, feature or unknown verdict, and never promotes a refused one; a demotion is one
|
|
194
|
+
step. The thresholds are one table, `HISTORY_POLICY` in `src/history-adjust.ts`. `--pins <file>` writes the pins with the
|
|
195
|
+
adjusted verdict of the first `--host-profile` instead of the measured one; it needs exactly one host class, so name one
|
|
196
|
+
with `--host-class` when the history holds several. `--now <seconds>` fixes the time the history is read at. A line of the
|
|
197
|
+
history outside its grammar is not read and is named on stderr; a history of a version this tool does not know is refused.
|
|
198
|
+
|
|
160
199
|
## LV2 plugins: the hosting measurement
|
|
161
200
|
|
|
162
201
|
`tools/lv2-measure.mjs` measures the hosting dimensions (`stability`, `rtSafety`, `features`) through
|
|
@@ -270,12 +309,27 @@ plugin-qualify --host-profile jalv,mod-host,openmixer-console <uri> # one verdi
|
|
|
270
309
|
plugin-qualify --host-profile ./my-host.json <uri> # your own host
|
|
271
310
|
```
|
|
272
311
|
|
|
273
|
-
| profile | isolation | instruments | LV2 features read from | cost budget (P95 of a core, rate / quantum) |
|
|
274
|
-
|
|
275
|
-
| `openmixer-console` (default) | in-process (the console's RT thread) | no | the console's in-process table | 0.05 at 96000 / 1024 |
|
|
276
|
-
| `jalv` | per-process | yes | jalv 1.6.8 `src/jalv.c` (zynthian's fork passes the same list) | 0.25 at 48000 / 256 |
|
|
277
|
-
| `mod-host` | shared-process | yes | mod-host f14a230 `src/effects.c` | 0.05 at 48000 / 128 |
|
|
278
|
-
| `omx-clap-host` | shared-process | yes | the console's in-process table (the same LV2 feature provider) | 0.05 at 48000 / 128 |
|
|
312
|
+
| profile | isolation | verdict | instruments | LV2 features read from | cost budget (P95 of a core, rate / quantum) |
|
|
313
|
+
|---|---|---|---|---|---|
|
|
314
|
+
| `openmixer-console` (default) | in-process (the console's RT thread) | judged from a declaration | no | the console's in-process table | 0.05 at 96000 / 1024 |
|
|
315
|
+
| `jalv` | per-process | **measured by a run** (`jalv -n <name> <uri>`, headless) | yes | jalv 1.6.8 `src/jalv.c` (zynthian's fork passes the same list) | 0.25 at 48000 / 256 |
|
|
316
|
+
| `mod-host` | shared-process | **measured by a run** (`add`, `activate`, `remove` on mod-host's socket) | yes | mod-host f14a230 `src/effects.c` | 0.05 at 48000 / 128 |
|
|
317
|
+
| `omx-clap-host` | shared-process | judged from a declaration | yes | the console's in-process table (the same LV2 feature provider) | 0.05 at 48000 / 128 |
|
|
318
|
+
|
|
319
|
+
**Measured or declared.** A *declared* verdict is what the profile's feature list and cost budget make of
|
|
320
|
+
the one probe output. A *measured* verdict (`jalv`, `mod-host`, LV2 plugins) also runs the plugin under that
|
|
321
|
+
host, one admission per plugin, and its record entry carries `arm: "run"` and `measured: true`. A refusal
|
|
322
|
+
(`resp -102`), a crash (the signal), a host that dies on `add`, or a port type the host does not know makes the
|
|
323
|
+
plugin `unsuitable` for that host, with the code as the deciding reason (`mod-host.resp-102`,
|
|
324
|
+
`mod-host.signal-SIGABRT`, `mod-host.died-on-add`, `jalv.unknown-port-type`, `jalv.signal-SIGABRT`). A host
|
|
325
|
+
that is not installed leaves the verdict unmeasured (`unknown`, deciding `<host>.not-installed`). After any
|
|
326
|
+
death on mod-host the next admission starts a fresh host. `plugin-qualify judge` reads a probe output
|
|
327
|
+
that holds no run, so its `jalv` and `mod-host` entries stay `arm: "declaration"`.
|
|
328
|
+
|
|
329
|
+
**The console's two tiers.** Every record carries `tiers`: the *in-process* verdict (`openmixer-console`) and the
|
|
330
|
+
*isolated* verdict (`mod-host` for LV2, `omx-clap-host` for CLAP), `offeredOn` (the tiers whose verdict qualified)
|
|
331
|
+
and, when neither did, `dimmed` with the reason. A plugin that kills the isolated host and qualifies in-process is
|
|
332
|
+
offered in-process only. The tiers are judged for every plugin whether or not `--host-profile` names them.
|
|
279
333
|
|
|
280
334
|
`omx-clap-host` offers a CLAP plugin the console's six host extensions and three more,
|
|
281
335
|
`clap.preset-load/2`, `clap.track-info/1` and `clap.remote-controls/2`. A CLAP plugin judged
|
|
@@ -350,6 +404,65 @@ a last word: the SHA-256 of every file the host loads, the parameter-layout fing
|
|
|
350
404
|
file in plugin-hostd's settings as `pin_file` and an `add` of that plugin needs no `pin_set`; a changed file is refused
|
|
351
405
|
with -509. The verdict is the first `--host-profile`'s. A run that took no pin leaves an existing file alone.
|
|
352
406
|
|
|
407
|
+
### Qualifying every plugin on a machine
|
|
408
|
+
|
|
409
|
+
`plugin-qualify-sync` qualifies every plugin installed on the machine, and again each one whose files
|
|
410
|
+
change. With its pin file in plugin-hostd's settings, the daemon runs in its default mode (`require_pins 1`)
|
|
411
|
+
and chains any plugin that qualified, the image's own and any installed later:
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
pin_file /var/lib/omx/qualified.pins # in plugin-hostd.conf
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The package ships it as a service that runs two minutes after boot and whenever a plugin directory
|
|
418
|
+
changes, once the directory has been quiet for a minute. A run finds every `.clap` under the CLAP
|
|
419
|
+
directories and every plugin of every bundle under the LV2 ones, and hashes each one's files as
|
|
420
|
+
plugin-hostd does for a pin. A plugin whose files hash as recorded is skipped. A new or changed one gets the
|
|
421
|
+
quick qualification through the probe runner: its pin, the RT-safety sweep, cost and latency, its
|
|
422
|
+
qualification record and its descriptor. The soak is not part of it, and every record it writes says so in
|
|
423
|
+
`notMeasured` (`hosting.stability.unsoaked`).
|
|
424
|
+
|
|
425
|
+
Each plugin has `budget_s` seconds. One that crashes or hangs is recorded as `crashed` or `hung`, one that
|
|
426
|
+
fails the sweep is pinned `verdict=refused`, and the run goes on to the next. The pin file and the state are
|
|
427
|
+
written whole after every plugin, so a run cut short keeps what it finished. The last line counts the run:
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
qualified 2, skipped 0, refused 0, crashed 1, hung 1, unmeasured 0, 4.6 s
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
A plugin that is no longer found, or whose bytes changed, leaves nothing behind: its pin, its qualification
|
|
434
|
+
record (with the probe output copied beside it) and its descriptor are removed. Records the sync did not write
|
|
435
|
+
stay, and nothing is removed in a run that could not read a bundle, so a half-written install is no uninstall.
|
|
436
|
+
A running plugin-hostd is told with `pin_clear`.
|
|
437
|
+
|
|
438
|
+
When plugin-hostd answers on `hostd_port`, the changed pins are also sent to it with `pin_set` and apply at
|
|
439
|
+
once. plugin-hostd serves one controller at a time, so while one is connected the run says so and the pins
|
|
440
|
+
apply at the daemon's next start.
|
|
441
|
+
|
|
442
|
+
The settings are `/etc/omx/qualify.conf`, one `key value` per line, and each is also a flag
|
|
443
|
+
(`--budget-s 30`):
|
|
444
|
+
|
|
445
|
+
| key | default |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| `lv2_path` | `$LV2_PATH`, else plugin-hostd's: `/usr/lib64/lv2:/usr/lib/lv2:/usr/local/lib/lv2` |
|
|
448
|
+
| `lv2_extra` | `/zynthian/zynthian-plugins/lv2:/zynthian/zynthian-my-data/plugins/lv2` |
|
|
449
|
+
| `clap_path`, `clap_extra` | `$CLAP_PATH`, then `~/.clap`, then `/usr/lib/clap`; none |
|
|
450
|
+
| `pin_file` | `/var/lib/omx/qualified.pins` |
|
|
451
|
+
| `qualification_dir`, `descriptor_dir` | `/var/lib/omx/qualification`, `/var/lib/omx/descriptors` |
|
|
452
|
+
| `state_file` | `/var/lib/omx/qualify-sync.json` |
|
|
453
|
+
| `budget_s` | `60` |
|
|
454
|
+
| `host_profile` | `omx-clap-host` |
|
|
455
|
+
| `hostd_port`, `hostd_feedback_port` | `5555`, the next port |
|
|
456
|
+
|
|
457
|
+
Those are the service's (`--system`); run by a user, the files go under `$XDG_STATE_HOME/omx`. An LV2
|
|
458
|
+
directory added here belongs in plugin-hostd's `lv2_path` too, or the daemon cannot find the bundle it
|
|
459
|
+
pinned. `--force` qualifies everything again.
|
|
460
|
+
|
|
461
|
+
The path unit names the standard folders (`/usr/lib/lv2`, `/usr/lib64/lv2`, `/usr/local/lib/lv2`,
|
|
462
|
+
`/usr/lib/clap`, `/usr/lib64/clap`). The folders the settings file adds (`lv2_path`, `lv2_extra`, `clap_path`,
|
|
463
|
+
`clap_extra`) are written into a drop-in of the unit by a systemd generator at boot and at every
|
|
464
|
+
`systemctl daemon-reload`, so the file is the one place a machine's extra folders are written.
|
|
465
|
+
|
|
353
466
|
### The container image
|
|
354
467
|
|
|
355
468
|
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
/**
|
|
5
|
+
* `plugin-qualify-sync`, the entry point. Every decision is in `src/qualify-sync.ts`; this file reads argv and the
|
|
6
|
+
* settings file, runs one sync, prints its summary line and exits. The exit is 0 whenever the run went through,
|
|
7
|
+
* whatever the plugins' verdicts: a refused plugin is a result, not a failure of the service.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
import { SYNC_CONF, parseSyncArgs, parseSyncConf, resolveSyncSettings, runSync, summaryLine, syncHelpText } from "../dist/index.js";
|
|
12
|
+
|
|
13
|
+
const log = (line) => process.stderr.write(`plugin-qualify-sync: ${line}\n`);
|
|
14
|
+
|
|
15
|
+
const parsed = parseSyncArgs(process.argv.slice(2));
|
|
16
|
+
if (!parsed.ok) {
|
|
17
|
+
log(parsed.error);
|
|
18
|
+
process.stderr.write(`\n${syncHelpText()}`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
if (parsed.args.help) {
|
|
22
|
+
process.stdout.write(syncHelpText());
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
const confPath = parsed.args.config ?? SYNC_CONF;
|
|
26
|
+
let confText = "";
|
|
27
|
+
try {
|
|
28
|
+
confText = readFileSync(confPath, "utf8");
|
|
29
|
+
} catch (e) {
|
|
30
|
+
// the defaults hold without a settings file; a named one that cannot be read is an error
|
|
31
|
+
if (parsed.args.config !== undefined || e.code !== "ENOENT") {
|
|
32
|
+
log(`cannot read ${confPath}: ${e.message}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const conf = parseSyncConf(confText, confPath);
|
|
37
|
+
if (!conf.ok) {
|
|
38
|
+
log(conf.error);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
const resolved = resolveSyncSettings(conf.values, parsed.args, process.env);
|
|
42
|
+
if (!resolved.ok) {
|
|
43
|
+
log(resolved.error);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
const s = resolved.settings;
|
|
47
|
+
log(`CLAP ${s.clapDirs.join(":")}; LV2 ${s.lv2Dirs.join(":")}; host ${s.hostProfile.id}; ${String(s.budgetS)} s a plugin`);
|
|
48
|
+
try {
|
|
49
|
+
const summary = await runSync(s, { force: parsed.args.force, log });
|
|
50
|
+
if (summary !== undefined) {
|
|
51
|
+
process.stdout.write(`${summaryLine(summary)}\n`);
|
|
52
|
+
log(`pins in ${s.pinFile}${summary.sent ? `, ${String(summary.sent)} sent to plugin-hostd` : ""}`);
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
log(`the run stopped: ${e.message}`);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
package/bin/plugin-qualify.mjs
CHANGED
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
* Every named plugin, CLAP or LV2, is measured by plugin-hostd's probe runner (`plugin-hostd-probe`,
|
|
9
9
|
* named by `OMX_PROBE_RUNNER` when it is not on `PATH`), which keeps the deadline and the limits, and is
|
|
10
10
|
* judged from the probe output it writes: the verdict and the qualification record, per host profile.
|
|
11
|
-
* `--all` finds the CLAP plugins on the standard CLAP search path (`CLAP_PATH`, then `~/.clap`, then
|
|
11
|
+
* `--all` finds the LV2 plugins of the bundles on the LV2 path, then the CLAP plugins on the standard CLAP search path (`CLAP_PATH`, then `~/.clap`, then
|
|
12
12
|
* `/usr/lib/clap`). `plugin-qualify judge` judges probe outputs already written.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import { dirname } from "node:path";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
16
15
|
|
|
17
16
|
import {
|
|
17
|
+
adjustHelpText,
|
|
18
|
+
adjustRecords,
|
|
19
|
+
adjustTable,
|
|
20
|
+
adjustedPinEntries,
|
|
21
|
+
parseAdjustArgs,
|
|
22
|
+
readRecords,
|
|
23
|
+
rewriteRecord,
|
|
18
24
|
ClapScanError,
|
|
19
25
|
EXIT_CODE,
|
|
20
26
|
clapLoader,
|
|
@@ -22,6 +28,7 @@ import {
|
|
|
22
28
|
defaultQualificationDir,
|
|
23
29
|
judgeExitCode,
|
|
24
30
|
judgeFiles,
|
|
31
|
+
mergeRunsByRecord,
|
|
25
32
|
judgeHelpText,
|
|
26
33
|
parseJudgeArgs,
|
|
27
34
|
discoverAllTargets,
|
|
@@ -36,6 +43,7 @@ import {
|
|
|
36
43
|
resolveTargets,
|
|
37
44
|
writeQualification,
|
|
38
45
|
textTable,
|
|
46
|
+
writeFileWhole,
|
|
39
47
|
} from "../dist/index.js";
|
|
40
48
|
|
|
41
49
|
/**
|
|
@@ -49,16 +57,7 @@ function writePins(path, file) {
|
|
|
49
57
|
process.stderr.write(`plugin-qualify: no pin was taken, ${path} is left as it was\n`);
|
|
50
58
|
return;
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
// the bytes on disk before the name moves, so a crash leaves the old file or the whole new one
|
|
54
|
-
const fd = openSync(`${path}.new`, "w");
|
|
55
|
-
try {
|
|
56
|
-
writeSync(fd, file.text);
|
|
57
|
-
fsyncSync(fd);
|
|
58
|
-
} finally {
|
|
59
|
-
closeSync(fd);
|
|
60
|
-
}
|
|
61
|
-
renameSync(`${path}.new`, path);
|
|
60
|
+
writeFileWhole(path, file.text);
|
|
62
61
|
process.stderr.write(`plugin-qualify: ${file.written.length} pin${file.written.length === 1 ? "" : "s"} written to ${path}\n`);
|
|
63
62
|
}
|
|
64
63
|
|
|
@@ -89,6 +88,57 @@ if (process.argv[2] === "judge") {
|
|
|
89
88
|
process.exit(judgeExitCode(result));
|
|
90
89
|
}
|
|
91
90
|
|
|
91
|
+
if (process.argv[2] === "adjust") {
|
|
92
|
+
const a = parseAdjustArgs(process.argv.slice(3));
|
|
93
|
+
if (!a.ok) {
|
|
94
|
+
process.stderr.write(`plugin-qualify adjust: ${a.error}\n\n${adjustHelpText()}`);
|
|
95
|
+
process.exit(EXIT_CODE.couldNotMeasure);
|
|
96
|
+
}
|
|
97
|
+
if (a.args.help) {
|
|
98
|
+
process.stdout.write(adjustHelpText());
|
|
99
|
+
process.exit(EXIT_CODE.allQualified);
|
|
100
|
+
}
|
|
101
|
+
const hosts = resolveHostProfiles(a.args.hostProfiles);
|
|
102
|
+
if (!hosts.ok) {
|
|
103
|
+
for (const r of hosts.refused) process.stderr.write(`plugin-qualify adjust: --host-profile ${r.spec}: ${r.problem}\n`);
|
|
104
|
+
process.exit(EXIT_CODE.couldNotMeasure);
|
|
105
|
+
}
|
|
106
|
+
let text;
|
|
107
|
+
try {
|
|
108
|
+
text = readFileSync(a.args.history, "utf8");
|
|
109
|
+
} catch (e) {
|
|
110
|
+
process.stderr.write(`plugin-qualify adjust: --history ${a.args.history}: cannot read: ${e.message}\n`);
|
|
111
|
+
process.exit(EXIT_CODE.couldNotMeasure);
|
|
112
|
+
}
|
|
113
|
+
const dir = a.args.qualificationDir ?? defaultQualificationDir(process.env);
|
|
114
|
+
const read = readRecords(dir);
|
|
115
|
+
for (const f of read.failures) process.stderr.write(`plugin-qualify adjust: ${f.file}: ${f.problem}\n`);
|
|
116
|
+
if (read.records.length === 0) {
|
|
117
|
+
process.stderr.write(`plugin-qualify adjust: no qualification record in ${dir}\n`);
|
|
118
|
+
process.exit(EXIT_CODE.couldNotMeasure);
|
|
119
|
+
}
|
|
120
|
+
const now = a.args.now ?? Math.floor(Date.now() / 1000);
|
|
121
|
+
const done = adjustRecords(read.records, text, { profiles: hosts.profiles, file: a.args.history, now, ...(a.args.hostClass === undefined ? {} : { hostClass: a.args.hostClass }) });
|
|
122
|
+
if (!done.ok) {
|
|
123
|
+
process.stderr.write(`plugin-qualify adjust: ${done.problem}\n`);
|
|
124
|
+
process.exit(EXIT_CODE.couldNotMeasure);
|
|
125
|
+
}
|
|
126
|
+
if (a.args.pins !== undefined && done.hostClasses.length !== 1) {
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
`plugin-qualify adjust: --pins writes one adjusted verdict per plugin, and ${a.args.history} holds ${String(done.hostClasses.length)} host classes (${done.hostClasses.join(", ")}); name one with --host-class\n`,
|
|
129
|
+
);
|
|
130
|
+
process.exit(EXIT_CODE.couldNotMeasure);
|
|
131
|
+
}
|
|
132
|
+
for (const i of done.ignored) process.stderr.write(`plugin-qualify adjust: ${a.args.history}:${String(i.line)}: not read: ${i.problem}\n`);
|
|
133
|
+
for (const r of done.records) {
|
|
134
|
+
rewriteRecord(r.path, r.record);
|
|
135
|
+
process.stderr.write(`plugin-qualify adjust: record ${r.path}\n`);
|
|
136
|
+
}
|
|
137
|
+
if (a.args.pins !== undefined) writePins(a.args.pins, pinsFile(adjustedPinEntries(done.records, done.hostClasses[0], hosts.profiles[0].id)));
|
|
138
|
+
process.stdout.write(a.args.format === "json" ? `${JSON.stringify(done.records.map((r) => r.record), null, 2)}\n` : adjustTable(done.records));
|
|
139
|
+
process.exit(EXIT_CODE.allQualified);
|
|
140
|
+
}
|
|
141
|
+
|
|
92
142
|
const parsed = parseQualifyArgs(process.argv.slice(2));
|
|
93
143
|
if (!parsed.ok) {
|
|
94
144
|
process.stderr.write(`plugin-qualify: ${parsed.error}\n\n${helpText()}`);
|
|
@@ -126,12 +176,13 @@ if (plugins.length === 0) {
|
|
|
126
176
|
process.exit(EXIT_CODE.couldNotMeasure);
|
|
127
177
|
}
|
|
128
178
|
if (parsed.args.all) {
|
|
129
|
-
|
|
179
|
+
const lv2 = plugins.filter((p) => p.format === "lv2").length;
|
|
180
|
+
process.stderr.write(`plugin-qualify: --all qualifies ${String(lv2)} LV2 and ${String(plugins.length - lv2)} CLAP plugins on the search paths\n`);
|
|
130
181
|
}
|
|
131
182
|
const pass = qualifyTargets(plugins, { rates: parsed.args.rates, profiles: profiles.profiles });
|
|
132
183
|
for (const f of pass.failures) process.stderr.write(`plugin-qualify: ${f.uri}: ${f.problem}\n`);
|
|
133
184
|
if (parsed.args.out !== undefined) {
|
|
134
|
-
for (const run of pass.runs) {
|
|
185
|
+
for (const run of mergeRunsByRecord(pass.runs)) {
|
|
135
186
|
const w = writeQualification(parsed.args.out, run.bytes, run.judgment);
|
|
136
187
|
process.stderr.write(`plugin-qualify: record ${w.recordPath}\n`);
|
|
137
188
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type HistoryRead, type HistoryRecord } from "./history-file.js";
|
|
2
|
+
import type { HostProfile } from "./host-profiles.js";
|
|
3
|
+
import { type PinEntry, type PinVerdict } from "./pins-file.js";
|
|
4
|
+
import { type HistoryBlock, type QualificationRecord } from "./qualification-record.js";
|
|
5
|
+
import { type OutputFormat } from "./qualify-options.js";
|
|
6
|
+
export interface AdjustArgs {
|
|
7
|
+
readonly history: string;
|
|
8
|
+
readonly hostProfiles: readonly string[];
|
|
9
|
+
readonly qualificationDir?: string;
|
|
10
|
+
readonly hostClass?: string;
|
|
11
|
+
readonly pins?: string;
|
|
12
|
+
/** UTC seconds the history is read at; the clock when absent. */
|
|
13
|
+
readonly now?: number;
|
|
14
|
+
readonly format: OutputFormat;
|
|
15
|
+
readonly help: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type ParseAdjustResult = {
|
|
18
|
+
ok: true;
|
|
19
|
+
args: AdjustArgs;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function parseAdjustArgs(argv: readonly string[]): ParseAdjustResult;
|
|
25
|
+
export declare function adjustHelpText(): string;
|
|
26
|
+
/** A record read from the qualification directory, with the file it came from. */
|
|
27
|
+
export interface RecordFile {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly record: QualificationRecord;
|
|
30
|
+
}
|
|
31
|
+
/** Every `*.qualification.json` of the directory with this reader's schema; a file that does not read is a failure, and the others go on. */
|
|
32
|
+
export declare function readRecords(dir: string, read?: (path: string) => string): {
|
|
33
|
+
readonly records: readonly RecordFile[];
|
|
34
|
+
readonly failures: readonly {
|
|
35
|
+
readonly file: string;
|
|
36
|
+
readonly problem: string;
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
/** plugin-hostd's key of a record's plugin: its uri and the digest of the first file of its pin (files sorted by path, as the pin lists them). `undefined` when the record holds no whole pin. */
|
|
40
|
+
export declare function historyKeyOf(record: QualificationRecord): {
|
|
41
|
+
readonly uri: string;
|
|
42
|
+
readonly digest: string;
|
|
43
|
+
} | undefined;
|
|
44
|
+
/** The `history` block of one record for one host class. */
|
|
45
|
+
export declare function historyBlock(record: QualificationRecord, history: readonly HistoryRecord[], hostClass: string, profiles: readonly HostProfile[], options: {
|
|
46
|
+
file: string;
|
|
47
|
+
now: number;
|
|
48
|
+
}): HistoryBlock;
|
|
49
|
+
export interface AdjustedRecord {
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly record: QualificationRecord;
|
|
52
|
+
}
|
|
53
|
+
export type AdjustResult = {
|
|
54
|
+
readonly ok: true;
|
|
55
|
+
readonly records: readonly AdjustedRecord[];
|
|
56
|
+
/** The host classes read. */
|
|
57
|
+
readonly hostClasses: readonly string[];
|
|
58
|
+
readonly ignored: Extract<HistoryRead, {
|
|
59
|
+
ok: true;
|
|
60
|
+
}>["ignored"];
|
|
61
|
+
} | {
|
|
62
|
+
readonly ok: false;
|
|
63
|
+
readonly problem: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Each record with its `history` block replaced by the blocks of the host classes read (an earlier block of a class not read
|
|
67
|
+
* is kept, so two histories of two classes can be applied one after the other). The measured fields are copied as they are.
|
|
68
|
+
*/
|
|
69
|
+
export declare function adjustRecords(records: readonly RecordFile[], historyText: string, options: {
|
|
70
|
+
profiles: readonly HostProfile[];
|
|
71
|
+
file: string;
|
|
72
|
+
now: number;
|
|
73
|
+
hostClass?: string;
|
|
74
|
+
}): AdjustResult;
|
|
75
|
+
/** The adjusted verdict of a record's host for a host class: the pin verdict, `null` when it has none, `undefined` when the record holds no block for the pair. */
|
|
76
|
+
export declare function adjustedVerdictOf(record: QualificationRecord, hostClass: string, host: string): PinVerdict | null | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The pin entries of adjusted records for one host and one host class: the integrity is the record's, the verdict is the
|
|
79
|
+
* ADJUSTED one. A record with no verdict row for the host is pinned without a verdict word, as a run without one is.
|
|
80
|
+
*/
|
|
81
|
+
export declare function adjustedPinEntries(records: readonly AdjustedRecord[], hostClass: string, host: string): PinEntry[];
|
|
82
|
+
/** One row per plugin, host and host class: the measured verdict, the adjusted one, the reason. */
|
|
83
|
+
export declare function adjustTable(records: readonly AdjustedRecord[]): string;
|