diff --git a/.tangled/workflows/ci.yaml b/.tangled/workflows/ci.yaml index cacd2da..2ac8c6d 100644 --- a/.tangled/workflows/ci.yaml +++ b/.tangled/workflows/ci.yaml @@ -1,6 +1,6 @@ when: - event: ['push', 'pull_request'] - branch: ['*'] + branch: ['**'] engine: nixery diff --git a/test/serve/fd-dup-diag.test.ts b/test/serve/fd-dup-diag.test.ts index f68e447..c402c85 100644 --- a/test/serve/fd-dup-diag.test.ts +++ b/test/serve/fd-dup-diag.test.ts @@ -68,22 +68,103 @@ describe('fd dup diagnostics', () => { server.close(); }); - it('alternative: dup via process.binding if /proc/self/fd fails', { timeout: 5000 }, async () => { - // Test whether we can use a different dup mechanism - try { - const binding = (process as any).binding('spawn_sync'); - console.log('spawn_sync binding available:', !!binding); - } catch (e: any) { - console.log('spawn_sync binding not available:', e.message); - } + it('alternative: detach handle without closing fd', { timeout: 5000 }, async () => { + const { Worker } = await import('node:worker_threads'); - // Check if native addon-free dup is possible via other means - try { - const { execSync } = await import('node:child_process'); - const result = execSync('ls /proc/self/fd/ 2>&1 || echo "NO_PROC"', { encoding: 'utf8' }); - console.log('/proc/self/fd listing:', result.trim().slice(0, 200)); - } catch (e: any) { - console.log('ls /proc/self/fd failed:', e.message); - } + const workerCode = ` + const { parentPort } = require('node:worker_threads'); + const { Socket } = require('node:net'); + parentPort.on('message', ({ fd }) => { + const sock = new Socket({ fd, readable: true, writable: true }); + sock.setNoDelay(true); + sock.write('hello from detach\\n'); + sock.end(); + }); + `; + const worker = new Worker(workerCode, { eval: true }); + + const server = createServer(); + server.listen(0); + await once(server, 'listening'); + const port = (server.address() as any).port; + + server.on('connection', (socket: any) => { + const handle = socket._handle; + const fd: number = handle.fd; + // Stop libuv from reading on this fd + handle.readStop(); + // Prevent socket.destroy() from closing the fd + handle.close = (cb: any) => { + if (cb) cb(); + }; + // Detach + socket._handle = null; + socket.destroy(); + worker.postMessage({ fd }); + }); + + const client = connect(port); + const chunks: Buffer[] = []; + client.on('data', (c: any) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c))); + await once(client, 'end'); + + const result = Buffer.concat(chunks).toString('utf8'); + console.log('detach approach result:', JSON.stringify(result)); + assert.equal(result, 'hello from detach\n'); + + server.close(); + await worker.terminate(); + }); + + it('alternative: detach handle with bidirectional HTTP', { timeout: 10000 }, async () => { + const { Worker } = await import('node:worker_threads'); + const http = await import('node:http'); + + const workerCode = ` + const { parentPort } = require('node:worker_threads'); + const net = require('net'); + const http = require('http'); + const srv = http.createServer((req, res) => { res.end('bidi-ok'); }); + parentPort.on('message', ({ fd }) => { + const sock = new net.Socket({ fd, readable: true, writable: true }); + sock.setNoDelay(true); + srv.emit('connection', sock); + }); + `; + const worker = new Worker(workerCode, { eval: true }); + + const server = createServer(); + server.listen(0); + await once(server, 'listening'); + const port = (server.address() as any).port; + + server.on('connection', (socket: any) => { + const handle = socket._handle; + const fd: number = handle.fd; + handle.readStop(); + handle.close = (cb: any) => { + if (cb) cb(); + }; + socket._handle = null; + socket.destroy(); + worker.postMessage({ fd }); + }); + + // Full HTTP roundtrip + const body = await new Promise((resolve, reject) => { + const req = http.request({ host: '127.0.0.1', port, path: '/' }, (res) => { + const chunks: Buffer[] = []; + res.on('data', (c: any) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c))); + res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + }); + req.on('error', reject); + req.end(); + }); + + console.log('bidi HTTP result:', JSON.stringify(body)); + assert.equal(body, 'bidi-ok'); + + server.close(); + await worker.terminate(); }); });