diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index da0a198..cda9e5f 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@strv/heimdall": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@strv/heimdall/-/heimdall-1.0.0.tgz", + "integrity": "sha512-+w/UQEgdfZ0RPSzbCB49VTryaEMqb9rsMPRtbc0jDWfi9lRInjf+vSnV4ocXPkgKGtyE7NtnQ+kFGpLIfcbDlw==" + }, "ansi": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", diff --git a/packages/cli/package.json b/packages/cli/package.json index 37c06ff..97222e6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -10,6 +10,7 @@ "contributors": [], "dependencies": { "@atlas.js/repl": "^2.1.2", + "@strv/heimdall": "^1.0.0", "caporal": "^1.1.0", "source-map-support": "^0.5.0" }, diff --git a/packages/cli/src/commands/start.mjs b/packages/cli/src/commands/start.mjs index ad33bd4..929c332 100644 --- a/packages/cli/src/commands/start.mjs +++ b/packages/cli/src/commands/start.mjs @@ -1,4 +1,5 @@ import * as cluster from 'cluster' +import { heimdall } from '@strv/heimdall' import Command from '../command' class Start extends Command { @@ -9,78 +10,24 @@ class Start extends Command { // ['--cluster ', 'Start the app clustered with worker processes', caporal.INT], ] - doExit = () => this.exit() - async run() { - process.once('SIGINT', this.doExit) - process.once('SIGTERM', this.doExit) - process.once('beforeExit', this.doExit) - await this.atlas.start() - .catch(fatal) - } - - /** - * Cleanly shut down the Atlas instance so that the process may exit - * - * @private - * @return {Promise} - */ - exit() { - // Prevent calling this.atlas.stop() multiple times when repeatedly pressing ctrl-c. Next time - // you press ctrl+c, we'll just brute-force-quit the whole thing. 🔥 - process.removeListener('SIGINT', this.doExit) - process.removeListener('SIGTERM', this.doExit) - process.removeListener('beforeExit', this.doExit) - process.once('SIGINT', forcequit) - process.once('SIGTERM', forcequit) + const atlas = this.atlas - return this.atlas.stop() - .then(() => { - process.removeListener('SIGINT', forcequit) - process.removeListener('SIGTERM', forcequit) + await heimdall({ + async execute() { + await atlas.start() + }, + async exit() { + await atlas.stop() // If this is a clusterised worker, disconnect the worker from the master once we are done // here so the process can exit gracefully without the master having to do anything special. if (cluster.isWorker) { cluster.worker.disconnect() } - }) - .catch(fatal) + }, + }) } } - -/** - * Cause the process to forcefully shut down by throwing an uncaught exception - * - * @private - * @return {void} - */ -function forcequit() { - process.removeListener('SIGINT', forcequit) - process.removeListener('SIGTERM', forcequit) - - throw new Error('Forced quit') -} - -/** - * Handle a fatal error in the start/stop methods of the Atlas instance - * - * @private - * @param {Error} err The error object which caused the fatal error - * @return {void} - */ -function fatal(err) { - process.exitCode = 1 - // eslint-disable-next-line no-console - console.error(err.stack) - - // A fatal error occured. We have no guarantee that the instance will shut down properly. We will - // wait 10 seconds to see if it manages to shut down gracefully, then we will use brute force to - // stop the process. 💣 - // eslint-disable-next-line no-process-exit - setTimeout(() => process.exit(), 10000) - .unref() -} - export default Start diff --git a/packages/cli/test/commands/start.test.mjs b/packages/cli/test/commands/start.test.mjs index 8562b6f..87af0a2 100644 --- a/packages/cli/test/commands/start.test.mjs +++ b/packages/cli/test/commands/start.test.mjs @@ -4,16 +4,15 @@ import Start from '../../src/commands/start' describe('CLI: start', () => { let start - beforeEach(() => { + beforeEach(function() { start = new Start() start.atlas = { start: sinon.stub().resolves(), stop: sinon.stub().resolves(), } - }) - - afterEach(() => start.doExit()) + this.sandbox.stub(process, 'once') + }) it('exists', () => { expect(Start).to.be.a('function') @@ -37,93 +36,30 @@ describe('CLI: start', () => { expect(start.atlas.start).to.have.callCount(1) }) - it('registers SIGINT, SIGTERM and beforeExit event listeners', async () => { - const counts = { - SIGINT: process.listenerCount('SIGINT'), - SIGTERM: process.listenerCount('SIGTERM'), - beforeExit: process.listenerCount('beforeExit'), - } - - await start.run() - - expect(process.listenerCount('SIGINT')).to.equal(counts.SIGINT + 1) - expect(process.listenerCount('SIGTERM')).to.equal(counts.SIGTERM + 1) - expect(process.listenerCount('beforeExit')).to.equal(counts.beforeExit + 1) - }) - - it('removes SIGINT, SIGTERM and beforeExit listeners upon receiving a signal', async () => { - const counts = { - SIGINT: process.listenerCount('SIGINT'), - SIGTERM: process.listenerCount('SIGTERM'), - beforeExit: process.listenerCount('beforeExit'), - } - - await start.run() - await start.doExit() - - expect(process.listenerCount('SIGINT')).to.equal(counts.SIGINT) - expect(process.listenerCount('SIGTERM')).to.equal(counts.SIGTERM) - expect(process.listenerCount('beforeExit')).to.equal(counts.beforeExit) - }) - - it('stops the atlas instance upon exit', async () => { - // Sanity check - expect(start.atlas.stop).to.have.callCount(0) - + it('stops the atlas instance on signal', async () => { await start.run() - await start.doExit() + // Grab Heimdall's listener and invoke it to fake a signal + const onsignal = process.once.lastCall.args[1] + await onsignal() expect(start.atlas.stop).to.have.callCount(1) }) + it('disconnects from cluster master upon exit', async () => { const disconnect = sinon.stub() cluster.isWorker = true cluster.worker = { disconnect } await start.run() - await start.doExit() + // Grab Heimdall's listener and invoke it to fake a signal + const onsignal = process.once.lastCall.args[1] + await onsignal() cluster.isWorker = false delete cluster.worker expect(disconnect).to.have.callCount(1) }) - - it('throws an error to stop the process if a terminating signal is sent again', async () => { - await start.run() - start.doExit() - - // Sending SIGINT causes the test suite to exit with code 130, so just test SIGTERM 🤷‍♂️ - expect(() => process.emit('SIGTERM')).to.throw(Error, /Forced quit/u) - - delete process.exitCode - }) - - it('kills the process after 10s if an error occurs during stop procedure', async function() { - this.sandbox.stub(process, 'exit') - this.sandbox.stub(console, 'error') - - const error = new Error('u-oh') - const clock = sinon.useFakeTimers({ - toFake: ['setTimeout'], - }) - - start.atlas.stop.rejects(error) - await start.doExit() - - // eslint-disable-next-line no-console - expect(console.error).to.have.been.calledWith(error.stack) - expect(process.exit).to.have.callCount(0) - - clock.runAll() - clock.restore() - - expect(process.exit).to.have.callCount(1) - expect(process.exitCode).to.equal(1) - expect(clock.now).to.equal(10000) - - delete process.exitCode - }) }) })