Keep checked browser runtime versions independent of authority upgrades
This commit is contained in:
@@ -10,10 +10,16 @@ import {
|
|||||||
type CandidateMetadataScope,
|
type CandidateMetadataScope,
|
||||||
} from "./metadata.js";
|
} from "./metadata.js";
|
||||||
const rootPattern = /^\/nix\/store\/[a-z0-9]{32}-[^/\s]+$/;
|
const rootPattern = /^\/nix\/store\/[a-z0-9]{32}-[^/\s]+$/;
|
||||||
async function readJson(root: string, file: string, scope: CandidateMetadataScope): Promise<any> {
|
async function readJson(root: string, file: string, scope: CandidateMetadataScope) {
|
||||||
const input = await readCandidateMetadata(join(root, file), scope, 32 * 1024 * 1024);
|
const input = await readCandidateMetadata(join(root, file), scope, 32 * 1024 * 1024);
|
||||||
try {
|
try {
|
||||||
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes));
|
return {
|
||||||
|
value: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.bytes)),
|
||||||
|
releaseTree: input.releaseTree,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
input.releaseTree();
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
input.releaseInput();
|
input.releaseInput();
|
||||||
}
|
}
|
||||||
@@ -30,6 +36,7 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
|
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
|
||||||
throw Error("Candidate must be an immutable Nix output directory");
|
throw Error("Candidate must be an immutable Nix output directory");
|
||||||
const scope = (resources.budget ?? candidateMetadataBudget).acquire(resources.owner ?? root);
|
const scope = (resources.budget ?? candidateMetadataBudget).acquire(resources.owner ?? root);
|
||||||
|
let releaseValidation = () => {};
|
||||||
try {
|
try {
|
||||||
// Sequential reads avoid partially rejected Promise.all() work allocating
|
// Sequential reads avoid partially rejected Promise.all() work allocating
|
||||||
// after the failed candidate's reservation has been released.
|
// after the failed candidate's reservation has been released.
|
||||||
@@ -43,8 +50,13 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
"kernel.json",
|
"kernel.json",
|
||||||
"queries.json",
|
"queries.json",
|
||||||
"graph-execution.json",
|
"graph-execution.json",
|
||||||
])
|
]) {
|
||||||
values.push(await readJson(root, file, scope));
|
const parsed = await readJson(root, file, scope);
|
||||||
|
values.push(parsed.value);
|
||||||
|
// candidate.json duplicates the checked plan for validation. Nothing in
|
||||||
|
// the installed candidate retains this input tree after validation.
|
||||||
|
if (file === "candidate.json") releaseValidation = parsed.releaseTree;
|
||||||
|
}
|
||||||
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = values;
|
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = values;
|
||||||
const { executionContracts: _legacyContracts, ...workspace } = candidate;
|
const { executionContracts: _legacyContracts, ...workspace } = candidate;
|
||||||
if (
|
if (
|
||||||
@@ -149,6 +161,8 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
scope.release();
|
scope.release();
|
||||||
throw error;
|
throw error;
|
||||||
|
} finally {
|
||||||
|
releaseValidation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export async function readCandidateMetadata(
|
|||||||
path: string,
|
path: string,
|
||||||
scope: CandidateMetadataScope,
|
scope: CandidateMetadataScope,
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<{ bytes: Uint8Array; releaseInput: () => void }> {
|
): Promise<{ bytes: Uint8Array; releaseInput: () => void; releaseTree: () => void }> {
|
||||||
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
||||||
let releaseInput: (() => void) | undefined;
|
let releaseInput: (() => void) | undefined;
|
||||||
try {
|
try {
|
||||||
@@ -123,8 +123,8 @@ export async function readCandidateMetadata(
|
|||||||
if (offset !== stat.size) throw Error("CANDIDATE_METADATA_CHANGED");
|
if (offset !== stat.size) throw Error("CANDIDATE_METADATA_CHANGED");
|
||||||
const content = bytes.subarray(0, offset);
|
const content = bytes.subarray(0, offset);
|
||||||
// No JSON.parse/decoded tree exists until its shape-based charge succeeds.
|
// No JSON.parse/decoded tree exists until its shape-based charge succeeds.
|
||||||
scope.charge(jsonAllocation(content));
|
const releaseTree = scope.reserve(jsonAllocation(content));
|
||||||
return { bytes: content, releaseInput };
|
return { bytes: content, releaseInput, releaseTree };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
releaseInput?.();
|
releaseInput?.();
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ test("metadata files reserve parse/index space before allocation and refuse over
|
|||||||
await assert.rejects(readCandidateMetadata(join(root, "linked"), scope, 100), { code: "ELOOP" });
|
await assert.rejects(readCandidateMetadata(join(root, "linked"), scope, 100), { code: "ELOOP" });
|
||||||
await assert.rejects(readCandidateMetadata(root, scope, 100), /CANDIDATE_METADATA_FILE_LIMIT/);
|
await assert.rejects(readCandidateMetadata(root, scope, 100), /CANDIDATE_METADATA_FILE_LIMIT/);
|
||||||
assert.equal(scope.bytes, first);
|
assert.equal(scope.bytes, first);
|
||||||
|
input.releaseTree();
|
||||||
|
input.releaseTree();
|
||||||
|
assert.equal(scope.bytes, 0, "Discarded validation trees can release independently of the candidate");
|
||||||
} finally {
|
} finally {
|
||||||
scope.release();
|
scope.release();
|
||||||
await rm(root, { recursive: true, force: true });
|
await rm(root, { recursive: true, force: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user