Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows NodeJS to terminate also if there is an active service. #656

Merged
merged 4 commits into from
Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}