mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
feat(native): add prebuilt Node-API flock support
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
/** Load the private callback API to test native completion independently of its Promise wrapper. */
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/** @returns The built addon's private callback binding for this host. */
|
||||
export function loadFlockBinding() {
|
||||
const libc = process.platform === 'linux'
|
||||
? `${process.report.getReport().header.glibcVersionRuntime ? 'glibc' : 'musl'}/`
|
||||
: '';
|
||||
const binary = new URL(`../../packages/${process.platform}-${process.arch}/bin/${libc}system.node`, import.meta.url);
|
||||
return createRequire(import.meta.url)(fileURLToPath(binary));
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Isolate an uncaught native-callback exception from the test runner. */
|
||||
import { loadFlockBinding } from './flock-binding.js';
|
||||
|
||||
const binding = loadFlockBinding();
|
||||
process.send({ type: 'ready' });
|
||||
binding.tryLock(-1, () => { throw new Error('flock callback failure'); });
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/** IPC-controlled lock holder; acknowledgements follow settled syscalls or close. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { once, on } from 'node:events';
|
||||
import { closeSync, openSync } from 'node:fs';
|
||||
import { tryLockExclusive } from '../../packages/entry/lib/flock.js';
|
||||
|
||||
const messages = on(process, 'message');
|
||||
let fd = openSync(process.argv[2], 'a+', 0o600);
|
||||
try {
|
||||
await send({ type: 'ready' });
|
||||
for await (const [command] of messages) {
|
||||
if (command === 'close') {
|
||||
closeSync(fd);
|
||||
fd = undefined;
|
||||
await send({ type: 'closed' });
|
||||
break;
|
||||
}
|
||||
assert.equal(command, 'tryLock');
|
||||
let reply;
|
||||
try {
|
||||
await tryLockExclusive(fd);
|
||||
reply = { type: 'locked' };
|
||||
} catch (error) {
|
||||
reply = { type: 'error', code: error.code, errno: error.errno, syscall: error.syscall };
|
||||
}
|
||||
await send(reply);
|
||||
}
|
||||
} finally {
|
||||
await messages.return();
|
||||
if (fd !== undefined) closeSync(fd);
|
||||
if (process.connected) {
|
||||
const disconnected = once(process, 'disconnect');
|
||||
process.disconnect();
|
||||
await disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.send(message, (error) => error ? reject(error) : resolve());
|
||||
});
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/** A separate process keeps unsupported-platform simulation away from other tests. */
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const platform = process.argv[2];
|
||||
const descriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||
try {
|
||||
if (platform) Object.defineProperty(process, 'platform', { value: platform });
|
||||
const { tryLockExclusive } = await import('../../packages/entry/lib/flock.js');
|
||||
assert.equal(typeof tryLockExclusive, 'function');
|
||||
if (platform || (process.platform !== 'linux' && process.platform !== 'darwin')) {
|
||||
await assert.rejects(tryLockExclusive(-1), {
|
||||
code: 'ERR_FLOCK_UNSUPPORTED_PLATFORM',
|
||||
syscall: 'flock',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', descriptor);
|
||||
}
|
||||
await new Promise((resolve, reject) => {
|
||||
process.send({ type: 'ready' }, (error) => error ? reject(error) : resolve());
|
||||
});
|
||||
process.disconnect();
|
||||
@@ -0,0 +1,37 @@
|
||||
/** fd 4 is inherited through spawn's stdio mapping, never reopened by path. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { on } from 'node:events';
|
||||
import { closeSync, fstatSync } from 'node:fs';
|
||||
import { tryLockExclusive } from '../../packages/entry/lib/flock.js';
|
||||
|
||||
const messages = on(process, 'message');
|
||||
let fd = 4;
|
||||
try {
|
||||
assert.ok(fstatSync(fd).isFile());
|
||||
await send({ type: 'ready' });
|
||||
for await (const [command] of messages) {
|
||||
if (command === 'quit') {
|
||||
await send({ type: 'bye' });
|
||||
break;
|
||||
}
|
||||
if (command === 'close') {
|
||||
closeSync(fd);
|
||||
fd = undefined;
|
||||
await send({ type: 'closed' });
|
||||
continue;
|
||||
}
|
||||
assert.equal(command, 'tryLock');
|
||||
await tryLockExclusive(fd);
|
||||
await send({ type: 'locked' });
|
||||
}
|
||||
} finally {
|
||||
await messages.return();
|
||||
if (fd !== undefined) closeSync(fd);
|
||||
if (process.connected) process.disconnect();
|
||||
}
|
||||
|
||||
function send(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.send(message, (error) => error ? reject(error) : resolve());
|
||||
});
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/** Ordinary file I/O from a process that never acquires a lock. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const request = once(process, 'message');
|
||||
await send({ type: 'ready' });
|
||||
const [command] = await request;
|
||||
assert.equal(command, 'read-write');
|
||||
const previous = readFileSync(process.argv[2], 'utf8');
|
||||
writeFileSync(process.argv[2], 'written without acquiring a lock');
|
||||
await send({ type: 'written', previous });
|
||||
process.disconnect();
|
||||
|
||||
function send(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.send(message, (error) => error ? reject(error) : resolve());
|
||||
});
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/* Independent system flock(2) oracle; stdin commands produce flushed JSON lines. */
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int reply(int fd, int operation) {
|
||||
const int result = flock(fd, operation);
|
||||
const int error = result == 0 ? 0 : errno;
|
||||
if (printf("{\"errno\":%d}\n", error) < 0 || fflush(stdout) == EOF) {
|
||||
perror("flock-oracle: stdout");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int operation = LOCK_EX;
|
||||
if (argc < 2 || argc > 3) {
|
||||
fprintf(stderr, "usage: flock-oracle <path> [exclusive|shared]\n");
|
||||
return 1;
|
||||
}
|
||||
if (argc == 3) {
|
||||
if (strcmp(argv[2], "shared") == 0) {
|
||||
operation = LOCK_SH;
|
||||
} else if (strcmp(argv[2], "exclusive") != 0) {
|
||||
fprintf(stderr, "flock-oracle: mode must be exclusive or shared\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const int fd = open(argv[1], O_RDWR | O_CREAT, 0600);
|
||||
if (fd == -1) {
|
||||
perror("flock-oracle: open");
|
||||
return 1;
|
||||
}
|
||||
int status = 0;
|
||||
if (puts("{\"ready\":true}") == EOF || fflush(stdout) == EOF) {
|
||||
perror("flock-oracle: stdout");
|
||||
status = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
char command[3];
|
||||
while (fgets(command, sizeof(command), stdin) != NULL) {
|
||||
if (command[1] != '\n') {
|
||||
fprintf(stderr, "flock-oracle: commands must be one letter followed by a newline\n");
|
||||
status = 1;
|
||||
break;
|
||||
}
|
||||
if (command[0] == 'q') break;
|
||||
if (command[0] != 't' && command[0] != 'u') {
|
||||
fprintf(stderr, "flock-oracle: expected t, u, or q\n");
|
||||
status = 1;
|
||||
break;
|
||||
}
|
||||
if (reply(fd, command[0] == 't' ? operation | LOCK_NB : LOCK_UN) != 0) {
|
||||
status = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ferror(stdin)) {
|
||||
perror("flock-oracle: stdin");
|
||||
status = 1;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (close(fd) == -1) {
|
||||
perror("flock-oracle: close");
|
||||
status = 1;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/** Hold work before or inside its callback while the parent terminates this environment. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHook } from 'node:async_hooks';
|
||||
import { parentPort, workerData } from 'node:worker_threads';
|
||||
import { tryLockExclusive } from '../../packages/entry/lib/flock.js';
|
||||
import { loadFlockBinding } from './flock-binding.js';
|
||||
|
||||
let nativeWork = 0;
|
||||
const hook = createHook({
|
||||
init(_id, type) {
|
||||
if (type === 'flock') nativeWork++;
|
||||
},
|
||||
});
|
||||
hook.enable();
|
||||
try {
|
||||
if (workerData.phase === 'callback') {
|
||||
loadFlockBinding().tryLock(workerData.fd, () => {
|
||||
parentPort.postMessage({ type: 'callback', nativeWork });
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0);
|
||||
});
|
||||
} else {
|
||||
const pending = tryLockExclusive(workerData.fd);
|
||||
assert.equal(nativeWork, 1);
|
||||
parentPort.postMessage({ type: 'queued', nativeWork });
|
||||
// No JS yield precedes this wait, so the native completion cannot run first.
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0);
|
||||
await pending;
|
||||
}
|
||||
} finally {
|
||||
hook.disable();
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
/** Kernel behavior through the built flock entry; each case owns its files and processes. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { fork, spawn } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { constants, tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createInterface } from 'node:readline';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import { tryLockExclusive } from '../packages/entry/lib/flock.js';
|
||||
import { loadFlockBinding } from './fixtures/flock-binding.js';
|
||||
|
||||
const posix = process.platform === 'linux' || process.platform === 'darwin';
|
||||
const timeout = 120_000;
|
||||
const nativeOnly = { timeout, skip: posix ? false : 'The flock addon requires Linux or macOS' };
|
||||
|
||||
function resources(t) {
|
||||
const disposers = [];
|
||||
// Match the repository's process-e2e budget for both cases and cleanup.
|
||||
t.after(async () => {
|
||||
// Cleanup must await close even when the case's signal is already aborted.
|
||||
const signal = AbortSignal.timeout(timeout);
|
||||
const errors = [];
|
||||
for (const dispose of disposers.reverse()) {
|
||||
try {
|
||||
await dispose(signal);
|
||||
} catch (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
if (errors.length) throw new AggregateError(errors, 'flock test cleanup failed');
|
||||
}, { timeout });
|
||||
// On local Linux filesystems, flock and OFD byte-range locks are independent;
|
||||
// network filesystems can translate between them and hide a wrong syscall.
|
||||
const root = mkdtempSync(join(tmpdir(), 'node-addon-system-flock-'));
|
||||
disposers.push(() => rmSync(root, { recursive: true, force: true }));
|
||||
return {
|
||||
file: join(root, 'lock'),
|
||||
defer: (dispose) => disposers.push(dispose),
|
||||
open(name = 'lock') {
|
||||
const fd = openSync(join(root, name), 'a+', 0o600);
|
||||
let closed = false;
|
||||
const close = () => {
|
||||
if (!closed) {
|
||||
closeSync(fd);
|
||||
closed = true;
|
||||
}
|
||||
};
|
||||
disposers.push(close);
|
||||
return { fd, close };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function flockError(error, codes) {
|
||||
assert.ok(codes.includes(error.code), `Unexpected flock error: ${error.code}`);
|
||||
assert.equal(error.errno, constants.errno[error.code]);
|
||||
assert.ok(error.errno > 0);
|
||||
assert.equal(error.syscall, 'flock');
|
||||
return true;
|
||||
}
|
||||
|
||||
const busy = (error) => flockError(error, ['EAGAIN', 'EWOULDBLOCK']);
|
||||
|
||||
async function until(promise, signal) {
|
||||
let abort;
|
||||
const cancelled = new Promise((_, reject) => {
|
||||
abort = () => reject(signal.reason);
|
||||
if (signal.aborted) abort();
|
||||
else signal.addEventListener('abort', abort, { once: true });
|
||||
});
|
||||
try {
|
||||
return await Promise.race([promise, cancelled]);
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abort);
|
||||
}
|
||||
}
|
||||
|
||||
function childEnvironment() {
|
||||
return Object.fromEntries(Object.entries(process.env)
|
||||
.filter(([name]) => !/KEY|SECRET|TOKEN|PASSWORD/i.test(name)));
|
||||
}
|
||||
|
||||
function observeChild(t, scope, child) {
|
||||
let closed = false;
|
||||
let stderr = '';
|
||||
let processError;
|
||||
const done = new Promise((resolve) => child.once('close', (code, signal) => {
|
||||
closed = true;
|
||||
resolve({ code, signal, stderr, error: processError });
|
||||
}));
|
||||
scope.defer(async (signal) => {
|
||||
if (!closed) child.kill('SIGKILL');
|
||||
await until(done, signal);
|
||||
});
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
child.on('error', (error) => { processError = error; });
|
||||
|
||||
async function response(emitter, event, send = () => Promise.resolve()) {
|
||||
const waiting = new AbortController();
|
||||
const signal = AbortSignal.any([t.signal, waiting.signal]);
|
||||
try {
|
||||
const received = Promise.race([
|
||||
once(emitter, event, { signal }).then(([message]) => message),
|
||||
done.then((result) => {
|
||||
throw new Error(`flock fixture exited before replying: ${JSON.stringify(result)}`);
|
||||
}),
|
||||
]);
|
||||
const [message] = await until(Promise.all([received, send()]), signal);
|
||||
return message;
|
||||
} finally {
|
||||
waiting.abort();
|
||||
}
|
||||
}
|
||||
|
||||
return { child, response, waitForExit: () => until(done, t.signal) };
|
||||
}
|
||||
|
||||
async function childFixture(t, scope, fixture, args = [], { execArgv = [], inheritedFd } = {}) {
|
||||
const stdio = ['ignore', 'ignore', 'pipe', 'ipc'];
|
||||
if (inheritedFd !== undefined) stdio.push(inheritedFd);
|
||||
const child = fork(new URL(`./fixtures/${fixture}`, import.meta.url), args, {
|
||||
execArgv,
|
||||
env: childEnvironment(),
|
||||
stdio,
|
||||
});
|
||||
const observed = observeChild(t, scope, child);
|
||||
const exchange = (command) => observed.response(child, 'message', () => (
|
||||
command === undefined ? Promise.resolve() : new Promise((resolve, reject) => {
|
||||
child.send(command, (error) => error ? reject(error) : resolve());
|
||||
})
|
||||
));
|
||||
assert.deepEqual(await exchange(), { type: 'ready' });
|
||||
return { child, waitForExit: observed.waitForExit, exchange };
|
||||
}
|
||||
|
||||
async function oracleFixture(t, scope, mode) {
|
||||
const binary = process.platform === 'linux'
|
||||
? `./bin/${process.report.getReport().header.glibcVersionRuntime ? 'glibc' : 'musl'}/flock-oracle`
|
||||
: './bin/flock-oracle';
|
||||
const child = spawn(fileURLToPath(new URL(binary, import.meta.url)), [scope.file, mode], {
|
||||
env: childEnvironment(),
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
const observed = observeChild(t, scope, child);
|
||||
const lines = createInterface({ input: child.stdout });
|
||||
child.once('close', () => lines.close());
|
||||
let inputError;
|
||||
child.stdin.on('error', (error) => { inputError = error; });
|
||||
const send = (command) => until(new Promise((resolve, reject) => {
|
||||
if (inputError) reject(inputError);
|
||||
else child.stdin.write(`${command}\n`, (error) => error ? reject(error) : resolve());
|
||||
}), t.signal);
|
||||
const exchange = async (command) => JSON.parse(await observed.response(lines, 'line', () => (
|
||||
command === undefined ? Promise.resolve() : send(command)
|
||||
)));
|
||||
assert.deepEqual(await exchange(), { ready: true });
|
||||
return {
|
||||
exchange,
|
||||
async quit() {
|
||||
await send('q');
|
||||
cleanExit(await observed.waitForExit());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cleanExit(result) {
|
||||
assert.equal(result.signal, null, result.stderr);
|
||||
assert.equal(result.error, undefined);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
}
|
||||
|
||||
test('import succeeds with native addons disabled', { timeout }, async (t) => {
|
||||
const scope = resources(t);
|
||||
const child = await childFixture(t, scope, 'flock-import.js', [], { execArgv: ['--no-addons'] });
|
||||
cleanExit(await child.waitForExit());
|
||||
});
|
||||
|
||||
for (const platform of ['win32', 'freebsd']) {
|
||||
test(`calling flock on ${platform} rejects without loading an addon`, { timeout }, async (t) => {
|
||||
const scope = resources(t);
|
||||
const child = await childFixture(t, scope, 'flock-import.js', [platform], { execArgv: ['--no-addons'] });
|
||||
cleanExit(await child.waitForExit());
|
||||
});
|
||||
}
|
||||
|
||||
test('acquisition resolves asynchronously to void and the same fd can reacquire', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const result = tryLockExclusive(owner.fd);
|
||||
assert.ok(result instanceof Promise);
|
||||
assert.equal(await result, undefined);
|
||||
assert.equal(await tryLockExclusive(owner.fd), undefined);
|
||||
});
|
||||
|
||||
test('separate opens of one file contend', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
});
|
||||
|
||||
test('different files can be locked concurrently', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const first = scope.open('first');
|
||||
const second = scope.open('second');
|
||||
await Promise.all([tryLockExclusive(first.fd), tryLockExclusive(second.fd)]);
|
||||
});
|
||||
|
||||
test('closing the locked fd allows an already-open contender to acquire', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
owner.close();
|
||||
await tryLockExclusive(contender.fd);
|
||||
});
|
||||
|
||||
test('closing another fd for the same file does not release the lock', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const other = scope.open();
|
||||
const contender = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
other.close();
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
owner.close();
|
||||
await tryLockExclusive(contender.fd);
|
||||
});
|
||||
|
||||
test('invalid fd rejects asynchronously with EBADF and positive errno', nativeOnly, async () => {
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = tryLockExclusive(-1); });
|
||||
assert.ok(result instanceof Promise);
|
||||
await assert.rejects(result, (error) => flockError(error, ['EBADF']));
|
||||
});
|
||||
|
||||
test('native argument errors reject the JavaScript promise without throwing from the entry', nativeOnly, async () => {
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = tryLockExclusive(2 ** 31); });
|
||||
assert.ok(result instanceof Promise);
|
||||
await assert.rejects(result, { name: 'RangeError', message: 'fd must be a signed C int' });
|
||||
});
|
||||
|
||||
test('native callbacks receive asynchronous, request-local success and errno results', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
const binding = loadFlockBinding();
|
||||
const results = await Promise.all([owner.fd, contender.fd, -1].map((fd) => new Promise((resolve) => {
|
||||
let returned = false;
|
||||
const result = binding.tryLock(fd, (errno) => {
|
||||
assert.equal(returned, true);
|
||||
resolve(errno);
|
||||
});
|
||||
assert.equal(result, undefined);
|
||||
returned = true;
|
||||
})));
|
||||
assert.equal(results[0], 0);
|
||||
assert.ok([constants.errno.EAGAIN, constants.errno.EWOULDBLOCK].includes(results[1]));
|
||||
assert.equal(results[2], constants.errno.EBADF);
|
||||
});
|
||||
|
||||
test('an exception in the native completion callback is reported as uncaught', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const child = await childFixture(t, scope, 'flock-callback-throws.js');
|
||||
const exit = await child.waitForExit();
|
||||
assert.equal(exit.signal, null, exit.stderr);
|
||||
assert.equal(exit.error, undefined);
|
||||
assert.equal(exit.code, 1, exit.stderr);
|
||||
assert.match(exit.stderr, /Error: flock callback failure/);
|
||||
});
|
||||
|
||||
test('concurrent calls retain their own syscall errno', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
await Promise.all([
|
||||
assert.rejects(tryLockExclusive(contender.fd), busy),
|
||||
assert.rejects(tryLockExclusive(-1), (error) => flockError(error, ['EBADF'])),
|
||||
assert.rejects(tryLockExclusive(contender.fd), busy),
|
||||
assert.rejects(tryLockExclusive(-1), (error) => flockError(error, ['EBADF'])),
|
||||
]);
|
||||
});
|
||||
|
||||
test('two child processes exclude each other and normal close transfers ownership', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const observer = scope.open();
|
||||
const children = await Promise.all([
|
||||
childFixture(t, scope, 'flock-child.js', [scope.file]),
|
||||
childFixture(t, scope, 'flock-child.js', [scope.file]),
|
||||
]);
|
||||
const results = await Promise.all(children.map((child) => child.exchange('tryLock')));
|
||||
assert.equal(results.filter((result) => result.type === 'locked').length, 1);
|
||||
assert.equal(results.filter((result) => result.type === 'error').length, 1);
|
||||
const winnerIndex = results.findIndex((result) => result.type === 'locked');
|
||||
const winner = children[winnerIndex];
|
||||
const loser = children[1 - winnerIndex];
|
||||
busy(results[1 - winnerIndex]);
|
||||
await assert.rejects(tryLockExclusive(observer.fd), busy);
|
||||
|
||||
assert.deepEqual(await winner.exchange('close'), { type: 'closed' });
|
||||
cleanExit(await winner.waitForExit());
|
||||
assert.deepEqual(await loser.exchange('tryLock'), { type: 'locked' });
|
||||
await assert.rejects(tryLockExclusive(observer.fd), busy);
|
||||
assert.deepEqual(await loser.exchange('close'), { type: 'closed' });
|
||||
cleanExit(await loser.waitForExit());
|
||||
await tryLockExclusive(observer.fd);
|
||||
});
|
||||
|
||||
test('SIGKILL releases a child lock after exit', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const observer = scope.open();
|
||||
const owner = await childFixture(t, scope, 'flock-child.js', [scope.file]);
|
||||
const contender = await childFixture(t, scope, 'flock-child.js', [scope.file]);
|
||||
assert.deepEqual(await owner.exchange('tryLock'), { type: 'locked' });
|
||||
const rejected = await contender.exchange('tryLock');
|
||||
assert.equal(rejected.type, 'error');
|
||||
busy(rejected);
|
||||
assert.equal(owner.child.kill('SIGKILL'), true);
|
||||
const exit = await owner.waitForExit();
|
||||
assert.equal(exit.error, undefined);
|
||||
assert.equal(exit.signal, 'SIGKILL');
|
||||
assert.equal(exit.code, null);
|
||||
assert.deepEqual(await contender.exchange('tryLock'), { type: 'locked' });
|
||||
await assert.rejects(tryLockExclusive(observer.fd), busy);
|
||||
});
|
||||
|
||||
for (const phase of ['queued', 'callback']) {
|
||||
test(`worker termination during ${phase} drains native work without taking ownership of the fd`, nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
const worker = new Worker(new URL('./fixtures/flock-worker.js', import.meta.url), {
|
||||
workerData: { fd: owner.fd, phase },
|
||||
execArgv: [],
|
||||
});
|
||||
scope.defer((signal) => until(worker.terminate(), signal));
|
||||
const exited = once(worker, 'exit');
|
||||
const waiting = new AbortController();
|
||||
try {
|
||||
const [message] = await Promise.race([
|
||||
once(worker, 'message', { signal: AbortSignal.any([t.signal, waiting.signal]) }),
|
||||
exited.then(([code]) => { throw new Error(`flock worker exited before ${phase}: ${code}`); }),
|
||||
]);
|
||||
assert.deepEqual(message, { type: phase, nativeWork: 1 });
|
||||
} finally {
|
||||
waiting.abort();
|
||||
}
|
||||
assert.equal(await until(worker.terminate(), t.signal), 1);
|
||||
await until(exited, t.signal);
|
||||
await tryLockExclusive(owner.fd);
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
owner.close();
|
||||
await tryLockExclusive(contender.fd);
|
||||
});
|
||||
}
|
||||
|
||||
for (const mode of ['exclusive', 'shared']) {
|
||||
test(`an addon exclusive lock blocks an independent C ${mode} flock until its fd closes`, nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
const oracle = await oracleFixture(t, scope, mode);
|
||||
const result = await oracle.exchange('t');
|
||||
assert.ok([constants.errno.EAGAIN, constants.errno.EWOULDBLOCK].includes(result.errno));
|
||||
owner.close();
|
||||
assert.deepEqual(await oracle.exchange('t'), { errno: 0 });
|
||||
await oracle.quit();
|
||||
});
|
||||
|
||||
test(`an independent C ${mode} flock blocks the addon until explicit unlock`, nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const contender = scope.open();
|
||||
const oracle = await oracleFixture(t, scope, mode);
|
||||
assert.deepEqual(await oracle.exchange('t'), { errno: 0 });
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
assert.deepEqual(await oracle.exchange('u'), { errno: 0 });
|
||||
await tryLockExclusive(contender.fd);
|
||||
await oracle.quit();
|
||||
});
|
||||
}
|
||||
|
||||
test('an advisory exclusive lock permits another process to read and write without locking', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
writeFileSync(scope.file, 'written before locking');
|
||||
await tryLockExclusive(owner.fd);
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
const child = await childFixture(t, scope, 'flock-io-child.js', [scope.file]);
|
||||
assert.deepEqual(await child.exchange('read-write'), {
|
||||
type: 'written', previous: 'written before locking',
|
||||
});
|
||||
cleanExit(await child.waitForExit());
|
||||
assert.equal(readFileSync(scope.file, 'utf8'), 'written without acquiring a lock');
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
});
|
||||
|
||||
test('an inherited fd shares the lock after parent close until the child closes its last reference', nativeOnly, async (t) => {
|
||||
const scope = resources(t);
|
||||
const owner = scope.open();
|
||||
const contender = scope.open();
|
||||
await tryLockExclusive(owner.fd);
|
||||
const child = await childFixture(t, scope, 'flock-inherited-child.js', [], { inheritedFd: owner.fd });
|
||||
assert.deepEqual(await child.exchange('tryLock'), { type: 'locked' });
|
||||
owner.close();
|
||||
await assert.rejects(tryLockExclusive(contender.fd), busy);
|
||||
assert.deepEqual(await child.exchange('close'), { type: 'closed' });
|
||||
await tryLockExclusive(contender.fd);
|
||||
// The child stays alive, so its close acknowledgement—not process exit—releases the lock.
|
||||
assert.equal(child.child.exitCode, null);
|
||||
assert.equal(child.child.signalCode, null);
|
||||
assert.deepEqual(await child.exchange('quit'), { type: 'bye' });
|
||||
cleanExit(await child.waitForExit());
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Link the downloaded platform artifact for dependency-free ABI tests. */
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const packages = fileURLToPath(new URL('../packages/', import.meta.url));
|
||||
const platform = `${process.platform}-${process.arch}`;
|
||||
const parent = path.join(packages, 'entry/node_modules/@deepseek-ai');
|
||||
fs.mkdirSync(parent, { recursive: true });
|
||||
fs.symlinkSync(path.join(packages, platform), path.join(parent, `node-addon-system-${platform}`), 'junction');
|
||||
@@ -0,0 +1,138 @@
|
||||
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 { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { verifyPlatformBinaries } from '../scripts/repo.mjs';
|
||||
|
||||
// These minimal headers exercise format rejection, not executable behavior.
|
||||
// flock.test.js and packed-install verification execute the real addon.
|
||||
function fixture(t, { platform = 'linux', arch = 'x64', kind = 'node-api' } = {}) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'system-package-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
const executable = kind === 'static-musl';
|
||||
const binary = executable
|
||||
? { tool: 'landlock-run', kind, path: 'bin/landlock-run' }
|
||||
: { tool: 'flock', kind, napi: 8, ...(platform === 'linux' ? { libc: 'glibc' } : {}), path: 'bin/system.node' };
|
||||
const spec = { platform: `${platform}-${arch}`, binaries: [binary] };
|
||||
const manifest = { name: 'fixture', os: [platform], cpu: [arch] };
|
||||
const bytes = Buffer.alloc(256);
|
||||
if (platform === 'linux') {
|
||||
bytes.writeUInt32LE(0x464c457f, 0);
|
||||
bytes[4] = 2;
|
||||
bytes[5] = 1;
|
||||
bytes.writeUInt16LE(executable ? 2 : 3, 16);
|
||||
bytes.writeUInt16LE(arch === 'x64' ? 62 : 183, 18);
|
||||
} else {
|
||||
bytes.writeUInt32LE(0xfeedfacf, 0);
|
||||
bytes.writeUInt32LE(arch === 'x64' ? 0x01000007 : 0x0100000c, 4);
|
||||
bytes.writeUInt32LE(8, 12);
|
||||
}
|
||||
bytes.write('napi_register_module_v1\0node_api_module_get_api_version_v1', 64);
|
||||
const file = path.join(dir, binary.path);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, bytes, { mode: executable ? 0o755 : 0o644 });
|
||||
const save = () => {
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(manifest));
|
||||
fs.writeFileSync(path.join(dir, 'prebuilds.json'), JSON.stringify(spec));
|
||||
};
|
||||
save();
|
||||
return { dir, file, bytes, binary, spec, manifest, save };
|
||||
}
|
||||
|
||||
for (const platform of ['linux', 'darwin']) {
|
||||
for (const arch of ['x64', 'arm64']) {
|
||||
test(`accepts ${platform}-${arch} addon metadata and header`, (t) => {
|
||||
assert.equal(verifyPlatformBinaries(fixture(t, { platform, arch }).dir).count, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test('accepts the Linux static launcher format', (t) => {
|
||||
assert.equal(verifyPlatformBinaries(fixture(t, { kind: 'static-musl' }).dir).count, 1);
|
||||
});
|
||||
|
||||
for (const [name, change, expected] of [
|
||||
['unknown platform', (f) => { f.manifest.os = ['win32']; }, /os\/cpu/],
|
||||
['mismatched platform', (f) => { f.spec.platform = 'linux-arm64'; }, /disagrees/],
|
||||
['path outside bin', (f) => { f.binary.path = '../system.node'; }, /inside bin/],
|
||||
['duplicate binary', (f) => { f.spec.binaries.push({ ...f.binary }); }, /duplicate/],
|
||||
['unknown kind', (f) => { f.binary.kind = 'unknown'; }, /kind\/tool\/NAPI/],
|
||||
['wrong NAPI version', (f) => { f.binary.napi = 9; }, /kind\/tool\/NAPI/],
|
||||
['missing Linux libc', (f) => { delete f.binary.libc; }, /declare glibc or musl/],
|
||||
['missing payload', (f) => { fs.unlinkSync(f.file); }, /missing/],
|
||||
['wrong ELF architecture', (f) => { f.bytes.writeUInt16LE(183, 18); fs.writeFileSync(f.file, f.bytes); }, /ELF architecture/],
|
||||
['wrong ELF type', (f) => { f.bytes.writeUInt16LE(2, 16); fs.writeFileSync(f.file, f.bytes); }, /ELF file type/],
|
||||
['truncated ELF', (f) => { fs.writeFileSync(f.file, Buffer.alloc(8)); }, /ELF64/],
|
||||
['missing NAPI exports', (f) => { f.bytes.fill(0, 64); fs.writeFileSync(f.file, f.bytes); }, /Node-API entry points/],
|
||||
['undeclared nested file', (f) => { fs.mkdirSync(path.join(f.dir, 'bin/extra')); fs.writeFileSync(path.join(f.dir, 'bin/extra/other.node'), 'x'); }, /undeclared/],
|
||||
]) {
|
||||
test(`rejects ${name}`, (t) => {
|
||||
const f = fixture(t);
|
||||
change(f);
|
||||
f.save();
|
||||
assert.throws(() => verifyPlatformBinaries(f.dir), expected);
|
||||
});
|
||||
}
|
||||
|
||||
test('rejects Linux libc metadata on macOS', (t) => {
|
||||
const f = fixture(t, { platform: 'darwin' });
|
||||
f.binary.libc = 'musl';
|
||||
f.save();
|
||||
assert.throws(() => verifyPlatformBinaries(f.dir), /must not declare/);
|
||||
});
|
||||
|
||||
for (const [offset, value] of [[0, 0], [4, 0], [12, 2]]) {
|
||||
test(`rejects invalid Mach-O field at ${offset}`, (t) => {
|
||||
const f = fixture(t, { platform: 'darwin' });
|
||||
f.bytes.writeUInt32LE(value, offset);
|
||||
fs.writeFileSync(f.file, f.bytes);
|
||||
assert.throws(() => verifyPlatformBinaries(f.dir), /Mach-O/);
|
||||
});
|
||||
}
|
||||
|
||||
test('rejects a launcher whose executable bit was lost', { skip: process.platform === 'win32' }, (t) => {
|
||||
const f = fixture(t, { kind: 'static-musl' });
|
||||
fs.chmodSync(f.file, 0o644);
|
||||
assert.throws(() => verifyPlatformBinaries(f.dir), /not executable/);
|
||||
});
|
||||
|
||||
test('rejects a symbolic-link payload', { skip: process.platform === 'win32' }, (t) => {
|
||||
const f = fixture(t);
|
||||
fs.renameSync(f.file, f.file + '.target');
|
||||
fs.symlinkSync(f.file + '.target', f.file);
|
||||
assert.throws(() => verifyPlatformBinaries(f.dir), /not a regular file/);
|
||||
});
|
||||
|
||||
test('entry prepack rejects a missing exported flock file even when the root entry exists', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'system-entry-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(dir, 'lib'));
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
||||
name: 'entry-fixture',
|
||||
exports: {
|
||||
'.': { types: './lib/index.d.ts', default: './lib/index.js' },
|
||||
'./flock': { types: './lib/flock.d.ts', default: './lib/flock.js' },
|
||||
},
|
||||
}));
|
||||
for (const file of ['index.js', 'index.d.ts', 'flock.d.ts']) fs.writeFileSync(path.join(dir, 'lib', file), '');
|
||||
const script = fileURLToPath(new URL('../scripts/verify-entry-lib.mjs', import.meta.url));
|
||||
const options = {
|
||||
cwd: dir,
|
||||
encoding: 'utf8',
|
||||
timeout: 120_000,
|
||||
env: Object.fromEntries(Object.entries(process.env).filter(([key]) => !/KEY|TOKEN|SECRET|PASSWORD/i.test(key))),
|
||||
};
|
||||
const missing = spawnSync(process.execPath, [script], options);
|
||||
assert.equal(missing.error, undefined);
|
||||
assert.equal(missing.signal, null);
|
||||
assert.equal(missing.status, 1);
|
||||
assert.match(missing.stderr, /lib\/flock\.js/);
|
||||
fs.writeFileSync(path.join(dir, 'lib/flock.js'), '');
|
||||
const complete = spawnSync(process.execPath, [script], options);
|
||||
assert.equal(complete.error, undefined);
|
||||
assert.equal(complete.signal, null);
|
||||
assert.equal(complete.status, 0, complete.stderr);
|
||||
});
|
||||
Reference in New Issue
Block a user