Skip to content

Commit 60e60b1

Browse files
committed
More
1 parent 7ad75d0 commit 60e60b1

3 files changed

Lines changed: 77 additions & 18 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ jobs:
416416
strategy:
417417
fail-fast: false
418418
matrix:
419-
os: [ubuntu-latest, macos-latest, windows-latest]
419+
os: [ubuntu-latest, macos-latest, windows-2022]
420420
node: [18, 26]
421421
steps:
422422
- name: Check out current commit

src/index.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/* eslint-disable no-console */
22
import type { AsyncLocalStorage } from 'node:async_hooks';
33
import { spawnSync } from 'node:child_process';
4-
import * as fs from 'node:fs';
54
import * as path from 'node:path';
65
import { env } from 'node:process';
76
import { threadId } from 'node:worker_threads';
87
import { abi, arch, identifier, platform, stdlib } from './constants';
98
import { copyBinary } from './copy-binary';
9+
import { withRebuildLock } from './rebuild-lock';
1010

1111
type AsyncStorageArgs = {
1212
/** The AsyncLocalStorage instance used to fetch the store */
@@ -257,27 +257,13 @@ function getNativeModule(): Native {
257257
return nativeModule;
258258
}
259259

260-
// Exclusive-create a lock file so only one process/thread runs node-gyp at a
261-
// time. Losers (EEXIST) wait for the winner to finish before trying to load.
262-
const lockFile = path.join(__dirname, '..', '.rebuild-lock');
263-
let lockFd: number | undefined;
264-
try { lockFd = fs.openSync(lockFile, 'wx'); } catch { /* another caller holds the lock */ }
265-
266-
if (lockFd !== undefined) {
260+
withRebuildLock(() => {
267261
try {
268262
recompileFromSource();
269263
} catch (e) {
270264
console.warn('Failed to compile from source:', e);
271-
} finally {
272-
try { fs.closeSync(lockFd); } catch { /* ignore */ }
273-
try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
274-
}
275-
} else {
276-
const timer = new Int32Array(new SharedArrayBuffer(4));
277-
while (fs.existsSync(lockFile)) {
278-
Atomics.wait(timer, 0, 0, 250);
279265
}
280-
}
266+
});
281267

282268
// Try again after recompile (or after another caller finished theirs).
283269
nativeModule = tryLoad();

src/rebuild-lock.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
const lockFile = path.join(__dirname, '..', '.rebuild-lock');
5+
6+
function isProcessAlive(pid: number): boolean {
7+
try {
8+
process.kill(pid, 0);
9+
return true;
10+
} catch (e) {
11+
// EPERM: process exists but we can't signal it — treat as alive
12+
// ESRCH: no such process — stale lock
13+
return (e as NodeJS.ErrnoException).code === 'EPERM';
14+
}
15+
}
16+
17+
function tryAcquire(): number | undefined {
18+
let fd: number | undefined;
19+
try {
20+
fd = fs.openSync(lockFile, 'wx');
21+
fs.writeSync(fd, String(process.pid));
22+
return fd;
23+
} catch {
24+
try { if (fd !== undefined) fs.closeSync(fd); } catch { /* ignore */ }
25+
return undefined;
26+
}
27+
}
28+
29+
function release(fd: number): void {
30+
try { fs.closeSync(fd); } catch { /* ignore */ }
31+
try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
32+
}
33+
34+
function clearStaleLock(): void {
35+
try {
36+
const pid = parseInt(fs.readFileSync(lockFile, 'utf8'), 10);
37+
if (!isNaN(pid) && !isProcessAlive(pid)) {
38+
fs.unlinkSync(lockFile);
39+
}
40+
} catch { /* ignore — another waiter may have already cleared it */ }
41+
}
42+
43+
function waitForLockRelease(): void {
44+
const timer = new Int32Array(new SharedArrayBuffer(4));
45+
while (fs.existsSync(lockFile)) {
46+
clearStaleLock();
47+
Atomics.wait(timer, 0, 0, 250);
48+
}
49+
}
50+
51+
/**
52+
* Runs `work` while holding an exclusive lock on `lockFile`. If another caller
53+
* holds the lock, blocks until it is released (or until a stale lock from a
54+
* crashed process is detected and cleared).
55+
*/
56+
export function withRebuildLock(work: () => void): void {
57+
let fd = tryAcquire();
58+
59+
if (fd === undefined) {
60+
waitForLockRelease();
61+
// Try once more after waiting — another waiter may have cleared a stale
62+
// lock and then done the rebuild itself, so we may not need to rebuild.
63+
fd = tryAcquire();
64+
}
65+
66+
if (fd !== undefined) {
67+
try {
68+
work();
69+
} finally {
70+
release(fd);
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)