@openmixer/plugin-qualify 0.16.0 → 0.17.0
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 +154 -7
- package/bin/plugin-qualify-records-package.mjs +31 -0
- package/bin/plugin-qualify-sync.mjs +6 -1
- package/bin/plugin-qualify.mjs +12 -1
- package/dist/distributed-records.d.ts +88 -0
- package/dist/distributed-records.js +210 -0
- package/dist/distributed-records.js.map +1 -0
- package/dist/host-profiles.d.ts +5 -0
- package/dist/host-profiles.js +13 -0
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-suitability.js +9 -1
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/measurement-grid.d.ts +49 -0
- package/dist/measurement-grid.js +76 -0
- package/dist/measurement-grid.js.map +1 -0
- package/dist/memory-bound.d.ts +23 -0
- package/dist/memory-bound.js +46 -0
- package/dist/memory-bound.js.map +1 -0
- package/dist/probe-judge.js +22 -1
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-merge.d.ts +30 -0
- package/dist/probe-merge.js +80 -0
- package/dist/probe-merge.js.map +1 -0
- package/dist/probe-output.d.ts +8 -0
- package/dist/probe-output.js +7 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +26 -2
- package/dist/probe-runner.js +38 -10
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +19 -1
- package/dist/qualification-record.js +30 -1
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-cli.d.ts +9 -0
- package/dist/qualify-cli.js +30 -1
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +15 -0
- package/dist/qualify-declarations.js +15 -0
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-options.js +11 -1
- package/dist/qualify-options.js.map +1 -1
- package/dist/qualify-sync.d.ts +23 -3
- package/dist/qualify-sync.js +172 -16
- package/dist/qualify-sync.js.map +1 -1
- package/dist/records-package.d.ts +63 -0
- package/dist/records-package.js +300 -0
- package/dist/records-package.js.map +1 -0
- package/package.json +3 -2
- package/src/distributed-records.ts +251 -0
- package/src/host-profiles.ts +13 -0
- package/src/hosting-suitability.ts +11 -1
- package/src/index.ts +5 -0
- package/src/measurement-grid.ts +97 -0
- package/src/memory-bound.ts +55 -0
- package/src/probe-judge.ts +25 -1
- package/src/probe-merge.ts +89 -0
- package/src/probe-output.ts +13 -0
- package/src/probe-runner.ts +75 -13
- package/src/qualification-record.ts +44 -2
- package/src/qualify-cli.ts +32 -1
- package/src/qualify-declarations.ts +17 -0
- package/src/qualify-options.ts +11 -1
- package/src/qualify-sync.ts +175 -16
- package/src/records-package.ts +333 -0
- package/tools/{ai-trace-commit-check.sh → commit-message-check.sh} +30 -28
- package/tools/{ai-trace-patterns.json → commit-message-patterns.json} +1 -1
package/src/qualify-sync.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
24
24
|
import { basename, dirname, join, resolve } from "node:path";
|
|
25
25
|
|
|
26
26
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
27
|
+
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
27
28
|
import { clapSearchPath, parseClapScan } from "./clap-loader.js";
|
|
28
29
|
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
29
30
|
import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
|
|
@@ -31,8 +32,10 @@ import { lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
|
|
|
31
32
|
import { judgmentPinEntries, pinsFile, type PinEntry } from "./pins-file.js";
|
|
32
33
|
import type { Judgment } from "./probe-judge.js";
|
|
33
34
|
import { qualifyTargets } from "./probe-runner.js";
|
|
35
|
+
import { GRID_HOSTS, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
36
|
+
import { memoryBoundOf, parseMemoryLimit, type MemoryBound } from "./memory-bound.js";
|
|
34
37
|
import { writeQualification, type NotMeasured } from "./qualification-record.js";
|
|
35
|
-
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM } from "./qualify-declarations.js";
|
|
38
|
+
import { DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
|
|
36
39
|
import { toolsDir } from "./tool-paths.js";
|
|
37
40
|
import { writeFileWhole } from "./whole-file.js";
|
|
38
41
|
|
|
@@ -64,11 +67,16 @@ export const SYNC_SETTINGS: readonly SyncSetting[] = [
|
|
|
64
67
|
{ key: "qualification_dir", meaning: "where the qualification records go", default: "/var/lib/omx/qualification (system), else $XDG_STATE_HOME/omx/qualification" },
|
|
65
68
|
{ key: "descriptor_dir", meaning: "where the scanned descriptors go", default: `${DESCRIPTOR_DIR_SYSTEM} (system), else $XDG_STATE_HOME/${DESCRIPTOR_DIR_STATE_SUBDIR}` },
|
|
66
69
|
{ key: "state_file", meaning: "what was qualified, from which bytes, and how it ended", default: "/var/lib/omx/qualify-sync.json (system), else $XDG_STATE_HOME/omx/qualify-sync.json" },
|
|
67
|
-
{ key: "budget_s", meaning: "the wall clock one
|
|
70
|
+
{ key: "budget_s", meaning: "the wall clock one run gets (one cell of an LV2 plugin, one quantum of a CLAP one), in seconds; past it the plugin is recorded as hung", default: "60" },
|
|
68
71
|
{ key: "host_profile", meaning: "the host the verdict is for (a shipped profile id)", default: "omx-clap-host" },
|
|
72
|
+
{ key: "grid", meaning: "the cells measured: hosts (the host's and the console tiers' rate/quantum, then 512 at every rate), full, or <rate>/<quantum>,...", default: "hosts" },
|
|
73
|
+
{ key: "cells_per_plugin_budget", meaning: "measure at most the first n cells of the grid a plugin; 0 is every cell", default: "0" },
|
|
74
|
+
{ 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" },
|
|
69
75
|
{ key: "hostd_port", meaning: "plugin-hostd's command port the new pins are sent to; 0 sends none", default: "5555" },
|
|
70
76
|
{ key: "hostd_feedback_port", meaning: "plugin-hostd's feedback port, connected before the command port; 0 is the command port's next", default: "0" },
|
|
71
77
|
{ key: "settle_s", meaning: "wait until the plugin directories have been quiet this long before a run", default: "0" },
|
|
78
|
+
{ key: "host_class", meaning: "the machine class whose distributed records are read (a directory name)", default: "the board's own from the device tree (rpi5 on a Raspberry Pi 5), else none" },
|
|
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` },
|
|
72
80
|
];
|
|
73
81
|
|
|
74
82
|
export interface SyncSettings {
|
|
@@ -80,9 +88,19 @@ export interface SyncSettings {
|
|
|
80
88
|
readonly stateFile: string;
|
|
81
89
|
readonly budgetS: number;
|
|
82
90
|
readonly hostProfile: HostProfile;
|
|
91
|
+
/** The cells measured, and at most how many a plugin (0: every cell). */
|
|
92
|
+
readonly grid: GridSpec;
|
|
93
|
+
readonly cellsPerPluginBudget: number;
|
|
94
|
+
/** The memory bound of every run; `undefined` is `off`. */
|
|
95
|
+
readonly memory?: MemoryBound;
|
|
83
96
|
readonly hostdPort: number;
|
|
84
97
|
readonly hostdFeedbackPort: number;
|
|
85
98
|
readonly settleS: number;
|
|
99
|
+
/** The machine class the distributed records are read for; none, and the layer is not read. */
|
|
100
|
+
readonly hostClass?: string;
|
|
101
|
+
readonly distributedDirs: readonly string[];
|
|
102
|
+
/** False under `--no-distributed`. */
|
|
103
|
+
readonly distributed: boolean;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
/** A settings file read: one `key value` per line, `#` starts a comment; a key alone is the empty value. An unknown key is refused by line. */
|
|
@@ -105,6 +123,7 @@ export interface SyncArgs {
|
|
|
105
123
|
readonly config?: string;
|
|
106
124
|
readonly system: boolean;
|
|
107
125
|
readonly force: boolean;
|
|
126
|
+
readonly noDistributed: boolean;
|
|
108
127
|
readonly help: boolean;
|
|
109
128
|
/** The settings given as flags, by key. */
|
|
110
129
|
readonly values: ReadonlyMap<string, string>;
|
|
@@ -118,11 +137,13 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
118
137
|
let config: string | undefined;
|
|
119
138
|
let system = false;
|
|
120
139
|
let force = false;
|
|
140
|
+
let noDistributed = false;
|
|
121
141
|
let help = false;
|
|
122
142
|
for (let i = 0; i < argv.length; i++) {
|
|
123
143
|
const a = argv[i]!;
|
|
124
144
|
if (a === "--system") system = true;
|
|
125
145
|
else if (a === "--force") force = true;
|
|
146
|
+
else if (a === "--no-distributed") noDistributed = true;
|
|
126
147
|
else if (a === "--help") help = true;
|
|
127
148
|
else if (a === "--config" || byFlag.has(a)) {
|
|
128
149
|
const v = argv[++i];
|
|
@@ -131,18 +152,19 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
131
152
|
else values.set(byFlag.get(a)!, v);
|
|
132
153
|
} else return { ok: false, error: `unknown argument ${a}` };
|
|
133
154
|
}
|
|
134
|
-
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, help, values } };
|
|
155
|
+
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, help, values } };
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
export function syncHelpText(): string {
|
|
138
|
-
const rows = SYNC_SETTINGS.map((s) => ` ${`${flagOf(s.key)} <value>`.padEnd(
|
|
159
|
+
const rows = SYNC_SETTINGS.map((s) => ` ${`${flagOf(s.key)} <value>`.padEnd(36)}${s.meaning}${s.default === "" ? "" : ` (default: ${s.default})`}`);
|
|
139
160
|
return [
|
|
140
161
|
"plugin-qualify-sync: qualify every plugin on the search paths once, and again when its files change",
|
|
141
162
|
"",
|
|
142
|
-
` --config <file>
|
|
143
|
-
" --system
|
|
144
|
-
" --force
|
|
145
|
-
" --
|
|
163
|
+
` ${"--config <file>".padEnd(36)}the settings file (default: ${SYNC_CONF}; a missing file is the defaults)`,
|
|
164
|
+
" --system a system service's directories, under /var/lib/omx",
|
|
165
|
+
" --force qualify every plugin again, recorded or not; no distributed record is read",
|
|
166
|
+
" --no-distributed read no distributed record: every plugin not recorded is measured",
|
|
167
|
+
" --help this text",
|
|
146
168
|
...rows,
|
|
147
169
|
"",
|
|
148
170
|
"every setting is also a line of the settings file, its key as the first word",
|
|
@@ -159,6 +181,7 @@ export function resolveSyncSettings(
|
|
|
159
181
|
args: SyncArgs,
|
|
160
182
|
env: Readonly<Record<string, string | undefined>>,
|
|
161
183
|
home: string = homedir(),
|
|
184
|
+
boardModel: () => string | undefined = readBoardModel,
|
|
162
185
|
): { ok: true; settings: SyncSettings } | { ok: false; error: string } {
|
|
163
186
|
const get = (key: string): string | undefined => args.values.get(key) ?? file.get(key);
|
|
164
187
|
const state = env["XDG_STATE_HOME"] !== undefined && env["XDG_STATE_HOME"] !== "" ? env["XDG_STATE_HOME"] : join(home, ".local", "state");
|
|
@@ -182,10 +205,31 @@ export function resolveSyncSettings(
|
|
|
182
205
|
if (hostdPort === undefined || !Number.isInteger(hostdPort) || hostdPort > 65535) return { ok: false, error: "hostd_port must be a port number, or 0" };
|
|
183
206
|
if (feedbackPort === undefined || !Number.isInteger(feedbackPort) || feedbackPort > 65535) return { ok: false, error: "hostd_feedback_port must be a port number, or 0" };
|
|
184
207
|
if (settleS === undefined) return { ok: false, error: "settle_s must be a number of seconds" };
|
|
208
|
+
const gridText = get("grid");
|
|
209
|
+
const grid = gridText === undefined || gridText === "" ? { ok: true as const, spec: GRID_HOSTS } : parseGridSpec(gridText);
|
|
210
|
+
if (!grid.ok) return { ok: false, error: `grid: ${grid.error}` };
|
|
211
|
+
const cellsPerPluginBudget = number("cells_per_plugin_budget", 0);
|
|
212
|
+
if (cellsPerPluginBudget === undefined || !Number.isInteger(cellsPerPluginBudget)) return { ok: false, error: "cells_per_plugin_budget must be a whole number, 0 or more" };
|
|
213
|
+
const memoryText = get("memory_limit");
|
|
214
|
+
const memoryLimit = memoryText === undefined || memoryText === "" ? undefined : parseMemoryLimit(memoryText);
|
|
215
|
+
if (memoryLimit !== undefined && !memoryLimit.ok) return { ok: false, error: `memory_limit: ${memoryLimit.error}` };
|
|
216
|
+
const memory = memoryBoundOf(memoryLimit?.bytes);
|
|
185
217
|
const profileId = get("host_profile") || "omx-clap-host";
|
|
186
218
|
const hostProfile = shippedHostProfile(profileId);
|
|
187
219
|
if (hostProfile === undefined) return { ok: false, error: `host_profile ${profileId} is not a shipped host profile` };
|
|
188
220
|
const once = (dirs: readonly string[]): string[] => [...new Set(dirs.map((d) => resolve(d)))];
|
|
221
|
+
const dataHome = env["XDG_DATA_HOME"] !== undefined && env["XDG_DATA_HOME"] !== "" ? env["XDG_DATA_HOME"] : join(home, ".local", "share");
|
|
222
|
+
const dataDirs = dirList(env["XDG_DATA_DIRS"] ?? "").length > 0 ? dirList(env["XDG_DATA_DIRS"]!) : ["/usr/local/share", "/usr/share"];
|
|
223
|
+
const distributedNamed = get("distributed_dir");
|
|
224
|
+
const distributedDirs =
|
|
225
|
+
distributedNamed !== undefined && distributedNamed !== ""
|
|
226
|
+
? dirList(distributedNamed)
|
|
227
|
+
: args.system
|
|
228
|
+
? [DISTRIBUTED_DIR_SYSTEM]
|
|
229
|
+
: [dataHome, ...dataDirs].map((d) => join(d, "omx", "qualification"));
|
|
230
|
+
const classNamed = get("host_class");
|
|
231
|
+
const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel());
|
|
232
|
+
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` };
|
|
189
233
|
return {
|
|
190
234
|
ok: true,
|
|
191
235
|
settings: {
|
|
@@ -197,9 +241,15 @@ export function resolveSyncSettings(
|
|
|
197
241
|
stateFile: place("state_file", "/var/lib/omx/qualify-sync.json", join("omx", "qualify-sync.json")),
|
|
198
242
|
budgetS,
|
|
199
243
|
hostProfile,
|
|
244
|
+
grid: grid.spec,
|
|
245
|
+
cellsPerPluginBudget,
|
|
246
|
+
...(memory === undefined ? {} : { memory }),
|
|
200
247
|
hostdPort,
|
|
201
248
|
hostdFeedbackPort: feedbackPort === 0 && hostdPort !== 0 ? hostdPort + 1 : feedbackPort,
|
|
202
249
|
settleS,
|
|
250
|
+
...(hostClass === undefined ? {} : { hostClass }),
|
|
251
|
+
distributedDirs: once(distributedDirs),
|
|
252
|
+
distributed: !args.noDistributed,
|
|
203
253
|
},
|
|
204
254
|
};
|
|
205
255
|
}
|
|
@@ -390,6 +440,8 @@ export interface SyncEntry {
|
|
|
390
440
|
readonly plugins: readonly SyncPlugin[];
|
|
391
441
|
readonly qualifiedAt: string;
|
|
392
442
|
readonly seconds: number;
|
|
443
|
+
/** The distributed set the plugins were taken from, when they were not measured here. */
|
|
444
|
+
readonly distributed?: string;
|
|
393
445
|
}
|
|
394
446
|
|
|
395
447
|
export interface SyncState {
|
|
@@ -484,7 +536,17 @@ function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgmen
|
|
|
484
536
|
}
|
|
485
537
|
|
|
486
538
|
function runPass(ctx: SyncContext, target: Parameters<typeof qualifyTargets>[0][number], env: Readonly<Record<string, string | undefined>>) {
|
|
487
|
-
|
|
539
|
+
const s = ctx.settings;
|
|
540
|
+
return qualifyTargets([target], {
|
|
541
|
+
rates: QUALIFY_RATES,
|
|
542
|
+
quanta: QUALIFY_QUANTA,
|
|
543
|
+
grid: s.grid,
|
|
544
|
+
cellsPerPluginBudget: s.cellsPerPluginBudget,
|
|
545
|
+
...(s.memory === undefined ? {} : { memory: s.memory }),
|
|
546
|
+
profiles: [s.hostProfile],
|
|
547
|
+
wallS: s.budgetS,
|
|
548
|
+
env,
|
|
549
|
+
});
|
|
488
550
|
}
|
|
489
551
|
|
|
490
552
|
/** The descriptors of a scanned `.clap`, through the one descriptor writer; a failure is named and the run goes on. */
|
|
@@ -566,6 +628,66 @@ export function qualifyItem(ctx: SyncContext, item: SyncItem, files: readonly Fi
|
|
|
566
628
|
return { format: item.format, root: item.root, files: all, plugins, qualifiedAt: new Date(started).toISOString(), seconds: (Date.now() - started) / 1000 };
|
|
567
629
|
}
|
|
568
630
|
|
|
631
|
+
// --- the distributed records ----------------------------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
/** The layer a run reads: none under `--force` or `--no-distributed`, or without a host class. */
|
|
634
|
+
function distributedLayerOf(settings: SyncSettings, force: boolean, log: (line: string) => void): DistributedLayer {
|
|
635
|
+
if (force || !settings.distributed) return EMPTY_LAYER;
|
|
636
|
+
if (settings.hostClass === undefined) {
|
|
637
|
+
log("distributed: no host class (host_class unset, and the board names none); no distributed record is read");
|
|
638
|
+
return EMPTY_LAYER;
|
|
639
|
+
}
|
|
640
|
+
const layer = readDistributedLayer(settings.distributedDirs, settings.hostClass, log);
|
|
641
|
+
if (layer.records > 0) log(`distributed: ${String(layer.records)} records of ${layer.sets.join(", ")} for ${settings.hostClass}`);
|
|
642
|
+
return layer;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The item's entry taken from the distributed records, or `undefined` when it is to be measured: no record finds it,
|
|
647
|
+
* or the ones that do were not taken, each reason said. Its descriptors are written as a measured item's are: a
|
|
648
|
+
* `.clap` is scanned for them (the scan lists the plugins and measures nothing), an LV2 plugin joins the run's
|
|
649
|
+
* descriptor pass.
|
|
650
|
+
*/
|
|
651
|
+
function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncItem, files: readonly FileDigest[]): SyncEntry | undefined {
|
|
652
|
+
if (layer.records === 0) return undefined;
|
|
653
|
+
const started = Date.now();
|
|
654
|
+
const host = ctx.settings.hostProfile.id;
|
|
655
|
+
const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files));
|
|
656
|
+
if (match.status === "none") return undefined;
|
|
657
|
+
if (match.status === "ignored") {
|
|
658
|
+
for (const line of match.lines) ctx.log(line);
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
let plugins: SyncPlugin[];
|
|
662
|
+
try {
|
|
663
|
+
plugins = applyDistributed(ctx.settings.qualificationDir, item, match, host);
|
|
664
|
+
} catch (e) {
|
|
665
|
+
ctx.log(`distributed: ${distributedUri(item, match.records[0]!)}: ${(e as Error).message}; measured here`);
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
if (item.format === "clap") scanClapDescriptors(ctx, item);
|
|
669
|
+
const all = digestFiles(item.root, [...item.files, ...match.files], files) ?? [...files];
|
|
670
|
+
return { format: item.format, root: item.root, files: all, plugins, qualifiedAt: new Date(started).toISOString(), seconds: (Date.now() - started) / 1000, distributed: match.set };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** The descriptors of a `.clap` taken from the layer: the scanner lists its plugins, the descriptor writer writes them. */
|
|
674
|
+
function scanClapDescriptors(ctx: SyncContext, item: SyncItem): void {
|
|
675
|
+
const file = join(item.root, item.files[0]!);
|
|
676
|
+
let tool: string;
|
|
677
|
+
try {
|
|
678
|
+
tool = clapQualifyTools(ctx.env, existsSync).tool;
|
|
679
|
+
} catch (e) {
|
|
680
|
+
ctx.log(`${file}: descriptors not written: ${(e as Error).message}`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20 });
|
|
684
|
+
if (r.error !== undefined || r.status !== 0) {
|
|
685
|
+
ctx.log(`${file}: descriptors not written: the scan ended ${r.error?.message ?? `with exit ${String(r.status)}`}`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
writeClapDescriptors(ctx, r.stdout);
|
|
689
|
+
}
|
|
690
|
+
|
|
569
691
|
// --- the outputs ------------------------------------------------------------------------------------------------------
|
|
570
692
|
|
|
571
693
|
/** The pin file's text: every whole pin of the state, the worse verdict kept for a uri pinned twice. */
|
|
@@ -598,17 +720,32 @@ export function recordStem(recordPath: string): string | undefined {
|
|
|
598
720
|
return /^(.*-[0-9a-f]{12})\.(?:[^.]+\.(?:qualification|probe)|unmeasured)\.json$/.exec(basename(recordPath))?.[1];
|
|
599
721
|
}
|
|
600
722
|
|
|
723
|
+
/**
|
|
724
|
+
* A stem's id and binary hash, its version segment dropped: what a record and its descriptor share even when their
|
|
725
|
+
* version segments differ (a CLAP record is keyed `@unversioned` while its descriptor carries the plugin's declared
|
|
726
|
+
* version). Undefined for a name that is not of the `<id>@<version>-<hash>` shape.
|
|
727
|
+
*/
|
|
728
|
+
function idAndHash(stem: string): string | undefined {
|
|
729
|
+
const m = /^(.+)@.+-([0-9a-f]{12})$/.exec(stem);
|
|
730
|
+
return m === null ? undefined : `${m[1]}\0${m[2]}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
601
733
|
/**
|
|
602
734
|
* What a plugin the sync no longer finds, or finds as other bytes, leaves behind: its qualification record, the probe
|
|
603
735
|
* output copied beside it and its descriptor. A stem the state before held and the state after does not hold is
|
|
604
|
-
* stale; every file of that stem
|
|
605
|
-
*
|
|
736
|
+
* stale; every record file of that stem is removed, and so is a descriptor of its id and binary hash whichever
|
|
737
|
+
* version suffix it carries — unless that id and hash is still kept under another stem (two copies of one binary, or
|
|
738
|
+
* a record and a descriptor the sync wrote at different version suffixes for the same plugin). A record another
|
|
739
|
+
* plugin of the state still names (two copies of one binary) is kept, as is a descriptor the sync did not write.
|
|
740
|
+
* Returns the files removed.
|
|
606
741
|
*/
|
|
607
742
|
export function pruneStale(settings: Pick<SyncSettings, "qualificationDir" | "descriptorDir">, before: SyncState, after: SyncState): string[] {
|
|
608
743
|
const stems = (state: SyncState): Set<string> =>
|
|
609
744
|
new Set(Object.values(state.entries).flatMap((e) => e.plugins.flatMap((p) => (p.record === undefined ? [] : [recordStem(p.record) ?? ""]))).filter((x) => x !== ""));
|
|
610
745
|
const kept = stems(after);
|
|
611
746
|
const stale = [...stems(before)].filter((stem) => !kept.has(stem));
|
|
747
|
+
const keptIdHash = new Set([...kept].map(idAndHash).filter((x): x is string => x !== undefined));
|
|
748
|
+
const staleIdHash = new Set(stale.map(idAndHash).filter((x): x is string => x !== undefined));
|
|
612
749
|
const removed: string[] = [];
|
|
613
750
|
const drop = (dir: string, file: string): void => {
|
|
614
751
|
try {
|
|
@@ -624,9 +761,17 @@ export function pruneStale(settings: Pick<SyncSettings, "qualificationDir" | "de
|
|
|
624
761
|
} catch {
|
|
625
762
|
// no directory, no record
|
|
626
763
|
}
|
|
627
|
-
for (const stem of stale) {
|
|
628
|
-
|
|
629
|
-
|
|
764
|
+
for (const stem of stale) for (const f of records) if (f.startsWith(`${stem}.`) && recordStem(f) === stem) drop(settings.qualificationDir, f);
|
|
765
|
+
let descriptors: string[] = [];
|
|
766
|
+
try {
|
|
767
|
+
descriptors = readdirSync(settings.descriptorDir);
|
|
768
|
+
} catch {
|
|
769
|
+
// no directory, no descriptor
|
|
770
|
+
}
|
|
771
|
+
for (const f of descriptors) {
|
|
772
|
+
if (!f.endsWith(".json")) continue;
|
|
773
|
+
const key = idAndHash(f.slice(0, -".json".length));
|
|
774
|
+
if (key !== undefined && staleIdHash.has(key) && !keptIdHash.has(key)) drop(settings.descriptorDir, f);
|
|
630
775
|
}
|
|
631
776
|
return removed;
|
|
632
777
|
}
|
|
@@ -731,13 +876,15 @@ export interface SyncSummary {
|
|
|
731
876
|
readonly crashed: number;
|
|
732
877
|
readonly hung: number;
|
|
733
878
|
readonly unmeasured: number;
|
|
879
|
+
/** The plugins taken from the distributed records, not measured. */
|
|
880
|
+
readonly distributed: number;
|
|
734
881
|
readonly seconds: number;
|
|
735
882
|
/** The pins a running daemon took, when one answered. */
|
|
736
883
|
readonly sent?: number;
|
|
737
884
|
}
|
|
738
885
|
|
|
739
886
|
export function summaryLine(s: SyncSummary): string {
|
|
740
|
-
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)}, ${s.seconds.toFixed(1)} s`;
|
|
887
|
+
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.seconds.toFixed(1)} s`;
|
|
741
888
|
}
|
|
742
889
|
|
|
743
890
|
/** The lock of one state file: a second sync on it while one runs returns at once. A lock whose process is gone is taken over. */
|
|
@@ -791,7 +938,8 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
791
938
|
const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
|
|
792
939
|
await waitQuiet(dirs, settings.settleS, log);
|
|
793
940
|
const ctx: SyncContext = { settings, env, log };
|
|
794
|
-
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0 };
|
|
941
|
+
const count = { qualified: 0, skipped: 0, refused: 0, crashed: 0, hung: 0, unmeasured: 0, distributed: 0 };
|
|
942
|
+
const layer = distributedLayerOf(settings, options.force === true, log);
|
|
795
943
|
let before: string;
|
|
796
944
|
try {
|
|
797
945
|
before = readFileSync(settings.pinFile, "utf8");
|
|
@@ -823,6 +971,17 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
823
971
|
log(`${item.key}: a file of it cannot be read; skipped this run`);
|
|
824
972
|
continue;
|
|
825
973
|
}
|
|
974
|
+
const taken = takeDistributed(ctx, layer, item, files);
|
|
975
|
+
if (taken !== undefined) {
|
|
976
|
+
entries[item.key] = taken;
|
|
977
|
+
if (item.format === "lv2") lv2Pinned.push(...taken.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|
|
978
|
+
count.distributed += taken.plugins.length;
|
|
979
|
+
for (const p of taken.plugins) log(`distributed: ${p.uri}: applied (${taken.distributed!})`);
|
|
980
|
+
state = { schema: SYNC_STATE_SCHEMA, entries };
|
|
981
|
+
writeFileWhole(settings.stateFile, `${JSON.stringify(state, null, 1)}\n`);
|
|
982
|
+
writeFileWhole(settings.pinFile, syncPinsText(state));
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
826
985
|
const entry = qualifyItem(ctx, item, files);
|
|
827
986
|
entries[item.key] = entry;
|
|
828
987
|
if (item.format === "lv2") lv2Pinned.push(...entry.plugins.filter((p) => p.pin !== undefined).map((p) => p.uri));
|