@openmixer/plugin-qualify 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/bin/plugin-qualify-sync.mjs +1 -1
- package/dist/host-admission.d.ts +25 -6
- package/dist/host-admission.js +45 -17
- package/dist/host-admission.js.map +1 -1
- package/dist/hostd-pin-files.d.ts +25 -0
- package/dist/hostd-pin-files.js +58 -0
- package/dist/hostd-pin-files.js.map +1 -0
- package/dist/probe-judge.d.ts +2 -0
- package/dist/probe-judge.js +3 -2
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output-fixtures.js +4 -1
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +2 -1
- package/dist/probe-output.js +4 -1
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +5 -0
- package/dist/probe-runner.js +8 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +16 -1
- package/dist/qualification-record.js +8 -2
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-sync.d.ts +22 -3
- package/dist/qualify-sync.js +139 -38
- package/dist/qualify-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/host-admission.ts +69 -23
- package/src/hostd-pin-files.ts +75 -0
- package/src/probe-judge.ts +5 -1
- package/src/probe-output-fixtures.ts +4 -1
- package/src/probe-output.ts +6 -2
- package/src/probe-runner.ts +13 -3
- package/src/qualification-record.ts +16 -2
- package/src/qualify-sync.ts +146 -37
- package/tools/descriptors.py +8 -1
- package/tools/mod-host-admit.mjs +3 -1
|
@@ -93,13 +93,15 @@ export function unmeasuredFigures(output: ProbeOutput): readonly UnmeasuredFigur
|
|
|
93
93
|
|
|
94
94
|
/** A cell's latency when the run took no figure: the contract's unmeasured word and the hosting code a reader classifies by. */
|
|
95
95
|
export interface UnmeasuredLatency {
|
|
96
|
+
/** The figure the plugin declared (LV2 latency port, CLAP latency extension) when it loaded but the probe did not measure. */
|
|
97
|
+
readonly declaredFrames?: number;
|
|
96
98
|
readonly unmeasuredReason: string;
|
|
97
99
|
readonly code: string;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/** A grid point as the record writes it: every cell carries `latency`, the figures or why there are none (#201). */
|
|
101
103
|
export type RecordGridPoint = Omit<ProbeGridPoint, "latency"> & {
|
|
102
|
-
readonly latency: NonNullable<ProbeGridPoint["latency"]> | UnmeasuredLatency;
|
|
104
|
+
readonly latency: (Omit<NonNullable<ProbeGridPoint["latency"]>, "measuredFrames"> & { readonly measuredFrames: number }) | UnmeasuredLatency;
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
const UNMEASURED_WORDS: ReadonlySet<string> = new Set(Object.values(MEASUREMENT_UNMEASURED_REASONS).map((r) => r.word));
|
|
@@ -120,7 +122,13 @@ const OUTCOME_WORD: Readonly<Record<string, string>> = {
|
|
|
120
122
|
* older than the latency arm writes it.
|
|
121
123
|
*/
|
|
122
124
|
export function latencyOfCell(output: ProbeOutput, point: ProbeGridPoint): RecordGridPoint["latency"] {
|
|
123
|
-
|
|
125
|
+
const had = point.latency;
|
|
126
|
+
if (had?.measuredFrames !== undefined) return { ...had, measuredFrames: had.measuredFrames };
|
|
127
|
+
const why = unmeasuredOfCell(output, point);
|
|
128
|
+
return had === undefined ? why : { declaredFrames: had.declaredFrames, ...why };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unmeasuredOfCell(output: ProbeOutput, point: ProbeGridPoint): UnmeasuredLatency {
|
|
124
132
|
const code = HOSTING_CODE.latencyUnmeasured;
|
|
125
133
|
if (point.outcome !== PROBE_OK) return { unmeasuredReason: OUTCOME_WORD[point.outcome] ?? word("probeError"), code };
|
|
126
134
|
const step = output.steps.find((st) => st.step === "latency" && st.rate === point.rate && (st.quantum === undefined || st.quantum === point.quantum));
|
|
@@ -162,6 +170,11 @@ export interface QualificationRecord {
|
|
|
162
170
|
readonly testedVersion?: string;
|
|
163
171
|
readonly binary: ProbeOutput["binary"];
|
|
164
172
|
readonly verdicts: readonly RecordVerdict[];
|
|
173
|
+
/**
|
|
174
|
+
* An LV2 plugin run under plugin-hostd's LV2 adapter (omx-clap-host), as under mod-host: admitted, or refused with
|
|
175
|
+
* its code, or `measured: false` when it was not tried. Beside the verdicts, which it does not change (#223).
|
|
176
|
+
*/
|
|
177
|
+
readonly adapter?: { readonly host: string; readonly measured: boolean; readonly admitted: boolean; readonly code?: string; readonly params: Readonly<Record<string, string | number>> };
|
|
165
178
|
readonly tiers: RecordTiers;
|
|
166
179
|
/** The cost reasons at each host's rate and quantum, beside the verdicts; an instrument's with the notes it held. */
|
|
167
180
|
readonly cost: readonly {
|
|
@@ -266,6 +279,7 @@ export function buildQualificationRecord(
|
|
|
266
279
|
...(h.verdict.deciding === undefined ? {} : { deciding: h.verdict.deciding }),
|
|
267
280
|
reasons: h.reasons,
|
|
268
281
|
})),
|
|
282
|
+
...(judgment.adapter === undefined ? {} : { adapter: judgment.adapter }),
|
|
269
283
|
tiers: tiersOf(judgment),
|
|
270
284
|
cost: judgment.hosts.map((h) => ({ host: h.verdict.host, ...h.cost })),
|
|
271
285
|
measurements: {
|
package/src/qualify-sync.ts
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* When a daemon answers on the command port, the pins that changed are also sent to it with `pin_set`, and the ones
|
|
17
17
|
* of a plugin that is gone with `pin_clear`. What a gone plugin left in the records and the descriptors is removed.
|
|
18
18
|
*/
|
|
19
|
-
import { spawnSync } from "node:child_process";
|
|
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, symlinkSync, unlinkSync,
|
|
21
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, 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 } from "node:path";
|
|
@@ -30,6 +30,7 @@ import { HOSTING_CODE } from "./hosting-codes.js";
|
|
|
30
30
|
import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
|
|
31
31
|
import { lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
|
|
32
32
|
import { judgmentPinEntries, pinsFile, type PinEntry } from "./pins-file.js";
|
|
33
|
+
import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
|
|
33
34
|
import type { Judgment } from "./probe-judge.js";
|
|
34
35
|
import { qualifyTargets } from "./probe-runner.js";
|
|
35
36
|
import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
@@ -124,6 +125,7 @@ export interface SyncArgs {
|
|
|
124
125
|
readonly system: boolean;
|
|
125
126
|
readonly force: boolean;
|
|
126
127
|
readonly noDistributed: boolean;
|
|
128
|
+
readonly noWait: boolean;
|
|
127
129
|
readonly help: boolean;
|
|
128
130
|
/** The settings given as flags, by key. */
|
|
129
131
|
readonly values: ReadonlyMap<string, string>;
|
|
@@ -138,12 +140,14 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
138
140
|
let system = false;
|
|
139
141
|
let force = false;
|
|
140
142
|
let noDistributed = false;
|
|
143
|
+
let noWait = false;
|
|
141
144
|
let help = false;
|
|
142
145
|
for (let i = 0; i < argv.length; i++) {
|
|
143
146
|
const a = argv[i]!;
|
|
144
147
|
if (a === "--system") system = true;
|
|
145
148
|
else if (a === "--force") force = true;
|
|
146
149
|
else if (a === "--no-distributed") noDistributed = true;
|
|
150
|
+
else if (a === "--no-wait") noWait = true;
|
|
147
151
|
else if (a === "--help") help = true;
|
|
148
152
|
else if (a === "--grid-hosts") values.set("grid", "hosts");
|
|
149
153
|
else if (a === "--config" || byFlag.has(a)) {
|
|
@@ -153,7 +157,7 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
|
|
|
153
157
|
else values.set(byFlag.get(a)!, v);
|
|
154
158
|
} else return { ok: false, error: `unknown argument ${a}` };
|
|
155
159
|
}
|
|
156
|
-
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, help, values } };
|
|
160
|
+
return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, help, values } };
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
export function syncHelpText(): string {
|
|
@@ -164,6 +168,7 @@ export function syncHelpText(): string {
|
|
|
164
168
|
` ${"--config <file>".padEnd(36)}the settings file (default: ${SYNC_CONF}; a missing file is the defaults)`,
|
|
165
169
|
" --system a system service's directories, under /var/lib/omx",
|
|
166
170
|
" --force qualify every plugin again, recorded or not; no distributed record is read",
|
|
171
|
+
" --no-wait a run in progress is not waited for: exit 0, and the holder runs once more at its end",
|
|
167
172
|
" --no-distributed read no distributed record: every plugin not recorded is measured",
|
|
168
173
|
" --grid-hosts the fast grid, the same as --grid hosts (7 cells with the shipped hosts)",
|
|
169
174
|
" --help this text",
|
|
@@ -491,6 +496,23 @@ export interface SyncContext {
|
|
|
491
496
|
readonly settings: SyncSettings;
|
|
492
497
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
493
498
|
readonly log: (line: string) => void;
|
|
499
|
+
/** The run of `plugin-hostd --pin-files`, a stand-in in tests. */
|
|
500
|
+
readonly pinFilesRun?: PinFilesRun;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The pin of a judged run with the files plugin-hostd collects for its plugin (plugin-qualify#228): the probe's list is
|
|
505
|
+
* 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
|
|
506
|
+
* them; the plugin is then pinned on its first add (pin_mode first-use), and the log says why.
|
|
507
|
+
*/
|
|
508
|
+
function withHostdFiles(ctx: SyncContext, pin: PinEntry | undefined): PinEntry | undefined {
|
|
509
|
+
if (pin === undefined || pin.integrity.files === undefined) return pin;
|
|
510
|
+
const r = hostdPinFiles(pin.uri, ctx.settings.lv2Dirs, ctx.env, ...(ctx.pinFilesRun === undefined ? [] : [ctx.pinFilesRun]));
|
|
511
|
+
if (!r.ok) {
|
|
512
|
+
ctx.log(`${pin.uri}: not pinned: ${r.detail}`);
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
return { ...pin, integrity: { ...pin.integrity, files: r.files } };
|
|
494
516
|
}
|
|
495
517
|
|
|
496
518
|
/** A judged run's outcome for the host: qualified, refused, or why it was not measured. */
|
|
@@ -528,7 +550,7 @@ function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgmen
|
|
|
528
550
|
const host = ctx.settings.hostProfile.id;
|
|
529
551
|
const entries = runs.map(({ bytes, judgment }) => {
|
|
530
552
|
const { recordPath } = writeQualification(ctx.settings.qualificationDir, bytes, judgment, QUICK_NOT_MEASURED);
|
|
531
|
-
const pin = judgmentPinEntries(judgment, host)[0];
|
|
553
|
+
const pin = withHostdFiles(ctx, judgmentPinEntries(judgment, host)[0]);
|
|
532
554
|
const { outcome, code } = outcomeOf(judgment, host);
|
|
533
555
|
const plugin: SyncPlugin = { uri: pin?.uri ?? judgment.output.plugin.id, outcome, ...(code === undefined ? {} : { code }), ...(pin === undefined ? {} : { pin }), record: recordPath };
|
|
534
556
|
return { plugin, judgedHere: judgment.hosts.some((h) => h.verdict.host === host) };
|
|
@@ -550,6 +572,8 @@ function runPass(ctx: SyncContext, target: Parameters<typeof qualifyTargets>[0][
|
|
|
550
572
|
profiles: [s.hostProfile],
|
|
551
573
|
wallS: s.budgetS,
|
|
552
574
|
env,
|
|
575
|
+
// mod-host and jalv are admitted on the device's LV2 directories, not on the one-bundle world the probe measures in
|
|
576
|
+
admissionLv2Path: s.lv2Dirs,
|
|
553
577
|
});
|
|
554
578
|
}
|
|
555
579
|
|
|
@@ -919,57 +943,143 @@ export interface SyncSummary {
|
|
|
919
943
|
readonly sent?: number;
|
|
920
944
|
}
|
|
921
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Why a reader that is not the sync's user (the console, #221) cannot read the descriptors: the directory or one of its
|
|
948
|
+
* parent without search permission for others, the directory or a descriptor without read permission for others. One
|
|
949
|
+
* line each; none when the directory does not exist yet or everything is open.
|
|
950
|
+
*/
|
|
951
|
+
export function descriptorAccessProblems(dir: string): string[] {
|
|
952
|
+
const problems: string[] = [];
|
|
953
|
+
const mode = (path: string): number | undefined => {
|
|
954
|
+
try {
|
|
955
|
+
return statSync(path).mode & 0o777;
|
|
956
|
+
} catch {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
const octal = (m: number): string => `0${m.toString(8)}`;
|
|
961
|
+
const top = mode(dir);
|
|
962
|
+
if (top === undefined) return problems;
|
|
963
|
+
// the parent too (StateDirectory's /var/lib/omx); not further up, where a private home is a user's own choice
|
|
964
|
+
const parent = dirname(resolve(dir));
|
|
965
|
+
const parentMode = mode(parent);
|
|
966
|
+
if (parentMode !== undefined && (parentMode & 0o001) === 0) problems.push(`${parent} is mode ${octal(parentMode)}: other users, the console among them, cannot enter it to read the descriptors`);
|
|
967
|
+
if ((top & 0o004) === 0) problems.push(`${dir} is mode ${octal(top)}: other users, the console among them, cannot list the descriptors`);
|
|
968
|
+
let files: string[] = [];
|
|
969
|
+
try {
|
|
970
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
971
|
+
} catch {
|
|
972
|
+
return problems;
|
|
973
|
+
}
|
|
974
|
+
const closed = files.filter((f) => ((mode(join(dir, f)) ?? 0o004) & 0o004) === 0);
|
|
975
|
+
if (closed.length > 0) problems.push(`${String(closed.length)} descriptor(s) in ${dir} cannot be read by other users, the console among them (first: ${closed[0]!})`);
|
|
976
|
+
return problems;
|
|
977
|
+
}
|
|
978
|
+
|
|
922
979
|
export function summaryLine(s: SyncSummary): string {
|
|
923
980
|
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`;
|
|
924
981
|
}
|
|
925
982
|
|
|
926
|
-
/** The
|
|
927
|
-
|
|
983
|
+
/** The exit status `flock -n` returns when the lock is held (-E), told apart from a failure of flock itself. */
|
|
984
|
+
const LOCK_HELD = 99;
|
|
985
|
+
|
|
986
|
+
export interface SyncLock {
|
|
987
|
+
release(): Promise<void>;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* The lock of one state file: flock(1) on `<state file>.lock`, held by a child process for as long as this one lives or
|
|
992
|
+
* until `release`, so a crashed run frees it. Waits for the holder, or with `wait` false returns `undefined` at once.
|
|
993
|
+
*/
|
|
994
|
+
export function takeLock(stateFile: string, wait: boolean): Promise<SyncLock | undefined> {
|
|
928
995
|
const lock = `${stateFile}.lock`;
|
|
929
996
|
mkdirSync(dirname(lock), { recursive: true });
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
unlinkSync(lock);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return undefined;
|
|
997
|
+
const child = spawn("flock", ["-x", "-E", String(LOCK_HELD), ...(wait ? [] : ["-n"]), lock, "sh", "-c", "echo locked; exec cat"], { stdio: ["pipe", "pipe", "inherit"] });
|
|
998
|
+
return new Promise((resolvePromise, reject) => {
|
|
999
|
+
const exited = new Promise<void>((done) => child.once("close", () => done()));
|
|
1000
|
+
child.once("error", reject);
|
|
1001
|
+
child.stdout.once("data", () =>
|
|
1002
|
+
resolvePromise({
|
|
1003
|
+
release: () => {
|
|
1004
|
+
child.stdin.end();
|
|
1005
|
+
return exited;
|
|
1006
|
+
},
|
|
1007
|
+
}),
|
|
1008
|
+
);
|
|
1009
|
+
child.once("close", (code) => {
|
|
1010
|
+
if (code === LOCK_HELD) resolvePromise(undefined);
|
|
1011
|
+
else reject(new Error(`flock ${lock} failed (exit ${String(code)})`));
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
951
1014
|
}
|
|
952
1015
|
|
|
1016
|
+
/** What a second run leaves for the holder: one more run at its end, over what changed meanwhile. */
|
|
1017
|
+
const againMarker = (stateFile: string): string => `${stateFile}.again`;
|
|
1018
|
+
|
|
953
1019
|
export interface SyncRunOptions {
|
|
954
1020
|
readonly force?: boolean;
|
|
1021
|
+
/** False under `--no-wait`: a run in progress is not waited for; the holder runs once more at its end instead. */
|
|
1022
|
+
readonly wait?: boolean;
|
|
955
1023
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
956
1024
|
readonly log?: (line: string) => void;
|
|
957
1025
|
}
|
|
958
1026
|
|
|
959
1027
|
/**
|
|
960
|
-
* One run:
|
|
961
|
-
*
|
|
962
|
-
* whole after every item, so a run cut short keeps all it finished. `undefined` when another run holds the lock.
|
|
1028
|
+
* One run under the lock: waits for a run in progress, or with `wait` false leaves a marker for it and returns
|
|
1029
|
+
* `undefined`. The holder runs once more at its end for every marker left meanwhile.
|
|
963
1030
|
*/
|
|
964
1031
|
export async function runSync(settings: SyncSettings, options: SyncRunOptions = {}): Promise<SyncSummary | undefined> {
|
|
965
1032
|
const log = options.log ?? (() => {});
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
|
|
1033
|
+
const marker = againMarker(settings.stateFile);
|
|
1034
|
+
let lock = await takeLock(settings.stateFile, options.wait !== false);
|
|
1035
|
+
if (lock === undefined) {
|
|
1036
|
+
// the marker first, then the lock once more: a holder that finished in between is not left without a successor
|
|
1037
|
+
writeFileSync(marker, "");
|
|
1038
|
+
lock = await takeLock(settings.stateFile, false);
|
|
1039
|
+
if (lock === undefined) {
|
|
1040
|
+
log(`a run is in progress (${settings.stateFile}.lock); it runs once more at its end over what changed`);
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
971
1043
|
}
|
|
972
1044
|
try {
|
|
1045
|
+
let total: SyncSummary | undefined;
|
|
1046
|
+
for (;;) {
|
|
1047
|
+
rmSync(marker, { force: true });
|
|
1048
|
+
const one = await runLocked(settings, options);
|
|
1049
|
+
total = total === undefined ? one : addSummaries(total, one);
|
|
1050
|
+
if (!existsSync(marker)) return total;
|
|
1051
|
+
log("a run was asked for while this one ran; running once more");
|
|
1052
|
+
}
|
|
1053
|
+
} finally {
|
|
1054
|
+
await lock.release();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function addSummaries(a: SyncSummary, b: SyncSummary): SyncSummary {
|
|
1059
|
+
const sent = (a.sent ?? 0) + (b.sent ?? 0);
|
|
1060
|
+
return {
|
|
1061
|
+
qualified: a.qualified + b.qualified,
|
|
1062
|
+
skipped: a.skipped + b.skipped,
|
|
1063
|
+
refused: a.refused + b.refused,
|
|
1064
|
+
crashed: a.crashed + b.crashed,
|
|
1065
|
+
hung: a.hung + b.hung,
|
|
1066
|
+
unmeasured: a.unmeasured + b.unmeasured,
|
|
1067
|
+
distributed: a.distributed + b.distributed,
|
|
1068
|
+
seconds: a.seconds + b.seconds,
|
|
1069
|
+
...(sent === 0 ? {} : { sent }),
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* One run: wait for quiet, qualify what is new or changed, drop what is gone, write the state, the records, the
|
|
1075
|
+
* descriptors and the pin file, and send the changed pins to a running daemon. The state and the pin file are written
|
|
1076
|
+
* whole after every item, so a run cut short keeps all it finished.
|
|
1077
|
+
*/
|
|
1078
|
+
async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promise<SyncSummary> {
|
|
1079
|
+
const log = options.log ?? (() => {});
|
|
1080
|
+
const env = options.env ?? process.env;
|
|
1081
|
+
log("sync run begins");
|
|
1082
|
+
{
|
|
973
1083
|
const started = Date.now();
|
|
974
1084
|
const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
|
|
975
1085
|
await waitQuiet(dirs, settings.settleS, log);
|
|
@@ -1037,6 +1147,7 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
1037
1147
|
const after = syncPinsText(state);
|
|
1038
1148
|
if (after !== before) writeFileWhole(settings.pinFile, after);
|
|
1039
1149
|
writeLv2Descriptors(ctx, lv2Pinned);
|
|
1150
|
+
for (const line of descriptorAccessProblems(settings.descriptorDir)) log(line);
|
|
1040
1151
|
// the files of a plugin that is gone, or was replaced by other bytes: only when every directory could be read, so
|
|
1041
1152
|
// an unreadable one is never taken for an uninstall
|
|
1042
1153
|
if (unreadable) log("a plugin directory could not be read; no record or descriptor is removed this run");
|
|
@@ -1056,8 +1167,6 @@ export async function runSync(settings: SyncSettings, options: SyncRunOptions =
|
|
|
1056
1167
|
for (let i = 0; i < replies.length; i++) if (replies[i] !== "resp 0") log(`plugin-hostd answered ${replies[i]!} to ${changed[i]!.split(" ")[1]!}`);
|
|
1057
1168
|
const taken = replies.filter((r) => r === "resp 0").length;
|
|
1058
1169
|
return { ...count, seconds: (Date.now() - started) / 1000, ...(taken === 0 ? {} : { sent: taken }) };
|
|
1059
|
-
} finally {
|
|
1060
|
-
unlock();
|
|
1061
1170
|
}
|
|
1062
1171
|
}
|
|
1063
1172
|
|
package/tools/descriptors.py
CHANGED
|
@@ -138,16 +138,23 @@ class SchemaValidator:
|
|
|
138
138
|
raise DescriptorRefused(DESCRIPTOR_CODE_SCHEMA, f"{where}: {e.message}")
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
DESCRIPTOR_DIR_MODE = 0o755
|
|
142
|
+
DESCRIPTOR_FILE_MODE = 0o644
|
|
143
|
+
|
|
144
|
+
|
|
141
145
|
def write_descriptor(directory, doc, sha256, validator):
|
|
142
146
|
"""Hold `doc` to the schema and write it whole into `directory`; the path written, or DescriptorRefused."""
|
|
143
147
|
validator.check(doc)
|
|
144
148
|
name = descriptor_file_name(doc["plugin"]["id"], doc["plugin"].get("version"), sha256)
|
|
145
149
|
try:
|
|
146
|
-
|
|
150
|
+
# the console reads this directory as another user (#221): published world-readable whatever the umask
|
|
151
|
+
os.makedirs(directory, mode=DESCRIPTOR_DIR_MODE, exist_ok=True)
|
|
152
|
+
os.chmod(directory, DESCRIPTOR_DIR_MODE)
|
|
147
153
|
path = os.path.join(directory, name)
|
|
148
154
|
pending = path + ".new"
|
|
149
155
|
with open(pending, "w", encoding="utf-8") as f:
|
|
150
156
|
f.write(json.dumps(doc, indent=2, ensure_ascii=False) + "\n")
|
|
157
|
+
os.chmod(pending, DESCRIPTOR_FILE_MODE)
|
|
151
158
|
os.replace(pending, path)
|
|
152
159
|
except OSError as e:
|
|
153
160
|
raise DescriptorRefused(DESCRIPTOR_CODE_DIR_UNWRITABLE, f"{directory}: {e.strerror}") from e
|
package/tools/mod-host-admit.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { spawn } from "node:child_process";
|
|
|
8
8
|
import { createConnection } from "node:net";
|
|
9
9
|
|
|
10
10
|
const uri = process.argv[2];
|
|
11
|
+
// the host to admit under: mod-host, or omx-clap-host for plugin-hostd's LV2 adapter, which answers the same protocol
|
|
12
|
+
const bin = process.argv[3] ?? "mod-host";
|
|
11
13
|
const port = 20000 + (process.pid % 20000);
|
|
12
14
|
const STEP_MS = 10000;
|
|
13
15
|
const INSTANCE = 0;
|
|
@@ -16,7 +18,7 @@ const COMMANDS = [
|
|
|
16
18
|
["remove", `remove ${INSTANCE}`],
|
|
17
19
|
];
|
|
18
20
|
|
|
19
|
-
const host = spawn(
|
|
21
|
+
const host = spawn(bin, ["-n", "-p", String(port)], { stdio: "ignore" });
|
|
20
22
|
let hostEnd; // how the host went, once it has
|
|
21
23
|
host.on("exit", (code, signal) => (hostEnd = signal ?? "exit"));
|
|
22
24
|
const spawnFailed = new Promise((resolve) => host.on("error", (e) => resolve(e.code)));
|