@openmixer/plugin-qualify 0.20.1 → 0.20.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/bin/plugin-qualify-sync.mjs +1 -0
- package/catalogs/ca.json +8 -0
- package/dist/clap-loader.js +2 -1
- package/dist/clap-loader.js.map +1 -1
- package/dist/clap-qualify.d.ts +11 -0
- package/dist/clap-qualify.js +16 -0
- package/dist/clap-qualify.js.map +1 -1
- package/dist/host-admission.d.ts +25 -0
- package/dist/host-admission.js +41 -5
- package/dist/host-admission.js.map +1 -1
- package/dist/hostd-search-path.d.ts +38 -0
- package/dist/hostd-search-path.js +58 -0
- package/dist/hostd-search-path.js.map +1 -0
- package/dist/jack-registry.js +1 -1
- package/dist/lv2-loader.d.ts +33 -1
- package/dist/lv2-loader.js +82 -19
- package/dist/lv2-loader.js.map +1 -1
- package/dist/measurement/vocabulary.js +3 -3
- package/dist/measurement/vocabulary.js.map +1 -1
- package/dist/measurement-grid.d.ts +1 -1
- package/dist/measurement-grid.js +5 -3
- package/dist/measurement-grid.js.map +1 -1
- package/dist/pins-file.d.ts +3 -2
- package/dist/pins-file.js +3 -2
- package/dist/pins-file.js.map +1 -1
- package/dist/probe-judge.js +5 -0
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output.d.ts +10 -0
- package/dist/probe-output.js +16 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.js +24 -5
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +5 -0
- package/dist/qualification-record.js +13 -1
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-adapter-pass.d.ts +1 -2
- package/dist/qualify-adapter-pass.js +6 -6
- package/dist/qualify-adapter-pass.js.map +1 -1
- package/dist/qualify-declarations.d.ts +23 -6
- package/dist/qualify-declarations.js +25 -6
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-isolated.d.ts +9 -0
- package/dist/qualify-isolated.js +16 -5
- package/dist/qualify-isolated.js.map +1 -1
- package/dist/qualify-pool.js +4 -3
- package/dist/qualify-pool.js.map +1 -1
- package/dist/qualify-rejudge.js +3 -3
- package/dist/qualify-rejudge.js.map +1 -1
- package/dist/qualify-sync.d.ts +12 -5
- package/dist/qualify-sync.js +48 -28
- package/dist/qualify-sync.js.map +1 -1
- package/dist/run-home.d.ts +4 -0
- package/dist/run-home.js +26 -0
- package/dist/run-home.js.map +1 -0
- package/package.json +6 -4
- package/schema/codes.json +15 -0
- package/src/clap-loader.ts +2 -1
- package/src/clap-qualify.ts +25 -0
- package/src/host-admission.ts +52 -5
- package/src/hostd-search-path.ts +85 -0
- package/src/jack-registry.ts +1 -1
- package/src/lv2-loader.ts +89 -17
- package/src/measurement/vocabulary.ts +3 -3
- package/src/measurement-grid.ts +5 -3
- package/src/pins-file.ts +3 -2
- package/src/probe-judge.ts +5 -0
- package/src/probe-output.ts +23 -0
- package/src/probe-runner.ts +25 -4
- package/src/qualification-record.ts +13 -1
- package/src/qualify-adapter-pass.ts +6 -5
- package/src/qualify-declarations.ts +27 -6
- package/src/qualify-isolated.ts +18 -5
- package/src/qualify-pool.ts +4 -3
- package/src/qualify-rejudge.ts +3 -3
- package/src/qualify-sync.ts +52 -31
- package/src/run-home.ts +28 -0
- package/tools/_declarations.py +2 -1
- package/tools/descriptors.py +40 -3
- package/tools/gen-python-declarations.mjs +1 -0
- package/tools/isolated-step.mjs +24 -3
- package/tools/mod-host-admit.mjs +57 -1
- package/tools/scan.py +29 -5
package/src/host-admission.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
|
+
import { withRunHome } from "./run-home.js";
|
|
14
15
|
import type { HostProfile } from "./host-profiles.js";
|
|
15
16
|
import { ADMIT_STEP_MS_ENV } from "./qualify-declarations.js";
|
|
16
17
|
|
|
@@ -83,7 +84,17 @@ export interface ModHostRun {
|
|
|
83
84
|
* not be read. Absent for a plugin that is no instrument, and for any plugin under mod-host.
|
|
84
85
|
*/
|
|
85
86
|
/** `notes`: how many keys were played; a run from when one key was played has none. */
|
|
86
|
-
readonly sound?: { readonly peak?: number; readonly notes?: number; readonly problem?: string };
|
|
87
|
+
readonly sound?: { readonly peak?: number; readonly notes?: number; readonly problem?: string; readonly content?: AdapterContent };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The content an instrument declares it plays from (its `patch:writable` parameters of range `atom:Path`, `lv2ContentParams`)
|
|
92
|
+
* and what came of giving each a short sample before the notes: `loaded` the host took, `refused` it did not (with its answer).
|
|
93
|
+
*/
|
|
94
|
+
export interface AdapterContent {
|
|
95
|
+
readonly params: readonly string[];
|
|
96
|
+
readonly loaded: readonly string[];
|
|
97
|
+
readonly refused?: readonly string[];
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
/** At or under this magnitude (-120 dBFS) a held note is taken as digital silence: clap-qualify's own SILENCE. */
|
|
@@ -97,6 +108,22 @@ export const SILENT_PEAK = 1e-6;
|
|
|
97
108
|
export const ADAPTER_SILENT_CODE = `${ADAPTER_HOST}.silent-under-note`;
|
|
98
109
|
export const ADAPTER_LEVEL_UNMEASURED_CODE = `${ADAPTER_HOST}.level-unmeasured`;
|
|
99
110
|
const UNPROVEN_SOUND: readonly string[] = [ADAPTER_SILENT_CODE, ADAPTER_LEVEL_UNMEASURED_CODE];
|
|
111
|
+
/**
|
|
112
|
+
* An instrument that declares content it plays from and stayed silent under the notes even so: the content could not be
|
|
113
|
+
* given (the host refused the sample) or the sample did not reach the keys played. Its silence says nothing of the plugin,
|
|
114
|
+
* so the note test is not observable for it: the run admits it, and the record names this code and the content (#318).
|
|
115
|
+
* No other host is asked instead.
|
|
116
|
+
*/
|
|
117
|
+
export const ADAPTER_CONTENT_UNOBSERVED_CODE = `${ADAPTER_HOST}.silent-needs-content`;
|
|
118
|
+
|
|
119
|
+
/** The run's content, as a reason's params carry it: the declared parameters, the ones loaded, the ones refused. */
|
|
120
|
+
function contentParams(content: AdapterContent): Record<string, string> {
|
|
121
|
+
return {
|
|
122
|
+
content: content.params.join(" "),
|
|
123
|
+
loaded: content.loaded.join(" "),
|
|
124
|
+
...(content.refused === undefined || content.refused.length === 0 ? {} : { refused: content.refused.join("; ") }),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
100
127
|
|
|
101
128
|
/** The rating a run that did not admit the plugin gives its host's verdict. */
|
|
102
129
|
export function admissionRating(admission: HostAdmission): "unsuitable" | "unknown" {
|
|
@@ -109,6 +136,19 @@ export function admissionRating(admission: HostAdmission): "unsuitable" | "unkno
|
|
|
109
136
|
*/
|
|
110
137
|
const JACK_CLIENT_RESPS: readonly number[] = [-201, -202];
|
|
111
138
|
|
|
139
|
+
/** The name of the reason a run stops on when no JACK server answers: the admission cannot say anything of any plugin. */
|
|
140
|
+
export const NO_JACK_SERVER = "no JACK server";
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The refusal to go on when an admission found no JACK client to have (`<host>.no-jack-client`): the server the host is
|
|
144
|
+
* pointed at (the default one, or `JACK_DEFAULT_SERVER`) is not there, and every plugin would be recorded with the same
|
|
145
|
+
* resp -201 as if it were the plugin's. `undefined` for any other run.
|
|
146
|
+
*/
|
|
147
|
+
export function noJackServerProblem(admission: HostAdmission | undefined): string | undefined {
|
|
148
|
+
if (admission === undefined || admission.measured || !admission.code?.endsWith(".no-jack-client")) return undefined;
|
|
149
|
+
return `${NO_JACK_SERVER}: ${admission.host} could not open a JACK client (resp ${String(admission.params["resp"] ?? "-201")}), so nothing is learned of any plugin; start the JACK server it is pointed at (JACK_DEFAULT_SERVER names it) and run again`;
|
|
150
|
+
}
|
|
151
|
+
|
|
112
152
|
/** The verdict of a run of a host that speaks mod-host's protocol: the first step that did not answer `resp 0` decides. */
|
|
113
153
|
export function classifyProtocolRun(host: ProtocolHost, run: ModHostRun): HostAdmission {
|
|
114
154
|
if (!run.installed) return notInstalled(host);
|
|
@@ -128,7 +168,14 @@ export function classifyProtocolRun(host: ProtocolHost, run: ModHostRun): HostAd
|
|
|
128
168
|
if (run.sound !== undefined) {
|
|
129
169
|
const { peak, problem } = run.sound;
|
|
130
170
|
if (peak === undefined) return { host, measured: true, admitted: false, code: ADAPTER_LEVEL_UNMEASURED_CODE, params: { problem: problem ?? "no figure" } };
|
|
131
|
-
|
|
171
|
+
const notes: Record<string, number> = run.sound.notes === undefined ? {} : { notes: run.sound.notes };
|
|
172
|
+
const content = run.sound.content;
|
|
173
|
+
if (!(peak > SILENT_PEAK)) {
|
|
174
|
+
if (content !== undefined && content.params.length > 0) return { host, measured: true, admitted: true, code: ADAPTER_CONTENT_UNOBSERVED_CODE, params: { peak, ...notes, ...contentParams(content) } };
|
|
175
|
+
return { host, measured: true, admitted: false, code: ADAPTER_SILENT_CODE, params: { peak, ...notes } };
|
|
176
|
+
}
|
|
177
|
+
// it sounded on the content it was given: the record says which
|
|
178
|
+
if (content !== undefined && content.loaded.length > 0) return { ...admitted(host), params: { peak, ...notes, ...contentParams(content) } };
|
|
132
179
|
}
|
|
133
180
|
return admitted(host);
|
|
134
181
|
}
|
|
@@ -186,14 +233,14 @@ export const SPAWNED_ADMISSIONS: AdmissionRuns = {
|
|
|
186
233
|
// two commands at the limit the pass gives each, and the tool's own start and end: it kills the host itself first
|
|
187
234
|
const stepMs = Number(env?.[ADMIT_STEP_MS_ENV] ?? 0);
|
|
188
235
|
const timeout = stepMs > 0 ? 2 * stepMs + 15000 : MOD_HOST_ADMISSION_MS;
|
|
189
|
-
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri, ADAPTER_HOST], { encoding: "utf8", timeout, killSignal: "SIGKILL",
|
|
236
|
+
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri, ADAPTER_HOST], { encoding: "utf8", timeout, killSignal: "SIGKILL", env: withRunHome(env ?? process.env) });
|
|
190
237
|
if (r.error !== undefined || r.status !== 0) {
|
|
191
238
|
throw new Error(`${ADAPTER_HOST} admission of ${uri} gave no answer (${r.error?.message ?? `exit ${String(r.status)}`}): ${(r.stderr ?? "").trim()}`);
|
|
192
239
|
}
|
|
193
240
|
return JSON.parse(r.stdout) as ModHostRun;
|
|
194
241
|
},
|
|
195
242
|
modHost(uri, env) {
|
|
196
|
-
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri], { encoding: "utf8", timeout: MOD_HOST_ADMISSION_MS,
|
|
243
|
+
const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri], { encoding: "utf8", timeout: MOD_HOST_ADMISSION_MS, env: withRunHome(env ?? process.env) });
|
|
197
244
|
if (r.error !== undefined || r.status !== 0) {
|
|
198
245
|
throw new Error(`mod-host admission of ${uri} gave no answer (${r.error?.message ?? `exit ${String(r.status)}`}): ${(r.stderr ?? "").trim()}`);
|
|
199
246
|
}
|
|
@@ -203,7 +250,7 @@ export const SPAWNED_ADMISSIONS: AdmissionRuns = {
|
|
|
203
250
|
// SIGKILL at the deadline: jalv hosting some plugins ignores SIGTERM, and one stuck in jack_client_open never reaches
|
|
204
251
|
// the loop that reads the flag TERM sets, so a TERM left spawnSync waiting with it (#187). Alive at the deadline is
|
|
205
252
|
// all the verdict asks, so nothing is lost by not letting it exit cleanly.
|
|
206
|
-
const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, killSignal: "SIGKILL", input: "",
|
|
253
|
+
const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, killSignal: "SIGKILL", input: "", env: withRunHome(env ?? process.env) });
|
|
207
254
|
const code = (r.error as NodeJS.ErrnoException | undefined)?.code;
|
|
208
255
|
if (code === "ENOENT") return { installed: false, stillRunning: false, status: null, signal: null, stderr: "" };
|
|
209
256
|
return { installed: true, stillRunning: code === "ETIMEDOUT", status: r.status, signal: code === "ETIMEDOUT" ? null : r.signal, stderr: r.stderr ?? "" };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The directories the sync scans, as plugin-hostd searches them (plugin-qualify#9). One definition of the search path:
|
|
5
|
+
* the daemon's, which `plugin-hostd --search-path [-c <file>]` prints from the code it searches with (plugin-hostd 0.20.3
|
|
6
|
+
* and later). This module never works the lists out itself: it runs the daemon on its settings file and reads the
|
|
7
|
+
* `lv2` and `clap` lines, and where each list came from (`file`, `env` or `default`).
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { HOSTD_ENV } from "./hostd-pin-files.js";
|
|
11
|
+
|
|
12
|
+
/** Where plugin-hostd took a list from: its settings file, its environment (`LV2_PATH`, `CLAP_PATH`) or its default. */
|
|
13
|
+
export type SearchPathSource = "file" | "env" | "default";
|
|
14
|
+
|
|
15
|
+
export interface HostdSearchPath {
|
|
16
|
+
/** The LV2 directories, in the order the daemon searches them: `lv2_path` then `lv2_extra`. */
|
|
17
|
+
readonly lv2: readonly string[];
|
|
18
|
+
/** The CLAP directories, in the order the daemon searches them. */
|
|
19
|
+
readonly clap: readonly string[];
|
|
20
|
+
/** Where each of `lv2_path`, `lv2_extra` and `clap_path` came from. */
|
|
21
|
+
readonly sources: Readonly<Record<"lv2_path" | "lv2_extra" | "clap_path", SearchPathSource>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SearchPathRunResult {
|
|
25
|
+
readonly status: number | null;
|
|
26
|
+
readonly stdout: string;
|
|
27
|
+
readonly stderr: string;
|
|
28
|
+
/** The daemon is not installed. */
|
|
29
|
+
readonly missing?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One run of `plugin-hostd --search-path`, with `-c <conf>` when a settings file is named. */
|
|
33
|
+
export type SearchPathRun = (conf: string | undefined, env: Readonly<Record<string, string | undefined>>) => SearchPathRunResult;
|
|
34
|
+
|
|
35
|
+
export const SPAWNED_SEARCH_PATH: SearchPathRun = (conf, env) => {
|
|
36
|
+
const r = spawnSync(env[HOSTD_ENV] ?? "plugin-hostd", [...(conf === undefined ? [] : ["-c", conf]), "--search-path"], { encoding: "utf8", timeout: 30000, env: { ...env } });
|
|
37
|
+
const missing = (r.error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
38
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "", ...(missing ? { missing } : {}) };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const SOURCES: readonly string[] = ["file", "env", "default"];
|
|
42
|
+
const dirList = (text: string): string[] => text.split(":").filter((d) => d !== "");
|
|
43
|
+
|
|
44
|
+
/** The lines `plugin-hostd --search-path` prints, read: `<key> TAB <source> TAB <value>`; why not, when one is missing. */
|
|
45
|
+
export function parseSearchPath(stdout: string): { ok: true; path: HostdSearchPath } | { ok: false; error: string } {
|
|
46
|
+
const rows = new Map<string, { source: string; value: string }>();
|
|
47
|
+
for (const line of stdout.split("\n")) {
|
|
48
|
+
const f = line.split("\t");
|
|
49
|
+
if (f.length >= 2 && f[0] !== undefined && f[1] !== undefined) rows.set(f[0], { source: f[1], value: f.slice(2).join("\t") });
|
|
50
|
+
}
|
|
51
|
+
for (const key of ["lv2_path", "lv2_extra", "clap_path"]) {
|
|
52
|
+
const r = rows.get(key);
|
|
53
|
+
if (r === undefined || !SOURCES.includes(r.source)) return { ok: false, error: `plugin-hostd --search-path printed no ${key} with its source` };
|
|
54
|
+
}
|
|
55
|
+
const lv2 = rows.get("lv2");
|
|
56
|
+
const clap = rows.get("clap");
|
|
57
|
+
if (lv2?.source !== "effective" || clap?.source !== "effective") return { ok: false, error: "plugin-hostd --search-path printed no effective lv2 and clap lists" };
|
|
58
|
+
const src = (key: string) => rows.get(key)!.source as SearchPathSource;
|
|
59
|
+
return { ok: true, path: { lv2: dirList(lv2.value), clap: dirList(clap.value), sources: { lv2_path: src("lv2_path"), lv2_extra: src("lv2_extra"), clap_path: src("clap_path") } } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The directories plugin-hostd searches with the settings file `conf` (its own default when undefined); why not, when it cannot say. */
|
|
63
|
+
export function hostdSearchPath(
|
|
64
|
+
conf: string | undefined,
|
|
65
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
66
|
+
run: SearchPathRun = SPAWNED_SEARCH_PATH,
|
|
67
|
+
): { ok: true; path: HostdSearchPath } | { ok: false; error: string } {
|
|
68
|
+
const r = run(conf, env);
|
|
69
|
+
if (r.missing) return { ok: false, error: "plugin-hostd is not installed: the directories scanned are the ones plugin-hostd searches" };
|
|
70
|
+
if (r.status !== 0) {
|
|
71
|
+
const why = /unrecognized option|^Usage:/m.test(r.stdout + r.stderr)
|
|
72
|
+
? "this plugin-hostd has no --search-path (plugin-hostd 0.20.3 or later names the directories it searches)"
|
|
73
|
+
: r.stderr.trim().split("\n").pop() || `plugin-hostd --search-path exited ${String(r.status)}`;
|
|
74
|
+
return { ok: false, error: why };
|
|
75
|
+
}
|
|
76
|
+
return parseSearchPath(r.stdout);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What the run says when a list is not the settings file's: the running daemon, started with another environment, may search other directories. */
|
|
80
|
+
export function searchPathWarnings(path: HostdSearchPath, conf: string | undefined): string[] {
|
|
81
|
+
const where = conf ?? "plugin-hostd's own settings file";
|
|
82
|
+
return (["lv2_path", "clap_path"] as const)
|
|
83
|
+
.filter((k) => path.sources[k] !== "file")
|
|
84
|
+
.map((k) => `${k} is plugin-hostd's ${path.sources[k] === "env" ? `environment (${k === "lv2_path" ? "LV2_PATH" : "CLAP_PATH"})` : "default"}, not ${where}: the running daemon may search other directories; write ${k} in ${where}`);
|
|
85
|
+
}
|
package/src/jack-registry.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* JACK's table of its servers, /dev/shm/jack-shm-registry: a header, then eight rows of a pid and a name
|
|
5
5
|
* (`jack-<uid>:<server>:`). A server that is killed never clears its row, and once the eight are taken no server starts.
|
|
6
|
-
* The adapter pass starts and stops servers of its own, `omx-qualify-<pid>-<quantum>`; a row of one of those whose
|
|
6
|
+
* The adapter pass starts and stops servers of its own, `omx-qualify-<pid>-<rate>-<quantum>`; a row of one of those whose
|
|
7
7
|
* process is gone is freed (pid 0, name empty) before the next start, as jackd frees its own row when it ends well.
|
|
8
8
|
* Rows of any other server are never touched, and a file of another layout is left as it is.
|
|
9
9
|
*/
|
package/src/lv2-loader.ts
CHANGED
|
@@ -64,27 +64,52 @@ function turtleTokens(ttl: string): Token[] {
|
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The path a file IRI names, as lilv and serd read it: a `file:` URI loses its scheme and authority, and the percent
|
|
69
|
+
* escapes of its path are decoded (`libSurge%20XT.so` is `libSurge XT.so` on the disk). A relative IRI is decoded the
|
|
70
|
+
* same way. A malformed escape leaves the text as written: the name is then looked up as it stands and not found, which
|
|
71
|
+
* is its own, named failure.
|
|
72
|
+
*/
|
|
73
|
+
export function iriPath(iri: string): string {
|
|
74
|
+
let path = iri;
|
|
75
|
+
const scheme = /^file:(?:\/\/[^/]*)?(?=\/)/i.exec(iri);
|
|
76
|
+
if (scheme !== null) path = iri.slice(scheme[0].length);
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(path);
|
|
79
|
+
} catch {
|
|
80
|
+
return path;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/** A plugin a manifest declares, with the files it names for it, each relative to the bundle, in the order named. */
|
|
68
85
|
export interface ManifestPlugin {
|
|
69
86
|
readonly uri: string;
|
|
70
87
|
readonly files: readonly string[];
|
|
71
|
-
/** The basename of its `lv2:binary
|
|
88
|
+
/** The basename of its `lv2:binary` as it is on the disk (the IRI's escapes decoded), when it names one. `files` keep the IRI's word, which is what a pin holds. */
|
|
72
89
|
readonly binary?: string;
|
|
73
90
|
}
|
|
74
91
|
|
|
92
|
+
/** A statement of a Turtle text: the subject and predicate expanded, the object as written and, when it names one, expanded. */
|
|
93
|
+
export interface TurtleTriple {
|
|
94
|
+
readonly subject: string;
|
|
95
|
+
readonly predicate: string;
|
|
96
|
+
/** The object's token: an `iri` is as written (a relative one stays relative), a `literal` carries no text. */
|
|
97
|
+
readonly object: { readonly kind: "iri" | "name" | "literal"; readonly text: string };
|
|
98
|
+
/** The object expanded: an absolute or relative IRI, or a prefixed name with a known prefix. */
|
|
99
|
+
readonly objectIri?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
103
|
+
* The statements of a Turtle text, as plugin-hostd reads a manifest to pin a plugin: statements of IRIs and prefixed
|
|
104
|
+
* names, with blank nodes and collections stepped over, not a whole Turtle parser. `lv2`, `rdfs` and `rdf` read even
|
|
105
|
+
* when a file leaves them undeclared.
|
|
79
106
|
*/
|
|
80
|
-
export function
|
|
81
|
-
// the three vocabularies every manifest uses, so a manifest that leaves a prefix undeclared still reads
|
|
107
|
+
export function turtleTriples(ttl: string): TurtleTriple[] {
|
|
82
108
|
const prefixes = new Map<string, string>([
|
|
83
109
|
["lv2", "http://lv2plug.in/ns/lv2core#"],
|
|
84
110
|
["rdfs", "http://www.w3.org/2000/01/rdf-schema#"],
|
|
85
111
|
["rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"],
|
|
86
112
|
]);
|
|
87
|
-
const subjects = new Map<string, { typed: boolean; files: string[]; binary?: string }>();
|
|
88
113
|
const expand = (t: Token): string | undefined => {
|
|
89
114
|
if (t.kind === "iri") return t.text;
|
|
90
115
|
if (t.kind !== "name") return undefined;
|
|
@@ -93,6 +118,7 @@ export function manifestPlugins(ttl: string): ManifestPlugin[] {
|
|
|
93
118
|
const base = colon < 0 ? undefined : prefixes.get(t.text.slice(0, colon));
|
|
94
119
|
return base === undefined ? undefined : base + t.text.slice(colon + 1);
|
|
95
120
|
};
|
|
121
|
+
const out: TurtleTriple[] = [];
|
|
96
122
|
const tokens = turtleTokens(ttl);
|
|
97
123
|
let state: "subject" | "predicate" | "object" | "after" = "subject";
|
|
98
124
|
let depth = 0;
|
|
@@ -134,16 +160,29 @@ export function manifestPlugins(ttl: string): ManifestPlugin[] {
|
|
|
134
160
|
state = "object";
|
|
135
161
|
} else if (state === "object") {
|
|
136
162
|
state = "after";
|
|
137
|
-
if (subject === undefined) continue;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
if (subject === undefined || predicate === undefined) continue;
|
|
164
|
+
const objectIri = expand(t);
|
|
165
|
+
out.push({ subject, predicate, object: { kind: t.kind as "iri" | "name" | "literal", text: t.text }, ...(objectIri === undefined ? {} : { objectIri }) });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every plugin a bundle manifest declares and the files it names for it with `lv2:binary` or `rdfs:seeAlso`: the
|
|
173
|
+
* files plugin-hostd's pin holds besides `manifest.ttl` itself. A file named outside the bundle (an absolute IRI) is
|
|
174
|
+
* not one of them.
|
|
175
|
+
*/
|
|
176
|
+
export function manifestPlugins(ttl: string): ManifestPlugin[] {
|
|
177
|
+
const subjects = new Map<string, { typed: boolean; files: string[]; binary?: string }>();
|
|
178
|
+
for (const { subject, predicate, object, objectIri } of turtleTriples(ttl)) {
|
|
179
|
+
const entry = subjects.get(subject) ?? { typed: false, files: [] };
|
|
180
|
+
subjects.set(subject, entry);
|
|
181
|
+
if (predicate === RDF_TYPE && objectIri === LV2_PLUGIN) entry.typed = true;
|
|
182
|
+
if ((predicate === LV2_BINARY || predicate === RDFS_SEE_ALSO) && object.kind === "iri" && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(object.text) && !object.text.startsWith("/")) {
|
|
183
|
+
const file = object.text.replace(/^(\.\/)+/, "");
|
|
184
|
+
if (!entry.files.includes(file)) entry.files.push(file);
|
|
185
|
+
if (predicate === LV2_BINARY && entry.binary === undefined) entry.binary = basename(iriPath(file));
|
|
147
186
|
}
|
|
148
187
|
}
|
|
149
188
|
return [...subjects.entries()]
|
|
@@ -151,6 +190,39 @@ export function manifestPlugins(ttl: string): ManifestPlugin[] {
|
|
|
151
190
|
.map(([uri, e]) => ({ uri, files: e.files, ...(e.binary === undefined ? {} : { binary: e.binary }) }));
|
|
152
191
|
}
|
|
153
192
|
|
|
193
|
+
const PATCH_WRITABLE = "http://lv2plug.in/ns/ext/patch#writable";
|
|
194
|
+
const RDFS_RANGE = "http://www.w3.org/2000/01/rdf-schema#range";
|
|
195
|
+
const ATOM_PATH = "http://lv2plug.in/ns/ext/atom#Path";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The content an LV2 plugin declares it plays from: the parameters it lists as `patch:writable` whose `rdfs:range` is
|
|
199
|
+
* `atom:Path` (a sampler's sample, a drum machine's kit), sorted. Read from the bundle on the search path whose
|
|
200
|
+
* manifest declares `uri`, in the manifest and every file it names for the plugin. Empty when the plugin declares none
|
|
201
|
+
* or no bundle on the path holds it (#318).
|
|
202
|
+
*/
|
|
203
|
+
export function lv2ContentParams(uri: string, env: Readonly<Record<string, string | undefined>>, home: string = homedir()): string[] {
|
|
204
|
+
for (const bundle of lv2BundlesOnPath(env, home)) {
|
|
205
|
+
let manifest: string;
|
|
206
|
+
try {
|
|
207
|
+
manifest = readFileSync(join(bundle, "manifest.ttl"), "utf8");
|
|
208
|
+
} catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const plugin = manifestPlugins(manifest).find((p) => p.uri === uri);
|
|
212
|
+
if (plugin === undefined) continue;
|
|
213
|
+
const triples = [manifest, ...plugin.files.filter((f) => f.endsWith(".ttl")).map((f) => {
|
|
214
|
+
try {
|
|
215
|
+
return readFileSync(join(bundle, f), "utf8");
|
|
216
|
+
} catch {
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
})].flatMap(turtleTriples);
|
|
220
|
+
const paths = new Set(triples.filter((t) => t.predicate === RDFS_RANGE && t.objectIri === ATOM_PATH).map((t) => t.subject));
|
|
221
|
+
return [...new Set(triples.filter((t) => t.subject === uri && t.predicate === PATCH_WRITABLE && t.objectIri !== undefined && paths.has(t.objectIri)).map((t) => t.objectIri!))].sort();
|
|
222
|
+
}
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
154
226
|
/** The plugins a bundle manifest declares, each with its `lv2:binary` basename. */
|
|
155
227
|
export function pluginsInManifest(ttl: string): { uri: string; binary?: string }[] {
|
|
156
228
|
return manifestPlugins(ttl).map((p) => (p.binary === undefined ? { uri: p.uri } : { uri: p.uri, binary: p.binary }));
|
|
@@ -94,9 +94,9 @@ const UNMEASURED_DESCRIPTIONS: Readonly<Record<UnmeasuredCode, string>> = {
|
|
|
94
94
|
"needs-impulse-response":
|
|
95
95
|
"A convolver requiring an IR file the prober did not load.",
|
|
96
96
|
"unpinnable":
|
|
97
|
-
"The files the host loads cannot be pinned (plugin-qualify spec §11.1): a
|
|
98
|
-
"
|
|
99
|
-
"
|
|
97
|
+
"The files the host loads cannot be pinned (plugin-qualify spec §11.1): a manifest using @base, a file name " +
|
|
98
|
+
"longer than a pin path, or a CLAP plugin with more parameters than the host accepts. A name with a space or a " +
|
|
99
|
+
"separator of pin_set is pinned percent-encoded. The plugin stays unpinned, never partly pinned.",
|
|
100
100
|
"no-voices":
|
|
101
101
|
"An instrument whose voice-info declares a voice capacity of 0: there is no voice to hold down, so its " +
|
|
102
102
|
"cost with notes held cannot be taken.",
|
package/src/measurement-grid.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Pure: no file, no process. `probe-runner.ts` runs what `runnerInvocations` plans.
|
|
9
9
|
*/
|
|
10
|
-
import { GRID_CONTINUITY_QUANTUM } from "./qualify-declarations.js";
|
|
10
|
+
import { COST_REFERENCE_RATE, GRID_CONTINUITY_QUANTUM, LONG_QUANTUM_FRAMES } from "./qualify-declarations.js";
|
|
11
11
|
|
|
12
12
|
/** One cell of the grid. */
|
|
13
13
|
export interface GridCell {
|
|
@@ -55,7 +55,7 @@ const byRateThenQuantum = (a: GridCell, b: GridCell): number => a.rate - b.rate
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* The cells of a grid, in the order they are taken: for `hosts`, each host's own cell in the order the hosts are given,
|
|
58
|
-
* then `GRID_CONTINUITY_QUANTUM` at every rate; for `full`, those cells first as well, then the rest of the product by
|
|
58
|
+
* then `GRID_CONTINUITY_QUANTUM` at every rate, then the long quantum at the reference rate when the run measures that rate; for `full`, those cells first as well, then the rest of the product by
|
|
59
59
|
* rate and quantum; for named cells, as named. A cell is listed once.
|
|
60
60
|
*/
|
|
61
61
|
export function gridCells(
|
|
@@ -64,7 +64,9 @@ export function gridCells(
|
|
|
64
64
|
): GridCell[] {
|
|
65
65
|
if (spec.kind === "cells") return unique(spec.cells);
|
|
66
66
|
const continuity = [...context.rates].sort((a, b) => a - b).map((rate) => ({ rate, quantum: GRID_CONTINUITY_QUANTUM }));
|
|
67
|
-
|
|
67
|
+
// the console was judged at the long quantum until its basis moved to the reference one: that cell stays measured
|
|
68
|
+
const long = context.rates.includes(COST_REFERENCE_RATE) ? [{ rate: COST_REFERENCE_RATE, quantum: LONG_QUANTUM_FRAMES }] : [];
|
|
69
|
+
const lead = unique([...context.hosts, ...continuity, ...long]);
|
|
68
70
|
if (spec.kind === "hosts") return lead;
|
|
69
71
|
const product = context.rates.flatMap((rate) => context.quanta.map((quantum) => ({ rate, quantum }))).sort(byRateThenQuantum);
|
|
70
72
|
const inProduct = new Set(product.map(cellKey));
|
package/src/pins-file.ts
CHANGED
|
@@ -127,8 +127,9 @@ export function pinClearLine(uri: string): string {
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* The line of one pin, or `undefined` when the integrity block is not a whole pin. The uri is one word, quoted when it
|
|
130
|
-
* holds a space. A file path
|
|
131
|
-
*
|
|
130
|
+
* holds a space. A file path is the prober's, percent-encoded as plugin-hostd-protocol writes a pin path (Surge XT's
|
|
131
|
+
* "Surge XT.clap" is Surge%20XT.clap), so it holds no whitespace, comma or equals sign; one that does was not encoded,
|
|
132
|
+
* and the plugin has no pin, refused by name.
|
|
132
133
|
*/
|
|
133
134
|
export function pinLine(entry: PinEntry): string | undefined {
|
|
134
135
|
const { files, layout } = entry.integrity;
|
package/src/probe-judge.ts
CHANGED
|
@@ -320,6 +320,11 @@ export function judgeProbeOutput(output: ProbeOutput, profiles: readonly HostPro
|
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
+
// a run that admitted the plugin and says why its note test could not be observed (a sampler with nothing to play): shown,
|
|
324
|
+
// never deciding (#318)
|
|
325
|
+
if (ran && admission !== undefined && admission.admitted && admission.code !== undefined) {
|
|
326
|
+
reasons = [{ dimension: "hostAdmission", rating: "suitable", code: admission.code, params: admission.params }, ...reasons];
|
|
327
|
+
}
|
|
323
328
|
const owed = facts.owed?.[profile.id];
|
|
324
329
|
if (owed !== undefined) reasons = [owed, ...reasons];
|
|
325
330
|
const rating = worstRating(reasons.map((r) => r.rating));
|
package/src/probe-output.ts
CHANGED
|
@@ -63,6 +63,14 @@ export interface ProbeCost {
|
|
|
63
63
|
readonly warmupBlocks: number;
|
|
64
64
|
/** For an instrument: the notes it held while it was timed, how many and why that many. */
|
|
65
65
|
readonly voices?: VoicesHeld;
|
|
66
|
+
/** For an instrument: its cost at each count of notes held (probe output 1.4), in the cost's own unit. */
|
|
67
|
+
readonly voiceCurve?: readonly VoiceCurvePoint[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface VoiceCurvePoint {
|
|
71
|
+
readonly held: number;
|
|
72
|
+
readonly median: number;
|
|
73
|
+
readonly percentile: { readonly p: number; readonly value: number };
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export interface ProbeRtSafety {
|
|
@@ -255,15 +263,30 @@ function readCost(v: unknown, where: string): ProbeCost {
|
|
|
255
263
|
if (median < 0 || value < 0) invalid(`${where} holds a negative cost`);
|
|
256
264
|
const voices = o["voices"] === undefined ? undefined : readVoicesHeld(o["voices"]);
|
|
257
265
|
if (o["voices"] !== undefined && voices === undefined) invalid(`${where} voices is not a count of held notes with its basis`);
|
|
266
|
+
const voiceCurve = o["voiceCurve"] === undefined ? undefined : readVoiceCurve(o["voiceCurve"], `${where} voiceCurve`);
|
|
258
267
|
return {
|
|
259
268
|
median,
|
|
260
269
|
percentile: { p, value },
|
|
261
270
|
blocks: posInt(o, "blocks", where),
|
|
262
271
|
warmupBlocks: count(o, "warmupBlocks", where),
|
|
263
272
|
...(voices === undefined ? {} : { voices }),
|
|
273
|
+
...(voiceCurve === undefined ? {} : { voiceCurve }),
|
|
264
274
|
};
|
|
265
275
|
}
|
|
266
276
|
|
|
277
|
+
function readVoiceCurve(v: unknown, where: string): VoiceCurvePoint[] {
|
|
278
|
+
if (!Array.isArray(v)) invalid(`${where} is not a list`);
|
|
279
|
+
return (v as unknown[]).map((e, i) => {
|
|
280
|
+
const at = `${where} entry ${i}`;
|
|
281
|
+
const o = record(e, at);
|
|
282
|
+
const pct = record(o["percentile"], `${at} percentile`);
|
|
283
|
+
const median = num(o, "median", at);
|
|
284
|
+
const value = num(pct, "value", `${at} percentile`);
|
|
285
|
+
if (median < 0 || value < 0) invalid(`${at} holds a negative cost`);
|
|
286
|
+
return { held: posInt(o, "held", at), median, percentile: { p: num(pct, "p", `${at} percentile`), value } };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
267
290
|
function readRtSafety(v: unknown, where: string): ProbeRtSafety {
|
|
268
291
|
const o = record(v, where);
|
|
269
292
|
const events = o["events"] === undefined ? undefined : (stringList(o["events"]) ?? invalid(`${where} events is not a list of strings`));
|
package/src/probe-runner.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* unavailable, an output that could not be written), 3 is no such plugin.
|
|
13
13
|
*/
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
16
16
|
import { availableParallelism, loadavg, tmpdir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
@@ -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 { ADAPTER_RUN_HOSTS, SPAWNED_ADMISSIONS, admitPlugin, admitUnderAdapter, admittedHostOf, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
|
|
25
|
+
import { ADAPTER_RUN_HOSTS, SPAWNED_ADMISSIONS, noJackServerProblem, 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, hostCells, runnerInvocations, type GridCell, type GridSpec, type RunnerInvocation } from "./measurement-grid.js";
|
|
@@ -30,6 +30,10 @@ import type { MemoryBound } from "./memory-bound.js";
|
|
|
30
30
|
import { mergeProbeRuns, ProbeMergeError } from "./probe-merge.js";
|
|
31
31
|
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
32
32
|
import {
|
|
33
|
+
MEMORY_PASS_PROBE_NOTES,
|
|
34
|
+
PROBE_NOTES_CURVE,
|
|
35
|
+
PROBE_NOTES_CURVE_ENV,
|
|
36
|
+
PROBE_NOTES_ENV,
|
|
33
37
|
CLAP_QUALIFY_TIMEOUT_S,
|
|
34
38
|
CLAP_SWEEP_BLOCKS,
|
|
35
39
|
CLAP_SWEEP_FRAMES,
|
|
@@ -182,7 +186,11 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: R
|
|
|
182
186
|
const log = join(work, "child.stderr.log");
|
|
183
187
|
try {
|
|
184
188
|
let args: string[];
|
|
185
|
-
|
|
189
|
+
// a HOME of the run's own, under its scratch directory: a plugin that reads its user data from HOME (Surge XT) dies
|
|
190
|
+
// or stays silent without one, and a probe must never write into the caller's real home
|
|
191
|
+
const home = join(work, "home");
|
|
192
|
+
mkdirSync(home);
|
|
193
|
+
let runEnv: Record<string, string | undefined> = { ...env, HOME: home };
|
|
186
194
|
let expectedId: string;
|
|
187
195
|
const wall = options.wallS === undefined ? {} : { wallS: options.wallS };
|
|
188
196
|
const bound = options.memory === undefined ? {} : { dataBytes: options.memory.bytes };
|
|
@@ -196,7 +204,7 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: R
|
|
|
196
204
|
target.binaryPath,
|
|
197
205
|
expectedId,
|
|
198
206
|
);
|
|
199
|
-
runEnv = { ...
|
|
207
|
+
runEnv = { ...runEnv, LD_PRELOAD: interposer, [PROBE_NOTES_CURVE_ENV]: env[PROBE_NOTES_CURVE_ENV] ?? PROBE_NOTES_CURVE.join(",") };
|
|
200
208
|
} else {
|
|
201
209
|
expectedId = target.uri;
|
|
202
210
|
const child = locateNativeTool(LV2_MEASURE_TOOL_NAME, env, options.exists ?? existsSync).path;
|
|
@@ -204,6 +212,14 @@ function runOne(target: PluginTarget, options: ProbeTargetOptions, invocation: R
|
|
|
204
212
|
{ ...at, ...wall, out, log, ...(child === undefined ? {} : { child }) },
|
|
205
213
|
target.uri,
|
|
206
214
|
);
|
|
215
|
+
// an instrument that declares no voice capacity is played with this many notes held, so its cost and its memory
|
|
216
|
+
// under notes are measured, not left voices-unknown; a count the caller set wins
|
|
217
|
+
runEnv = {
|
|
218
|
+
...runEnv,
|
|
219
|
+
[PROBE_NOTES_ENV]: env[PROBE_NOTES_ENV] ?? String(MEMORY_PASS_PROBE_NOTES),
|
|
220
|
+
// and timed at each count of the curve, the cost against the notes held
|
|
221
|
+
[PROBE_NOTES_CURVE_ENV]: env[PROBE_NOTES_CURVE_ENV] ?? PROBE_NOTES_CURVE.join(","),
|
|
222
|
+
};
|
|
207
223
|
}
|
|
208
224
|
// under a bound, the interposer ends the child the moment an allocation is refused, with the status that names it
|
|
209
225
|
if (options.memory !== undefined) runEnv = { ...runEnv, [MEMORY_BOUND_EXIT_ENV]: "1" };
|
|
@@ -322,12 +338,17 @@ export function qualifyTargets(
|
|
|
322
338
|
const admissions: Record<string, HostAdmission> = {};
|
|
323
339
|
// an LV2 plugin is also run once under plugin-hostd's LV2 adapter, recorded beside the verdicts (#223)
|
|
324
340
|
const adapter = target.format === "lv2" ? admitUnderAdapter(target.uri, admissionRuns, admissionEnv) : undefined;
|
|
341
|
+
// no JACK server is the machine's, not the plugin's: the run stops by name instead of recording resp -201 in every record
|
|
342
|
+
const noJack = noJackServerProblem(adapter);
|
|
343
|
+
if (noJack !== undefined) throw new ProbeRunnerError(noJack);
|
|
325
344
|
// and that run decides its omx-clap-host verdict, the isolated tier
|
|
326
345
|
if (adapter !== undefined) for (const host of ADAPTER_RUN_HOSTS) admissions[host] = adapter;
|
|
327
346
|
if (target.format === "lv2") {
|
|
328
347
|
for (const profile of judgedProfiles) {
|
|
329
348
|
const host = admittedHostOf(profile);
|
|
330
349
|
if (host !== undefined) admissions[profile.id] = admitPlugin(host, target.uri, admissionRuns, admissionEnv);
|
|
350
|
+
const noJackHost = noJackServerProblem(admissions[profile.id]);
|
|
351
|
+
if (noJackHost !== undefined) throw new ProbeRunnerError(noJackHost);
|
|
331
352
|
}
|
|
332
353
|
}
|
|
333
354
|
for (const profile of judgedProfiles) {
|
|
@@ -434,7 +434,19 @@ export interface NotMeasured {
|
|
|
434
434
|
readonly code: string;
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
|
|
437
|
+
/**
|
|
438
|
+
* A name part as plugin-hostd-protocol's descriptor name rule writes it (PHD_DESCRIPTOR_SAFE_CHAR): each byte of the
|
|
439
|
+
* UTF-8 text outside [A-Za-z0-9._-] is one "_", as plugin-hostd's descriptors.c reads the name. "é" is two bytes: "__".
|
|
440
|
+
*/
|
|
441
|
+
export function descriptorNamePart(text: string): string {
|
|
442
|
+
let out = "";
|
|
443
|
+
for (const byte of Buffer.from(text, "utf8")) {
|
|
444
|
+
const c = String.fromCharCode(byte);
|
|
445
|
+
out += /[A-Za-z0-9._-]/.test(c) && byte < 0x80 ? c : "_";
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
const safe = descriptorNamePart;
|
|
438
450
|
|
|
439
451
|
/** `<id>@<version>-<sha256 first 12>.<offer>`: one binary and one offer, whatever the run. */
|
|
440
452
|
export function qualificationBaseName(output: ProbeOutput): string {
|
|
@@ -22,7 +22,6 @@ import { HOSTD_ENV } from "./hostd-pin-files.js";
|
|
|
22
22
|
import { freeDeadJackSlots } from "./jack-registry.js";
|
|
23
23
|
import { cellKey, type GridCell } from "./measurement-grid.js";
|
|
24
24
|
import { defaultAddressSpaceLimit } from "./direct-limit.js";
|
|
25
|
-
import { PROBE_OK } from "./probe-output.js";
|
|
26
25
|
import { QUALIFICATION_RECORD_SCHEMA, type AdapterBlock, type QualificationRecord } from "./qualification-record.js";
|
|
27
26
|
import { ADMIT_ADDRESS_KB_ENV, ADMIT_STATE_DIR_ENV, ADMIT_STEP_MS_ENV, PI_CELLS } from "./qualify-declarations.js";
|
|
28
27
|
import { digestFiles, type SyncContext, type SyncState } from "./qualify-sync.js";
|
|
@@ -90,7 +89,8 @@ const REAPING_JACKD = `trap 'kill -TERM $p 2>/dev/null' TERM; trap 'kill -KILL $
|
|
|
90
89
|
|
|
91
90
|
/** `jackd` on a name of its own and the dummy driver, at the cell's rate and quantum; `undefined` when none came up. */
|
|
92
91
|
export const startPrivateJack: JackStarter = (cell) => {
|
|
93
|
-
|
|
92
|
+
// the rate is in the name: the cells of one pass run at the same quantum at two rates, on servers alive together
|
|
93
|
+
const name = `omx-qualify-${String(process.pid)}-${String(cell.rate)}-${String(cell.quantum)}`;
|
|
94
94
|
// the rows of servers of ours that were killed, freed before this one asks for a row
|
|
95
95
|
freeDeadJackSlots();
|
|
96
96
|
let child: ChildProcess;
|
|
@@ -238,8 +238,7 @@ const readRecord = (path: string): QualificationRecord | undefined => {
|
|
|
238
238
|
};
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
|
-
* The run of each LV2 plugin of the state
|
|
242
|
-
* each cell. Each record is written whole as its plugin ends.
|
|
241
|
+
* The run of each LV2 plugin of the state, whatever its probe ended with, at each cell. Each record is written whole as its plugin ends.
|
|
243
242
|
*/
|
|
244
243
|
export function adapterPass(ctx: SyncContext, state: SyncState, options: AdapterPassOptions = {}): AdapterPassResult {
|
|
245
244
|
const read = hostVersions(ctx.env, options.versions);
|
|
@@ -279,7 +278,9 @@ export function adapterPass(ctx: SyncContext, state: SyncState, options: Adapter
|
|
|
279
278
|
count.failed++;
|
|
280
279
|
continue;
|
|
281
280
|
}
|
|
282
|
-
|
|
281
|
+
// a probe that crashed, hung or measured nothing is run too: the adapter's own process is not the probe's, and its
|
|
282
|
+
// answer replaces whatever the main run recorded there (a missing JACK server's no-jack-client), so no record keeps a
|
|
283
|
+
// stale code. The probe's outcome stays in the record beside it.
|
|
283
284
|
if (options.force !== true && adapterFinished(record, versions)) {
|
|
284
285
|
count.already++;
|
|
285
286
|
continue;
|