diff --git a/app/lib/lnd/neutrino.js b/app/lib/lnd/neutrino.js index dd2c55f3..68c60f50 100644 --- a/app/lib/lnd/neutrino.js +++ b/app/lib/lnd/neutrino.js @@ -15,7 +15,7 @@ const CHAIN_SYNC_COMPLETE = 'chain-sync-finished' // Events const ERROR = 'error' -const CLOSE = 'close' +const EXIT = 'exit' const WALLET_UNLOCKER_GRPC_ACTIVE = 'wallet-unlocker-grpc-active' const LIGHTNING_GRPC_ACTIVE = 'lightning-grpc-active' const GOT_CURRENT_BLOCK_HEIGHT = 'got-current-block-height' @@ -90,11 +90,25 @@ class Neutrino extends EventEmitter { // neutrinoArgs.push('--neutrino.connect=testnet2-btcd.zaphq.io') } + mainLog.info( + 'Spawning Neutrino process: %s %s', + this.lndConfig.binaryPath, + neutrinoArgs.join(' ') + ) + this.process = spawn(this.lndConfig.binaryPath, neutrinoArgs) - .on('error', error => this.emit(ERROR, error)) - .on('close', code => { - this.emit(CLOSE, code, this.lastError) + .on('error', error => { + mainLog.debug('Neutrino process received "error" event with error: %s', error) + this.emit(ERROR, error, this.lastError) + }) + .on('exit', (code, signal) => { + mainLog.debug( + 'Neutrino process received "exit" event with code %s and signal %s', + code, + signal + ) this.process = null + this.emit(EXIT, code, signal, this.lastError) }) // Listen for when neutrino prints data to stderr. @@ -230,11 +244,10 @@ class Neutrino extends EventEmitter { /** * Stop the Lnd process. */ - kill() { + kill(signalName: string = 'SIGINT') { if (this.process) { mainLog.info('Killing Neutrino process...') - this.process.kill() - this.process = null + this.process.kill(signalName) } } diff --git a/app/lib/zap/controller.js b/app/lib/zap/controller.js index 8f1255ce..368005e2 100644 --- a/app/lib/zap/controller.js +++ b/app/lib/zap/controller.js @@ -288,11 +288,8 @@ class ZapController { } } - /** /** * Starts the LND node and attach event listeners. - * @param {string} alias Alias to assign to the lnd node. - * @param {boolean} autopilot True if autopilot should be enabled. * @return {Neutrino} Neutrino instance. */ startNeutrino() { @@ -307,12 +304,12 @@ class ZapController { }) }) - this.neutrino.on('close', (code, lastError) => { - mainLog.info(`Lnd process has shut down (code ${code})`) + this.neutrino.on('exit', (code, signal, lastError) => { + mainLog.info(`Lnd process has shut down (code: ${code}, signal: ${signal})`) if (this.is('running') || this.is('connected')) { dialog.showMessageBox({ type: 'error', - message: `Lnd has unexpectedly quit: ${lastError}` + message: `Lnd has unexpectedly quit:\n\nError code: ${code}\nExit signal: ${signal}\nLast error: ${lastError}` }) this.terminate() } @@ -358,61 +355,45 @@ class ZapController { */ async shutdownNeutrino() { // We only want to shut down LND if we are running it locally. - if (this.lndConfig.type !== 'local') { + if (this.lndConfig.type !== 'local' || !this.neutrino || !this.neutrino.process) { return Promise.resolve() } - // Attempt a graceful shutdown if we can. - if (this.lightning && this.lightning.can('terminate')) { - mainLog.info('Shutting down Neutrino...') - - return new Promise(resolve => { - // HACK: Sometimes there are errors during the shutdown process that prevent the daeming from shutting down at - // all. If we haven't received notification of the process closing within 10 seconds, kill it. - // See https://github.com/lightningnetwork/lnd/pull/1781 - // See https://github.com/lightningnetwork/lnd/pull/1783 - const shutdownTimeout = setTimeout(() => { - this.neutrino.removeListener('close', closeHandler) - if (this.neutrino) { - mainLog.warn('Graceful shutdown failed to complete within 10 seconds.') - this.neutrino.kill() - resolve() - } - }, 1000 * 10) - - // HACK: The Lightning.stopDaemon` call returns before lnd has actually fully completed the shutdown process - // so we add a listener on the close event so that we can wrap things up once the process has been fully closed - // out. - const closeHandler = function() { - mainLog.info('Neutrino shutdown complete.') - clearTimeout(shutdownTimeout) + mainLog.info('Shutting down Neutrino...') + + return new Promise(async resolve => { + // HACK: Sometimes there are errors during the shutdown process that prevent the daeming from shutting down at + // all. If we haven't received notification of the process closing within 10 seconds, kill it. + // See https://github.com/lightningnetwork/lnd/pull/1781 + // See https://github.com/lightningnetwork/lnd/pull/1783 + const shutdownTimeout = setTimeout(() => { + this.neutrino.removeListener('exit', exitHandler) + if (this.neutrino) { + mainLog.warn('Graceful shutdown failed to complete within 10 seconds.') + this.neutrino.kill('SIGTERM') resolve() } - this.neutrino.once('close', closeHandler) - - this.lightning - .terminate() - .then(() => mainLog.info('Neutrino Daemon shutdown complete')) - .catch(err => { - mainLog.error('Unable to gracefully shutdown LND: %o', err) - // Kill the process ourselves here to ensure that we don't leave hanging processes. - if (this.neutrino) { - this.neutrino.kill() - resolve() - } - }) - }) - } + }, 1000 * 10) + + const exitHandler = () => { + clearTimeout(shutdownTimeout) + resolve() + } + this.neutrino.once('exit', exitHandler) - // The Lightning service is only active after the wallet has been unlocked and a gRPC connection has been - // established. In this case, kill the Neutrino process to ensure that we don't leave hanging process. - // FIXME: This currencly doesn't do a graceful shutdown as LND does not properly handle SIGTERM. - // See https://github.com/lightningnetwork/lnd/issues/1028 - else if (this.neutrino) { + // The Lightning service is only active once the wallet has been unlocked and a gRPC connection has been made. + // If it is active, disconnect from it before we terminate neutrino. + if (this.lightning && this.lightning.can('terminate')) { + await this.lightning.disconnect() + } + // Kill the Neutrino process (sends SIGINT to Neutrino process) this.neutrino.kill() - } + }).then(() => mainLog.info('Neutrino shutdown complete')) } + /** + * Start or connect to lnd process after onboarding has been completed by the app. + */ finishOnboarding(options: onboardingOptions) { mainLog.info('Finishing onboarding') // Save the lnd config options that we got from the renderer. diff --git a/test/unit/lnd/neutrino.spec.js b/test/unit/lnd/neutrino.spec.js index 7c645ee6..4c80d918 100644 --- a/test/unit/lnd/neutrino.spec.js +++ b/test/unit/lnd/neutrino.spec.js @@ -200,19 +200,4 @@ describe('Neutrino', function() { }) }) }) - - describe('.kill', () => { - describe('called when neutrino is already running', () => { - beforeEach(() => { - this.neutrino = new Neutrino(new LndConfig()) - this.neutrino.process = { - kill: jest.fn() - } - this.neutrino.kill() - }) - it('should kill the neutrino process', () => { - expect(this.neutrino.process).toBeNull() - }) - }) - }) })