Skip to content

Commit fd33206

Browse files
authored
fix(sea): patch process.dlopen in worker threads (#297)
Native addons failed to load from SEA worker threads with ERR_DLOPEN_FAILED on the raw C:\snapshot path. Each worker thread gets its own `process` object, so the `shared.patchDlopen()` call in sea-bootstrap-core only covers the main thread. sea-worker-entry already reapplies `patchIntlSegmenter` for the same reason but was missing `patchDlopen`, so workers never got the extract-to-cache step that makes addons loadable. Classic (non-SEA) pkg is unaffected: bootstrap.js runs in every thread and patches there. test-90-sea-worker-threads now loads a `.node` from the snapshot inside the worker and asserts it was extracted to the native cache. The fake addon is not a real shared library so dlopen throws either way — the extraction is what distinguishes patched from unpatched. Closes #293 Claude-Session: https://claude.ai/code/session_01PUj6PfkgQHcCg2bfXucBzk
1 parent 8d3d7af commit fd33206

6 files changed

Lines changed: 62 additions & 1 deletion

File tree

prelude/sea-worker-entry.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ shared.setupProcessPkg(
2626
// Worker threads get their own Intl, so the main-thread patch does not
2727
// carry over — see patchIntlSegmenter in bootstrap-shared.
2828
shared.patchIntlSegmenter();
29+
30+
// Same story for process.dlopen: each worker thread gets its own `process`,
31+
// so native addons would be handed the raw C:\snapshot path and fail with
32+
// ERR_DLOPEN_FAILED — see patchDlopen in bootstrap-shared.
33+
shared.patchDlopen(vfs.insideSnapshot);

test/test-90-sea-worker-threads/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
'use strict';
22

33
const { Worker } = require('worker_threads');
4+
const os = require('os');
45
const path = require('path');
56

7+
// Redirect the native addon cache so the worker's dlopen check writes to a
8+
// throwaway directory instead of the real ~/.cache. Must be set before the
9+
// Worker is created — the worker copies process.env at spawn time and its
10+
// bootstrap reads this while patching dlopen.
11+
process.env.PKG_NATIVE_CACHE_PATH = path.join(
12+
os.tmpdir(),
13+
'pkg-test-90-native-cache',
14+
);
15+
616
function runWorker() {
717
return new Promise((resolve, reject) => {
818
const worker = new Worker(path.join(__dirname, 'worker.js'), {
@@ -42,6 +52,7 @@ async function main() {
4252
console.log('hasDirname:' + result.hasDirname);
4353
console.log('hasProcessPkg:' + result.hasProcessPkg);
4454
console.log('helperResult:' + result.helperResult);
55+
console.log('addonExtracted:' + result.addonExtracted);
4556
} catch (e) {
4657
console.log('worker-error:' + e.message);
4758
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
not-a-real-shared-library

test/test-90-sea-worker-threads/main.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
'use strict';
44

55
const assert = require('assert');
6+
const fs = require('fs');
7+
const os = require('os');
8+
const path = require('path');
69
const utils = require('../utils.js');
710

811
// Worker thread support in SEA requires Node.js >= 22
@@ -15,6 +18,10 @@ assert(__dirname === process.cwd());
1518
const input = './package.json';
1619
const testName = 'test-90-sea-worker-threads';
1720

21+
// Kept in sync with index.js — the worker asserts the addon landed here.
22+
const nativeCache = path.join(os.tmpdir(), 'pkg-test-90-native-cache');
23+
fs.rmSync(nativeCache, { recursive: true, force: true });
24+
1825
const newcomers = utils.seaHostOutputs(testName);
1926

2027
const before = utils.filesBefore(newcomers);
@@ -26,8 +33,11 @@ const expected =
2633
'hasFilename:true\n' +
2734
'hasDirname:true\n' +
2835
'hasProcessPkg:true\n' +
29-
'helperResult:hello world\n';
36+
'helperResult:hello world\n' +
37+
'addonExtracted:true\n';
3038

3139
utils.assertSeaOutput(testName, expected);
3240

3341
utils.filesAfter(before, newcomers, { tolerateWindowsEbusy: true });
42+
43+
fs.rmSync(nativeCache, { recursive: true, force: true });

test/test-90-sea-worker-threads/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"pkg": {
77
"scripts": [
88
"worker.js"
9+
],
10+
"assets": [
11+
"lib/fake.node"
912
]
1013
}
1114
}

test/test-90-sea-worker-threads/worker.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use strict';
22

33
const { parentPort, workerData } = require('worker_threads');
4+
const crypto = require('crypto');
5+
const fs = require('fs');
6+
const path = require('path');
47

58
// Verify we can access __filename and __dirname
69
const hasFilename = typeof __filename === 'string' && __filename.length > 0;
@@ -25,10 +28,38 @@ try {
2528
helperResult = 'ERROR:' + e.message;
2629
}
2730

31+
// Native addons can only be dlopen'd from a real filesystem path, so pkg
32+
// patches process.dlopen to extract them from the snapshot first. Each
33+
// worker thread gets its own `process`, so the main-thread patch does not
34+
// carry over and has to be reapplied per thread.
35+
//
36+
// fake.node is not a loadable shared library, so dlopen throws either way.
37+
// What distinguishes patched from unpatched is whether the addon was
38+
// extracted to the native cache before dlopen was called.
39+
let addonExtracted;
40+
try {
41+
const addon = path.join(__dirname, 'lib', 'fake.node');
42+
try {
43+
require(addon);
44+
} catch (_e) {
45+
// expected — see above
46+
}
47+
const hash = crypto
48+
.createHash('sha256')
49+
.update(fs.readFileSync(addon))
50+
.digest('hex');
51+
addonExtracted = fs.existsSync(
52+
path.join(process.env.PKG_NATIVE_CACHE_PATH, 'pkg', hash, 'fake.node'),
53+
);
54+
} catch (e) {
55+
addonExtracted = 'ERROR:' + e.message;
56+
}
57+
2858
parentPort.postMessage({
2959
echo: workerData.message,
3060
hasFilename,
3161
hasDirname,
3262
hasProcessPkg,
3363
helperResult,
64+
addonExtracted,
3465
});

0 commit comments

Comments
 (0)