Skip to content

Commit

Permalink
Allows NodeJS to terminate also if there is an active service. (#656)
Browse files Browse the repository at this point in the history
  • Loading branch information
Salvatore Previti authored Jan 12, 2021
1 parent 28222c5 commit 590302f
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ jobs:
- name: JS API Tests
run: node scripts/js-api-tests.js

- name: NodeJS Unref Tests
run: node scripts/node-unref-tests.js

- name: Plugin Tests
run: node scripts/plugin-tests.js

Expand Down
53 changes: 44 additions & 9 deletions lib/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export let startService: typeof types.startService = common.referenceCountedServ
windowsHide: true,
stdio: ['pipe', 'pipe', 'inherit'],
});

let { readFromStdout, afterClose, service } = common.createChannel({
writeToStdin(bytes) {
child.stdin.write(bytes);
Expand All @@ -159,25 +160,59 @@ export let startService: typeof types.startService = common.referenceCountedServ
isSync: false,
isBrowser: false,
});
child.stdout.on('data', readFromStdout);
child.stdout.on('end', afterClose);

const stdin: typeof child.stdin & { unref?(): void } = child.stdin;
const stdout: typeof child.stdout & { unref?(): void } = child.stdout;

stdout.on('data', readFromStdout);
stdout.on('end', afterClose);

let refCount = 0;
child.unref();
if (stdin.unref) {
stdin.unref();
}
if (stdout.unref) {
stdout.unref();
}

const unref = () => {
if (--refCount === 0) {
child.unref();
}
};

const refPromise = <T>(promise: Promise<T>): Promise<T> => {
if (++refCount === 1) {
child.ref();
}
promise.then(unref, unref);
return promise;
}

// Create an asynchronous Promise-based API
return Promise.resolve({
build: (options: types.BuildOptions): Promise<any> =>
new Promise<types.BuildResult>((resolve, reject) =>
refPromise(new Promise<types.BuildResult>((resolve, reject) =>
service.buildOrServe('build', null, options, isTTY(), (err, res) =>
err ? reject(err) : resolve(res as types.BuildResult))),
err ? reject(err) : resolve(res as types.BuildResult)))),
serve: (serveOptions, buildOptions) => {
if (serveOptions === null || typeof serveOptions !== 'object')
throw new Error('The first argument must be an object')
return new Promise((resolve, reject) =>
service.buildOrServe('serve', serveOptions, buildOptions, isTTY(), (err, res) =>
err ? reject(err) : resolve(res as types.ServeResult)))
return refPromise(new Promise((resolve, reject) =>
service.buildOrServe('serve', serveOptions, buildOptions, isTTY(), (err, res) => {
if (err) {
reject(err);
} else {
refPromise((res as types.ServeResult).wait)
resolve(res as types.ServeResult);
}
})
))
},
transform: (input, options) => {
input += '';
return new Promise((resolve, reject) =>
return refPromise(new Promise((resolve, reject) =>
service.transform('transform', input, options || {}, isTTY(), {
readFile(tempFile, callback) {
try {
Expand All @@ -201,7 +236,7 @@ export let startService: typeof types.startService = common.referenceCountedServ
callback(null);
}
},
}, (err, res) => err ? reject(err) : resolve(res!)));
}, (err, res) => err ? reject(err) : resolve(res!))));
},
stop() { child.kill(); },
});
Expand Down
76 changes: 76 additions & 0 deletions scripts/node-unref-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// This test verifies that:
// - a running service will not prevent NodeJS to exit if there is no compilation in progress.
// - the NodeJS process will continue running if there is a serve() active or a transform or build in progress.

const assert = require('assert')
const { fork } = require('child_process');

// The tests to run in the child process
async function tests() {
const esbuild = require('./esbuild').installForTests()

async function testTransform() {
const t1 = await esbuild.transform(`1+2`)
const t2 = await esbuild.transform(`1+3`)
assert.strictEqual(t1.code, `1 + 2;\n`)
assert.strictEqual(t2.code, `1 + 3;\n`)
}

async function testServe() {
const server = await esbuild.serve({}, {})
assert.strictEqual(server.host, '127.0.0.1')
assert.strictEqual(typeof server.port, 'number')
server.stop()
await server.wait
}

const service = await esbuild.startService();
try {
await testTransform()
await testServe()
} catch (error) {
service.stop();
throw error;
}
}

// Called when this is the hild process to run the tests.
function runTests() {
process.exitCode = 1;
tests().then(() => {
process.exitCode = 0;
}, (error) => {
console.error('❌', error)
});
}

// A child process need to be started to verify that a running service is not hanging node.
function startChildProcess() {
const child = fork(__filename, ['__forked__'], { stdio: 'inherit', env: process.env });

const timeout = setTimeout(()=> {
console.error('❌ node unref test timeout - child_process.unref() broken?')
process.exit(1);
}, 6000);

child.on('error', (error) => {
console.error('❌', error);
process.exit(1);
})

child.on('exit', (code) => {
clearTimeout(timeout);
if (code) {
console.error('❌ node unref tests failed')
process.exit(1);
} else {
console.log(`✅ node unref tests passed`)
}
})
}

if (process.argv[2] === '__forked__') {
runTests();
} else {
startChildProcess();
}

0 comments on commit 590302f

Please sign in to comment.