@openmixer/plugin-qualify 0.18.0 → 0.18.2
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 +93 -3
- package/bin/plugin-qualify-sync.mjs +3 -1
- package/dist/clap-qualify.d.ts +3 -1
- package/dist/clap-qualify.js +3 -1
- package/dist/clap-qualify.js.map +1 -1
- 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 +21 -0
- package/dist/host-admission.js +33 -3
- package/dist/host-admission.js.map +1 -1
- package/dist/hostd-pin-files.js +3 -1
- package/dist/hostd-pin-files.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement/format.d.ts +10 -0
- package/dist/measurement/format.js +8 -0
- package/dist/measurement/format.js.map +1 -1
- package/dist/measurement/probe-json.js +2 -1
- package/dist/measurement/probe-json.js.map +1 -1
- package/dist/probe-judge.d.ts +11 -0
- package/dist/probe-judge.js +6 -3
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-runner.js +3 -2
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +113 -8
- package/dist/qualification-record.js +18 -13
- 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 +30 -0
- package/dist/qualify-declarations.js +24 -1
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-isolated.d.ts +70 -0
- package/dist/qualify-isolated.js +184 -0
- package/dist/qualify-isolated.js.map +1 -0
- package/dist/qualify-rejudge.d.ts +82 -0
- package/dist/qualify-rejudge.js +434 -0
- package/dist/qualify-rejudge.js.map +1 -0
- package/dist/qualify-sync.d.ts +97 -1
- package/dist/qualify-sync.js +276 -24
- 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 +5 -1
- package/src/distributed-records.ts +1 -1
- package/src/host-admission.ts +40 -3
- package/src/hostd-pin-files.ts +3 -1
- package/src/index.ts +2 -0
- package/src/measurement/format.ts +17 -0
- package/src/measurement/probe-json.ts +2 -1
- package/src/probe-judge.ts +10 -3
- package/src/probe-runner.ts +2 -2
- package/src/qualification-record.ts +126 -15
- package/src/qualify-adapter-pass.ts +278 -0
- package/src/qualify-declarations.ts +26 -1
- package/src/qualify-isolated.ts +201 -0
- package/src/qualify-rejudge.ts +466 -0
- package/src/qualify-sync.ts +295 -24
- package/tools/isolated-step.mjs +122 -0
- package/tools/lv2_resolve.py +49 -0
- package/tools/mod-host-admit.mjs +99 -7
package/src/qualify-sync.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
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
24
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
@@ -32,11 +32,14 @@ import { lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
|
|
|
32
32
|
import { judgmentPinEntries, pinsFile, type PinEntry } from "./pins-file.js";
|
|
33
33
|
import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
|
|
34
34
|
import type { Judgment } from "./probe-judge.js";
|
|
35
|
-
import { qualifyTargets } from "./probe-runner.js";
|
|
35
|
+
import { qualifyTargets, type ProbeRun } from "./probe-runner.js";
|
|
36
36
|
import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
37
|
+
import { adapterPass, type AdapterPassOptions, type AdapterPassResult } from "./qualify-adapter-pass.js";
|
|
38
|
+
import { ISOLATED_RESULT, baseSteps, completeIsolated, isolatedCell, missingSteps, judgeIsolatedRun, owesIsolated, pinAfterStep, spawnedIsolatedRunner, type IsolatedRunner } from "./qualify-isolated.js";
|
|
39
|
+
import { lv2World, rejudgeState, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
|
|
37
40
|
import { memoryBoundOf, parseMemoryLimit, type MemoryBound } from "./memory-bound.js";
|
|
38
|
-
import { writeQualification, type NotMeasured } from "./qualification-record.js";
|
|
39
|
-
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
41
|
+
import { rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
|
|
42
|
+
import { ADMIT_STEP_MS_ENV, DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, MEMORY_PASS_PROBE_NOTES, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
40
43
|
import { toolsDir } from "./tool-paths.js";
|
|
41
44
|
import { writeFileWhole } from "./whole-file.js";
|
|
42
45
|
|
|
@@ -73,6 +76,7 @@ export const SYNC_SETTINGS: readonly SyncSetting[] = [
|
|
|
73
76
|
{ 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" },
|
|
74
77
|
{ key: "cells_per_plugin_budget", meaning: "measure at most the first n cells of the grid a plugin; 0 is every cell", default: "0" },
|
|
75
78
|
{ 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" },
|
|
79
|
+
{ 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) },
|
|
76
80
|
{ key: "hostd_port", meaning: "plugin-hostd's command port the new pins are sent to; 0 sends none", default: "5555" },
|
|
77
81
|
{ key: "hostd_feedback_port", meaning: "plugin-hostd's feedback port, connected before the command port; 0 is the command port's next", default: "0" },
|
|
78
82
|
{ key: "settle_s", meaning: "wait until the plugin directories have been quiet this long before a run", default: "0" },
|
|
@@ -94,6 +98,8 @@ export interface SyncSettings {
|
|
|
94
98
|
readonly cellsPerPluginBudget: number;
|
|
95
99
|
/** The memory bound of every run; `undefined` is `off`. */
|
|
96
100
|
readonly memory?: MemoryBound;
|
|
101
|
+
/** The notes an instrument with no declared capacity holds in a memory-only run. */
|
|
102
|
+
readonly memoryNotes: number;
|
|
97
103
|
readonly hostdPort: number;
|
|
98
104
|
readonly hostdFeedbackPort: number;
|
|
99
105
|
readonly settleS: number;
|
|
@@ -128,6 +134,14 @@ export interface SyncArgs {
|
|
|
128
134
|
readonly noWait: boolean;
|
|
129
135
|
/** `--refresh-pins`: refresh the pins' files from plugin-hostd and measure nothing (the package's upgrade runs it). */
|
|
130
136
|
readonly refreshPins: boolean;
|
|
137
|
+
/** `--rejudge`: the verdicts, tiers and pins derived again from the records' figures, measuring nothing. */
|
|
138
|
+
readonly rejudge: boolean;
|
|
139
|
+
/** `--memory-only`: the footprint of each recorded plugin at the Pi's cells, merged into its record. */
|
|
140
|
+
readonly memoryOnly: boolean;
|
|
141
|
+
/** `--adapter-only`: the run of each LV2 plugin under the adapter at the Pi's cells, merged into its record. */
|
|
142
|
+
readonly adapterOnly: boolean;
|
|
143
|
+
/** `--isolated-only`: the missing isolated step of each record already judged, extending its run. */
|
|
144
|
+
readonly isolatedOnly: boolean;
|
|
131
145
|
readonly help: boolean;
|
|
132
146
|
/** The settings given as flags, by key. */
|
|
133
147
|
readonly values: ReadonlyMap<string, string>;
|
|
@@ -144,6 +158,10 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
144
158
|
let noDistributed = false;
|
|
145
159
|
let noWait = false;
|
|
146
160
|
let refreshPinsOnly = false;
|
|
161
|
+
let rejudge = false;
|
|
162
|
+
let memoryOnly = false;
|
|
163
|
+
let adapterOnly = false;
|
|
164
|
+
let isolatedOnly = false;
|
|
147
165
|
let help = false;
|
|
148
166
|
for (let i = 0; i < argv.length; i++) {
|
|
149
167
|
const a = argv[i]!;
|
|
@@ -152,6 +170,10 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
152
170
|
else if (a === "--no-distributed") noDistributed = true;
|
|
153
171
|
else if (a === "--no-wait") noWait = true;
|
|
154
172
|
else if (a === "--refresh-pins") refreshPinsOnly = true;
|
|
173
|
+
else if (a === "--rejudge") rejudge = true;
|
|
174
|
+
else if (a === "--memory-only") memoryOnly = true;
|
|
175
|
+
else if (a === "--adapter-only") adapterOnly = true;
|
|
176
|
+
else if (a === "--isolated-only") isolatedOnly = true;
|
|
155
177
|
else if (a === "--help") help = true;
|
|
156
178
|
else if (a === "--grid-hosts") values.set("grid", "hosts");
|
|
157
179
|
else if (a === "--config" || byFlag.has(a)) {
|
|
@@ -161,7 +183,9 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
161
183
|
else values.set(byFlag.get(a)!, v);
|
|
162
184
|
} else return { ok: false, error: `unknown argument ${a}` };
|
|
163
185
|
}
|
|
164
|
-
|
|
186
|
+
if (rejudge && (memoryOnly || adapterOnly || isolatedOnly || force || refreshPinsOnly)) return { ok: false, error: "--rejudge measures nothing: it goes with none of --memory-only, --adapter-only, --force and --refresh-pins" };
|
|
187
|
+
if ([memoryOnly, adapterOnly, isolatedOnly, refreshPinsOnly].filter(Boolean).length > 1) return { ok: false, error: "--memory-only, --adapter-only and --refresh-pins are different runs: one at a time" };
|
|
188
|
+
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, refreshPins: refreshPinsOnly, rejudge, memoryOnly, adapterOnly, isolatedOnly, help, values } };
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
export function syncHelpText(): string {
|
|
@@ -175,6 +199,10 @@ export function syncHelpText(): string {
|
|
|
175
199
|
" --no-wait a run in progress is not waited for: exit 0, and the holder runs once more at its end",
|
|
176
200
|
" --no-distributed read no distributed record: every plugin not recorded is measured",
|
|
177
201
|
" --refresh-pins only bring every pin's files up to the ones plugin-hostd names, measuring nothing",
|
|
202
|
+
" --rejudge derive every record's verdicts, tiers and pin again from the figures it holds; measures nothing",
|
|
203
|
+
" --memory-only measure only the footprint of each recorded plugin at the Pi's cells and merge it; with --force, those already merged too",
|
|
204
|
+
" --isolated-only run only the isolated step each record still owes, adding it to the record's run",
|
|
205
|
+
" --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",
|
|
178
206
|
" --grid-hosts the fast grid, the same as --grid hosts (7 cells with the shipped hosts)",
|
|
179
207
|
" --help this text",
|
|
180
208
|
...rows,
|
|
@@ -226,6 +254,8 @@ export function resolveSyncSettings(
|
|
|
226
254
|
const memoryLimit = memoryText === undefined || memoryText === "" ? undefined : parseMemoryLimit(memoryText);
|
|
227
255
|
if (memoryLimit !== undefined && !memoryLimit.ok) return { ok: false, error: `memory_limit: ${memoryLimit.error}` };
|
|
228
256
|
const memory = memoryBoundOf(memoryLimit?.bytes);
|
|
257
|
+
const memoryNotes = number("memory_notes", 1);
|
|
258
|
+
if (memoryNotes === undefined || !Number.isInteger(memoryNotes) || memoryNotes > 128) return { ok: false, error: "memory_notes must be a whole number of notes, 1 to 128" };
|
|
229
259
|
const profileId = get("host_profile") || "omx-clap-host";
|
|
230
260
|
const hostProfile = shippedHostProfile(profileId);
|
|
231
261
|
if (hostProfile === undefined) return { ok: false, error: `host_profile ${profileId} is not a shipped host profile` };
|
|
@@ -256,6 +286,7 @@ export function resolveSyncSettings(
|
|
|
256
286
|
grid: grid.spec,
|
|
257
287
|
cellsPerPluginBudget,
|
|
258
288
|
...(memory === undefined ? {} : { memory }),
|
|
289
|
+
memoryNotes,
|
|
259
290
|
hostdPort,
|
|
260
291
|
hostdFeedbackPort: feedbackPort === 0 && hostdPort !== 0 ? hostdPort + 1 : feedbackPort,
|
|
261
292
|
settleS,
|
|
@@ -279,6 +310,40 @@ export interface SyncItem {
|
|
|
279
310
|
readonly files: readonly string[];
|
|
280
311
|
/** The LV2 plugin's uri. */
|
|
281
312
|
readonly uri?: string;
|
|
313
|
+
/** The LV2 plugin's binary as lilv loads it, an absolute path that may lie outside `root` (a complement's). */
|
|
314
|
+
readonly binary?: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** One LV2 plugin as lilv resolves it over the whole search path: the bundle its version rule takes, and its binary. */
|
|
318
|
+
export interface Lv2Resolved {
|
|
319
|
+
readonly uri: string;
|
|
320
|
+
readonly bundle: string;
|
|
321
|
+
readonly binary?: string | null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** The bundles lilv takes over `dirs`, or why it could not say. A stand-in in tests. */
|
|
325
|
+
export type Lv2Resolve = (dirs: readonly string[]) => readonly Lv2Resolved[] | { readonly problem: string };
|
|
326
|
+
|
|
327
|
+
/** lilv itself, one world over the whole search path (tools/lv2_resolve.py), as every host built on it resolves (#199). */
|
|
328
|
+
export const LILV_RESOLVE: Lv2Resolve = (dirs) => {
|
|
329
|
+
const r = spawnSync("python3", [join(toolsDir(), "lv2_resolve.py")], {
|
|
330
|
+
encoding: "utf8", timeout: 120_000, killSignal: "SIGKILL", maxBuffer: 64 << 20, env: { ...process.env, LV2_PATH: dirs.join(":") },
|
|
331
|
+
});
|
|
332
|
+
if (r.error !== undefined || r.status !== 0) return { problem: (r.stderr ?? "").trim().split("\n").pop() || (r.error?.message ?? `exit ${String(r.status)}`) };
|
|
333
|
+
try {
|
|
334
|
+
return (JSON.parse(r.stdout) as { plugins: Lv2Resolved[] }).plugins;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
return { problem: `lv2_resolve.py printed no JSON: ${(e as Error).message}` };
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/** Whether `path` exists and is, through its links, the file `target` is. */
|
|
341
|
+
function linksTo(path: string, target: string): boolean {
|
|
342
|
+
try {
|
|
343
|
+
return realpathSync(path) === realpathSync(target);
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
282
347
|
}
|
|
283
348
|
|
|
284
349
|
const CLAP_SUFFIX = ".clap";
|
|
@@ -322,9 +387,13 @@ function clapFiles(dir: string, seen: Set<string>, problems: string[]): string[]
|
|
|
322
387
|
* LV2 directories that declares it, as plugin-hostd finds it. A directory that cannot be read is a problem named, and
|
|
323
388
|
* the others are still walked.
|
|
324
389
|
*/
|
|
325
|
-
export function discoverSyncItems(
|
|
390
|
+
export function discoverSyncItems(
|
|
391
|
+
settings: Pick<SyncSettings, "lv2Dirs" | "clapDirs">,
|
|
392
|
+
lv2Resolve?: Lv2Resolve,
|
|
393
|
+
): { items: SyncItem[]; problems: string[]; notes: string[] } {
|
|
326
394
|
const items: SyncItem[] = [];
|
|
327
395
|
const problems: string[] = [];
|
|
396
|
+
const notes: string[] = [];
|
|
328
397
|
const seen = new Set<string>();
|
|
329
398
|
const clap = new Set<string>();
|
|
330
399
|
for (const dir of settings.clapDirs) {
|
|
@@ -335,6 +404,11 @@ export function discoverSyncItems(settings: Pick<SyncSettings, "lv2Dirs" | "clap
|
|
|
335
404
|
items.push({ key: `clap:${path}`, format: "clap", root: dirname(path), files: [basename(path)] });
|
|
336
405
|
}
|
|
337
406
|
}
|
|
407
|
+
// the bundle lilv takes for each plugin uri; without lilv, the first bundle found (and the log says so)
|
|
408
|
+
const resolved = lv2Resolve === undefined || settings.lv2Dirs.length === 0 ? undefined : lv2Resolve(settings.lv2Dirs);
|
|
409
|
+
const taken = new Map<string, Lv2Resolved>();
|
|
410
|
+
if (resolved !== undefined && "problem" in resolved) notes.push(`lilv could not resolve the LV2 plugins (${resolved.problem}); each is keyed by the first bundle found`);
|
|
411
|
+
else if (resolved !== undefined) for (const p of resolved) taken.set(p.uri, p);
|
|
338
412
|
const lv2 = new Set<string>();
|
|
339
413
|
for (const dir of settings.lv2Dirs) {
|
|
340
414
|
let bundles: string[];
|
|
@@ -353,13 +427,20 @@ export function discoverSyncItems(settings: Pick<SyncSettings, "lv2Dirs" | "clap
|
|
|
353
427
|
continue;
|
|
354
428
|
}
|
|
355
429
|
for (const p of manifestPlugins(ttl)) {
|
|
430
|
+
const lilv = taken.get(p.uri);
|
|
431
|
+
// lilv takes another bundle for this uri: this one is not the plugin a host loads
|
|
432
|
+
if (lilv !== undefined && resolve(lilv.bundle) !== resolve(bundle)) continue;
|
|
356
433
|
if (lv2.has(p.uri)) continue;
|
|
357
434
|
lv2.add(p.uri);
|
|
358
|
-
|
|
435
|
+
const binary = lilv?.binary ?? (p.binary === undefined ? undefined : join(bundle, p.binary));
|
|
436
|
+
const files = ["manifest.ttl", ...p.files.filter((f) => f !== "manifest.ttl")];
|
|
437
|
+
// a binary named by an absolute IRI (a complement's) is tracked through the bundle's link to it, when it has one
|
|
438
|
+
if (binary !== undefined && !files.includes(basename(binary)) && linksTo(join(bundle, basename(binary)), binary)) files.push(basename(binary));
|
|
439
|
+
items.push({ key: `lv2:${p.uri}`, format: "lv2", root: bundle, files, uri: p.uri, ...(binary === undefined ? {} : { binary }) });
|
|
359
440
|
}
|
|
360
441
|
}
|
|
361
442
|
}
|
|
362
|
-
return { items, problems };
|
|
443
|
+
return { items, problems, notes };
|
|
363
444
|
}
|
|
364
445
|
|
|
365
446
|
// --- the file set -----------------------------------------------------------------------------------------------------
|
|
@@ -428,6 +509,19 @@ export const SYNC_CODE = {
|
|
|
428
509
|
notMeasured: "qualify.sync.not-measured",
|
|
429
510
|
} as const;
|
|
430
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Why a plugin is unmeasured because of the machine it ran on rather than what it is (#192): an EACCES, EPERM or
|
|
514
|
+
* ENOSPC the run hit, or a measurement that could not run or wrote no document. Such a plugin is measured again by
|
|
515
|
+
* the next run although its files are unchanged; a reason of the plugin's own (probe.limit.not-implemented, no such
|
|
516
|
+
* plugin, a crash) is not. `undefined` for every other outcome.
|
|
517
|
+
*/
|
|
518
|
+
export function machineReason(p: Pick<SyncPlugin, "outcome" | "code" | "detail">): string | undefined {
|
|
519
|
+
if (p.outcome !== "unmeasured" || p.code !== SYNC_CODE.notMeasured) return undefined;
|
|
520
|
+
const errno = /\b(EACCES|EPERM|ENOSPC)\b/.exec(p.detail ?? "")?.[1];
|
|
521
|
+
if (errno !== undefined) return errno;
|
|
522
|
+
return /wrote no document|cannot run the probe runner/.test(p.detail ?? "") ? "the measurement wrote no document" : undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
431
525
|
/** What the quick qualification never measures, stated in every record it writes. */
|
|
432
526
|
export const QUICK_NOT_MEASURED: readonly NotMeasured[] = [{ dimension: "stability", code: HOSTING_CODE.stabilityUnsoaked }];
|
|
433
527
|
|
|
@@ -495,6 +589,28 @@ export function recordedAsIs(item: SyncItem, entry: SyncEntry | undefined): { sa
|
|
|
495
589
|
return { same, files: now };
|
|
496
590
|
}
|
|
497
591
|
|
|
592
|
+
/**
|
|
593
|
+
* The entry of an LV2 plugin a host now loads from another bundle than it was qualified in, when its binary is the
|
|
594
|
+
* same bytes (#199): the entry with the new bundle and the new bundle's file digests, its verdicts and records kept.
|
|
595
|
+
* `undefined` when the item is not that case: not LV2, no entry, the same bundle, a binary that cannot be read, or one
|
|
596
|
+
* whose bytes differ from the recorded binary's (then it is measured again).
|
|
597
|
+
*/
|
|
598
|
+
export function sameBinaryElsewhere(item: SyncItem, entry: SyncEntry | undefined): SyncEntry | undefined {
|
|
599
|
+
if (item.format !== "lv2" || entry === undefined || item.binary === undefined || resolve(entry.root) === resolve(item.root)) return undefined;
|
|
600
|
+
const was = entry.files.find((f) => basename(f.path) === basename(item.binary!));
|
|
601
|
+
if (was === undefined) return undefined;
|
|
602
|
+
let now: string;
|
|
603
|
+
try {
|
|
604
|
+
now = fileSha256(item.binary);
|
|
605
|
+
} catch {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
if (now !== was.sha256) return undefined;
|
|
609
|
+
const files = digestFiles(item.root, item.files);
|
|
610
|
+
if (files === undefined) return undefined;
|
|
611
|
+
return { ...entry, root: item.root, files };
|
|
612
|
+
}
|
|
613
|
+
|
|
498
614
|
// --- one item ---------------------------------------------------------------------------------------------------------
|
|
499
615
|
|
|
500
616
|
export interface SyncContext {
|
|
@@ -503,6 +619,8 @@ export interface SyncContext {
|
|
|
503
619
|
readonly log: (line: string) => void;
|
|
504
620
|
/** The run of `plugin-hostd --pin-files`, a stand-in in tests. */
|
|
505
621
|
readonly pinFilesRun?: PinFilesRun;
|
|
622
|
+
/** The isolated step, a stand-in in tests. */
|
|
623
|
+
readonly isolatedRunner?: IsolatedRunner;
|
|
506
624
|
}
|
|
507
625
|
|
|
508
626
|
/**
|
|
@@ -586,13 +704,41 @@ function writeUnmeasuredRecord(ctx: SyncContext, item: SyncItem, files: readonly
|
|
|
586
704
|
return path;
|
|
587
705
|
}
|
|
588
706
|
|
|
707
|
+
/** The step's environment: each of its commands has the sync's budget. */
|
|
708
|
+
const isolatedEnv = (ctx: SyncContext): Readonly<Record<string, string | undefined>> => ({ ...ctx.env, [ADMIT_STEP_MS_ENV]: String(ctx.settings.budgetS * 1000) });
|
|
709
|
+
|
|
589
710
|
/** Each judged run of a pass as the plugins it qualified: the record written, the pin taken, the outcome. */
|
|
590
|
-
function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgment: Judgment }[]): SyncPlugin[] {
|
|
711
|
+
function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgment: Judgment }[], describeProblem?: string): SyncPlugin[] {
|
|
591
712
|
const host = ctx.settings.hostProfile.id;
|
|
592
713
|
const entries = runs.map(({ bytes, judgment }) => {
|
|
593
|
-
const { recordPath } = writeQualification(ctx.settings.qualificationDir, bytes, judgment, QUICK_NOT_MEASURED);
|
|
594
|
-
const
|
|
595
|
-
|
|
714
|
+
const { recordPath, record } = writeQualification(ctx.settings.qualificationDir, bytes, judgment, QUICK_NOT_MEASURED);
|
|
715
|
+
const steps = baseSteps(record, describeProblem);
|
|
716
|
+
let pin = withHostdFiles(ctx, judgmentPinEntries(judgment, host)[0]);
|
|
717
|
+
let { outcome, code } = outcomeOf(judgment, host);
|
|
718
|
+
if (owesIsolated(record)) {
|
|
719
|
+
// in this same pass: a record's verdict=isolated is a run seen alone, never a rating alone
|
|
720
|
+
const uri = pin?.uri ?? judgment.output.plugin.id;
|
|
721
|
+
const runner = ctx.isolatedRunner ?? spawnedIsolatedRunner();
|
|
722
|
+
const step = judgeIsolatedRun(runner(uri, isolatedCell(record, host), isolatedEnv(ctx)));
|
|
723
|
+
steps.push(step);
|
|
724
|
+
pin = pinAfterStep(pin, step);
|
|
725
|
+
// a crash or a hang in the probe stays what it was unless the step saw the plugin run: only a rating waited on the step
|
|
726
|
+
if (step.result === ISOLATED_RESULT.refused) {
|
|
727
|
+
if (outcome === "qualified") {
|
|
728
|
+
outcome = "refused";
|
|
729
|
+
code = step.code;
|
|
730
|
+
}
|
|
731
|
+
} else if (step.result === ISOLATED_RESULT.notRun) {
|
|
732
|
+
if (outcome === "qualified") {
|
|
733
|
+
outcome = "unmeasured";
|
|
734
|
+
code = step.code;
|
|
735
|
+
}
|
|
736
|
+
} else if (outcome !== "qualified") {
|
|
737
|
+
outcome = "qualified";
|
|
738
|
+
code = undefined;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
rewriteRecord(recordPath, { ...record, run: { id: judgment.output.run.id, steps } });
|
|
596
742
|
const plugin: SyncPlugin = { uri: pin?.uri ?? judgment.output.plugin.id, outcome, ...(code === undefined ? {} : { code }), ...(pin === undefined ? {} : { pin }), record: recordPath };
|
|
597
743
|
return { plugin, judgedHere: judgment.hosts.some((h) => h.verdict.host === host) };
|
|
598
744
|
});
|
|
@@ -686,21 +832,17 @@ function qualifyClap(ctx: SyncContext, item: SyncItem): SyncPlugin[] {
|
|
|
686
832
|
const pass = runPass(ctx, { format: "clap", uri: `urn:clap:${p.id}`, bundle: item.root, binary: item.files[0]!, binaryPath: file }, ctx.env);
|
|
687
833
|
for (const f of pass.failures) out.push({ uri, outcome: "unmeasured", code: SYNC_CODE.notMeasured, detail: f.problem });
|
|
688
834
|
const problem = descriptorFailed.get(p.id);
|
|
689
|
-
out.push(...judgedPlugins(ctx, pass.runs).map((j) => (problem === undefined ? j : { ...j, descriptorProblem: problem })));
|
|
835
|
+
out.push(...judgedPlugins(ctx, pass.runs, problem).map((j) => (problem === undefined ? j : { ...j, descriptorProblem: problem })));
|
|
690
836
|
}
|
|
691
837
|
return out;
|
|
692
838
|
}
|
|
693
839
|
|
|
694
840
|
/** An LV2 plugin, measured in a world of its own bundle only, as plugin-hostd runs a pinned LV2 plugin. */
|
|
695
841
|
function qualifyLv2(ctx: SyncContext, item: SyncItem): SyncPlugin[] {
|
|
696
|
-
|
|
697
|
-
try {
|
|
698
|
-
symlinkSync(item.root, join(world, basename(item.root)));
|
|
842
|
+
return lv2World(item.root, (world) => {
|
|
699
843
|
const pass = runPass(ctx, { format: "lv2", uri: item.uri!, bundle: item.root }, { ...ctx.env, LV2_PATH: world });
|
|
700
844
|
return [...pass.failures.map((f): SyncPlugin => ({ uri: item.uri!, outcome: "unmeasured", code: SYNC_CODE.notMeasured, detail: f.problem })), ...judgedPlugins(ctx, pass.runs)];
|
|
701
|
-
}
|
|
702
|
-
rmSync(world, { recursive: true, force: true });
|
|
703
|
-
}
|
|
845
|
+
});
|
|
704
846
|
}
|
|
705
847
|
|
|
706
848
|
/** 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. */
|
|
@@ -720,6 +862,46 @@ export function qualifyItem(ctx: SyncContext, item: SyncItem, files: readonly Fi
|
|
|
720
862
|
return { format: item.format, root: item.root, files: all, plugins, qualifiedAt: new Date(started).toISOString(), seconds: (Date.now() - started) / 1000 };
|
|
721
863
|
}
|
|
722
864
|
|
|
865
|
+
/**
|
|
866
|
+
* The isolated steps the records of a state still owe, each extending its record's run. A plugin's pin follows its step, and
|
|
867
|
+
* a step that could not run stays owed: the next pass tries it again.
|
|
868
|
+
*/
|
|
869
|
+
export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly state: SyncState; readonly isolated: number; readonly refused: number; readonly notRun: number } {
|
|
870
|
+
const count = { isolated: 0, refused: 0, notRun: 0 };
|
|
871
|
+
const runner = ctx.isolatedRunner ?? spawnedIsolatedRunner();
|
|
872
|
+
const host = ctx.settings.hostProfile.id;
|
|
873
|
+
const entries: Record<string, SyncEntry> = {};
|
|
874
|
+
for (const [key, entry] of Object.entries(state.entries)) {
|
|
875
|
+
entries[key] = {
|
|
876
|
+
...entry,
|
|
877
|
+
plugins: entry.plugins.map((p) => {
|
|
878
|
+
if (p.record === undefined || !p.record.endsWith(".qualification.json")) return p;
|
|
879
|
+
const path = join(ctx.settings.qualificationDir, basename(p.record));
|
|
880
|
+
let record: QualificationRecord;
|
|
881
|
+
try {
|
|
882
|
+
record = JSON.parse(readFileSync(path, "utf8")) as QualificationRecord;
|
|
883
|
+
} catch {
|
|
884
|
+
ctx.log(`${p.uri}: its record does not read; no isolated step`);
|
|
885
|
+
return p;
|
|
886
|
+
}
|
|
887
|
+
const done = completeIsolated(path, record, p.pin?.uri ?? record.plugin.id, isolatedCell(record, host), runner, isolatedEnv(ctx));
|
|
888
|
+
if (done === undefined) return p;
|
|
889
|
+
ctx.log(`${p.uri}: isolated step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
|
|
890
|
+
const pin = pinAfterStep(p.pin, done.step);
|
|
891
|
+
const { pin: _pin, code: _code, ...rest } = p;
|
|
892
|
+
if (done.step.result === ISOLATED_RESULT.isolated) {
|
|
893
|
+
count.isolated++;
|
|
894
|
+
return { ...rest, outcome: "qualified" as const, ...(pin === undefined ? {} : { pin }) };
|
|
895
|
+
}
|
|
896
|
+
if (done.step.result === ISOLATED_RESULT.refused) count.refused++;
|
|
897
|
+
else count.notRun++;
|
|
898
|
+
return { ...rest, outcome: done.step.result === ISOLATED_RESULT.refused ? ("refused" as const) : ("unmeasured" as const), ...(done.step.code === undefined ? {} : { code: done.step.code }), ...(pin === undefined ? {} : { pin }) };
|
|
899
|
+
}),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
return { ...count, state: count.isolated + count.refused + count.notRun === 0 ? state : { ...state, entries } };
|
|
903
|
+
}
|
|
904
|
+
|
|
723
905
|
// --- the distributed records ----------------------------------------------------------------------------------------
|
|
724
906
|
|
|
725
907
|
/** The layer a run reads: none under `--force` or `--no-distributed`, or without a host class. */
|
|
@@ -750,6 +932,13 @@ function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncIt
|
|
|
750
932
|
for (const line of match.lines) ctx.log(line);
|
|
751
933
|
return undefined;
|
|
752
934
|
}
|
|
935
|
+
for (const r of match.records) {
|
|
936
|
+
const missing = missingSteps(r.record);
|
|
937
|
+
if (missing.length > 0) {
|
|
938
|
+
ctx.log(`distributed: ${distributedUri(item, r)}: its record lacks the step${missing.length === 1 ? "" : "s"} ${missing.join(", ")}; measured here`);
|
|
939
|
+
return undefined;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
753
942
|
let plugins: SyncPlugin[];
|
|
754
943
|
try {
|
|
755
944
|
plugins = applyDistributed(ctx.settings.qualificationDir, item, match, host);
|
|
@@ -984,6 +1173,16 @@ export interface SyncSummary {
|
|
|
984
1173
|
readonly sent?: number;
|
|
985
1174
|
/** The pins whose files were refreshed from plugin-hostd without a measurement (plugin-qualify#239). */
|
|
986
1175
|
readonly refreshed?: number;
|
|
1176
|
+
/** `--rejudge`: the records whose verdicts changed, the ones that stood, and the ones that owe a measurement. */
|
|
1177
|
+
readonly rejudge?: { readonly changed: number; readonly unchanged: number; readonly owing: number };
|
|
1178
|
+
/** `--memory-only`: the plugins measured, those whose footprints were already merged, and those no run could read. */
|
|
1179
|
+
readonly memoryOnly?: { readonly measured: number; readonly already: number; readonly failed: number };
|
|
1180
|
+
/** Every plugin the run measured was unmeasured for this one reason of the machine ({@link machineReason}): the run failed. */
|
|
1181
|
+
readonly machineFailure?: string;
|
|
1182
|
+
/** `--isolated-only`: how the steps ended. */
|
|
1183
|
+
readonly isolatedOnly?: { readonly isolated: number; readonly refused: number; readonly notRun: number };
|
|
1184
|
+
/** `--adapter-only`: the plugins run, those done already at these host versions, and those whose host could not be run. */
|
|
1185
|
+
readonly adapterOnly?: { readonly measured: number; readonly already: number; readonly failed: number };
|
|
987
1186
|
}
|
|
988
1187
|
|
|
989
1188
|
/**
|
|
@@ -1020,7 +1219,7 @@ export function descriptorAccessProblems(dir: string): string[] {
|
|
|
1020
1219
|
}
|
|
1021
1220
|
|
|
1022
1221
|
export function summaryLine(s: SyncSummary): string {
|
|
1023
|
-
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`;
|
|
1222
|
+
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`;
|
|
1024
1223
|
}
|
|
1025
1224
|
|
|
1026
1225
|
/** The exit status `flock -n` returns when the lock is held (-E), told apart from a failure of flock itself. */
|
|
@@ -1066,8 +1265,24 @@ export interface SyncRunOptions {
|
|
|
1066
1265
|
readonly force?: boolean;
|
|
1067
1266
|
/** `--refresh-pins`: only refresh the pins' files from plugin-hostd and send the changed ones; measure nothing. */
|
|
1068
1267
|
readonly refreshOnly?: boolean;
|
|
1268
|
+
/** `--rejudge`: derive the verdicts again from the records' figures; measure nothing. */
|
|
1269
|
+
readonly rejudge?: boolean;
|
|
1270
|
+
/** `--memory-only`: take only the footprints; `force` takes those already merged again. */
|
|
1271
|
+
readonly memoryOnly?: boolean;
|
|
1272
|
+
/** The isolated step, a stand-in in tests. */
|
|
1273
|
+
readonly isolatedRunner?: IsolatedRunner;
|
|
1274
|
+
/** `--isolated-only`: take only the isolated steps records still owe. */
|
|
1275
|
+
readonly isolatedOnly?: boolean;
|
|
1276
|
+
/** `--adapter-only`: take only the adapter runs; `force` takes those already done again. */
|
|
1277
|
+
readonly adapterOnly?: boolean;
|
|
1278
|
+
/** Stand-ins for the pass's hosts, in tests. */
|
|
1279
|
+
readonly adapterPass?: AdapterPassOptions;
|
|
1069
1280
|
/** The run of `plugin-hostd --pin-files`, a stand-in in tests. */
|
|
1070
1281
|
readonly pinFilesRun?: PinFilesRun;
|
|
1282
|
+
/** The run of the probe runner in a memory-only pass, a stand-in in tests. */
|
|
1283
|
+
readonly probeRun?: ProbeRun;
|
|
1284
|
+
/** How LV2 plugins are resolved to the bundle a host loads: lilv itself unless a test stands in (#199). */
|
|
1285
|
+
readonly lv2Resolve?: Lv2Resolve;
|
|
1071
1286
|
/** False under `--no-wait`: a run in progress is not waited for; the holder runs once more at its end instead. */
|
|
1072
1287
|
readonly wait?: boolean;
|
|
1073
1288
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
@@ -1123,6 +1338,10 @@ function addSummaries(a: SyncSummary, b: SyncSummary): SyncSummary {
|
|
|
1123
1338
|
seconds: a.seconds + b.seconds,
|
|
1124
1339
|
...(sent === 0 ? {} : { sent }),
|
|
1125
1340
|
...(refreshed === 0 ? {} : { refreshed }),
|
|
1341
|
+
...(b.rejudge === undefined ? (a.rejudge === undefined ? {} : { rejudge: a.rejudge }) : { rejudge: b.rejudge }),
|
|
1342
|
+
...(b.memoryOnly === undefined ? (a.memoryOnly === undefined ? {} : { memoryOnly: a.memoryOnly }) : { memoryOnly: b.memoryOnly }),
|
|
1343
|
+
...(b.isolatedOnly === undefined ? (a.isolatedOnly === undefined ? {} : { isolatedOnly: a.isolatedOnly }) : { isolatedOnly: b.isolatedOnly }),
|
|
1344
|
+
...(b.adapterOnly === undefined ? (a.adapterOnly === undefined ? {} : { adapterOnly: a.adapterOnly }) : { adapterOnly: b.adapterOnly }),
|
|
1126
1345
|
};
|
|
1127
1346
|
}
|
|
1128
1347
|
|
|
@@ -1143,11 +1362,11 @@ function scannedBy(settings: Pick<SyncSettings, "lv2Dirs" | "clapDirs">, entry:
|
|
|
1143
1362
|
async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promise<SyncSummary> {
|
|
1144
1363
|
const log = options.log ?? (() => {});
|
|
1145
1364
|
const env = options.env ?? process.env;
|
|
1146
|
-
log(options.refreshOnly === true ? "pin refresh begins" : "sync run begins");
|
|
1365
|
+
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");
|
|
1147
1366
|
{
|
|
1148
1367
|
const started = Date.now();
|
|
1149
1368
|
const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
|
|
1150
|
-
const ctx: SyncContext = { settings, env, log, ...(options.pinFilesRun === undefined ? {} : { pinFilesRun: options.pinFilesRun }) };
|
|
1369
|
+
const ctx: SyncContext = { settings, env, log, ...(options.pinFilesRun === undefined ? {} : { pinFilesRun: options.pinFilesRun }), ...(options.isolatedRunner === undefined ? {} : { isolatedRunner: options.isolatedRunner }) };
|
|
1151
1370
|
// the pins first, before the quiet wait and any measurement: an upgrade of plugin-hostd that collects more files
|
|
1152
1371
|
// than the pins on disk hold refuses those plugins until their pins name them (plugin-qualify#239)
|
|
1153
1372
|
const fresh = refreshPins(ctx, readSyncState(settings.stateFile, log));
|
|
@@ -1155,6 +1374,29 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1155
1374
|
writeFileWhole(settings.stateFile, `${JSON.stringify(fresh.state, null, 1)}\n`);
|
|
1156
1375
|
}
|
|
1157
1376
|
if (options.refreshOnly === true) return sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1377
|
+
if (options.rejudge === true) {
|
|
1378
|
+
const r: RejudgeResult = rejudgeState(ctx, fresh.state);
|
|
1379
|
+
if (JSON.stringify(r.state) !== JSON.stringify(fresh.state)) writeFileWhole(settings.stateFile, `${JSON.stringify(r.state, null, 1)}\n`);
|
|
1380
|
+
const summary = await sendRefreshed(settings, r.state, fresh.refreshed.length, started, log);
|
|
1381
|
+
return { ...summary, rejudge: { changed: r.changed, unchanged: r.unchanged, owing: r.owing.length } };
|
|
1382
|
+
}
|
|
1383
|
+
if (options.isolatedOnly === true) {
|
|
1384
|
+
const r = isolatedPass(ctx, fresh.state);
|
|
1385
|
+
if (r.state !== fresh.state) writeFileWhole(settings.stateFile, `${JSON.stringify(r.state, null, 1)}\n`);
|
|
1386
|
+
const summary = await sendRefreshed(settings, r.state, fresh.refreshed.length, started, log);
|
|
1387
|
+
return { ...summary, isolatedOnly: { isolated: r.isolated, refused: r.refused, notRun: r.notRun } };
|
|
1388
|
+
}
|
|
1389
|
+
if (options.adapterOnly === true) {
|
|
1390
|
+
const r: AdapterPassResult = adapterPass(ctx, fresh.state, { ...options.adapterPass, force: options.force === true });
|
|
1391
|
+
log(`adapter runs under ${Object.entries(r.versions).map(([k, v]) => `${k} ${v}`).join(", ")}`);
|
|
1392
|
+
const summary = await sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1393
|
+
return { ...summary, adapterOnly: { measured: r.measured, already: r.already, failed: r.failed } };
|
|
1394
|
+
}
|
|
1395
|
+
if (options.memoryOnly === true) {
|
|
1396
|
+
const r: MemoryPassResult = memoryPass(ctx, fresh.state, { force: options.force === true, ...(options.probeRun === undefined ? {} : { run: options.probeRun }) });
|
|
1397
|
+
const summary = await sendRefreshed(settings, fresh.state, fresh.refreshed.length, started, log);
|
|
1398
|
+
return { ...summary, memoryOnly: { measured: r.measured, already: r.already, failed: r.failed } };
|
|
1399
|
+
}
|
|
1158
1400
|
await waitQuiet(dirs, settings.settleS, log);
|
|
1159
1401
|
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0, distributed: 0 };
|
|
1160
1402
|
const layer = distributedLayerOf(settings, options.force === true, log);
|
|
@@ -1168,11 +1410,15 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1168
1410
|
const stateBefore = state;
|
|
1169
1411
|
let unreadable = false;
|
|
1170
1412
|
const lv2Pinned: string[] = [];
|
|
1413
|
+
// the plugins measured this run, and how many of them the machine left unmeasured, by reason (#192)
|
|
1414
|
+
let measuredHere = 0;
|
|
1415
|
+
const byMachine = new Map<string, number>();
|
|
1171
1416
|
// a change made while a pass ran is caught by one more pass, a few at most
|
|
1172
1417
|
for (let pass = 0; pass < 3; pass++) {
|
|
1173
1418
|
const signature = directoriesSignature(dirs);
|
|
1174
|
-
const { items, problems } = discoverSyncItems(settings);
|
|
1419
|
+
const { items, problems, notes } = discoverSyncItems(settings, options.lv2Resolve ?? LILV_RESOLVE);
|
|
1175
1420
|
for (const p of problems) log(p);
|
|
1421
|
+
if (pass === 0) for (const n of notes) log(n);
|
|
1176
1422
|
unreadable = problems.length > 0;
|
|
1177
1423
|
const found = new Set(items.map((i) => i.key));
|
|
1178
1424
|
const entries: Record<string, SyncEntry> = {};
|
|
@@ -1186,10 +1432,25 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1186
1432
|
}
|
|
1187
1433
|
for (const item of items) {
|
|
1188
1434
|
const recorded = recordedAsIs(item, entries[item.key]);
|
|
1189
|
-
|
|
1435
|
+
const retry = recorded.same ? entries[item.key]!.plugins.map(machineReason).find((r) => r !== undefined) : undefined;
|
|
1436
|
+
if (recorded.same && options.force !== true && retry === undefined) {
|
|
1190
1437
|
if (pass === 0) count.skipped += entries[item.key]!.plugins.length;
|
|
1191
1438
|
continue;
|
|
1192
1439
|
}
|
|
1440
|
+
// an LV2 plugin a host now loads from another bundle with the same binary (a complement that restates its TTL,
|
|
1441
|
+
// #199): the record stands, keyed by its uri and its binary; only the bundle, its files and the pins' files move
|
|
1442
|
+
const moved = options.force === true ? undefined : sameBinaryElsewhere(item, entries[item.key]);
|
|
1443
|
+
if (moved !== undefined) {
|
|
1444
|
+
entries[item.key] = { ...moved, plugins: moved.plugins.map((p) => ({ ...p, ...(p.pin === undefined ? {} : { pin: withHostdFiles(ctx, p.pin) }) })) };
|
|
1445
|
+
if (item.format === "lv2") lv2Pinned.push(...entries[item.key]!.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|
|
1446
|
+
if (pass === 0) count.skipped += moved.plugins.length;
|
|
1447
|
+
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`);
|
|
1448
|
+
state = { schema: SYNC_STATE_SCHEMA, entries };
|
|
1449
|
+
writeFileWhole(settings.stateFile, `${JSON.stringify(state, null, 1)}\n`);
|
|
1450
|
+
writeFileWhole(settings.pinFile, syncPinsText(state));
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (recorded.same && retry !== undefined) log(`${item.key}: unmeasured last run by the machine (${retry}); measured again`);
|
|
1193
1454
|
const files = recorded.files ?? digestFiles(item.root, item.files);
|
|
1194
1455
|
if (files === undefined) {
|
|
1195
1456
|
log(`${item.key}: a file of it cannot be read; skipped this run`);
|
|
@@ -1211,6 +1472,11 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1211
1472
|
if (item.format === "lv2") lv2Pinned.push(...entry.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|
|
1212
1473
|
for (const p of entry.plugins) {
|
|
1213
1474
|
count[p.outcome]++;
|
|
1475
|
+
measuredHere++;
|
|
1476
|
+
// a reason of the machine is said once: the plugins after the first are counted, not listed
|
|
1477
|
+
const why = machineReason(p);
|
|
1478
|
+
if (why !== undefined) byMachine.set(why, (byMachine.get(why) ?? 0) + 1);
|
|
1479
|
+
if (why !== undefined && byMachine.get(why)! > 1) continue;
|
|
1214
1480
|
log(`${p.uri}: ${p.outcome}${p.code === undefined ? "" : ` (${p.code})`}${p.detail === undefined ? "" : `: ${p.detail}`}`);
|
|
1215
1481
|
}
|
|
1216
1482
|
state = { schema: SYNC_STATE_SCHEMA, entries };
|
|
@@ -1244,8 +1510,13 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
|
|
|
1244
1510
|
const replies = "replies" in sent ? sent.replies : [];
|
|
1245
1511
|
for (let i = 0; i < replies.length; i++) if (replies[i] !== "resp 0") log(`plugin-hostd answered ${replies[i]!} to ${changed[i]!.split(" ")[1]!}`);
|
|
1246
1512
|
const taken = replies.filter((r) => r === "resp 0").length;
|
|
1513
|
+
for (const [why, n] of byMachine) if (n > 1) log(`${String(n)} plugins unmeasured by the machine (${why}); the first is listed above`);
|
|
1514
|
+
const [only] = byMachine.size === 1 ? [...byMachine] : [];
|
|
1515
|
+
const machineFailure = only !== undefined && only[1] === measuredHere ? only[0] : undefined;
|
|
1516
|
+
if (machineFailure !== undefined) log(`every plugin this run measured was left unmeasured by the machine (${machineFailure}): the run failed`);
|
|
1247
1517
|
return {
|
|
1248
1518
|
...count,
|
|
1519
|
+
...(machineFailure === undefined ? {} : { machineFailure }),
|
|
1249
1520
|
seconds: (Date.now() - started) / 1000,
|
|
1250
1521
|
...(taken === 0 ? {} : { sent: taken }),
|
|
1251
1522
|
...(fresh.refreshed.length === 0 ? {} : { refreshed: fresh.refreshed.length }),
|