@openmixer/plugin-qualify 0.17.5 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -13
- package/bin/plugin-qualify-sync.mjs +3 -1
- package/dist/clap-qualify.d.ts +3 -1
- package/dist/clap-qualify.js +7 -1
- package/dist/clap-qualify.js.map +1 -1
- package/dist/direct-limit.d.ts +27 -0
- package/dist/direct-limit.js +52 -0
- package/dist/direct-limit.js.map +1 -0
- package/dist/distributed-records.d.ts +4 -2
- package/dist/distributed-records.js +1 -1
- package/dist/distributed-records.js.map +1 -1
- package/dist/host-admission.d.ts +12 -2
- package/dist/host-admission.js +24 -4
- package/dist/host-admission.js.map +1 -1
- package/dist/host-profiles.d.ts +8 -0
- package/dist/host-profiles.js +16 -1
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.generated.d.ts +10 -0
- package/dist/hosting-codes.generated.js +10 -0
- package/dist/hosting-codes.generated.js.map +1 -1
- package/dist/hosting-suitability.d.ts +34 -8
- package/dist/hosting-suitability.js +92 -7
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement/format.d.ts +54 -8
- package/dist/measurement/format.js +54 -3
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/plan.d.ts +3 -3
- package/dist/measurement/plan.js +1 -1
- package/dist/measurement-grid.d.ts +8 -0
- package/dist/measurement-grid.js +4 -0
- package/dist/measurement-grid.js.map +1 -1
- package/dist/memory-bound.d.ts +6 -0
- package/dist/memory-bound.js +8 -0
- package/dist/memory-bound.js.map +1 -1
- package/dist/memory-budget.d.ts +39 -0
- package/dist/memory-budget.js +32 -0
- package/dist/memory-budget.js.map +1 -0
- package/dist/probe-judge.d.ts +11 -0
- package/dist/probe-judge.js +26 -10
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-merge.js +6 -5
- package/dist/probe-merge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +6 -0
- package/dist/probe-output-fixtures.js +8 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +4 -2
- package/dist/probe-output.js +2 -1
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.js +7 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +116 -10
- package/dist/qualification-record.js +34 -30
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-adapter-pass.d.ts +67 -0
- package/dist/qualify-adapter-pass.js +255 -0
- package/dist/qualify-adapter-pass.js.map +1 -0
- package/dist/qualify-declarations.d.ts +53 -9
- package/dist/qualify-declarations.js +54 -2
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-rejudge.d.ts +81 -0
- package/dist/qualify-rejudge.js +421 -0
- package/dist/qualify-rejudge.js.map +1 -0
- package/dist/qualify-sync.d.ts +72 -1
- package/dist/qualify-sync.js +194 -21
- package/dist/qualify-sync.js.map +1 -1
- package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.probe.json +46 -0
- package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.qualification.json +185 -0
- package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.probe.json +51 -0
- package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.qualification.json +232 -0
- package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.probe.json +51 -0
- package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.qualification.json +245 -0
- package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
- package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.qualification.json +145 -0
- package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
- package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.qualification.json +155 -0
- package/package.json +1 -1
- package/src/clap-qualify.ts +8 -1
- package/src/direct-limit.ts +68 -0
- package/src/distributed-records.ts +1 -1
- package/src/host-admission.ts +27 -4
- package/src/host-profiles.ts +17 -1
- package/src/hosting-codes.generated.ts +10 -0
- package/src/hosting-suitability.ts +105 -11
- package/src/index.ts +3 -0
- package/src/measurement/format.ts +97 -3
- package/src/measurement/plan.ts +3 -3
- package/src/measurement-grid.ts +5 -0
- package/src/memory-bound.ts +10 -0
- package/src/memory-budget.ts +55 -0
- package/src/probe-judge.ts +29 -9
- package/src/probe-merge.ts +6 -5
- package/src/probe-output-fixtures.ts +10 -1
- package/src/probe-output.ts +5 -2
- package/src/probe-runner.ts +5 -3
- package/src/qualification-record.ts +121 -18
- package/src/qualify-adapter-pass.ts +278 -0
- package/src/qualify-declarations.ts +67 -3
- package/src/qualify-rejudge.ts +452 -0
- package/src/qualify-sync.ts +210 -20
- package/tools/_declarations.py +1 -0
- package/tools/benchmark.py +25 -2
- package/tools/gen-python-declarations.mjs +1 -0
- package/tools/lv2-measure.mjs +7 -4
- package/tools/lv2_presets.py +1 -1
- package/tools/lv2_resolve.py +49 -0
- package/tools/mod-host-admit.mjs +23 -6
package/dist/qualify-sync.js
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { spawn, spawnSync } from "node:child_process";
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
|
-
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync,
|
|
21
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { createConnection } from "node:net";
|
|
23
23
|
import { homedir, tmpdir } from "node:os";
|
|
24
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
24
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
25
25
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
26
26
|
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, matchDistributed, readBoardModel, readDistributedLayer } from "./distributed-records.js";
|
|
27
27
|
import { clapSearchPath, parseClapScan } from "./clap-loader.js";
|
|
@@ -32,9 +32,11 @@ import { judgmentPinEntries, pinsFile } from "./pins-file.js";
|
|
|
32
32
|
import { hostdPinFiles } from "./hostd-pin-files.js";
|
|
33
33
|
import { qualifyTargets } from "./probe-runner.js";
|
|
34
34
|
import { GRID_FULL, parseGridSpec } from "./measurement-grid.js";
|
|
35
|
+
import { adapterPass } from "./qualify-adapter-pass.js";
|
|
36
|
+
import { lv2World, rejudgeState, memoryPass } from "./qualify-rejudge.js";
|
|
35
37
|
import { memoryBoundOf, parseMemoryLimit } from "./memory-bound.js";
|
|
36
38
|
import { writeQualification } from "./qualification-record.js";
|
|
37
|
-
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
39
|
+
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, MEMORY_PASS_PROBE_NOTES, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
38
40
|
import { toolsDir } from "./tool-paths.js";
|
|
39
41
|
import { writeFileWhole } from "./whole-file.js";
|
|
40
42
|
// --- the settings -----------------------------------------------------------------------------------------------------
|
|
@@ -59,6 +61,7 @@ export const SYNC_SETTINGS = [
|
|
|
59
61
|
{ key: "grid", meaning: "the cells measured: full (every rate at every block size, 16 cells), hosts (the fast option: the host's and the console tiers' rate/quantum, then 512 at every rate; also --grid-hosts), or <rate>/<quantum>,...", default: "full" },
|
|
60
62
|
{ key: "cells_per_plugin_budget", meaning: "measure at most the first n cells of the grid a plugin; 0 is every cell", default: "0" },
|
|
61
63
|
{ key: "memory_limit", meaning: "the memory bound of every run: bytes, or with K, M, G; off for none; past it a cell is probe.oom", default: "half of the RAM" },
|
|
64
|
+
{ key: "memory_notes", meaning: "the notes an instrument that declares no voice capacity holds in a --memory-only run, 1 to 128 (basis probe)", default: String(MEMORY_PASS_PROBE_NOTES) },
|
|
62
65
|
{ key: "hostd_port", meaning: "plugin-hostd's command port the new pins are sent to; 0 sends none", default: "5555" },
|
|
63
66
|
{ key: "hostd_feedback_port", meaning: "plugin-hostd's feedback port, connected before the command port; 0 is the command port's next", default: "0" },
|
|
64
67
|
{ key: "settle_s", meaning: "wait until the plugin directories have been quiet this long before a run", default: "0" },
|
|
@@ -92,6 +95,9 @@ export function parseSyncArgs(argv) {
|
|
|
92
95
|
let noDistributed = false;
|
|
93
96
|
let noWait = false;
|
|
94
97
|
let refreshPinsOnly = false;
|
|
98
|
+
let rejudge = false;
|
|
99
|
+
let memoryOnly = false;
|
|
100
|
+
let adapterOnly = false;
|
|
95
101
|
let help = false;
|
|
96
102
|
for (let i = 0; i < argv.length; i++) {
|
|
97
103
|
const a = argv[i];
|
|
@@ -105,6 +111,12 @@ export function parseSyncArgs(argv) {
|
|
|
105
111
|
noWait = true;
|
|
106
112
|
else if (a === "--refresh-pins")
|
|
107
113
|
refreshPinsOnly = true;
|
|
114
|
+
else if (a === "--rejudge")
|
|
115
|
+
rejudge = true;
|
|
116
|
+
else if (a === "--memory-only")
|
|
117
|
+
memoryOnly = true;
|
|
118
|
+
else if (a === "--adapter-only")
|
|
119
|
+
adapterOnly = true;
|
|
108
120
|
else if (a === "--help")
|
|
109
121
|
help = true;
|
|
110
122
|
else if (a === "--grid-hosts")
|
|
@@ -121,7 +133,11 @@ export function parseSyncArgs(argv) {
|
|
|
121
133
|
else
|
|
122
134
|
return { ok: false, error: `unknown argument ${a}` };
|
|
123
135
|
}
|
|
124
|
-
|
|
136
|
+
if (rejudge && (memoryOnly || adapterOnly || force || refreshPinsOnly))
|
|
137
|
+
return { ok: false, error: "--rejudge measures nothing: it goes with none of --memory-only, --adapter-only, --force and --refresh-pins" };
|
|
138
|
+
if ([memoryOnly, adapterOnly, refreshPinsOnly].filter(Boolean).length > 1)
|
|
139
|
+
return { ok: false, error: "--memory-only, --adapter-only and --refresh-pins are different runs: one at a time" };
|
|
140
|
+
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, refreshPins: refreshPinsOnly, rejudge, memoryOnly, adapterOnly, help, values } };
|
|
125
141
|
}
|
|
126
142
|
export function syncHelpText() {
|
|
127
143
|
const rows = SYNC_SETTINGS.map((s) => ` ${`${flagOf(s.key)} <value>`.padEnd(36)}${s.meaning}${s.default === "" ? "" : ` (default: ${s.default})`}`);
|
|
@@ -134,6 +150,9 @@ export function syncHelpText() {
|
|
|
134
150
|
" --no-wait a run in progress is not waited for: exit 0, and the holder runs once more at its end",
|
|
135
151
|
" --no-distributed read no distributed record: every plugin not recorded is measured",
|
|
136
152
|
" --refresh-pins only bring every pin's files up to the ones plugin-hostd names, measuring nothing",
|
|
153
|
+
" --rejudge derive every record's verdicts, tiers and pin again from the figures it holds; measures nothing",
|
|
154
|
+
" --memory-only measure only the footprint of each recorded plugin at the Pi's cells and merge it; with --force, those already merged too",
|
|
155
|
+
" --adapter-only run each LV2 plugin only under omx-clap-host's LV2 adapter at the Pi's cells, on private JACK servers, and merge it; with --force, those already done too",
|
|
137
156
|
" --grid-hosts the fast grid, the same as --grid hosts (7 cells with the shipped hosts)",
|
|
138
157
|
" --help this text",
|
|
139
158
|
...rows,
|
|
@@ -184,6 +203,9 @@ export function resolveSyncSettings(file, args, env, home = homedir(), boardMode
|
|
|
184
203
|
if (memoryLimit !== undefined && !memoryLimit.ok)
|
|
185
204
|
return { ok: false, error: `memory_limit: ${memoryLimit.error}` };
|
|
186
205
|
const memory = memoryBoundOf(memoryLimit?.bytes);
|
|
206
|
+
const memoryNotes = number("memory_notes", 1);
|
|
207
|
+
if (memoryNotes === undefined || !Number.isInteger(memoryNotes) || memoryNotes > 128)
|
|
208
|
+
return { ok: false, error: "memory_notes must be a whole number of notes, 1 to 128" };
|
|
187
209
|
const profileId = get("host_profile") || "omx-clap-host";
|
|
188
210
|
const hostProfile = shippedHostProfile(profileId);
|
|
189
211
|
if (hostProfile === undefined)
|
|
@@ -215,6 +237,7 @@ export function resolveSyncSettings(file, args, env, home = homedir(), boardMode
|
|
|
215
237
|
grid: grid.spec,
|
|
216
238
|
cellsPerPluginBudget,
|
|
217
239
|
...(memory === undefined ? {} : { memory }),
|
|
240
|
+
memoryNotes,
|
|
218
241
|
hostdPort,
|
|
219
242
|
hostdFeedbackPort: feedbackPort === 0 && hostdPort !== 0 ? hostdPort + 1 : feedbackPort,
|
|
220
243
|
settleS,
|
|
@@ -224,6 +247,29 @@ export function resolveSyncSettings(file, args, env, home = homedir(), boardMode
|
|
|
224
247
|
},
|
|
225
248
|
};
|
|
226
249
|
}
|
|
250
|
+
/** lilv itself, one world over the whole search path (tools/lv2_resolve.py), as every host built on it resolves (#199). */
|
|
251
|
+
export const LILV_RESOLVE = (dirs) => {
|
|
252
|
+
const r = spawnSync("python3", [join(toolsDir(), "lv2_resolve.py")], {
|
|
253
|
+
encoding: "utf8", timeout: 120_000, killSignal: "SIGKILL", maxBuffer: 64 << 20, env: { ...process.env, LV2_PATH: dirs.join(":") },
|
|
254
|
+
});
|
|
255
|
+
if (r.error !== undefined || r.status !== 0)
|
|
256
|
+
return { problem: (r.stderr ?? "").trim().split("\n").pop() || (r.error?.message ?? `exit ${String(r.status)}`) };
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(r.stdout).plugins;
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
return { problem: `lv2_resolve.py printed no JSON: ${e.message}` };
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
/** Whether `path` exists and is, through its links, the file `target` is. */
|
|
265
|
+
function linksTo(path, target) {
|
|
266
|
+
try {
|
|
267
|
+
return realpathSync(path) === realpathSync(target);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
227
273
|
const CLAP_SUFFIX = ".clap";
|
|
228
274
|
/** Every `.clap` under a directory, its subdirectories included, in name order; a directory reached twice is walked once. */
|
|
229
275
|
function clapFiles(dir, seen, problems) {
|
|
@@ -270,9 +316,10 @@ function clapFiles(dir, seen, problems) {
|
|
|
270
316
|
* LV2 directories that declares it, as plugin-hostd finds it. A directory that cannot be read is a problem named, and
|
|
271
317
|
* the others are still walked.
|
|
272
318
|
*/
|
|
273
|
-
export function discoverSyncItems(settings) {
|
|
319
|
+
export function discoverSyncItems(settings, lv2Resolve) {
|
|
274
320
|
const items = [];
|
|
275
321
|
const problems = [];
|
|
322
|
+
const notes = [];
|
|
276
323
|
const seen = new Set();
|
|
277
324
|
const clap = new Set();
|
|
278
325
|
for (const dir of settings.clapDirs) {
|
|
@@ -284,6 +331,14 @@ export function discoverSyncItems(settings) {
|
|
|
284
331
|
items.push({ key: `clap:${path}`, format: "clap", root: dirname(path), files: [basename(path)] });
|
|
285
332
|
}
|
|
286
333
|
}
|
|
334
|
+
// the bundle lilv takes for each plugin uri; without lilv, the first bundle found (and the log says so)
|
|
335
|
+
const resolved = lv2Resolve === undefined || settings.lv2Dirs.length === 0 ? undefined : lv2Resolve(settings.lv2Dirs);
|
|
336
|
+
const taken = new Map();
|
|
337
|
+
if (resolved !== undefined && "problem" in resolved)
|
|
338
|
+
notes.push(`lilv could not resolve the LV2 plugins (${resolved.problem}); each is keyed by the first bundle found`);
|
|
339
|
+
else if (resolved !== undefined)
|
|
340
|
+
for (const p of resolved)
|
|
341
|
+
taken.set(p.uri, p);
|
|
287
342
|
const lv2 = new Set();
|
|
288
343
|
for (const dir of settings.lv2Dirs) {
|
|
289
344
|
let bundles;
|
|
@@ -304,14 +359,23 @@ export function discoverSyncItems(settings) {
|
|
|
304
359
|
continue;
|
|
305
360
|
}
|
|
306
361
|
for (const p of manifestPlugins(ttl)) {
|
|
362
|
+
const lilv = taken.get(p.uri);
|
|
363
|
+
// lilv takes another bundle for this uri: this one is not the plugin a host loads
|
|
364
|
+
if (lilv !== undefined && resolve(lilv.bundle) !== resolve(bundle))
|
|
365
|
+
continue;
|
|
307
366
|
if (lv2.has(p.uri))
|
|
308
367
|
continue;
|
|
309
368
|
lv2.add(p.uri);
|
|
310
|
-
|
|
369
|
+
const binary = lilv?.binary ?? (p.binary === undefined ? undefined : join(bundle, p.binary));
|
|
370
|
+
const files = ["manifest.ttl", ...p.files.filter((f) => f !== "manifest.ttl")];
|
|
371
|
+
// a binary named by an absolute IRI (a complement's) is tracked through the bundle's link to it, when it has one
|
|
372
|
+
if (binary !== undefined && !files.includes(basename(binary)) && linksTo(join(bundle, basename(binary)), binary))
|
|
373
|
+
files.push(basename(binary));
|
|
374
|
+
items.push({ key: `lv2:${p.uri}`, format: "lv2", root: bundle, files, uri: p.uri, ...(binary === undefined ? {} : { binary }) });
|
|
311
375
|
}
|
|
312
376
|
}
|
|
313
377
|
}
|
|
314
|
-
return { items, problems };
|
|
378
|
+
return { items, problems, notes };
|
|
315
379
|
}
|
|
316
380
|
/** The SHA-256 of a file's bytes, read in pieces over one descriptor. */
|
|
317
381
|
export function fileSha256(path) {
|
|
@@ -365,6 +429,20 @@ export const SYNC_CODE = {
|
|
|
365
429
|
/** The probe runner wrote no document to judge. */
|
|
366
430
|
notMeasured: "qualify.sync.not-measured",
|
|
367
431
|
};
|
|
432
|
+
/**
|
|
433
|
+
* Why a plugin is unmeasured because of the machine it ran on rather than what it is (#192): an EACCES, EPERM or
|
|
434
|
+
* ENOSPC the run hit, or a measurement that could not run or wrote no document. Such a plugin is measured again by
|
|
435
|
+
* the next run although its files are unchanged; a reason of the plugin's own (probe.limit.not-implemented, no such
|
|
436
|
+
* plugin, a crash) is not. `undefined` for every other outcome.
|
|
437
|
+
*/
|
|
438
|
+
export function machineReason(p) {
|
|
439
|
+
if (p.outcome !== "unmeasured" || p.code !== SYNC_CODE.notMeasured)
|
|
440
|
+
return undefined;
|
|
441
|
+
const errno = /\b(EACCES|EPERM|ENOSPC)\b/.exec(p.detail ?? "")?.[1];
|
|
442
|
+
if (errno !== undefined)
|
|
443
|
+
return errno;
|
|
444
|
+
return /wrote no document|cannot run the probe runner/.test(p.detail ?? "") ? "the measurement wrote no document" : undefined;
|
|
445
|
+
}
|
|
368
446
|
/** What the quick qualification never measures, stated in every record it writes. */
|
|
369
447
|
export const QUICK_NOT_MEASURED = [{ dimension: "stability", code: HOSTING_CODE.stabilityUnsoaked }];
|
|
370
448
|
const EMPTY_STATE = { schema: SYNC_STATE_SCHEMA, entries: {} };
|
|
@@ -402,6 +480,32 @@ export function recordedAsIs(item, entry) {
|
|
|
402
480
|
const same = now.length === entry.files.length && now.every((f) => entry.files.some((e) => e.path === f.path && e.sha256 === f.sha256));
|
|
403
481
|
return { same, files: now };
|
|
404
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* The entry of an LV2 plugin a host now loads from another bundle than it was qualified in, when its binary is the
|
|
485
|
+
* same bytes (#199): the entry with the new bundle and the new bundle's file digests, its verdicts and records kept.
|
|
486
|
+
* `undefined` when the item is not that case: not LV2, no entry, the same bundle, a binary that cannot be read, or one
|
|
487
|
+
* whose bytes differ from the recorded binary's (then it is measured again).
|
|
488
|
+
*/
|
|
489
|
+
export function sameBinaryElsewhere(item, entry) {
|
|
490
|
+
if (item.format !== "lv2" || entry === undefined || item.binary === undefined || resolve(entry.root) === resolve(item.root))
|
|
491
|
+
return undefined;
|
|
492
|
+
const was = entry.files.find((f) => basename(f.path) === basename(item.binary));
|
|
493
|
+
if (was === undefined)
|
|
494
|
+
return undefined;
|
|
495
|
+
let now;
|
|
496
|
+
try {
|
|
497
|
+
now = fileSha256(item.binary);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
if (now !== was.sha256)
|
|
503
|
+
return undefined;
|
|
504
|
+
const files = digestFiles(item.root, item.files);
|
|
505
|
+
if (files === undefined)
|
|
506
|
+
return undefined;
|
|
507
|
+
return { ...entry, root: item.root, files };
|
|
508
|
+
}
|
|
405
509
|
/**
|
|
406
510
|
* The pin of a judged run with the files plugin-hostd collects for its plugin (plugin-qualify#228): the probe's list is
|
|
407
511
|
* the probe's view, and an add checks the daemon's, so a pin holds the daemon's. No pin when plugin-hostd cannot name
|
|
@@ -595,15 +699,10 @@ function qualifyClap(ctx, item) {
|
|
|
595
699
|
}
|
|
596
700
|
/** An LV2 plugin, measured in a world of its own bundle only, as plugin-hostd runs a pinned LV2 plugin. */
|
|
597
701
|
function qualifyLv2(ctx, item) {
|
|
598
|
-
|
|
599
|
-
try {
|
|
600
|
-
symlinkSync(item.root, join(world, basename(item.root)));
|
|
702
|
+
return lv2World(item.root, (world) => {
|
|
601
703
|
const pass = runPass(ctx, { format: "lv2", uri: item.uri, bundle: item.root }, { ...ctx.env, LV2_PATH: world });
|
|
602
704
|
return [...pass.failures.map((f) => ({ uri: item.uri, outcome: "unmeasured", code: SYNC_CODE.notMeasured, detail: f.problem })), ...judgedPlugins(ctx, pass.runs)];
|
|
603
|
-
}
|
|
604
|
-
finally {
|
|
605
|
-
rmSync(world, { recursive: true, force: true });
|
|
606
|
-
}
|
|
705
|
+
});
|
|
607
706
|
}
|
|
608
707
|
/** One item qualified, whatever happens to it: an error of the sync's own while measuring it is its outcome, not the run's end. */
|
|
609
708
|
export function qualifyItem(ctx, item, files) {
|
|
@@ -913,7 +1012,7 @@ export function descriptorAccessProblems(dir) {
|
|
|
913
1012
|
return problems;
|
|
914
1013
|
}
|
|
915
1014
|
export function summaryLine(s) {
|
|
916
|
-
return `qualified ${String(s.qualified)}, skipped ${String(s.skipped)}, refused ${String(s.refused)}, crashed ${String(s.crashed)}, hung ${String(s.hung)}, unmeasured ${String(s.unmeasured)}, distributed ${String(s.distributed)}${s.refreshed ? `, pins refreshed ${String(s.refreshed)}` : ""}, ${s.seconds.toFixed(1)} s`;
|
|
1015
|
+
return `qualified ${String(s.qualified)}, skipped ${String(s.skipped)}, refused ${String(s.refused)}, crashed ${String(s.crashed)}, hung ${String(s.hung)}, unmeasured ${String(s.unmeasured)}, distributed ${String(s.distributed)}${s.refreshed ? `, pins refreshed ${String(s.refreshed)}` : ""}${s.rejudge === undefined ? "" : `, rejudged ${String(s.rejudge.changed)}, unchanged ${String(s.rejudge.unchanged)}, owing ${String(s.rejudge.owing)}`}${s.memoryOnly === undefined ? "" : `, memory measured ${String(s.memoryOnly.measured)}, already ${String(s.memoryOnly.already)}, failed ${String(s.memoryOnly.failed)}, leaks not measured`}${s.adapterOnly === undefined ? "" : `, adapter run ${String(s.adapterOnly.measured)}, already ${String(s.adapterOnly.already)}, failed ${String(s.adapterOnly.failed)}`}, ${s.seconds.toFixed(1)} s`;
|
|
917
1016
|
}
|
|
918
1017
|
/** The exit status `flock -n` returns when the lock is held (-E), told apart from a failure of flock itself. */
|
|
919
1018
|
const LOCK_HELD = 99;
|
|
@@ -995,8 +1094,19 @@ function addSummaries(a, b) {
|
|
|
995
1094
|
seconds: a.seconds + b.seconds,
|
|
996
1095
|
...(sent === 0 ? {} : { sent }),
|
|
997
1096
|
...(refreshed === 0 ? {} : { refreshed }),
|
|
1097
|
+
...(b.rejudge === undefined ? (a.rejudge === undefined ? {} : { rejudge: a.rejudge }) : { rejudge: b.rejudge }),
|
|
1098
|
+
...(b.memoryOnly === undefined ? (a.memoryOnly === undefined ? {} : { memoryOnly: a.memoryOnly }) : { memoryOnly: b.memoryOnly }),
|
|
1099
|
+
...(b.adapterOnly === undefined ? (a.adapterOnly === undefined ? {} : { adapterOnly: a.adapterOnly }) : { adapterOnly: b.adapterOnly }),
|
|
998
1100
|
};
|
|
999
1101
|
}
|
|
1102
|
+
/** Whether this run's scan covers an entry: its root lies under one of the run's directories of its own format. */
|
|
1103
|
+
function scannedBy(settings, entry) {
|
|
1104
|
+
const root = resolve(entry.root);
|
|
1105
|
+
return (entry.format === "clap" ? settings.clapDirs : settings.lv2Dirs).some((d) => {
|
|
1106
|
+
const dir = resolve(d);
|
|
1107
|
+
return root === dir || root.startsWith(dir.endsWith(sep) ? dir : dir + sep);
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1000
1110
|
/**
|
|
1001
1111
|
* One run: wait for quiet, qualify what is new or changed, drop what is gone, write the state, the records, the
|
|
1002
1112
|
* descriptors and the pin file, and send the changed pins to a running daemon. The state and the pin file are written
|
|
@@ -1005,7 +1115,7 @@ function addSummaries(a, b) {
|
|
|
1005
1115
|
async function runLocked(settings, options) {
|
|
1006
1116
|
const log = options.log ?? (() => { });
|
|
1007
1117
|
const env = options.env ?? process.env;
|
|
1008
|
-
log(options.refreshOnly === true ? "pin refresh begins" : "sync run begins");
|
|
1118
|
+
log(options.rejudge === true ? "re-judge begins: nothing is measured" : options.memoryOnly === true ? "memory-only pass begins" : options.adapterOnly === true ? "adapter-only pass begins" : options.refreshOnly === true ? "pin refresh begins" : "sync run begins");
|
|
1009
1119
|
{
|
|
1010
1120
|
const started = Date.now();
|
|
1011
1121
|
const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
|
|
@@ -1018,6 +1128,24 @@ async function runLocked(settings, options) {
|
|
|
1018
1128
|
}
|
|
1019
1129
|
if (options.refreshOnly === true)
|
|
1020
1130
|
return sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1131
|
+
if (options.rejudge === true) {
|
|
1132
|
+
const r = rejudgeState(ctx, fresh.state);
|
|
1133
|
+
if (JSON.stringify(r.state) !== JSON.stringify(fresh.state))
|
|
1134
|
+
writeFileWhole(settings.stateFile, `${JSON.stringify(r.state, null, 1)}\n`);
|
|
1135
|
+
const summary = await sendRefreshed(settings, r.state, fresh.refreshed.length, started, log);
|
|
1136
|
+
return { ...summary, rejudge: { changed: r.changed, unchanged: r.unchanged, owing: r.owing.length } };
|
|
1137
|
+
}
|
|
1138
|
+
if (options.adapterOnly === true) {
|
|
1139
|
+
const r = adapterPass(ctx, fresh.state, { ...options.adapterPass, force: options.force === true });
|
|
1140
|
+
log(`adapter runs under ${Object.entries(r.versions).map(([k, v]) => `${k} ${v}`).join(", ")}`);
|
|
1141
|
+
const summary = await sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1142
|
+
return { ...summary, adapterOnly: { measured: r.measured, already: r.already, failed: r.failed } };
|
|
1143
|
+
}
|
|
1144
|
+
if (options.memoryOnly === true) {
|
|
1145
|
+
const r = memoryPass(ctx, fresh.state, { force: options.force === true, ...(options.probeRun === undefined ? {} : { run: options.probeRun }) });
|
|
1146
|
+
const summary = await sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1147
|
+
return { ...summary, memoryOnly: { measured: r.measured, already: r.already, failed: r.failed } };
|
|
1148
|
+
}
|
|
1021
1149
|
await waitQuiet(dirs, settings.settleS, log);
|
|
1022
1150
|
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0, distributed: 0 };
|
|
1023
1151
|
const layer = distributedLayerOf(settings, options.force === true, log);
|
|
@@ -1032,28 +1160,58 @@ async function runLocked(settings, options) {
|
|
|
1032
1160
|
const stateBefore = state;
|
|
1033
1161
|
let unreadable = false;
|
|
1034
1162
|
const lv2Pinned = [];
|
|
1163
|
+
// the plugins measured this run, and how many of them the machine left unmeasured, by reason (#192)
|
|
1164
|
+
let measuredHere = 0;
|
|
1165
|
+
const byMachine = new Map();
|
|
1035
1166
|
// a change made while a pass ran is caught by one more pass, a few at most
|
|
1036
1167
|
for (let pass = 0; pass < 3; pass++) {
|
|
1037
1168
|
const signature = directoriesSignature(dirs);
|
|
1038
|
-
const { items, problems } = discoverSyncItems(settings);
|
|
1169
|
+
const { items, problems, notes } = discoverSyncItems(settings, options.lv2Resolve ?? LILV_RESOLVE);
|
|
1039
1170
|
for (const p of problems)
|
|
1040
1171
|
log(p);
|
|
1172
|
+
if (pass === 0)
|
|
1173
|
+
for (const n of notes)
|
|
1174
|
+
log(n);
|
|
1041
1175
|
unreadable = problems.length > 0;
|
|
1042
1176
|
const found = new Set(items.map((i) => i.key));
|
|
1043
1177
|
const entries = {};
|
|
1044
|
-
for (const [key, entry] of Object.entries(state.entries))
|
|
1178
|
+
for (const [key, entry] of Object.entries(state.entries)) {
|
|
1045
1179
|
if (found.has(key))
|
|
1046
1180
|
entries[key] = entry;
|
|
1047
|
-
|
|
1048
|
-
if (!
|
|
1181
|
+
// a run scoped to some directories keeps what it did not scan: gone means not found where this run looked
|
|
1182
|
+
else if (!scannedBy(settings, entry)) {
|
|
1183
|
+
entries[key] = entry;
|
|
1184
|
+
if (pass === 0)
|
|
1185
|
+
log(`${key}: not under a directory this run scans; its pin is kept`);
|
|
1186
|
+
}
|
|
1187
|
+
else
|
|
1049
1188
|
log(`${key}: gone, its pin is dropped`);
|
|
1189
|
+
}
|
|
1050
1190
|
for (const item of items) {
|
|
1051
1191
|
const recorded = recordedAsIs(item, entries[item.key]);
|
|
1052
|
-
|
|
1192
|
+
const retry = recorded.same ? entries[item.key].plugins.map(machineReason).find((r) => r !== undefined) : undefined;
|
|
1193
|
+
if (recorded.same && options.force !== true && retry === undefined) {
|
|
1053
1194
|
if (pass === 0)
|
|
1054
1195
|
count.skipped += entries[item.key].plugins.length;
|
|
1055
1196
|
continue;
|
|
1056
1197
|
}
|
|
1198
|
+
// an LV2 plugin a host now loads from another bundle with the same binary (a complement that restates its TTL,
|
|
1199
|
+
// #199): the record stands, keyed by its uri and its binary; only the bundle, its files and the pins' files move
|
|
1200
|
+
const moved = options.force === true ? undefined : sameBinaryElsewhere(item, entries[item.key]);
|
|
1201
|
+
if (moved !== undefined) {
|
|
1202
|
+
entries[item.key] = { ...moved, plugins: moved.plugins.map((p) => ({ ...p, ...(p.pin === undefined ? {} : { pin: withHostdFiles(ctx, p.pin) }) })) };
|
|
1203
|
+
if (item.format === "lv2")
|
|
1204
|
+
lv2Pinned.push(...entries[item.key].plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|
|
1205
|
+
if (pass === 0)
|
|
1206
|
+
count.skipped += moved.plugins.length;
|
|
1207
|
+
log(`${item.key}: loaded from ${item.root} now, with the binary it was qualified with; its record stands and its pin's files follow the bundle`);
|
|
1208
|
+
state = { schema: SYNC_STATE_SCHEMA, entries };
|
|
1209
|
+
writeFileWhole(settings.stateFile, `${JSON.stringify(state, null, 1)}\n`);
|
|
1210
|
+
writeFileWhole(settings.pinFile, syncPinsText(state));
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (recorded.same && retry !== undefined)
|
|
1214
|
+
log(`${item.key}: unmeasured last run by the machine (${retry}); measured again`);
|
|
1057
1215
|
const files = recorded.files ?? digestFiles(item.root, item.files);
|
|
1058
1216
|
if (files === undefined) {
|
|
1059
1217
|
log(`${item.key}: a file of it cannot be read; skipped this run`);
|
|
@@ -1078,6 +1236,13 @@ async function runLocked(settings, options) {
|
|
|
1078
1236
|
lv2Pinned.push(...entry.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|
|
1079
1237
|
for (const p of entry.plugins) {
|
|
1080
1238
|
count[p.outcome]++;
|
|
1239
|
+
measuredHere++;
|
|
1240
|
+
// a reason of the machine is said once: the plugins after the first are counted, not listed
|
|
1241
|
+
const why = machineReason(p);
|
|
1242
|
+
if (why !== undefined)
|
|
1243
|
+
byMachine.set(why, (byMachine.get(why) ?? 0) + 1);
|
|
1244
|
+
if (why !== undefined && byMachine.get(why) > 1)
|
|
1245
|
+
continue;
|
|
1081
1246
|
log(`${p.uri}: ${p.outcome}${p.code === undefined ? "" : ` (${p.code})`}${p.detail === undefined ? "" : `: ${p.detail}`}`);
|
|
1082
1247
|
}
|
|
1083
1248
|
state = { schema: SYNC_STATE_SCHEMA, entries };
|
|
@@ -1119,8 +1284,16 @@ async function runLocked(settings, options) {
|
|
|
1119
1284
|
if (replies[i] !== "resp 0")
|
|
1120
1285
|
log(`plugin-hostd answered ${replies[i]} to ${changed[i].split(" ")[1]}`);
|
|
1121
1286
|
const taken = replies.filter((r) => r === "resp 0").length;
|
|
1287
|
+
for (const [why, n] of byMachine)
|
|
1288
|
+
if (n > 1)
|
|
1289
|
+
log(`${String(n)} plugins unmeasured by the machine (${why}); the first is listed above`);
|
|
1290
|
+
const [only] = byMachine.size === 1 ? [...byMachine] : [];
|
|
1291
|
+
const machineFailure = only !== undefined && only[1] === measuredHere ? only[0] : undefined;
|
|
1292
|
+
if (machineFailure !== undefined)
|
|
1293
|
+
log(`every plugin this run measured was left unmeasured by the machine (${machineFailure}): the run failed`);
|
|
1122
1294
|
return {
|
|
1123
1295
|
...count,
|
|
1296
|
+
...(machineFailure === undefined ? {} : { machineFailure }),
|
|
1124
1297
|
seconds: (Date.now() - started) / 1000,
|
|
1125
1298
|
...(taken === 0 ? {} : { sent: taken }),
|
|
1126
1299
|
...(fresh.refreshed.length === 0 ? {} : { refreshed: fresh.refreshed.length }),
|