chore(deps): refresh locks and harden engine promotion

- refresh compatible npm and Cargo lockfile dependencies
- update locked Windows and Linux FFmpeg provider artifacts by checksum
- publish verified engine payloads with interrupted-promotion recovery
- retry Windows-safe cleanup and cover worst-case promotion states
This commit is contained in:
NimBold
2026-08-12 17:54:30 +03:30
parent 64a836f09f
commit 885e3d0100
6 changed files with 401 additions and 207 deletions
+137
View File
@@ -0,0 +1,137 @@
import fs from 'node:fs';
import path from 'node:path';
const RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000];
function pathExists(value) {
try {
fs.lstatSync(value);
return true;
} catch (error) {
if (error?.code === 'ENOENT') return false;
throw error;
}
}
function previousPayloadPrefix(destination) {
return `.${path.basename(destination)}.previous-`;
}
function previousPayloads(destination) {
const parent = path.dirname(destination);
const prefix = previousPayloadPrefix(destination);
return fs.readdirSync(parent, { withFileTypes: true })
.filter(entry => entry.name.startsWith(prefix))
.map(entry => path.join(parent, entry.name));
}
function previousPayloadOwner(destination, candidate) {
const suffix = path.basename(candidate).slice(previousPayloadPrefix(destination).length);
const separator = suffix.indexOf('-');
const pid = separator >= 0 ? suffix.slice(0, separator) : suffix;
return /^\d+$/.test(pid) ? Number(pid) : null;
}
function isProcessAlive(pid) {
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error?.code === 'EPERM';
}
}
async function removeOrphanedPreviousPayloads(destination) {
for (const candidate of previousPayloads(destination)) {
const owner = previousPayloadOwner(destination, candidate);
if (owner === null || isProcessAlive(owner)) continue;
await removePathWithRetry(candidate);
}
}
/**
* Restores the only previous payload left by a process that died after moving
* the destination aside but before publishing its replacement. Multiple
* candidates are ambiguous and remain untouched for manual recovery.
*/
export function recoverInterruptedPromotion(destination) {
if (pathExists(destination)) return;
const candidates = previousPayloads(destination);
if (candidates.length === 0) return;
if (candidates.length > 1) {
throw new Error(
`Cannot recover engine payload at ${destination}: found ${candidates.length} previous payloads`
);
}
const candidate = candidates[0];
if (!fs.lstatSync(candidate).isDirectory()) {
throw new Error(`Cannot recover engine payload from non-directory backup: ${candidate}`);
}
fs.renameSync(candidate, destination);
}
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
export async function removePathWithRetry(value) {
for (let attempt = 0; ; attempt += 1) {
try {
fs.rmSync(value, { recursive: true, force: true });
return;
} catch (error) {
const retryable = process.platform === 'win32'
&& ['EACCES', 'EBUSY', 'EPERM'].includes(error?.code);
if (!retryable || attempt >= RETRY_DELAYS_MS.length) throw error;
await sleep(RETRY_DELAYS_MS[attempt]);
}
}
}
/**
* Publishes a fully verified payload without exposing a partially written
* directory. The staging directory must share a filesystem with destination.
*/
export async function promoteDirectory(staging, destination) {
recoverInterruptedPromotion(destination);
const stagingStats = fs.lstatSync(staging);
if (!stagingStats.isDirectory()) {
throw new Error(`Engine payload staging path is not a directory: ${staging}`);
}
const parent = path.dirname(destination);
const backup = path.join(
parent,
`.${path.basename(destination)}.previous-${process.pid}-${process.hrtime.bigint()}`
);
let movedExisting = false;
try {
if (pathExists(destination)) {
fs.renameSync(destination, backup);
movedExisting = true;
}
fs.renameSync(staging, destination);
} catch (error) {
if (movedExisting && !pathExists(destination) && pathExists(backup)) {
try {
fs.renameSync(backup, destination);
} catch (restoreError) {
throw new AggregateError(
[error, restoreError],
`Failed to publish engine payload and restore the previous payload at ${destination}`
);
}
} else if (movedExisting && pathExists(destination) && pathExists(backup)) {
// Another provisioner won the promotion race; discard only our backup.
await removePathWithRetry(backup);
}
throw error;
}
if (movedExisting) await removePathWithRetry(backup);
await removeOrphanedPreviousPayloads(destination);
}
@@ -0,0 +1,128 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
promoteDirectory,
recoverInterruptedPromotion,
removePathWithRetry,
} from './engine-payload-promotion.js';
function temporaryDirectory() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-payload-promotion-'));
}
test('promotes a verified staging directory and replaces the previous payload', async () => {
const root = temporaryDirectory();
try {
const destination = path.join(root, 'target');
const staging = path.join(root, 'staging', 'payload');
fs.mkdirSync(destination, { recursive: true });
fs.writeFileSync(path.join(destination, 'engine'), 'old');
fs.mkdirSync(staging, { recursive: true });
fs.writeFileSync(path.join(staging, 'engine'), 'new');
await promoteDirectory(staging, destination);
assert.equal(fs.readFileSync(path.join(destination, 'engine'), 'utf8'), 'new');
assert.equal(fs.existsSync(staging), false);
assert.equal(fs.readdirSync(root).some(name => name.includes('.previous-')), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('rejects an invalid staging path without removing the previous payload', async () => {
const root = temporaryDirectory();
try {
const destination = path.join(root, 'target');
const staging = path.join(root, 'staging-file');
fs.mkdirSync(destination, { recursive: true });
fs.writeFileSync(path.join(destination, 'engine'), 'old');
fs.writeFileSync(staging, 'not a directory');
await assert.rejects(() => promoteDirectory(staging, destination), /not a directory/);
assert.equal(fs.readFileSync(path.join(destination, 'engine'), 'utf8'), 'old');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('recovers one interrupted previous payload before publishing a replacement', async () => {
const root = temporaryDirectory();
try {
const destination = path.join(root, 'target');
const backup = path.join(root, '.target.previous-123-456');
const staging = path.join(root, 'staging', 'payload');
fs.mkdirSync(backup, { recursive: true });
fs.writeFileSync(path.join(backup, 'engine'), 'old');
fs.mkdirSync(staging, { recursive: true });
fs.writeFileSync(path.join(staging, 'engine'), 'new');
await promoteDirectory(staging, destination);
assert.equal(fs.readFileSync(path.join(destination, 'engine'), 'utf8'), 'new');
assert.equal(fs.existsSync(backup), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('fails closed when interrupted recovery has multiple candidates', () => {
const root = temporaryDirectory();
try {
const destination = path.join(root, 'target');
const firstBackup = path.join(root, '.target.previous-123-456');
const secondBackup = path.join(root, '.target.previous-789-012');
fs.mkdirSync(firstBackup, { recursive: true });
fs.mkdirSync(secondBackup, { recursive: true });
assert.throws(
() => recoverInterruptedPromotion(destination),
/found 2 previous payloads/
);
assert.equal(fs.existsSync(destination), false);
assert.equal(fs.existsSync(firstBackup), true);
assert.equal(fs.existsSync(secondBackup), true);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('removes a temporary payload tree after it becomes disposable', async () => {
const root = temporaryDirectory();
try {
const temporary = path.join(root, 'temporary');
fs.mkdirSync(path.join(temporary, 'nested'), { recursive: true });
fs.writeFileSync(path.join(temporary, 'nested', 'archive'), 'payload');
await removePathWithRetry(temporary);
assert.equal(fs.existsSync(temporary), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('removes orphaned backups from dead provisioners after a successful publish', async () => {
const root = temporaryDirectory();
try {
const destination = path.join(root, 'target');
const orphanedBackup = path.join(root, '.target.previous-999999999-123456');
const staging = path.join(root, 'staging', 'payload');
fs.mkdirSync(destination, { recursive: true });
fs.writeFileSync(path.join(destination, 'engine'), 'old');
fs.mkdirSync(orphanedBackup, { recursive: true });
fs.writeFileSync(path.join(orphanedBackup, 'engine'), 'orphaned');
fs.mkdirSync(staging, { recursive: true });
fs.writeFileSync(path.join(staging, 'engine'), 'new');
await promoteDirectory(staging, destination);
assert.equal(fs.existsSync(orphanedBackup), false);
assert.equal(fs.readFileSync(path.join(destination, 'engine'), 'utf8'), 'new');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
+19 -11
View File
@@ -1,12 +1,16 @@
#!/usr/bin/env node
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { Readable, Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { collectRegularFiles, sha256 } from './engine-payload-integrity.js';
import {
promoteDirectory,
recoverInterruptedPromotion,
removePathWithRetry,
} from './engine-payload-promotion.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
@@ -34,7 +38,13 @@ if (!targetSources) {
}
const destination = path.join(repoRoot, 'src-tauri', 'provisioned-engines', target);
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), `firelink-engines-${target}-`));
const destinationParent = path.dirname(destination);
fs.mkdirSync(destinationParent, { recursive: true });
recoverInterruptedPromotion(destination);
// Keep staging on the destination filesystem so the final rename is atomic.
const temporary = fs.mkdtempSync(path.join(destinationParent, `.firelink-engines-${target}-`));
const payloadDestination = path.join(temporary, 'payload');
fs.mkdirSync(payloadDestination, { recursive: true });
const isWindows = target.includes('windows');
const executableSuffix = isWindows ? '.exe' : '';
const DOWNLOAD_ATTEMPTS = 3;
@@ -160,13 +170,13 @@ function findFile(root, names) {
}
function copyExecutable(source, engine) {
const output = path.join(destination, `${engine}-${target}${executableSuffix}`);
const output = path.join(payloadDestination, `${engine}-${target}${executableSuffix}`);
fs.copyFileSync(source, output);
if (!isWindows) fs.chmodSync(output, 0o755);
}
function writePayloadManifest() {
const files = collectRegularFiles(destination, {
const files = collectRegularFiles(payloadDestination, {
ignoredNames: ['payload-manifest.json'],
});
const manifest = {
@@ -184,27 +194,24 @@ function writePayloadManifest() {
),
files: Object.fromEntries(
files.map(file => [
path.relative(destination, file).split(path.sep).join('/'),
path.relative(payloadDestination, file).split(path.sep).join('/'),
sha256(file)
])
)
};
fs.writeFileSync(
path.join(destination, 'payload-manifest.json'),
path.join(payloadDestination, 'payload-manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`
);
}
try {
fs.rmSync(destination, { recursive: true, force: true });
fs.mkdirSync(destination, { recursive: true });
const ytdlp = await download('yt-dlp', targetSources['yt-dlp']);
copyExecutable(
findFile(ytdlp, isWindows ? ['yt-dlp.exe'] : ['yt-dlp_linux']),
'yt-dlp'
);
fs.cpSync(path.join(ytdlp, '_internal'), path.join(destination, '_internal'), {
fs.cpSync(path.join(ytdlp, '_internal'), path.join(payloadDestination, '_internal'), {
recursive: true,
preserveTimestamps: true
});
@@ -219,7 +226,8 @@ try {
copyExecutable(findFile(aria2, isWindows ? ['aria2c.exe'] : ['aria2c']), 'aria2c');
writePayloadManifest();
await promoteDirectory(payloadDestination, destination);
console.log(`Provisioned locked engine payload at ${destination}`);
} finally {
fs.rmSync(temporary, { recursive: true, force: true });
await removePathWithRetry(temporary);
}