@openmixer/plugin-qualify 0.17.1 → 0.17.3
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 +13 -6
- package/bin/plugin-qualify-sync.mjs +1 -1
- package/dist/host-admission.d.ts +25 -6
- package/dist/host-admission.js +45 -17
- package/dist/host-admission.js.map +1 -1
- package/dist/hostd-pin-files.d.ts +25 -0
- package/dist/hostd-pin-files.js +58 -0
- package/dist/hostd-pin-files.js.map +1 -0
- package/dist/probe-judge.d.ts +2 -0
- package/dist/probe-judge.js +3 -2
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output-fixtures.js +4 -1
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +2 -1
- package/dist/probe-output.js +4 -1
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +5 -0
- package/dist/probe-runner.js +8 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +16 -1
- package/dist/qualification-record.js +8 -2
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-cli.js +10 -4
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-options.js +3 -3
- package/dist/qualify-options.js.map +1 -1
- package/dist/qualify-sync.d.ts +26 -4
- package/dist/qualify-sync.js +191 -53
- package/dist/qualify-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/host-admission.ts +69 -23
- package/src/hostd-pin-files.ts +75 -0
- package/src/probe-judge.ts +5 -1
- package/src/probe-output-fixtures.ts +4 -1
- package/src/probe-output.ts +6 -2
- package/src/probe-runner.ts +13 -3
- package/src/qualification-record.ts +16 -2
- package/src/qualify-cli.ts +8 -4
- package/src/qualify-options.ts +3 -3
- package/src/qualify-sync.ts +197 -52
- package/tools/descriptors.py +8 -1
- package/tools/mod-host-admit.mjs +3 -1
package/src/host-admission.ts
CHANGED
|
@@ -22,9 +22,16 @@ export function admittedHostOf(profile: HostProfile): AdmittedHost | undefined {
|
|
|
22
22
|
return ADMITTED_HOSTS.find((h) => h === profile.id);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* plugin-hostd's LV2 adapter, omx-clap-host: an LV2 plugin is run under it once, as under mod-host, and the record
|
|
27
|
+
* carries the outcome as its own step beside the verdicts (plugin-qualify#223), which it does not change: the
|
|
28
|
+
* omx-clap-host verdict of an LV2 plugin stays the in-process measurement. It is what the removal of mod-host counts.
|
|
29
|
+
*/
|
|
30
|
+
export const ADAPTER_HOST = "omx-clap-host";
|
|
31
|
+
|
|
25
32
|
/** What running the plugin under one host came to. */
|
|
26
33
|
export interface HostAdmission {
|
|
27
|
-
readonly host: AdmittedHost;
|
|
34
|
+
readonly host: AdmittedHost | typeof ADAPTER_HOST;
|
|
28
35
|
/** False when the host could not be run (not installed): there is no evidence either way. */
|
|
29
36
|
readonly measured: boolean;
|
|
30
37
|
readonly admitted: boolean;
|
|
@@ -33,15 +40,16 @@ export interface HostAdmission {
|
|
|
33
40
|
readonly params: Readonly<Record<string, string | number>>;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
const
|
|
43
|
+
type ProtocolHost = "mod-host" | typeof ADAPTER_HOST;
|
|
44
|
+
const admitted = (host: HostAdmission["host"]): HostAdmission => ({ host, measured: true, admitted: true, params: {} });
|
|
45
|
+
const refused = (host: HostAdmission["host"], code: string, params: HostAdmission["params"] = {}): HostAdmission => ({
|
|
38
46
|
host,
|
|
39
47
|
measured: true,
|
|
40
48
|
admitted: false,
|
|
41
49
|
code: `${host}.${code}`,
|
|
42
50
|
params,
|
|
43
51
|
});
|
|
44
|
-
const notInstalled = (host:
|
|
52
|
+
const notInstalled = (host: HostAdmission["host"]): HostAdmission => ({ host, measured: false, admitted: false, code: `${host}.not-installed`, params: {} });
|
|
45
53
|
|
|
46
54
|
/** One command mod-host was sent and what came of it: its `resp` number, or how the host died. */
|
|
47
55
|
export interface ModHostStep {
|
|
@@ -57,20 +65,25 @@ export interface ModHostRun {
|
|
|
57
65
|
readonly steps: readonly ModHostStep[];
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
/** The verdict of a mod-host
|
|
61
|
-
export function
|
|
62
|
-
if (!run.installed) return notInstalled(
|
|
68
|
+
/** The verdict of a run of a host that speaks mod-host's protocol: the first step that did not answer `resp 0` decides. */
|
|
69
|
+
export function classifyProtocolRun(host: ProtocolHost, run: ModHostRun): HostAdmission {
|
|
70
|
+
if (!run.installed) return notInstalled(host);
|
|
63
71
|
for (const s of run.steps) {
|
|
64
72
|
if (s.died !== undefined) {
|
|
65
|
-
if (s.died.startsWith("SIG")) return refused(
|
|
66
|
-
return s.cmd === "add" ? refused(
|
|
73
|
+
if (s.died.startsWith("SIG")) return refused(host, `signal-${s.died}`, { step: s.cmd });
|
|
74
|
+
return s.cmd === "add" ? refused(host, "died-on-add", { how: s.died }) : refused(host, "died", { step: s.cmd, how: s.died });
|
|
67
75
|
}
|
|
68
|
-
if (s.resp === undefined) return refused(
|
|
69
|
-
//
|
|
70
|
-
if (s.resp < 0) return refused(
|
|
76
|
+
if (s.resp === undefined) return refused(host, "no-answer", { step: s.cmd });
|
|
77
|
+
// the host answers an error as a negative number (`resp -102`); the code carries its magnitude
|
|
78
|
+
if (s.resp < 0) return refused(host, `resp-${-s.resp}`, { step: s.cmd, resp: s.resp });
|
|
71
79
|
}
|
|
72
|
-
if (run.steps.length === 0) return refused(
|
|
73
|
-
return admitted(
|
|
80
|
+
if (run.steps.length === 0) return refused(host, "no-answer");
|
|
81
|
+
return admitted(host);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The verdict of a mod-host run. */
|
|
85
|
+
export function classifyModHostRun(run: ModHostRun): HostAdmission {
|
|
86
|
+
return classifyProtocolRun("mod-host", run);
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/** What `jalv -n <name> <uri>` did in its window. */
|
|
@@ -101,36 +114,69 @@ export const JALV_HOSTED_MS = 3000;
|
|
|
101
114
|
/** The wall clock of one whole mod-host admission, in milliseconds. */
|
|
102
115
|
export const MOD_HOST_ADMISSION_MS = 30000;
|
|
103
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The runs of one plugin under the installed hosts. `env` is the environment each host starts with: its `LV2_PATH`
|
|
119
|
+
* must be the device's LV2 directories, the ones the host sees in use (plugin-qualify#224: a mod-host started with no
|
|
120
|
+
* `LV2_PATH` sees only lilv's defaults and answers -101 for a plugin it runs every day).
|
|
121
|
+
*/
|
|
104
122
|
export interface AdmissionRuns {
|
|
105
|
-
readonly modHost: (uri: string) => ModHostRun;
|
|
106
|
-
readonly jalv: (uri: string) => JalvRun;
|
|
123
|
+
readonly modHost: (uri: string, env?: Readonly<Record<string, string | undefined>>) => ModHostRun;
|
|
124
|
+
readonly jalv: (uri: string, env?: Readonly<Record<string, string | undefined>>) => JalvRun;
|
|
125
|
+
/** The run under plugin-hostd's LV2 adapter; a set without it records no adapter step. */
|
|
126
|
+
readonly adapter?: (uri: string, env?: Readonly<Record<string, string | undefined>>) => ModHostRun;
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
const MOD_HOST_ADMIT = fileURLToPath(new URL("../tools/mod-host-admit.mjs", import.meta.url));
|
|
110
130
|
|
|
111
131
|
/** The runs against the installed hosts. */
|
|
112
132
|
export const SPAWNED_ADMISSIONS: AdmissionRuns = {
|
|
113
|
-
|
|
114
|
-
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri], { encoding: "utf8", timeout: MOD_HOST_ADMISSION_MS });
|
|
133
|
+
adapter(uri, env) {
|
|
134
|
+
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri, ADAPTER_HOST], { encoding: "utf8", timeout: MOD_HOST_ADMISSION_MS, ...(env === undefined ? {} : { env: { ...env } }) });
|
|
135
|
+
if (r.error !== undefined || r.status !== 0) {
|
|
136
|
+
throw new Error(`${ADAPTER_HOST} admission of ${uri} gave no answer (${r.error?.message ?? `exit ${String(r.status)}`}): ${(r.stderr ?? "").trim()}`);
|
|
137
|
+
}
|
|
138
|
+
return JSON.parse(r.stdout) as ModHostRun;
|
|
139
|
+
},
|
|
140
|
+
modHost(uri, env) {
|
|
141
|
+
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri], { encoding: "utf8", timeout: MOD_HOST_ADMISSION_MS, ...(env === undefined ? {} : { env: { ...env } }) });
|
|
115
142
|
if (r.error !== undefined || r.status !== 0) {
|
|
116
143
|
throw new Error(`mod-host admission of ${uri} gave no answer (${r.error?.message ?? `exit ${String(r.status)}`}): ${(r.stderr ?? "").trim()}`);
|
|
117
144
|
}
|
|
118
145
|
return JSON.parse(r.stdout) as ModHostRun;
|
|
119
146
|
},
|
|
120
|
-
jalv(uri) {
|
|
121
|
-
const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, input: "" });
|
|
147
|
+
jalv(uri, env) {
|
|
148
|
+
const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, input: "", ...(env === undefined ? {} : { env: { ...env } }) });
|
|
122
149
|
const code = (r.error as NodeJS.ErrnoException | undefined)?.code;
|
|
123
150
|
if (code === "ENOENT") return { installed: false, stillRunning: false, status: null, signal: null, stderr: "" };
|
|
124
151
|
return { installed: true, stillRunning: code === "ETIMEDOUT", status: r.status, signal: code === "ETIMEDOUT" ? null : r.signal, stderr: r.stderr ?? "" };
|
|
125
152
|
},
|
|
126
153
|
};
|
|
127
154
|
|
|
128
|
-
/** One admission of `uri` under `host
|
|
129
|
-
export function admitPlugin(
|
|
155
|
+
/** One admission of `uri` under `host`, the host started with `env` (its `LV2_PATH` the device's LV2 directories). */
|
|
156
|
+
export function admitPlugin(
|
|
157
|
+
host: AdmittedHost,
|
|
158
|
+
uri: string,
|
|
159
|
+
runs: AdmissionRuns = SPAWNED_ADMISSIONS,
|
|
160
|
+
env?: Readonly<Record<string, string | undefined>>,
|
|
161
|
+
): HostAdmission {
|
|
130
162
|
try {
|
|
131
|
-
return host === "mod-host" ? classifyModHostRun(runs.modHost(uri)) : classifyJalvRun(runs.jalv(uri));
|
|
163
|
+
return host === "mod-host" ? classifyModHostRun(runs.modHost(uri, env)) : classifyJalvRun(runs.jalv(uri, env));
|
|
132
164
|
} catch (e) {
|
|
133
165
|
// the run itself broke: nothing was learned about the plugin, so nothing is rated
|
|
134
166
|
return { host, measured: false, admitted: false, code: `${host}.admission-failed`, params: { problem: e instanceof Error ? e.message : String(e) } };
|
|
135
167
|
}
|
|
136
168
|
}
|
|
169
|
+
|
|
170
|
+
/** The run of an LV2 plugin under plugin-hostd's LV2 adapter, `undefined` when the runs have none (plugin-qualify#223). */
|
|
171
|
+
export function admitUnderAdapter(
|
|
172
|
+
uri: string,
|
|
173
|
+
runs: AdmissionRuns = SPAWNED_ADMISSIONS,
|
|
174
|
+
env?: Readonly<Record<string, string | undefined>>,
|
|
175
|
+
): HostAdmission | undefined {
|
|
176
|
+
if (runs.adapter === undefined) return undefined;
|
|
177
|
+
try {
|
|
178
|
+
return classifyProtocolRun(ADAPTER_HOST, runs.adapter(uri, env));
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return { host: ADAPTER_HOST, measured: false, admitted: false, code: `${ADAPTER_HOST}.admission-failed`, params: { problem: e instanceof Error ? e.message : String(e) } };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The files a pin holds, as plugin-hostd collects them (plugin-qualify#228). One definition of a plugin's files: the
|
|
5
|
+
* daemon's, the list an add checks before it hashes them (`pins_check`), which `plugin-hostd --pin-files <uri>` prints
|
|
6
|
+
* (plugin-hostd 0.15.0 and later). This module never works the list out itself: it runs the daemon on the device's LV2
|
|
7
|
+
* directories, reads `root <dir>` and the `file <name>` lines, and hashes exactly those files.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
/** The daemon binary, else `plugin-hostd` on the PATH. */
|
|
16
|
+
export const HOSTD_ENV = "PLUGIN_HOSTD";
|
|
17
|
+
|
|
18
|
+
export interface PinFilesRunResult {
|
|
19
|
+
readonly status: number | null;
|
|
20
|
+
readonly stdout: string;
|
|
21
|
+
readonly stderr: string;
|
|
22
|
+
/** The daemon is not installed. */
|
|
23
|
+
readonly missing?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One run of `plugin-hostd --pin-files <uri>` with `lv2_path` set to `lv2Dirs`. */
|
|
27
|
+
export type PinFilesRun = (uri: string, lv2Dirs: readonly string[], env: Readonly<Record<string, string | undefined>>) => PinFilesRunResult;
|
|
28
|
+
|
|
29
|
+
export const SPAWNED_PIN_FILES: PinFilesRun = (uri, lv2Dirs, env) => {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), "plugin-qualify-pin-files-"));
|
|
31
|
+
try {
|
|
32
|
+
const conf = join(dir, "plugin-hostd.conf");
|
|
33
|
+
writeFileSync(conf, `lv2_path ${lv2Dirs.join(":")}\n`);
|
|
34
|
+
const r = spawnSync(env[HOSTD_ENV] ?? "plugin-hostd", ["-c", conf, "--pin-files", uri], { encoding: "utf8", timeout: 30000, env: { ...env } });
|
|
35
|
+
const missing = (r.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
36
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", ...(missing ? { missing } : {}) };
|
|
37
|
+
} finally {
|
|
38
|
+
rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type PinFiles =
|
|
43
|
+
| { readonly ok: true; readonly root: string; readonly files: readonly { readonly path: string; readonly sha256: string }[] }
|
|
44
|
+
| { readonly ok: false; readonly detail: string };
|
|
45
|
+
|
|
46
|
+
/** The files plugin-hostd collects for `uri`, each hashed; why not, when it cannot say. */
|
|
47
|
+
export function hostdPinFiles(
|
|
48
|
+
uri: string,
|
|
49
|
+
lv2Dirs: readonly string[],
|
|
50
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
51
|
+
run: PinFilesRun = SPAWNED_PIN_FILES,
|
|
52
|
+
read: (path: string) => Buffer = (p) => readFileSync(p),
|
|
53
|
+
): PinFiles {
|
|
54
|
+
const r = run(uri, lv2Dirs, env);
|
|
55
|
+
if (r.missing) return { ok: false, detail: "plugin-hostd is not installed: the files of a pin are plugin-hostd's to name" };
|
|
56
|
+
if (r.status !== 0) {
|
|
57
|
+
const why = r.stdout.startsWith("Usage:")
|
|
58
|
+
? "this plugin-hostd has no --pin-files (plugin-hostd 0.15.0 or later names the files of a pin)"
|
|
59
|
+
: r.stderr.trim().split("\n").pop() || `plugin-hostd --pin-files exited ${String(r.status)}`;
|
|
60
|
+
return { ok: false, detail: why };
|
|
61
|
+
}
|
|
62
|
+
let root: string | undefined;
|
|
63
|
+
const names: string[] = [];
|
|
64
|
+
for (const line of r.stdout.split("\n")) {
|
|
65
|
+
if (line.startsWith("root ")) root = line.slice(5);
|
|
66
|
+
else if (line.startsWith("file ")) names.push(line.slice(5));
|
|
67
|
+
}
|
|
68
|
+
if (root === undefined || names.length === 0) return { ok: false, detail: "plugin-hostd --pin-files printed no root or no file" };
|
|
69
|
+
try {
|
|
70
|
+
const files = names.map((path) => ({ path, sha256: createHash("sha256").update(read(join(root!, path))).digest("hex") }));
|
|
71
|
+
return { ok: true, root, files: files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) };
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return { ok: false, detail: `a file plugin-hostd names cannot be read: ${e instanceof Error ? e.message : String(e)}` };
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/probe-judge.ts
CHANGED
|
@@ -133,7 +133,9 @@ export function clapLineFromProbeOutput(output: ProbeOutput, facts: JudgeFacts =
|
|
|
133
133
|
const points = okPoints(output);
|
|
134
134
|
const latency: Record<string, { declaredFrames: number; measuredFrames: number }> = {};
|
|
135
135
|
for (const p of points) {
|
|
136
|
-
if (p.latency !== undefined && latency[String(p.rate)] === undefined)
|
|
136
|
+
if (p.latency?.measuredFrames !== undefined && latency[String(p.rate)] === undefined) {
|
|
137
|
+
latency[String(p.rate)] = { declaredFrames: p.latency.declaredFrames, measuredFrames: p.latency.measuredFrames };
|
|
138
|
+
}
|
|
137
139
|
}
|
|
138
140
|
const withRt = points.filter((p) => p.rtSafety !== undefined);
|
|
139
141
|
return {
|
|
@@ -188,6 +190,8 @@ export interface HostJudgment {
|
|
|
188
190
|
|
|
189
191
|
export interface Judgment {
|
|
190
192
|
readonly output: ProbeOutput;
|
|
193
|
+
/** An LV2 plugin's run under plugin-hostd's LV2 adapter, beside the verdicts (plugin-qualify#223). */
|
|
194
|
+
readonly adapter?: HostAdmission;
|
|
191
195
|
readonly hosts: readonly HostJudgment[];
|
|
192
196
|
/** For a CLAP plugin, the document of each host that today's judge writes; the pins are taken from these. */
|
|
193
197
|
readonly documents: readonly RtSafetyDocument[];
|
|
@@ -198,7 +198,10 @@ export function probeOutputOfLv2Line(line: Lv2MeasureLine): Record<string, any>
|
|
|
198
198
|
method: latency.from === "note" ? "note-onset" : "impulse-onset",
|
|
199
199
|
...(latency.from === "note" ? { heard: true } : {}),
|
|
200
200
|
};
|
|
201
|
-
} else
|
|
201
|
+
} else {
|
|
202
|
+
absent("latency", latency);
|
|
203
|
+
if (latency?.declaredFrames !== undefined) point["latency"] = { declaredFrames: latency.declaredFrames };
|
|
204
|
+
}
|
|
202
205
|
if (cost?.nsPerSampleMedian !== undefined) {
|
|
203
206
|
steps.push({ step: "cost", rate, quantum, outcome: "probe.ok", elapsedMs: 1 });
|
|
204
207
|
point["cost"] = {
|
package/src/probe-output.ts
CHANGED
|
@@ -91,7 +91,8 @@ export interface ProbeGridPoint {
|
|
|
91
91
|
readonly rate: number;
|
|
92
92
|
readonly quantum: number;
|
|
93
93
|
readonly outcome: string;
|
|
94
|
-
|
|
94
|
+
/** `measuredFrames` is absent where the probe did not measure; the declared figure is kept whenever the plugin loaded (#201). */
|
|
95
|
+
readonly latency?: { readonly declaredFrames: number; readonly measuredFrames?: number };
|
|
95
96
|
readonly cost?: ProbeCost;
|
|
96
97
|
readonly rtSafety?: ProbeRtSafety;
|
|
97
98
|
readonly noteSweep?: ProbeNoteSweep;
|
|
@@ -296,7 +297,10 @@ function readPoint(v: unknown, i: number): ProbeGridPoint {
|
|
|
296
297
|
let latency: ProbeGridPoint["latency"];
|
|
297
298
|
if (latencyIn !== undefined) {
|
|
298
299
|
const l = record(latencyIn, `${where} latency`);
|
|
299
|
-
latency = {
|
|
300
|
+
latency = {
|
|
301
|
+
declaredFrames: count(l, "declaredFrames", `${where} latency`),
|
|
302
|
+
...(l["measuredFrames"] === undefined ? {} : { measuredFrames: count(l, "measuredFrames", `${where} latency`) }),
|
|
303
|
+
};
|
|
300
304
|
}
|
|
301
305
|
return {
|
|
302
306
|
rate: posInt(o, "rate", where),
|
package/src/probe-runner.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { HostProfile } from "./host-profiles.js";
|
|
|
22
22
|
import { OPENMIXER_CONSOLE_PROFILE, shippedHostProfile } from "./host-profiles.js";
|
|
23
23
|
import { IN_PROCESS_TIER_HOST, ISOLATED_TIER_HOST } from "./qualification-record.js";
|
|
24
24
|
import type { PluginTarget } from "./loader.js";
|
|
25
|
-
import { SPAWNED_ADMISSIONS, admitPlugin, admittedHostOf, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
|
|
25
|
+
import { SPAWNED_ADMISSIONS, admitPlugin, admitUnderAdapter, admittedHostOf, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
|
|
26
26
|
import { judgeProbeOutput, judgmentVerdicts, type Judgment } from "./probe-judge.js";
|
|
27
27
|
import { readProbeOutput, type ProbeOutput } from "./probe-output.js";
|
|
28
28
|
import { budgetCells, gridCells, runnerInvocations, type GridCell, type GridSpec, type RunnerInvocation } from "./measurement-grid.js";
|
|
@@ -261,6 +261,11 @@ export function qualifyTargets(
|
|
|
261
261
|
options: ProbeTargetOptions & {
|
|
262
262
|
readonly profiles?: readonly HostProfile[];
|
|
263
263
|
readonly admissions?: AdmissionRuns;
|
|
264
|
+
/**
|
|
265
|
+
* The LV2 directories the hosts of an admission see (`LV2_PATH`), the device's as the sync's settings name them;
|
|
266
|
+
* absent, they get the run's environment as it is.
|
|
267
|
+
*/
|
|
268
|
+
readonly admissionLv2Path?: readonly string[];
|
|
264
269
|
/** How the grid is chosen; with it, each target's cells come from the hosts it is judged under. */
|
|
265
270
|
readonly grid?: GridSpec;
|
|
266
271
|
/** The quanta of a `full` grid; else `QUALIFY_QUANTA`. */
|
|
@@ -271,6 +276,8 @@ export function qualifyTargets(
|
|
|
271
276
|
): ProbePass {
|
|
272
277
|
const profiles = options.profiles ?? [OPENMIXER_CONSOLE_PROFILE];
|
|
273
278
|
const admissionRuns = options.admissions ?? SPAWNED_ADMISSIONS;
|
|
279
|
+
const runEnvironment = options.env ?? process.env;
|
|
280
|
+
const admissionEnv = options.admissionLv2Path === undefined ? runEnvironment : { ...runEnvironment, LV2_PATH: options.admissionLv2Path.join(":") };
|
|
274
281
|
const runs: { bytes: Buffer; judgment: Judgment }[] = [];
|
|
275
282
|
const verdicts: QualifyVerdict[] = [];
|
|
276
283
|
const documents: RtSafetyDocument[] = [];
|
|
@@ -289,10 +296,12 @@ export function qualifyTargets(
|
|
|
289
296
|
const measured = cells === undefined ? {} : { cells: budgetCells(cells, options.cellsPerPluginBudget) };
|
|
290
297
|
// an LV2 plugin is run under each measured host once, whichever probe output it is judged with
|
|
291
298
|
const admissions: Record<string, HostAdmission> = {};
|
|
299
|
+
// an LV2 plugin is also run once under plugin-hostd's LV2 adapter, recorded beside the verdicts (#223)
|
|
300
|
+
const adapter = target.format === "lv2" ? admitUnderAdapter(target.uri, admissionRuns, admissionEnv) : undefined;
|
|
292
301
|
if (target.format === "lv2") {
|
|
293
302
|
for (const profile of judgedProfiles) {
|
|
294
303
|
const host = admittedHostOf(profile);
|
|
295
|
-
if (host !== undefined) admissions[profile.id] = admitPlugin(host, target.uri, admissionRuns);
|
|
304
|
+
if (host !== undefined) admissions[profile.id] = admitPlugin(host, target.uri, admissionRuns, admissionEnv);
|
|
296
305
|
}
|
|
297
306
|
}
|
|
298
307
|
for (const profile of judgedProfiles) {
|
|
@@ -309,7 +318,8 @@ export function qualifyTargets(
|
|
|
309
318
|
try {
|
|
310
319
|
for (const { offer, profiles: forOffer } of byOffer.values()) {
|
|
311
320
|
const probed = probeTarget(target, { ...options, ...measured, offer });
|
|
312
|
-
const
|
|
321
|
+
const plain = judgeProbeOutput(probed.output, forOffer, { admissions });
|
|
322
|
+
const judgment = adapter === undefined ? plain : { ...plain, adapter };
|
|
313
323
|
targetRuns.push({ bytes: probed.bytes, judgment });
|
|
314
324
|
for (const v of judgmentVerdicts(judgment)) judged.set(v.host, v);
|
|
315
325
|
}
|
|
@@ -93,13 +93,15 @@ export function unmeasuredFigures(output: ProbeOutput): readonly UnmeasuredFigur
|
|
|
93
93
|
|
|
94
94
|
/** A cell's latency when the run took no figure: the contract's unmeasured word and the hosting code a reader classifies by. */
|
|
95
95
|
export interface UnmeasuredLatency {
|
|
96
|
+
/** The figure the plugin declared (LV2 latency port, CLAP latency extension) when it loaded but the probe did not measure. */
|
|
97
|
+
readonly declaredFrames?: number;
|
|
96
98
|
readonly unmeasuredReason: string;
|
|
97
99
|
readonly code: string;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/** A grid point as the record writes it: every cell carries `latency`, the figures or why there are none (#201). */
|
|
101
103
|
export type RecordGridPoint = Omit<ProbeGridPoint, "latency"> & {
|
|
102
|
-
readonly latency: NonNullable<ProbeGridPoint["latency"]> | UnmeasuredLatency;
|
|
104
|
+
readonly latency: (Omit<NonNullable<ProbeGridPoint["latency"]>, "measuredFrames"> & { readonly measuredFrames: number }) | UnmeasuredLatency;
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
const UNMEASURED_WORDS: ReadonlySet<string> = new Set(Object.values(MEASUREMENT_UNMEASURED_REASONS).map((r) => r.word));
|
|
@@ -120,7 +122,13 @@ const OUTCOME_WORD: Readonly<Record<string, string>> = {
|
|
|
120
122
|
* older than the latency arm writes it.
|
|
121
123
|
*/
|
|
122
124
|
export function latencyOfCell(output: ProbeOutput, point: ProbeGridPoint): RecordGridPoint["latency"] {
|
|
123
|
-
|
|
125
|
+
const had = point.latency;
|
|
126
|
+
if (had?.measuredFrames !== undefined) return { ...had, measuredFrames: had.measuredFrames };
|
|
127
|
+
const why = unmeasuredOfCell(output, point);
|
|
128
|
+
return had === undefined ? why : { declaredFrames: had.declaredFrames, ...why };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unmeasuredOfCell(output: ProbeOutput, point: ProbeGridPoint): UnmeasuredLatency {
|
|
124
132
|
const code = HOSTING_CODE.latencyUnmeasured;
|
|
125
133
|
if (point.outcome !== PROBE_OK) return { unmeasuredReason: OUTCOME_WORD[point.outcome] ?? word("probeError"), code };
|
|
126
134
|
const step = output.steps.find((st) => st.step === "latency" && st.rate === point.rate && (st.quantum === undefined || st.quantum === point.quantum));
|
|
@@ -162,6 +170,11 @@ export interface QualificationRecord {
|
|
|
162
170
|
readonly testedVersion?: string;
|
|
163
171
|
readonly binary: ProbeOutput["binary"];
|
|
164
172
|
readonly verdicts: readonly RecordVerdict[];
|
|
173
|
+
/**
|
|
174
|
+
* An LV2 plugin run under plugin-hostd's LV2 adapter (omx-clap-host), as under mod-host: admitted, or refused with
|
|
175
|
+
* its code, or `measured: false` when it was not tried. Beside the verdicts, which it does not change (#223).
|
|
176
|
+
*/
|
|
177
|
+
readonly adapter?: { readonly host: string; readonly measured: boolean; readonly admitted: boolean; readonly code?: string; readonly params: Readonly<Record<string, string | number>> };
|
|
165
178
|
readonly tiers: RecordTiers;
|
|
166
179
|
/** The cost reasons at each host's rate and quantum, beside the verdicts; an instrument's with the notes it held. */
|
|
167
180
|
readonly cost: readonly {
|
|
@@ -266,6 +279,7 @@ export function buildQualificationRecord(
|
|
|
266
279
|
...(h.verdict.deciding === undefined ? {} : { deciding: h.verdict.deciding }),
|
|
267
280
|
reasons: h.reasons,
|
|
268
281
|
})),
|
|
282
|
+
...(judgment.adapter === undefined ? {} : { adapter: judgment.adapter }),
|
|
269
283
|
tiers: tiersOf(judgment),
|
|
270
284
|
cost: judgment.hosts.map((h) => ({ host: h.verdict.host, ...h.cost })),
|
|
271
285
|
measurements: {
|
package/src/qualify-cli.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { classifyPluginHosting, type QualifiedPlugin } from "./hosting-suitabili
|
|
|
20
20
|
import type { SuitabilityRating } from "./rating.js";
|
|
21
21
|
import type { PluginLoader, PluginTarget } from "./loader.js";
|
|
22
22
|
import { EXIT_CODE, OUTPUT_FORMATS, QUALIFY_OPTIONS, type OutputFormat } from "./qualify-options.js";
|
|
23
|
-
import {
|
|
23
|
+
import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
24
24
|
import { parseMemoryLimit } from "./memory-bound.js";
|
|
25
25
|
|
|
26
26
|
export interface QualifyArgs {
|
|
@@ -91,7 +91,7 @@ export function parseQualifyArgs(argv: readonly string[]): ParseResult {
|
|
|
91
91
|
if (quanta === undefined) return { ok: false, error: "--quanta must be positive integers, comma-separated" };
|
|
92
92
|
const gridText = seen.get("--grid") as string | undefined;
|
|
93
93
|
if (gridText !== undefined && seen.has("--grid-hosts") && gridText.trim() !== "hosts") return { ok: false, error: "--grid-hosts and --grid name two grids; give one" };
|
|
94
|
-
const grid = gridText === undefined ? { ok: true as const, spec:
|
|
94
|
+
const grid = gridText === undefined && seen.has("--grid-hosts") ? parseGridSpec("hosts") : gridText === undefined ? { ok: true as const, spec: GRID_FULL } : parseGridSpec(gridText);
|
|
95
95
|
if (!grid.ok) return { ok: false, error: `--grid: ${grid.error}` };
|
|
96
96
|
const count = (flag: string, min: number): number | undefined => {
|
|
97
97
|
const v = seen.get(flag) as string | undefined;
|
|
@@ -175,11 +175,15 @@ export function resolveTargets(targets: readonly string[], loaders: readonly Plu
|
|
|
175
175
|
continue;
|
|
176
176
|
}
|
|
177
177
|
for (const p of found) {
|
|
178
|
-
|
|
178
|
+
// Two files can hold a plugin of the same id (a bundle's formats, or copies of it): the file is part of the key.
|
|
179
|
+
const key = `${p.uri}\0${p.binaryPath ?? ""}`;
|
|
180
|
+
const prior = plugins.get(key);
|
|
179
181
|
// A URI and its bundle name the same plugin: keep the richer record (bundle, binary).
|
|
180
|
-
if (prior === undefined || (prior.binary === undefined && p.binary !== undefined)) plugins.set(
|
|
182
|
+
if (prior === undefined || (prior.binary === undefined && p.binary !== undefined)) plugins.set(key, p);
|
|
181
183
|
}
|
|
182
184
|
}
|
|
185
|
+
// a bare uri is the same plugin as a file's that holds it: it adds nothing once a file of that id is named
|
|
186
|
+
for (const [key, p] of plugins) if (p.binaryPath === undefined && [...plugins.values()].some((q) => q.uri === p.uri && q.binaryPath !== undefined)) plugins.delete(key);
|
|
183
187
|
return refused.length > 0 ? { ok: false, refused } : { ok: true, plugins: [...plugins.values()] };
|
|
184
188
|
}
|
|
185
189
|
|
package/src/qualify-options.ts
CHANGED
|
@@ -38,10 +38,10 @@ export const QUALIFY_OPTIONS: readonly QualifyOption[] = [
|
|
|
38
38
|
{
|
|
39
39
|
flag: "--grid",
|
|
40
40
|
value: "hosts|full|cells",
|
|
41
|
-
describe: "the cells measured: hosts = each judged host's rate/quantum, then 512 at every rate
|
|
42
|
-
default: "
|
|
41
|
+
describe: "the cells measured: full = every rate at every quantum; hosts = each judged host's rate/quantum, then 512 at every rate (the fast option); or <rate>/<quantum>,...",
|
|
42
|
+
default: "full",
|
|
43
43
|
},
|
|
44
|
-
{ flag: "--grid-hosts", describe: "the same as --grid hosts" },
|
|
44
|
+
{ flag: "--grid-hosts", describe: "the fast grid, the same as --grid hosts" },
|
|
45
45
|
{ flag: "--cells-per-plugin-budget", value: "n", describe: "measure at most the first n cells of the grid a plugin (0: every cell)", default: "0" },
|
|
46
46
|
{ flag: "--blocks", value: "n", describe: "blocks of the CLAP sweep a cell (the LV2 pass has a fixed shape)", default: String(CLAP_SWEEP_BLOCKS) },
|
|
47
47
|
{ flag: "--memory-limit", value: "size|off", describe: "the memory bound of every run (bytes, or with K, M, G); past it a cell is probe.oom", default: "half of the RAM" },
|