@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
|
@@ -14,9 +14,16 @@ import { ORACLE_FLOOR_RATES, RT_HARD_TARGET } from "@openmixer/omx-contract";
|
|
|
14
14
|
export const COST_REFERENCE_RATE = 96000;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The reference quantum in frames: the
|
|
17
|
+
* The reference quantum in frames: the buffer the console aims at (96000/256), the quantum its budget is judged at,
|
|
18
|
+
* and the block `benchmark.py` runs.
|
|
18
19
|
*/
|
|
19
|
-
export const QUANTUM_FRAMES =
|
|
20
|
+
export const QUANTUM_FRAMES = 256;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The long quantum, in frames: the console's basis until 0.20.2, kept as a measured cell so a plugin's cost at a large
|
|
24
|
+
* buffer stays on record beside its cost at the console's.
|
|
25
|
+
*/
|
|
26
|
+
export const LONG_QUANTUM_FRAMES = 1024;
|
|
20
27
|
|
|
21
28
|
/** The rate the offline capture window is sized at; other rates scale from it. */
|
|
22
29
|
export const MEASURE_BASE_RATE = 48000;
|
|
@@ -31,8 +38,8 @@ export const OPERATIONAL_RATE_FLOOR = 48000;
|
|
|
31
38
|
/** The rates a run measures when `--rates` is not given — recorded in `run.rates`: omx-contract's ORACLE_FLOOR_RATES. */
|
|
32
39
|
export const QUALIFY_RATES: readonly number[] = ORACLE_FLOOR_RATES;
|
|
33
40
|
|
|
34
|
-
/** The quanta (frames) a run rates cost at when `--quanta` is not given: the contract's hard-target quantum,
|
|
35
|
-
export const QUALIFY_QUANTA: readonly number[] = [RT_HARD_TARGET.quantum,
|
|
41
|
+
/** The quanta (frames) a run rates cost at when `--quanta` is not given: the contract's hard-target quantum, the reference, 512 and the long quantum. */
|
|
42
|
+
export const QUALIFY_QUANTA: readonly number[] = [RT_HARD_TARGET.quantum, QUANTUM_FRAMES, 512, LONG_QUANTUM_FRAMES];
|
|
36
43
|
|
|
37
44
|
/** Seconds one plugin's offline pass may take before its worker is killed and it reads as a timeout. */
|
|
38
45
|
export const PLUGIN_TIMEOUT_S = 15;
|
|
@@ -174,13 +181,22 @@ export const MEMORY_BOUND_EXIT_STATUS = 121;
|
|
|
174
181
|
/**
|
|
175
182
|
* The memory-only pass (`plugin-qualify-sync --memory-only`). With `PROBE_MEMORY_ONLY_ENV` set the probers take the
|
|
176
183
|
* footprint and nothing else; the probe runner passes its environment on, so its limits hold unchanged.
|
|
177
|
-
* `PROBE_NOTES_ENV` is the count of notes an instrument that declares no voice capacity holds, basis `probe
|
|
184
|
+
* `PROBE_NOTES_ENV` is the count of notes an instrument that declares no voice capacity holds, basis `probe`, in every
|
|
185
|
+
* LV2 run (probe-runner) and in the memory-only pass:
|
|
178
186
|
* `MEMORY_PASS_PROBE_NOTES` unless `memory_notes` says otherwise.
|
|
179
187
|
*/
|
|
180
188
|
export const PROBE_MEMORY_ONLY_ENV = "OMX_PROBE_MEMORY_ONLY";
|
|
181
189
|
export const PROBE_NOTES_ENV = "OMX_PROBE_NOTES";
|
|
182
190
|
export const MEMORY_PASS_PROBE_NOTES = 16;
|
|
183
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The counts of notes an LV2 instrument is timed at, besides its probe count, for its cost against the notes held
|
|
194
|
+
* (the rate's `voiceCurve`, probe output 1.4): the counts the Pi measurements held Surge XT at
|
|
195
|
+
* (openmixer.org/notes/instruments-on-a-pi-5), so a voice limit shows as the count past which the cost stops growing.
|
|
196
|
+
*/
|
|
197
|
+
export const PROBE_NOTES_CURVE_ENV = "OMX_PROBE_NOTES_CURVE";
|
|
198
|
+
export const PROBE_NOTES_CURVE = [1, 8, 16, 32, 48, 64] as const;
|
|
199
|
+
|
|
184
200
|
/**
|
|
185
201
|
* What `tools/mod-host-admit.mjs` is told in an adapter-only pass: how long the host has for one command, its address
|
|
186
202
|
* space (`ulimit -v`, in KB) and the directory for the state files of the plugin it loads.
|
|
@@ -239,6 +255,8 @@ export const DESCRIPTOR_CODE_SCHEMA_MISSING = "qualify.descriptor.schema-missing
|
|
|
239
255
|
export const DESCRIPTOR_CODE_SCHEMA_FLOOR = "qualify.descriptor.schema-floor";
|
|
240
256
|
export const DESCRIPTOR_CODE_BINARY_UNHASHED = "qualify.descriptor.binary-unhashed";
|
|
241
257
|
export const DESCRIPTOR_CODE_DIR_UNWRITABLE = "qualify.descriptor.dir-unwritable";
|
|
258
|
+
/** Two ports of one plugin carry the same symbol: neither can be addressed by it, so the plugin is not described. Args: `symbol`. */
|
|
259
|
+
export const DESCRIPTOR_CODE_DUPLICATE_SYMBOL = "qualify.descriptor.duplicate-symbol";
|
|
242
260
|
|
|
243
261
|
|
|
244
262
|
|
|
@@ -317,11 +335,14 @@ const piMemory = (ramGib: number): NonNullable<HostBudget["memory"]> => ({
|
|
|
317
335
|
/**
|
|
318
336
|
* The cells a Raspberry Pi runs: zynthian-sys's JACK settings, 48000/128 on a Pi 5 with the V5 kit and 48000/256 on a Pi 4 and
|
|
319
337
|
* the other kits, the one a Pi 5 with no kit card also ran its stress and first boot at. The Zynthian benchmark's matrix sweeps
|
|
320
|
-
* more cells; these two are what a Pi is set to.
|
|
338
|
+
* more cells; these two are what a Pi is set to. Every Pi matrix carries 96000 beside 48000, so each of the two quanta
|
|
339
|
+
* is also run at twice the base rate: the passes that follow these cells (adapter, isolated, memory, pool) cost twice as long.
|
|
321
340
|
*/
|
|
322
341
|
export const PI_CELLS = [
|
|
323
342
|
{ rate: MEASURE_BASE_RATE, quantum: 128 },
|
|
324
343
|
{ rate: MEASURE_BASE_RATE, quantum: 256 },
|
|
344
|
+
{ rate: 2 * MEASURE_BASE_RATE, quantum: 128 },
|
|
345
|
+
{ rate: 2 * MEASURE_BASE_RATE, quantum: 256 },
|
|
325
346
|
];
|
|
326
347
|
|
|
327
348
|
export const HOST_BUDGETS = {
|
package/src/qualify-isolated.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { spawnSync } from "node:child_process";
|
|
|
16
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
17
17
|
import { basename, join } from "node:path";
|
|
18
18
|
|
|
19
|
+
import { withRunHome } from "./run-home.js";
|
|
19
20
|
import { ADAPTER_PASS_CELLS, startPrivateJack, type JackStarter } from "./qualify-adapter-pass.js";
|
|
20
21
|
import type { GridCell } from "./measurement-grid.js";
|
|
21
22
|
import { clapPinUri, type PinEntry } from "./pins-file.js";
|
|
@@ -130,6 +131,21 @@ export function pinFromRecord(record: QualificationRecord): PinEntry | undefined
|
|
|
130
131
|
return pinAfterStep({ uri, integrity: { files: sorted, layout } }, step);
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether a step of the isolated kind is a verdict. A refusal (`isolated.silent-output`, `isolated.not-added`) is one: the
|
|
136
|
+
* plugin was tried and the answer is no. A step that could not run decided nothing, and neither did silence before the
|
|
137
|
+
* notes were played where a plugin could hear them: a step of that tool does not say how many inputs it linked. Those two
|
|
138
|
+
* are owed, and a pass tries them again.
|
|
139
|
+
*/
|
|
140
|
+
export function isolatedDecided(s: RunStep): boolean {
|
|
141
|
+
return s.step === RUN_STEP.isolated && s.result !== ISOLATED_RESULT.notRun && !(s.code === ISOLATED_CODE.silent && s.figures?.midiInputsLinked === undefined);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Whether the record holds a finished isolated step, of whatever answer: nothing about it is owed. */
|
|
145
|
+
export function isolatedFinished(record: QualificationRecord): boolean {
|
|
146
|
+
return record.run?.steps.some(isolatedDecided) === true;
|
|
147
|
+
}
|
|
148
|
+
|
|
133
149
|
/** Whether the isolated step saw this plugin running alone. A record's word alone never says so. */
|
|
134
150
|
export function provenAlone(record: QualificationRecord): boolean {
|
|
135
151
|
return record.run?.steps.some((s) => s.step === RUN_STEP.isolated && s.result === ISOLATED_RESULT.isolated) === true;
|
|
@@ -182,7 +198,7 @@ export function spawnedIsolatedRunner(jack: JackStarter = startPrivateJack): Iso
|
|
|
182
198
|
encoding: "utf8",
|
|
183
199
|
timeout: ISOLATED_WINDOW_MS + ISOLATED_STEP_SLACK_MS,
|
|
184
200
|
killSignal: "SIGKILL",
|
|
185
|
-
env: { ...env, JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
|
|
201
|
+
env: { ...withRunHome(env), JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
|
|
186
202
|
});
|
|
187
203
|
if (r.error !== undefined || r.status !== 0) return { started: false, problem: `isolated-step: ${r.error?.message ?? `exit ${String(r.status)}`}: ${(r.stderr ?? "").trim()}` };
|
|
188
204
|
return JSON.parse(r.stdout) as IsolatedRunFacts;
|
|
@@ -225,10 +241,7 @@ export function runIdOf(record: QualificationRecord, dir: string): string | unde
|
|
|
225
241
|
*/
|
|
226
242
|
export function completeIsolated(path: string, record: QualificationRecord, uri: string, cell: GridCell, runner: IsolatedRunner, env: Readonly<Record<string, string | undefined>>): { readonly record: QualificationRecord; readonly step: RunStep } | undefined {
|
|
227
243
|
const isStep = (s: RunStep): boolean => s.step === RUN_STEP.isolated;
|
|
228
|
-
|
|
229
|
-
// hear them: a step of that tool does not say how many inputs it linked. A pass tries these again, and it takes their place
|
|
230
|
-
const decided = (s: RunStep): boolean => s.result !== ISOLATED_RESULT.notRun && !(s.code === ISOLATED_CODE.silent && s.figures?.midiInputsLinked === undefined);
|
|
231
|
-
if (!owesIsolated(record) || record.run?.steps.some((s) => isStep(s) && decided(s)) === true) return undefined;
|
|
244
|
+
if (!owesIsolated(record) || isolatedFinished(record)) return undefined;
|
|
232
245
|
const id = runIdOf(record, join(path, ".."));
|
|
233
246
|
if (id === undefined) return undefined;
|
|
234
247
|
const step = judgeIsolatedRun(runner(uri, cell, env));
|
package/src/qualify-pool.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { existsSync } from "node:fs";
|
|
|
30
30
|
import { availableParallelism } from "node:os";
|
|
31
31
|
import { join } from "node:path";
|
|
32
32
|
|
|
33
|
+
import { withRunHome } from "./run-home.js";
|
|
33
34
|
import type { GridCell } from "./measurement-grid.js";
|
|
34
35
|
import type { PinEntry, PinPool } from "./pins-file.js";
|
|
35
36
|
import type { PoolFigures, QualificationRecord, RunBlock, RunStep } from "./qualification-record.js";
|
|
@@ -203,7 +204,7 @@ const POOL_STEP_SLACK_MS = 120000;
|
|
|
203
204
|
export function asksForPool(prober: string, uri: string, env: Readonly<Record<string, string | undefined>>): boolean | undefined {
|
|
204
205
|
const m = /^clap:(.*)#([^#]*)$/s.exec(uri);
|
|
205
206
|
if (m === null) return undefined;
|
|
206
|
-
const r = spawnSync(prober, ["--asks-thread-pool", m[1]!, m[2]!], { encoding: "utf8", timeout: 60000, killSignal: "SIGKILL", env:
|
|
207
|
+
const r = spawnSync(prober, ["--asks-thread-pool", m[1]!, m[2]!], { encoding: "utf8", timeout: 60000, killSignal: "SIGKILL", env: withRunHome(env) });
|
|
207
208
|
if (r.status !== 0) return undefined;
|
|
208
209
|
try {
|
|
209
210
|
return (JSON.parse(r.stdout) as { threadPool?: unknown }).threadPool === true;
|
|
@@ -233,7 +234,7 @@ export function watchOnHelpers(prober: string, interposer: string, uri: string,
|
|
|
233
234
|
encoding: "utf8",
|
|
234
235
|
timeout: 120000,
|
|
235
236
|
killSignal: "SIGKILL",
|
|
236
|
-
env: { ...env, LD_PRELOAD: preload },
|
|
237
|
+
env: { ...withRunHome(env), LD_PRELOAD: preload },
|
|
237
238
|
});
|
|
238
239
|
if (r.error !== undefined || r.status !== 0) return { problem: `${CLAP_QUALIFY_TOOL_NAME} --pool: ${r.error?.message ?? `exit ${String(r.status)}`}` };
|
|
239
240
|
const line = r.stdout.split("\n").filter((l) => l.startsWith("{")).pop();
|
|
@@ -273,7 +274,7 @@ export function spawnedPoolRunner(jack: JackStarter = startPrivateJack): PoolRun
|
|
|
273
274
|
encoding: "utf8",
|
|
274
275
|
timeout: 2 * POOL_ROUNDS * POOL_WINDOW_MS + POOL_STEP_SLACK_MS,
|
|
275
276
|
killSignal: "SIGKILL",
|
|
276
|
-
env: { ...env, JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
|
|
277
|
+
env: { ...withRunHome(env), JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
|
|
277
278
|
});
|
|
278
279
|
if (r.error !== undefined || r.status !== 0) return { started: false, problem: `pool-step: ${r.error?.message ?? `exit ${String(r.status)}`}: ${(r.stderr ?? "").trim()}` };
|
|
279
280
|
return { ...(JSON.parse(r.stdout) as PoolRunFacts), asks: true, rt: watched.rt };
|
package/src/qualify-rejudge.ts
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
type RecordVerdict,
|
|
49
49
|
type RunBlock,
|
|
50
50
|
} from "./qualification-record.js";
|
|
51
|
-
import { ISOLATED_RESULT, RUN_STEP,
|
|
51
|
+
import { ISOLATED_RESULT, RUN_STEP, isolatedFinished, pinAfterStep } from "./qualify-isolated.js";
|
|
52
52
|
import { pinAfterPool, poolStepOf } from "./qualify-pool.js";
|
|
53
53
|
import { PI_CELLS, PROBE_MEMORY_ONLY_ENV, PROBE_NOTES_ENV } from "./qualify-declarations.js";
|
|
54
54
|
import { SYNC_STATE_SCHEMA, digestFiles, recordStem, type SyncContext, type SyncEntry, type SyncPlugin, type SyncState } from "./qualify-sync.js";
|
|
@@ -198,9 +198,9 @@ export function rejudgeRecord(record: QualificationRecord, probeBytes: Uint8Arra
|
|
|
198
198
|
const { profiles, kept } = rejudgeProfiles(record, output, options.host);
|
|
199
199
|
const { reasons, owes: runs } = runsOwed(record, output, profiles);
|
|
200
200
|
const judgment = judgeProbeOutput(output, profiles, { admissions: admissionsOf(record), owed: reasons });
|
|
201
|
-
// decides no verdict: a record from before the steps that reads isolated stays offered
|
|
201
|
+
// decides no verdict: a record from before the steps that reads isolated stays offered and runs alone, and owes the step until one has finished; a refusal is finished: it is the step's verdict, not debt
|
|
202
202
|
const readsIsolated = judgment.hosts.some((h) => h.verdict.measured && h.verdict.rating === "conditional");
|
|
203
|
-
const owes = readsIsolated && !
|
|
203
|
+
const owes = readsIsolated && !isolatedFinished(record) ? [...runs, { measurement: "isolated-step" as const, code: OWED_CODE.isolatedStep }] : runs;
|
|
204
204
|
const memory = record.memoryRun === undefined ? {} : { memory: { tool: record.memoryRun.tool, measuredAt: record.memoryRun.measuredAt } };
|
|
205
205
|
const a = record.adapter;
|
|
206
206
|
const adapterTaken = a?.versions === undefined || a.measuredAt === undefined ? {} : { adapter: { versions: a.versions, measuredAt: a.measuredAt } };
|
package/src/qualify-sync.ts
CHANGED
|
@@ -23,12 +23,15 @@ import { createConnection } from "node:net";
|
|
|
23
23
|
import { homedir, tmpdir } from "node:os";
|
|
24
24
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
25
25
|
|
|
26
|
+
import { withRunHome } from "./run-home.js";
|
|
26
27
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
27
28
|
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, hostClassOfMachine, outcomeOfVerdict, matchDistributed, readBoardModel, readMachine, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
28
|
-
import {
|
|
29
|
+
import { parseClapScan } from "./clap-loader.js";
|
|
30
|
+
import { SPAWNED_SEARCH_PATH, hostdSearchPath, searchPathWarnings, type SearchPathRun } from "./hostd-search-path.js";
|
|
29
31
|
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
30
32
|
import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
|
|
31
|
-
import {
|
|
33
|
+
import { NO_JACK_SERVER } from "./host-admission.js";
|
|
34
|
+
import { iriPath, lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
|
|
32
35
|
import { judgmentPinEntries, pinClearLine, pinLineUri, pinsFile, type PinEntry } from "./pins-file.js";
|
|
33
36
|
import { POOL_RESULT, completePool, judgePoolRun, pinAfterPool, pinFromRecordWithPool, poolEnv, spawnedPoolRunner, type PoolRunner } from "./qualify-pool.js";
|
|
34
37
|
import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
|
|
@@ -39,7 +42,7 @@ import { adapterPass, type AdapterPassOptions, type AdapterPassResult } from "./
|
|
|
39
42
|
import { ISOLATED_RESULT, baseSteps, completeIsolated, isolatedCell, missingSteps, judgeIsolatedRun, owesIsolated, pinAfterStep, spawnedIsolatedRunner, type IsolatedRunner } from "./qualify-isolated.js";
|
|
40
43
|
import { lv2World, rejudgeRecord, rejudgeState, rejudgeToolName, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
|
|
41
44
|
import { memoryBoundOf, parseMemoryLimit, type MemoryBound } from "./memory-bound.js";
|
|
42
|
-
import { QUALIFICATION_RECORD_SCHEMA, latencyNeverTaken, rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
|
|
45
|
+
import { QUALIFICATION_RECORD_SCHEMA, descriptorNamePart, latencyNeverTaken, rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
|
|
43
46
|
import { ADMIT_STEP_MS_ENV, DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, MEMORY_PASS_PROBE_NOTES, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
44
47
|
import { toolsDir } from "./tool-paths.js";
|
|
45
48
|
import { writeFileWhole } from "./whole-file.js";
|
|
@@ -49,12 +52,6 @@ import { writeFileWhole } from "./whole-file.js";
|
|
|
49
52
|
/** The settings file a system service reads, `key value` per line. */
|
|
50
53
|
export const SYNC_CONF = "/etc/omx/qualify.conf";
|
|
51
54
|
|
|
52
|
-
/** The LV2 directories plugin-hostd searches when `LV2_PATH` is unset (its `lv2_path` default). */
|
|
53
|
-
export const HOSTD_LV2_PATH = ["/usr/lib64/lv2", "/usr/lib/lv2", "/usr/local/lib/lv2"];
|
|
54
|
-
|
|
55
|
-
/** Zynthian's own LV2 directories: the image's plugins and the ones its user installs. */
|
|
56
|
-
export const ZYNTHIAN_LV2_DIRS = ["/zynthian/zynthian-plugins/lv2", "/zynthian/zynthian-my-data/plugins/lv2"];
|
|
57
|
-
|
|
58
55
|
export interface SyncSetting {
|
|
59
56
|
readonly key: string;
|
|
60
57
|
readonly meaning: string;
|
|
@@ -64,10 +61,7 @@ export interface SyncSetting {
|
|
|
64
61
|
|
|
65
62
|
/** Every setting, declared once: the settings file, the flags (`--pin-file` for `pin_file`) and `--help` read this. */
|
|
66
63
|
export const SYNC_SETTINGS: readonly SyncSetting[] = [
|
|
67
|
-
{ key: "
|
|
68
|
-
{ key: "lv2_extra", meaning: "more LV2 directories, searched after lv2_path", default: ZYNTHIAN_LV2_DIRS.join(":") },
|
|
69
|
-
{ key: "clap_path", meaning: "the CLAP directories, colon-separated", default: "$CLAP_PATH, then ~/.clap, then /usr/lib/clap" },
|
|
70
|
-
{ key: "clap_extra", meaning: "more CLAP directories, searched after clap_path", default: "" },
|
|
64
|
+
{ key: "hostd_conf", meaning: "plugin-hostd's settings file: the LV2 and CLAP directories scanned are the ones plugin-hostd --search-path -c <file> prints", default: "plugin-hostd's own (its XDG_CONFIG_HOME/plugin-hostd.conf, else ~/.config/plugin-hostd.conf)" },
|
|
71
65
|
{ key: "pin_file", meaning: "the pin file plugin-hostd loads as pin_file", default: "/var/lib/omx/qualified.pins (system), else $XDG_STATE_HOME/omx/qualified.pins" },
|
|
72
66
|
{ key: "qualification_dir", meaning: "where the qualification records go", default: "/var/lib/omx/qualification (system), else $XDG_STATE_HOME/omx/qualification" },
|
|
73
67
|
{ key: "descriptor_dir", meaning: "where the scanned descriptors go", default: `${DESCRIPTOR_DIR_SYSTEM} (system), else $XDG_STATE_HOME/${DESCRIPTOR_DIR_STATE_SUBDIR}` },
|
|
@@ -85,8 +79,16 @@ export const SYNC_SETTINGS: readonly SyncSetting[] = [
|
|
|
85
79
|
{ key: "distributed_dir", meaning: "the directories of distributed records, colon-separated, each holding <host class>/<set>/", default: `${DISTRIBUTED_DIR_SYSTEM} (system), else $XDG_DATA_HOME/omx/qualification and each $XDG_DATA_DIRS/omx/qualification` },
|
|
86
80
|
];
|
|
87
81
|
|
|
82
|
+
/** The search path settings that moved to plugin-hostd's settings file (plugin-qualify#9): refused here, with where they went. */
|
|
83
|
+
export const MOVED_TO_HOSTD = ["lv2_path", "lv2_extra", "clap_path", "clap_extra"] as const;
|
|
84
|
+
|
|
85
|
+
const movedText = (key: string): string =>
|
|
86
|
+
`${key} is plugin-hostd's setting now: write it in plugin-hostd's settings file (plugin-hostd.conf, named here by hostd_conf); the directories scanned are the ones plugin-hostd --search-path prints${key === "clap_extra" ? " (plugin-hostd has no clap_extra: add the directories to clap_path)" : ""}`;
|
|
87
|
+
|
|
88
88
|
export interface SyncSettings {
|
|
89
|
+
/** The LV2 directories, as plugin-hostd searches them with hostd_conf (plugin-hostd --search-path). */
|
|
89
90
|
readonly lv2Dirs: readonly string[];
|
|
91
|
+
/** The CLAP directories, as plugin-hostd searches them with hostd_conf. */
|
|
90
92
|
readonly clapDirs: readonly string[];
|
|
91
93
|
readonly pinFile: string;
|
|
92
94
|
readonly qualificationDir: string;
|
|
@@ -121,6 +123,7 @@ export function parseSyncConf(text: string, source = SYNC_CONF): { ok: true; val
|
|
|
121
123
|
if (line === "") continue;
|
|
122
124
|
const m = /^(\S+)(?:\s+(.*))?$/.exec(line)!;
|
|
123
125
|
const key = m[1]!;
|
|
126
|
+
if ((MOVED_TO_HOSTD as readonly string[]).includes(key)) return { ok: false, error: `${source}:${String(n + 1)}: ${movedText(key)}` };
|
|
124
127
|
if (!known.has(key)) return { ok: false, error: `${source}:${String(n + 1)}: ${key} is not a setting` };
|
|
125
128
|
values.set(key, (m[2] ?? "").trim());
|
|
126
129
|
}
|
|
@@ -186,7 +189,8 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
186
189
|
if (v === undefined) return { ok: false, error: `${a} needs a value` };
|
|
187
190
|
if (a === "--config") config = v;
|
|
188
191
|
else values.set(byFlag.get(a)!, v);
|
|
189
|
-
} else return { ok: false, error:
|
|
192
|
+
} else if (MOVED_TO_HOSTD.some((k) => flagOf(k) === a)) return { ok: false, error: `${a}: ${movedText(a.slice(2).replace(/-/g, "_"))}` };
|
|
193
|
+
else return { ok: false, error: `unknown argument ${a}` };
|
|
190
194
|
}
|
|
191
195
|
if (rejudge && (memoryOnly || adapterOnly || isolatedOnly || poolOnly || force || refreshPinsOnly)) return { ok: false, error: "--rejudge measures nothing: it goes with none of --memory-only, --adapter-only, --isolated-only, --pool-only, --force and --refresh-pins" };
|
|
192
196
|
if ([memoryOnly, adapterOnly, isolatedOnly, poolOnly, refreshPinsOnly].filter(Boolean).length > 1) return { ok: false, error: "--memory-only, --adapter-only, --isolated-only, --pool-only and --refresh-pins are different runs: one at a time" };
|
|
@@ -229,17 +233,18 @@ export function resolveSyncSettings(
|
|
|
229
233
|
home: string = homedir(),
|
|
230
234
|
boardModel: () => string | undefined = readBoardModel,
|
|
231
235
|
machineArch: () => string | undefined = readMachine,
|
|
232
|
-
|
|
236
|
+
searchPathRun: SearchPathRun = SPAWNED_SEARCH_PATH,
|
|
237
|
+
): { ok: true; settings: SyncSettings; warnings: string[] } | { ok: false; error: string } {
|
|
233
238
|
const get = (key: string): string | undefined => args.values.get(key) ?? file.get(key);
|
|
234
239
|
const state = env["XDG_STATE_HOME"] !== undefined && env["XDG_STATE_HOME"] !== "" ? env["XDG_STATE_HOME"] : join(home, ".local", "state");
|
|
235
240
|
const place = (key: string, system: string, user: string): string => {
|
|
236
241
|
const v = get(key);
|
|
237
242
|
return v !== undefined && v !== "" ? v : args.system ? system : join(state, user);
|
|
238
243
|
};
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
244
|
+
const hostdConfNamed = get("hostd_conf");
|
|
245
|
+
const hostdConf = hostdConfNamed !== undefined && hostdConfNamed !== "" ? hostdConfNamed : undefined;
|
|
246
|
+
const search = hostdSearchPath(hostdConf, env, searchPathRun);
|
|
247
|
+
if (!search.ok) return { ok: false, error: `the directories to scan: ${search.error}` };
|
|
243
248
|
const number = (key: string, min: number): number | undefined => {
|
|
244
249
|
const v = Number(get(key) ?? SYNC_SETTINGS.find((s) => s.key === key)!.default);
|
|
245
250
|
return Number.isFinite(v) && v >= min ? v : undefined;
|
|
@@ -281,9 +286,10 @@ export function resolveSyncSettings(
|
|
|
281
286
|
if (hostClass !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(hostClass)) return { ok: false, error: `host_class ${hostClass} is not a directory name` };
|
|
282
287
|
return {
|
|
283
288
|
ok: true,
|
|
289
|
+
warnings: searchPathWarnings(search.path, hostdConf),
|
|
284
290
|
settings: {
|
|
285
|
-
lv2Dirs: once(
|
|
286
|
-
clapDirs: once(
|
|
291
|
+
lv2Dirs: once(search.path.lv2),
|
|
292
|
+
clapDirs: once(search.path.clap),
|
|
287
293
|
pinFile: place("pin_file", "/var/lib/omx/qualified.pins", join("omx", "qualified.pins")),
|
|
288
294
|
qualificationDir: place("qualification_dir", "/var/lib/omx/qualification", join("omx", "qualification")),
|
|
289
295
|
descriptorDir: place("descriptor_dir", DESCRIPTOR_DIR_SYSTEM, DESCRIPTOR_DIR_STATE_SUBDIR),
|
|
@@ -442,7 +448,7 @@ export function discoverSyncItems(
|
|
|
442
448
|
const binary = lilv?.binary ?? (p.binary === undefined ? undefined : join(bundle, p.binary));
|
|
443
449
|
const files = ["manifest.ttl", ...p.files.filter((f) => f !== "manifest.ttl")];
|
|
444
450
|
// a binary named by an absolute IRI (a complement's) is tracked through the bundle's link to it, when it has one
|
|
445
|
-
if (binary !== undefined && !files.
|
|
451
|
+
if (binary !== undefined && !files.some((f) => iriPath(f) === basename(binary)) && linksTo(join(bundle, basename(binary)), binary)) files.push(basename(binary));
|
|
446
452
|
items.push({ key: `lv2:${p.uri}`, format: "lv2", root: bundle, files, uri: p.uri, ...(binary === undefined ? {} : { binary }) });
|
|
447
453
|
}
|
|
448
454
|
}
|
|
@@ -482,7 +488,10 @@ export function fileSha256(path: string): string {
|
|
|
482
488
|
export function digestFiles(root: string, files: readonly string[], known: readonly FileDigest[] = []): FileDigest[] | undefined {
|
|
483
489
|
const out: FileDigest[] = [];
|
|
484
490
|
for (const path of [...new Set(files)].sort()) {
|
|
485
|
-
|
|
491
|
+
// a name may be an IRI's word (the pin keeps `libSurge%20XT.so`); the file is the path it decodes to, and a name
|
|
492
|
+
// that is a file as written (a .clap with a % in it) is read as written when the decoded one is not there
|
|
493
|
+
const decoded = join(root, iriPath(path));
|
|
494
|
+
const full = existsSync(decoded) ? decoded : join(root, path);
|
|
486
495
|
try {
|
|
487
496
|
const st = statSync(full);
|
|
488
497
|
if (!st.isFile()) return undefined;
|
|
@@ -526,6 +535,7 @@ export function machineReason(p: Pick<SyncPlugin, "outcome" | "code" | "detail">
|
|
|
526
535
|
if (p.outcome !== "unmeasured" || p.code !== SYNC_CODE.notMeasured) return undefined;
|
|
527
536
|
const errno = /\b(EACCES|EPERM|ENOSPC)\b/.exec(p.detail ?? "")?.[1];
|
|
528
537
|
if (errno !== undefined) return errno;
|
|
538
|
+
if ((p.detail ?? "").startsWith(NO_JACK_SERVER)) return NO_JACK_SERVER;
|
|
529
539
|
return /wrote no document|cannot run the probe runner/.test(p.detail ?? "") ? "the measurement wrote no document" : undefined;
|
|
530
540
|
}
|
|
531
541
|
|
|
@@ -623,7 +633,7 @@ export function recordedAsIs(item: SyncItem, entry: SyncEntry | undefined): { sa
|
|
|
623
633
|
*/
|
|
624
634
|
export function sameBinaryElsewhere(item: SyncItem, entry: SyncEntry | undefined): SyncEntry | undefined {
|
|
625
635
|
if (item.format !== "lv2" || entry === undefined || item.binary === undefined || resolve(entry.root) === resolve(item.root)) return undefined;
|
|
626
|
-
const was = entry.files.find((f) => basename(f.path) === basename(item.binary!));
|
|
636
|
+
const was = entry.files.find((f) => basename(iriPath(f.path)) === basename(item.binary!));
|
|
627
637
|
if (was === undefined) return undefined;
|
|
628
638
|
let now: string;
|
|
629
639
|
try {
|
|
@@ -715,7 +725,7 @@ function outcomeOf(judgment: Judgment, host: string): { outcome: SyncOutcome; co
|
|
|
715
725
|
return { outcome: v.rating === "unsuitable" ? "refused" : "unmeasured", ...(code === undefined ? {} : { code }) };
|
|
716
726
|
}
|
|
717
727
|
|
|
718
|
-
const safe =
|
|
728
|
+
const safe = descriptorNamePart;
|
|
719
729
|
|
|
720
730
|
/** The record of a plugin no probe output exists for: what it is, the bytes it was, and why nothing was measured. */
|
|
721
731
|
function writeUnmeasuredRecord(ctx: SyncContext, item: SyncItem, files: readonly FileDigest[], plugin: Omit<SyncPlugin, "record">): string {
|
|
@@ -735,8 +745,19 @@ function writeUnmeasuredRecord(ctx: SyncContext, item: SyncItem, files: readonly
|
|
|
735
745
|
return path;
|
|
736
746
|
}
|
|
737
747
|
|
|
738
|
-
/** The step
|
|
739
|
-
|
|
748
|
+
/** The directories the sync scanned, as the plugin-hostd a step starts reads them: with neither variable set it searches
|
|
749
|
+
* only its own defaults, and every plugin of `lv2_extra` or of a directory the settings add is refused at add. */
|
|
750
|
+
export const hostdSearchEnv = (settings: Pick<SyncSettings, "lv2Dirs" | "clapDirs">): Record<string, string> => ({
|
|
751
|
+
LV2_PATH: settings.lv2Dirs.join(":"),
|
|
752
|
+
CLAP_PATH: settings.clapDirs.join(":"),
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
/** The step's environment: the sync's directories, and each of its commands has the sync's budget. */
|
|
756
|
+
export const isolatedEnv = (ctx: Pick<SyncContext, "env" | "settings">): Readonly<Record<string, string | undefined>> => ({
|
|
757
|
+
...ctx.env,
|
|
758
|
+
...hostdSearchEnv(ctx.settings),
|
|
759
|
+
[ADMIT_STEP_MS_ENV]: String(ctx.settings.budgetS * 1000),
|
|
760
|
+
});
|
|
740
761
|
|
|
741
762
|
/** Each judged run of a pass as the plugins it qualified: the record written, the pin taken, the outcome. */
|
|
742
763
|
function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgment: Judgment; stderr?: readonly Buffer[] }[], describeProblem?: string): SyncPlugin[] {
|
|
@@ -772,7 +793,7 @@ function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgmen
|
|
|
772
793
|
// the pool is for a CLAP plugin that runs: the thread pool is granted by measuring it, here and nowhere else
|
|
773
794
|
if (judgment.output.plugin.format === "clap" && pin !== undefined && pin.verdict !== "refused" && outcome === "qualified") {
|
|
774
795
|
const runner = ctx.poolRunner ?? spawnedPoolRunner();
|
|
775
|
-
const step = judgePoolRun(runner(pin.uri, isolatedCell(record, host), poolEnv(ctx.env, ctx.settings.budgetS)));
|
|
796
|
+
const step = judgePoolRun(runner(pin.uri, isolatedCell(record, host), poolEnv({ ...ctx.env, ...hostdSearchEnv(ctx.settings) }, ctx.settings.budgetS)));
|
|
776
797
|
steps.push(step);
|
|
777
798
|
pin = pinAfterPool(pin, step);
|
|
778
799
|
}
|
|
@@ -845,7 +866,7 @@ function qualifyClap(ctx: SyncContext, item: SyncItem): SyncPlugin[] {
|
|
|
845
866
|
return [{ uri: fileUri, outcome: "unmeasured", code: SYNC_CODE.notMeasured, detail: (e as Error).message }];
|
|
846
867
|
}
|
|
847
868
|
// the scanner lists the plugins of the file in a child of its own, and says how that child ended
|
|
848
|
-
const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20 });
|
|
869
|
+
const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20, env: withRunHome(process.env) });
|
|
849
870
|
let scanned;
|
|
850
871
|
try {
|
|
851
872
|
if (r.error !== undefined) throw r.error;
|
|
@@ -975,7 +996,7 @@ export function poolPass(ctx: SyncContext, state: SyncState): { readonly state:
|
|
|
975
996
|
ctx.log(`${p.uri}: its record does not read; no pool step`);
|
|
976
997
|
return p;
|
|
977
998
|
}
|
|
978
|
-
const done = completePool(path, record, p.pin.uri, isolatedCell(record, host), runner, poolEnv(ctx.env, ctx.settings.budgetS));
|
|
999
|
+
const done = completePool(path, record, p.pin.uri, isolatedCell(record, host), runner, poolEnv({ ...ctx.env, ...hostdSearchEnv(ctx.settings) }, ctx.settings.budgetS));
|
|
979
1000
|
if (done === undefined) return p;
|
|
980
1001
|
ctx.log(`${p.uri}: pool step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
|
|
981
1002
|
if (done.step.result === POOL_RESULT.cores) count.cores++;
|
|
@@ -1079,7 +1100,7 @@ function scanClapDescriptors(ctx: SyncContext, item: SyncItem): void {
|
|
|
1079
1100
|
ctx.log(`${file}: descriptors not written: ${(e as Error).message}`);
|
|
1080
1101
|
return;
|
|
1081
1102
|
}
|
|
1082
|
-
const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20 });
|
|
1103
|
+
const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20, env: withRunHome(process.env) });
|
|
1083
1104
|
if (r.error !== undefined || r.status !== 0) {
|
|
1084
1105
|
ctx.log(`${file}: descriptors not written: the scan ended ${r.error?.message ?? `with exit ${String(r.status)}`}`);
|
|
1085
1106
|
return;
|
package/src/run-home.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The HOME every process that loads a plugin starts with: a scratch directory of this plugin-qualify run, never the
|
|
5
|
+
* caller's real home. A plugin that reads its user data from HOME (Surge XT) segfaults or stays silent without one,
|
|
6
|
+
* and a probe must not write into a user's home. Created on first use, removed when the process exits.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
let home: string | undefined;
|
|
13
|
+
|
|
14
|
+
/** The run's scratch HOME, made on first use. */
|
|
15
|
+
export function runHome(): string {
|
|
16
|
+
if (home === undefined) {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "plugin-qualify-home-"));
|
|
18
|
+
process.on("exit", () => rmSync(dir, { recursive: true, force: true }));
|
|
19
|
+
home = join(dir, "home");
|
|
20
|
+
mkdirSync(home);
|
|
21
|
+
}
|
|
22
|
+
return home;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `env` with HOME set to the run's scratch HOME. */
|
|
26
|
+
export function withRunHome(env: Readonly<Record<string, string | undefined>>): Record<string, string | undefined> {
|
|
27
|
+
return { ...env, HOME: runHome() };
|
|
28
|
+
}
|
package/tools/_declarations.py
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# Verify: node tools/gen-python-declarations.mjs --check
|
|
12
12
|
|
|
13
13
|
COST_REFERENCE_RATE = 96000
|
|
14
|
-
QUANTUM_FRAMES =
|
|
14
|
+
QUANTUM_FRAMES = 256
|
|
15
15
|
MEASURE_BASE_RATE = 48000
|
|
16
16
|
OPERATIONAL_RATE_FLOOR = 48000
|
|
17
17
|
QUALIFY_RATES = (44100, 48000, 96000, 192000)
|
|
@@ -33,3 +33,4 @@ DESCRIPTOR_CODE_SCHEMA_MISSING = "qualify.descriptor.schema-missing"
|
|
|
33
33
|
DESCRIPTOR_CODE_SCHEMA_FLOOR = "qualify.descriptor.schema-floor"
|
|
34
34
|
DESCRIPTOR_CODE_BINARY_UNHASHED = "qualify.descriptor.binary-unhashed"
|
|
35
35
|
DESCRIPTOR_CODE_DIR_UNWRITABLE = "qualify.descriptor.dir-unwritable"
|
|
36
|
+
DESCRIPTOR_CODE_DUPLICATE_SYMBOL = "qualify.descriptor.duplicate-symbol"
|
package/tools/descriptors.py
CHANGED
|
@@ -30,6 +30,7 @@ import sys
|
|
|
30
30
|
from _declarations import (
|
|
31
31
|
DESCRIPTOR_CODE_BINARY_UNHASHED,
|
|
32
32
|
DESCRIPTOR_CODE_DIR_UNWRITABLE,
|
|
33
|
+
DESCRIPTOR_CODE_DUPLICATE_SYMBOL,
|
|
33
34
|
DESCRIPTOR_CODE_SCHEMA,
|
|
34
35
|
DESCRIPTOR_CODE_SCHEMA_MISSING,
|
|
35
36
|
DESCRIPTOR_DIR_STATE_SUBDIR,
|
|
@@ -49,12 +50,44 @@ FEATURE_WORDS = ("audio-effect", "instrument", "note-effect", "analyzer")
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
class DescriptorRefused(Exception):
|
|
52
|
-
"""A descriptor that is not written, with the code it is refused by.
|
|
53
|
+
"""A descriptor that is not written, with the code it is refused by (and, for a code `schema/codes.json`
|
|
54
|
+
declares, the args its sentence names)."""
|
|
53
55
|
|
|
54
|
-
def __init__(self, code, problem):
|
|
56
|
+
def __init__(self, code, problem, args=None):
|
|
55
57
|
super().__init__(f"{code}: {problem}")
|
|
56
58
|
self.code = code
|
|
57
59
|
self.problem = problem
|
|
60
|
+
self.code_args = dict(args or {})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---- the codes' sentences -----------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
CODES_FILE = os.path.join(PKG, "schema", "codes.json")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def code_sentence(code, args, path=CODES_FILE):
|
|
69
|
+
"""The English source sentence of `code` (schema/codes.json) with `args` filled in. A code the file does not
|
|
70
|
+
declare, or no file, reads as the code and its args, so a refusal is never lost for want of its words."""
|
|
71
|
+
try:
|
|
72
|
+
with open(path, encoding="utf-8") as f:
|
|
73
|
+
entries = {e["code"]: e for e in json.load(f)["codes"].values()}
|
|
74
|
+
return entries[code]["text"]["en"].format(**args)
|
|
75
|
+
except (OSError, ValueError, KeyError, IndexError):
|
|
76
|
+
return f"{code} " + " ".join(f"{k}={v}" for k, v in sorted(args.items()))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def refused(code, **args):
|
|
80
|
+
"""A refusal by `code`, its args and its sentence."""
|
|
81
|
+
return DescriptorRefused(code, code_sentence(code, args), args)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def duplicate_symbol(symbols):
|
|
85
|
+
"""The first (sorted) symbol `symbols` holds twice, else None: two ports with one symbol cannot be addressed
|
|
86
|
+
by it (EQ10Q's eq1qs..eq10qs name both output meters out_2_vu_ctl, plugin-qualify#27)."""
|
|
87
|
+
seen, twice = set(), set()
|
|
88
|
+
for s in symbols:
|
|
89
|
+
(twice if s in seen else seen).add(s)
|
|
90
|
+
return min(twice) if twice else None
|
|
58
91
|
|
|
59
92
|
|
|
60
93
|
# ---- the directory and the name -----------------------------------------------------------------
|
|
@@ -71,7 +104,11 @@ def descriptor_dir(named=None, system=False, env=None, home=None):
|
|
|
71
104
|
|
|
72
105
|
|
|
73
106
|
def _safe(text):
|
|
74
|
-
|
|
107
|
+
"""each byte of the UTF-8 text outside [A-Za-z0-9._-] as one '_', plugin-hostd-protocol's descriptor name rule"""
|
|
108
|
+
return "".join(chr(b) if chr(b) in _SAFE else "_" for b in text.encode("utf-8"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_SAFE = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-")
|
|
75
112
|
|
|
76
113
|
|
|
77
114
|
def descriptor_file_name(plugin_id, version, sha256):
|
|
@@ -49,6 +49,7 @@ const PY_NAMES = [
|
|
|
49
49
|
'DESCRIPTOR_CODE_SCHEMA_FLOOR',
|
|
50
50
|
'DESCRIPTOR_CODE_BINARY_UNHASHED',
|
|
51
51
|
'DESCRIPTOR_CODE_DIR_UNWRITABLE',
|
|
52
|
+
'DESCRIPTOR_CODE_DUPLICATE_SYMBOL',
|
|
52
53
|
];
|
|
53
54
|
|
|
54
55
|
/** A Python literal: a tuple, a string (JSON's quoting is Python's for these ASCII words) or a number. */
|
package/tools/isolated-step.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// added alone, worker_list read, a sine through it for a window, and worker_list read again. Prints one JSON line for
|
|
6
6
|
// src/qualify-isolated.ts to judge. A host that never listened says started: false: nothing was learned about the plugin.
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs";
|
|
9
9
|
import { createConnection } from "node:net";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
@@ -25,7 +25,11 @@ const CLOCK_TICKS = 100;
|
|
|
25
25
|
const WORKER_SPAWN = -503;
|
|
26
26
|
|
|
27
27
|
const state = mkdtempSync(join(tmpdir(), "plugin-qualify-isolated-"));
|
|
28
|
-
|
|
28
|
+
// the daemon says why it refused a plugin on stderr only: kept, so a refused add carries that reason into the record
|
|
29
|
+
const hostLog = join(state, "plugin-hostd.stderr");
|
|
30
|
+
const hostLogFd = openSync(hostLog, "a");
|
|
31
|
+
const host = spawn(hostd, ["-n", "-p", String(port)], { stdio: ["ignore", "ignore", hostLogFd], detached: true, env: { ...process.env, XDG_STATE_HOME: state } });
|
|
32
|
+
closeSync(hostLogFd);
|
|
29
33
|
let ended = false;
|
|
30
34
|
host.on("exit", () => (ended = true));
|
|
31
35
|
host.on("error", () => (ended = true));
|
|
@@ -78,6 +82,19 @@ function cpuTicks(pid) {
|
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
/** The last thing the daemon said of `uri` beyond its having no pin yet (first-use learns one at add): "" when nothing. */
|
|
86
|
+
function daemonReason() {
|
|
87
|
+
let text = "";
|
|
88
|
+
try {
|
|
89
|
+
text = readFileSync(hostLog, "utf8");
|
|
90
|
+
} catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
const prefix = `plugin-hostd: ${uri}: `;
|
|
94
|
+
const said = text.split("\n").filter((l) => l.startsWith(prefix)).map((l) => l.slice(prefix.length).trim()).filter((l) => l !== "" && l !== "no pin");
|
|
95
|
+
return said.at(-1) ?? "";
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
async function main() {
|
|
82
99
|
const sock = await connect();
|
|
83
100
|
if (sock === undefined) return { started: false, problem: `${hostd} did not listen` };
|
|
@@ -85,7 +102,11 @@ async function main() {
|
|
|
85
102
|
const added = await send(`add ${pinWord(uri)} ${String(INSTANCE)}`);
|
|
86
103
|
// WORKER_SPAWN: no worker came up to hold the plugin, which says nothing of the plugin. The next pass tries it again.
|
|
87
104
|
if (added?.code === WORKER_SPAWN) return { started: false, problem: `no worker came up: add answered ${String(WORKER_SPAWN)}` };
|
|
88
|
-
if (added === undefined || added.code < 0)
|
|
105
|
+
if (added === undefined || added.code < 0) {
|
|
106
|
+
const why = daemonReason();
|
|
107
|
+
const answered = added === undefined ? "the host gave no answer to add" : `add answered ${String(added.code)}`;
|
|
108
|
+
return { started: true, added: false, problem: why === "" ? answered : `${answered}: ${why}` };
|
|
109
|
+
}
|
|
89
110
|
const before = workersOf(await send("worker_list"));
|
|
90
111
|
const worker = before[0];
|
|
91
112
|
const ticksBefore = worker === undefined ? undefined : cpuTicks(worker.pid);
|