fix(deps): refresh packages and harden engine staging

- update npm, Cargo, Deno, and FFmpeg lockfiles
- refresh verified cross-platform engine payload metadata
- serialize staging, verification, and Tauri packaging across concurrent runs
- recover interrupted lock records and cover crash and ownership races
This commit is contained in:
NimBold
2026-08-30 10:22:38 +03:30
parent 09ad412047
commit 1d629873b5
13 changed files with 655 additions and 107 deletions
+2
View File
@@ -46,6 +46,8 @@ target/
src-tauri/target/
src-tauri/gen/
src-tauri/engine-dist/
src-tauri/engine-dist.lock
src-tauri/engine-dist.lock.*
src-tauri/provisioned-engines/
# Locally provisioned native engines
+12 -12
View File
@@ -8,14 +8,14 @@
"sha256": "30b4c14aafab6082becff7881e41b76df46dc43ea7633479410a91e29da492bf"
},
"deno": {
"version": "2.9.5",
"url": "https://github.com/denoland/deno/releases/download/v2.9.5/deno-x86_64-pc-windows-msvc.zip",
"sha256": "171efab55ac6b9881fd53ee4c20f8bf3bb1340ffc618483746909014db12216a"
"version": "2.9.6",
"url": "https://github.com/denoland/deno/releases/download/v2.9.6/deno-x86_64-pc-windows-msvc.zip",
"sha256": "15e5300b0ba3c3695a7621d90160a746ec9e710228cee639afa9d580f6e3cd11"
},
"ffmpeg": {
"version": "8.1.2-46-g139afe709a",
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-26-13-06/ffmpeg-n8.1.2-46-g139afe709a-win64-gpl-8.1.zip",
"sha256": "f966bc2e843bcd680dedd6d1a2c0c895bab859a402c6dd107cbe72a796dfebcf"
"version": "8.1.2-50-g1a748fe2cd",
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-29-13-12/ffmpeg-n8.1.2-50-g1a748fe2cd-win64-gpl-8.1.zip",
"sha256": "3b56a6e46944cbe87692a4a72b29d95e527cbbd9925fc7beb07bb63d578cd5ae"
},
"aria2c": {
"version": "1.37.0",
@@ -30,14 +30,14 @@
"sha256": "32e72032766bef9199d99d15beb69fd52e46df8f8b06f0d8745db59e04d339e9"
},
"deno": {
"version": "2.9.5",
"url": "https://github.com/denoland/deno/releases/download/v2.9.5/deno-x86_64-unknown-linux-gnu.zip",
"sha256": "8b010a3b1a4a0188a67cdb8a7a27348b2a501af78aec7fc74f2ace167368d530"
"version": "2.9.6",
"url": "https://github.com/denoland/deno/releases/download/v2.9.6/deno-x86_64-unknown-linux-gnu.zip",
"sha256": "394f07f4da2bebe6ce6f1e7ce0fa16429b29b08c35e3fac3fe25972676dff4b2"
},
"ffmpeg": {
"version": "8.1.2-46-g139afe709a",
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-26-13-06/ffmpeg-n8.1.2-46-g139afe709a-linux64-gpl-8.1.tar.xz",
"sha256": "0814f4491c2673ea505be8fb65a76c2bfabaa5aad8f33d49b1c5b87a2262e8c5"
"version": "8.1.2-50-g1a748fe2cd",
"url": "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-08-29-13-12/ffmpeg-n8.1.2-50-g1a748fe2cd-linux64-gpl-8.1.tar.xz",
"sha256": "be243f2520d6e7e92c82b015ed72a3f5b57c7970801c0d51f031457122653c76"
},
"aria2c": {
"version": "1.37.0",
+2 -2
View File
@@ -23,10 +23,10 @@
"sha256": "734e6b72a0c2d0d5e089b5a0094be74fa058c15158f6b0689207a01fedafd8f5"
},
"deno": {
"version": "2.9.5",
"version": "2.9.6",
"source": "https://github.com/denoland/deno",
"build": "official aarch64-apple-darwin executable",
"sha256": "b5bd08edab254d42d7b05aa5b6cb4c9b8d4dede4975aff76951ce2cce18866fa"
"sha256": "b3ac3bd206e48c26026cadd80c1367e96c149f9c66130952382a642b09fa8a71"
}
},
"runtimeTrees": {
+8 -8
View File
@@ -25,7 +25,7 @@
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"i18next": "^26.4.0",
"lucide-react": "^1.34.0",
"lucide-react": "^1.37.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12",
@@ -35,7 +35,7 @@
"@tauri-apps/cli": "^2.11.4",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"@vitejs/plugin-react": "^6.1.1",
"autoprefixer": "^10.5.4",
"postcss": "^8.5.26",
"tailwindcss": "^4.3.3",
@@ -1416,9 +1416,9 @@
}
},
"node_modules/@vitejs/plugin-react": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz",
"integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
"integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2122,9 +2122,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.34.0.tgz",
"integrity": "sha512-vnjGJNI7Htk5+oWW8gXGuaLgwgAb0T6/iZbBrp9JCfRFwdNWZ0YTm3eyxjOLgwN6r8iyAf3UA70zNmBRBNv7yg==",
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.37.0.tgz",
"integrity": "sha512-LPsB4rD1TD6wZu1djKOf9vUnS1jTNaHbolXebXDgiTdb6jeA1agIJhJsIybCmjKmQClcOaal1o1OaiYahEftyQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+3 -3
View File
@@ -40,7 +40,7 @@
"test:torrent:rpc": "cd src-tauri && cargo test --test torrent_rpc -- --nocapture",
"verify:macos-signing": "node scripts/verify-macos-signing.js",
"preview": "vite preview",
"tauri": "tauri",
"tauri": "node scripts/tauri-command.js",
"test": "vitest"
},
"dependencies": {
@@ -60,7 +60,7 @@
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"i18next": "^26.4.0",
"lucide-react": "^1.34.0",
"lucide-react": "^1.37.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12",
@@ -70,7 +70,7 @@
"@tauri-apps/cli": "^2.11.4",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@vitejs/plugin-react": "^6.1.0",
"@vitejs/plugin-react": "^6.1.1",
"autoprefixer": "^10.5.4",
"postcss": "^8.5.26",
"tailwindcss": "^4.3.3",
+187
View File
@@ -0,0 +1,187 @@
import fs from 'node:fs';
import path from 'node:path';
const DEFAULT_TIMEOUT_MS = 60_000;
const RETRY_DELAYS_MS = [25, 50, 100, 250, 500];
const RETRYABLE_RENAME_ERRORS = new Set(['EACCES', 'EBUSY', 'EPERM']);
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function isProcessAlive(pid) {
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error?.code === 'EPERM';
}
}
function readLock(lockPath) {
let contents;
try {
contents = fs.readFileSync(lockPath, 'utf8');
} catch (error) {
if (error?.code === 'ENOENT') return null;
throw error;
}
try {
const owner = JSON.parse(contents);
if (
!Number.isSafeInteger(owner?.pid)
|| owner.pid <= 0
|| typeof owner.token !== 'string'
|| owner.token.length === 0
) {
return { malformed: true };
}
return { owner };
} catch {
return { malformed: true };
}
}
async function removeStaleLock(lockPath) {
const quarantinePath = `${lockPath}.stale-${process.pid}-${process.hrtime.bigint()}`;
for (let attempt = 0; ; attempt += 1) {
try {
// Rename is the compare-and-remove operation: another waiter cannot
// delete a newly acquired lock after this path has changed owners.
fs.renameSync(lockPath, quarantinePath);
break;
} catch (error) {
if (error?.code === 'ENOENT' || error?.code === 'EEXIST') return false;
if (!RETRYABLE_RENAME_ERRORS.has(error?.code) || attempt >= RETRY_DELAYS_MS.length) {
return false;
}
await sleep(RETRY_DELAYS_MS[attempt]);
}
}
try {
fs.unlinkSync(quarantinePath);
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
return true;
}
function releaseLock(lockPath, token) {
const currentLock = readLock(lockPath);
if (!currentLock) return;
if (currentLock.malformed) {
throw new Error(`Cannot release staging lock with an invalid owner record: ${lockPath}`);
}
const owner = currentLock.owner;
if (owner.token !== token || owner.pid !== process.pid) {
throw new Error(`Staging lock ownership changed before release: ${lockPath}`);
}
try {
fs.unlinkSync(lockPath);
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
}
function createLock(lockPath, owner) {
const temporaryPath = `${lockPath}.owner-${owner.token}`;
let temporaryCreated = false;
try {
// Write the complete record away from the contested path, then publish it
// with a hard link. Unlike rename, link never replaces an existing lock.
fs.writeFileSync(temporaryPath, `${JSON.stringify(owner)}\n`, {
encoding: 'utf8',
flag: 'wx',
mode: 0o600,
});
temporaryCreated = true;
fs.linkSync(temporaryPath, lockPath);
} finally {
if (temporaryCreated) {
try {
fs.unlinkSync(temporaryPath);
} catch (error) {
if (error?.code !== 'ENOENT') throw error;
}
}
}
}
export function readExclusiveFileLockOwner(lockPath) {
const currentLock = readLock(path.resolve(lockPath));
if (!currentLock?.owner) {
throw new Error(`Staging lock owner record is unavailable: ${path.resolve(lockPath)}`);
}
return currentLock.owner;
}
export function assertExclusiveFileLockHeld(lockPath, expectedOwner) {
if (
!Number.isSafeInteger(expectedOwner?.pid)
|| expectedOwner.pid <= 0
|| typeof expectedOwner.token !== 'string'
|| expectedOwner.token.length === 0
) {
throw new Error(`Invalid inherited staging lock owner: ${path.resolve(lockPath)}`);
}
const owner = readExclusiveFileLockOwner(lockPath);
if (
owner.pid !== expectedOwner.pid
|| owner.token !== expectedOwner.token
|| !isProcessAlive(owner.pid)
) {
throw new Error(`Inherited staging lock is no longer held: ${path.resolve(lockPath)}`);
}
}
/**
* Serializes operations that replace the shared engine-dist directory.
* Returns an idempotent release function after the caller owns the lock.
*/
export async function acquireExclusiveFileLock(lockPath, options = {}) {
const resolvedPath = path.resolve(lockPath);
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const startedAt = Date.now();
const token = `${process.pid}-${process.hrtime.bigint()}`;
const owner = {
pid: process.pid,
token,
startedAt: new Date().toISOString(),
};
for (let attempt = 0; ; attempt += 1) {
try {
createLock(resolvedPath, owner);
let released = false;
return () => {
if (released) return;
releaseLock(resolvedPath, token);
released = true;
};
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
const currentLock = readLock(resolvedPath);
if (currentLock?.malformed) {
if (await removeStaleLock(resolvedPath)) continue;
} else if (currentLock?.owner && !isProcessAlive(currentLock.owner.pid)) {
if (await removeStaleLock(resolvedPath)) continue;
}
if (Date.now() - startedAt >= timeoutMs) {
const ownerDescription = currentLock?.owner?.pid
? ` owned by PID ${currentLock.owner.pid}`
: ' with an unreadable owner record';
throw new Error(`Timed out waiting for staging lock ${resolvedPath}${ownerDescription}`);
}
const delay = RETRY_DELAYS_MS[Math.min(attempt, RETRY_DELAYS_MS.length - 1)];
await sleep(delay);
}
}
}
+186
View File
@@ -0,0 +1,186 @@
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { pathToFileURL } from 'node:url';
import {
acquireExclusiveFileLock,
assertExclusiveFileLockHeld,
readExclusiveFileLockOwner,
} from './engine-staging-lock.js';
const childOutputStates = new WeakMap();
function temporaryLockPath() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-stage-lock-'));
return path.join(directory, 'engine-dist.lock');
}
function waitForOutput(child, expected) {
const state = childOutputStates.get(child);
assert.ok(state, 'child output tracking must be installed before waiting');
if (state.output.includes(expected)) return Promise.resolve(state.output);
if (state.error) return Promise.reject(state.error);
if (state.exit) {
return Promise.reject(new Error(
`lock child exited before '${expected}' (code=${state.exit.code}, signal=${state.exit.signal})`,
));
}
return new Promise((resolve, reject) => {
state.waiters.push({ expected, resolve, reject });
});
}
function trackChildOutput(child) {
const state = { error: null, exit: null, output: '', waiters: [] };
childOutputStates.set(child, state);
child.stdout.setEncoding('utf8');
child.stdout.on('data', chunk => {
state.output += chunk;
for (const waiter of state.waiters.splice(0)) {
if (state.output.includes(waiter.expected)) {
waiter.resolve(state.output);
} else {
state.waiters.push(waiter);
}
}
});
child.once('error', error => {
state.error = error;
for (const waiter of state.waiters.splice(0)) waiter.reject(error);
});
child.once('exit', (code, signal) => {
state.exit = { code, signal };
const error = new Error(`lock child exited before a requested output (code=${code}, signal=${signal})`);
for (const waiter of state.waiters.splice(0)) waiter.reject(error);
});
}
function spawnLockChild(lockPath) {
const moduleUrl = pathToFileURL(path.resolve('scripts/engine-staging-lock.js')).href;
const source = `
import { acquireExclusiveFileLock } from ${JSON.stringify(moduleUrl)};
process.stdout.write('started\\n');
const release = await acquireExclusiveFileLock(process.argv[1], { timeoutMs: 5_000 });
process.stdout.write('acquired\\n');
process.stdin.once('data', () => {
release();
process.exit(0);
});
process.stdin.resume();
`;
const child = spawn(process.execPath, ['--input-type=module', '-e', source, lockPath], {
stdio: ['pipe', 'pipe', 'pipe'],
});
trackChildOutput(child);
return child;
}
test('exclusive staging lock waits for a live owner and releases idempotently', async () => {
const lockPath = temporaryLockPath();
const releaseFirst = await acquireExclusiveFileLock(lockPath, { timeoutMs: 1_000 });
const secondAcquisition = acquireExclusiveFileLock(lockPath, {
timeoutMs: 1_000,
});
assert.equal(fs.existsSync(lockPath), true);
releaseFirst();
releaseFirst();
const releaseSecond = await secondAcquisition;
assert.equal(fs.existsSync(lockPath), true);
releaseSecond();
assert.equal(fs.existsSync(lockPath), false);
});
test('exclusive staging lock recovers empty and malformed legacy records', async () => {
for (const contents of ['', '{not-json']) {
const lockPath = temporaryLockPath();
fs.writeFileSync(lockPath, contents);
const release = await acquireExclusiveFileLock(lockPath, { timeoutMs: 1_000 });
const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
assert.equal(owner.pid, process.pid);
release();
assert.equal(fs.existsSync(lockPath), false);
}
});
test('exclusive staging lock serializes separate processes', async () => {
const lockPath = temporaryLockPath();
const first = spawnLockChild(lockPath);
let second;
try {
await waitForOutput(first, 'acquired\n');
second = spawnLockChild(lockPath);
await waitForOutput(second, 'started\n');
const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
assert.equal(owner.pid, first.pid);
first.stdin.write('\n');
await once(first, 'exit');
await waitForOutput(second, 'acquired\n');
second.stdin.write('\n');
await once(second, 'exit');
} finally {
for (const child of [first, second]) {
if (child && child.exitCode === null && child.signalCode === null) child.kill();
}
}
});
test('exclusive staging lock recovers after an owner is force-killed', async () => {
const lockPath = temporaryLockPath();
const owner = spawnLockChild(lockPath);
try {
await waitForOutput(owner, 'acquired\n');
assert.equal(owner.kill('SIGKILL'), true);
await once(owner, 'exit');
const release = await acquireExclusiveFileLock(lockPath, { timeoutMs: 1_000 });
const replacement = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
assert.equal(replacement.pid, process.pid);
release();
assert.equal(fs.existsSync(lockPath), false);
} finally {
if (owner.exitCode === null && owner.signalCode === null) owner.kill();
}
});
test('exclusive staging lock recovers a lock owned by a dead process', async () => {
const lockPath = temporaryLockPath();
fs.writeFileSync(lockPath, JSON.stringify({
pid: 999_999_999,
token: 'dead-owner',
}));
const release = await acquireExclusiveFileLock(lockPath, { timeoutMs: 1_000 });
const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
assert.equal(owner.pid, process.pid);
assert.notEqual(owner.token, 'dead-owner');
release();
assert.equal(fs.existsSync(lockPath), false);
});
test('staging lock release fails closed when ownership changes', async () => {
const lockPath = temporaryLockPath();
const release = await acquireExclusiveFileLock(lockPath, { timeoutMs: 1_000 });
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, token: 'other-owner' }));
assert.throws(() => release(), /ownership changed/);
fs.unlinkSync(lockPath);
});
test('inherited staging lock validation rejects a released lease', async () => {
const lockPath = temporaryLockPath();
const release = await acquireExclusiveFileLock(lockPath, { timeoutMs: 1_000 });
const owner = readExclusiveFileLockOwner(lockPath);
assert.doesNotThrow(() => assertExclusiveFileLockHeld(lockPath, owner));
release();
assert.throws(() => assertExclusiveFileLockHeld(lockPath, owner), /owner record is unavailable/);
});
+43 -20
View File
@@ -4,6 +4,10 @@ import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { collectRegularFiles, sha256, treeDigest } from './engine-payload-integrity.js';
import {
acquireExclusiveFileLock,
assertExclusiveFileLockHeld,
} from './engine-staging-lock.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
@@ -104,29 +108,48 @@ if (targetLock) {
}
const destination = path.join(outputRoot, target);
fs.rmSync(outputRoot, { recursive: true, force: true });
fs.mkdirSync(destination, { recursive: true });
for (const name of expectedNames) {
fs.copyFileSync(path.join(source, name), path.join(destination, name));
if (!isWindowsTarget) {
fs.chmodSync(path.join(destination, name), 0o755);
}
const stagingLockPath = `${outputRoot}.lock`;
const inheritedLockPid = process.env.FIRELINK_ENGINE_STAGING_LOCK_PID;
const inheritedLockToken = process.env.FIRELINK_ENGINE_STAGING_LOCK_TOKEN;
const inheritedStagingLock = inheritedLockPid !== undefined || inheritedLockToken !== undefined;
if (inheritedStagingLock) {
assertExclusiveFileLockHeld(stagingLockPath, {
pid: Number(inheritedLockPid),
token: inheritedLockToken,
});
}
const releaseStageLock = inheritedStagingLock
? null
: await acquireExclusiveFileLock(stagingLockPath);
try {
// Tauri packages the shared engine-dist root. Keep exactly one target in it;
// the lock serializes this replacement when local stage commands overlap.
fs.rmSync(outputRoot, { recursive: true, force: true });
fs.mkdirSync(destination, { recursive: true });
for (const runtimeDir of ['_internal', 'aria2-libs']) {
const sourceDir = path.join(source, runtimeDir);
if (fs.existsSync(sourceDir)) {
fs.cpSync(sourceDir, path.join(destination, runtimeDir), {
recursive: true,
dereference: false,
preserveTimestamps: true,
});
for (const name of expectedNames) {
fs.copyFileSync(path.join(source, name), path.join(destination, name));
if (!isWindowsTarget) {
fs.chmodSync(path.join(destination, name), 0o755);
}
}
}
const payloadManifest = path.join(source, 'payload-manifest.json');
if (fs.existsSync(payloadManifest)) {
fs.copyFileSync(payloadManifest, path.join(destination, 'payload-manifest.json'));
for (const runtimeDir of ['_internal', 'aria2-libs']) {
const sourceDir = path.join(source, runtimeDir);
if (fs.existsSync(sourceDir)) {
fs.cpSync(sourceDir, path.join(destination, runtimeDir), {
recursive: true,
dereference: false,
preserveTimestamps: true,
});
}
}
const payloadManifest = path.join(source, 'payload-manifest.json');
if (fs.existsSync(payloadManifest)) {
fs.copyFileSync(payloadManifest, path.join(destination, 'payload-manifest.json'));
}
} finally {
releaseStageLock?.();
}
console.log(`Staged Firelink engines for ${target} from ${source}`);
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env node
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import {
acquireExclusiveFileLock,
readExclusiveFileLockOwner,
} from './engine-staging-lock.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
const tauriCli = path.join(repoRoot, 'node_modules', '@tauri-apps', 'cli', 'tauri.js');
const stagingLockPath = path.join(repoRoot, 'src-tauri', 'engine-dist.lock');
const ENGINE_TREE_COMMANDS = new Set(['dev', 'build', 'bundle']);
export function commandUsesEngineTree(args) {
return args.some(argument => ENGINE_TREE_COMMANDS.has(argument));
}
function signalExitCode(signal) {
return {
SIGHUP: 129,
SIGINT: 130,
SIGTERM: 143,
}[signal] ?? 1;
}
const args = process.argv.slice(2);
const holdsEngineLock = commandUsesEngineTree(args);
let releaseEngineLock;
let child;
function handleSignal(signal) {
if (child && child.exitCode === null && child.signalCode === null) {
try {
child.kill(signal);
} catch (error) {
if (error?.code !== 'ESRCH') {
console.error(`[WARN] Could not terminate Tauri: ${error.message}`);
}
}
return;
}
// No child has started yet, so there is no packaging work to preserve. If
// lock acquisition was interrupted after ownership was granted, release it
// before exiting; otherwise exit promptly instead of waiting out the lock.
try {
releaseEngineLock?.();
} catch (error) {
console.error(`[WARN] Could not release the engine staging lock: ${error.message}`);
}
process.exit(signalExitCode(signal));
}
async function run() {
const handlers = new Map(['SIGHUP', 'SIGINT', 'SIGTERM'].map(signal => [
signal,
() => handleSignal(signal),
]));
for (const [signal, handler] of handlers) process.once(signal, handler);
try {
const env = { ...process.env };
if (holdsEngineLock) {
releaseEngineLock = await acquireExclusiveFileLock(stagingLockPath);
const owner = readExclusiveFileLockOwner(stagingLockPath);
// beforeBuildCommand and beforeDevCommand run as child processes. The
// wrapper keeps the lock until Tauri has finished consuming resources;
// nested staging/verifying must therefore use this held lease instead
// of trying to acquire the same path again.
env.FIRELINK_ENGINE_STAGING_LOCK_PID = String(owner.pid);
env.FIRELINK_ENGINE_STAGING_LOCK_TOKEN = owner.token;
}
child = spawn(process.execPath, [tauriCli, ...args], {
cwd: repoRoot,
env,
stdio: 'inherit',
windowsHide: true,
});
const result = await new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (code, signal) => resolve({ code, signal }));
});
if (result.signal) {
process.exitCode = signalExitCode(result.signal);
} else {
process.exitCode = result.code ?? 1;
}
} finally {
for (const [signal, handler] of handlers) process.removeListener(signal, handler);
releaseEngineLock?.();
}
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
run().catch(error => {
console.error(`[FAIL] Tauri command failed: ${error.message}`);
process.exitCode = 1;
});
}
+11
View File
@@ -0,0 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { commandUsesEngineTree } from './tauri-command.js';
test('Tauri engine-consuming commands hold the shared staging lease', () => {
assert.equal(commandUsesEngineTree(['dev']), true);
assert.equal(commandUsesEngineTree(['build', '--target', 'x86_64-unknown-linux-gnu']), true);
assert.equal(commandUsesEngineTree(['bundle', '--bundles', 'appimage']), true);
assert.equal(commandUsesEngineTree(['info']), false);
assert.equal(commandUsesEngineTree(['--help']), false);
});
+20
View File
@@ -5,6 +5,10 @@ import os from 'node:os';
import net from 'node:net';
import { execFileSync, spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import {
acquireExclusiveFileLock,
assertExclusiveFileLockHeld,
} from './engine-staging-lock.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -89,6 +93,20 @@ const binariesDir = configuredRoot
? path.resolve(configuredRoot)
: path.join(scriptsDir, '..', 'src-tauri', 'binaries');
const requiredEngines = ['yt-dlp', 'aria2c', 'ffmpeg', 'deno'];
const stagedVerification = process.argv.includes('--staged');
const stagingLockPath = path.join(scriptsDir, '..', 'src-tauri', 'engine-dist.lock');
const inheritedLockPid = process.env.FIRELINK_ENGINE_STAGING_LOCK_PID;
const inheritedLockToken = process.env.FIRELINK_ENGINE_STAGING_LOCK_TOKEN;
const inheritedStagingLock = inheritedLockPid !== undefined || inheritedLockToken !== undefined;
if (stagedVerification && inheritedStagingLock) {
assertExclusiveFileLockHeld(stagingLockPath, {
pid: Number(inheritedLockPid),
token: inheritedLockToken,
});
}
const releaseStagingLock = stagedVerification && !inheritedStagingLock
? await acquireExclusiveFileLock(stagingLockPath)
: null;
const FORBIDDEN_OTOOL_PATHS = ['/opt/homebrew', '/usr/local/Cellar'];
const FORBIDDEN_STDERR = [
@@ -171,6 +189,7 @@ for (const eng of requiredEngines) {
if (exitCode !== 0) {
console.error('\nAborting: missing required sidecars.');
releaseStagingLock?.();
process.exit(1);
}
@@ -583,6 +602,7 @@ if (canExecuteTarget) {
// ───── Result ─────
console.log('');
releaseStagingLock?.();
if (exitCode !== 0) {
console.error(`[FAIL] ${exitCode} engine verification check(s) failed.`);
process.exit(1);
+75 -62
View File
@@ -10,13 +10,13 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
version = "0.8.4"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures 0.2.17",
"cpubits",
"cpufeatures 0.3.1",
]
[[package]]
@@ -396,11 +396,11 @@ dependencies = [
[[package]]
name = "block-padding"
version = "0.3.3"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
dependencies = [
"generic-array",
"hybrid-array",
]
[[package]]
@@ -557,9 +557,9 @@ dependencies = [
[[package]]
name = "cbc"
version = "0.1.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
dependencies = [
"cipher",
]
@@ -609,12 +609,12 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"cpufeatures 0.3.1",
"rand_core 0.10.1",
]
@@ -634,11 +634,11 @@ dependencies = [
[[package]]
name = "cipher"
version = "0.4.4"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
dependencies = [
"crypto-common 0.1.7",
"crypto-common 0.2.2",
"inout",
]
@@ -752,6 +752,12 @@ dependencies = [
"libc",
]
[[package]]
name = "cpubits"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -763,9 +769,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
@@ -1053,7 +1059,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common 0.1.7",
"subtle",
]
[[package]]
@@ -1403,7 +1408,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"futures-util",
"hmac 0.13.0",
"hmac",
"keepawake",
"keyring-core",
"log 0.4.34",
@@ -1453,12 +1458,13 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.9"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.9.1",
"zlib-rs",
]
[[package]]
@@ -2001,20 +2007,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
dependencies = [
"hmac 0.12.1",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
"hmac",
]
[[package]]
@@ -2092,9 +2089,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
@@ -2320,9 +2317,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
@@ -2341,12 +2338,12 @@ dependencies = [
[[package]]
name = "inout"
version = "0.1.4"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
dependencies = [
"block-padding",
"generic-array",
"hybrid-array",
]
[[package]]
@@ -2677,9 +2674,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.20"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96"
dependencies = [
"libc",
]
@@ -2811,6 +2808,16 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.2.2"
@@ -3403,7 +3410,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap 2.14.0",
"indexmap 2.14.1",
]
[[package]]
@@ -3489,7 +3496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
dependencies = [
"base64 0.22.1",
"indexmap 2.14.0",
"indexmap 2.14.1",
"quick-xml",
"serde",
"time",
@@ -3505,7 +3512,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
"miniz_oxide 0.8.9",
]
[[package]]
@@ -3518,7 +3525,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
"miniz_oxide 0.8.9",
]
[[package]]
@@ -4135,20 +4142,20 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "secret-service"
version = "5.1.0"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
checksum = "5107b24b91445dd2aa449a258a1807b63240942157292354dc5bfdbeb8bc6db8"
dependencies = [
"aes",
"cbc",
"futures-util",
"generic-array",
"getrandom 0.2.17",
"getrandom 0.4.3",
"hkdf",
"hybrid-array",
"num",
"once_cell",
"serde",
"sha2 0.10.9",
"sha2 0.11.0",
"zbus",
]
@@ -4333,7 +4340,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.14.0",
"indexmap 2.14.1",
"jiff",
"schemars 0.9.0",
"schemars 1.2.2",
@@ -4404,7 +4411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"cpufeatures 0.3.1",
"digest 0.11.3",
]
@@ -4426,7 +4433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"cpufeatures 0.3.1",
"digest 0.11.3",
]
@@ -5459,7 +5466,7 @@ version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"indexmap 2.14.0",
"indexmap 2.14.1",
"serde_core",
"serde_spanned 1.1.1",
"toml_datetime 0.7.5+spec-1.1.0",
@@ -5474,7 +5481,7 @@ version = "1.1.4+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
dependencies = [
"indexmap 2.14.0",
"indexmap 2.14.1",
"serde_core",
"serde_spanned 1.1.1",
"toml_datetime 1.1.1+spec-1.1.0",
@@ -5516,7 +5523,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.14.0",
"indexmap 2.14.1",
"toml_datetime 0.6.3",
"winnow 0.5.40",
]
@@ -5527,7 +5534,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.14.0",
"indexmap 2.14.1",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.3",
@@ -5540,7 +5547,7 @@ version = "0.25.13+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
dependencies = [
"indexmap 2.14.0",
"indexmap 2.14.1",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.4",
@@ -5894,9 +5901,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.25.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -7132,6 +7139,12 @@ dependencies = [
"syn 3.0.4",
]
[[package]]
name = "zlib-rs"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]]
name = "zmij"
version = "1.0.23"
Binary file not shown.