Browse Source
`main.dev.js` was hard to follow and understand the code flow. It housed code to handle quite a few distinct things. This is a fairly substantial refactor of the application startup code in which we: A new `Neutrino` class has been created with 2 public methods. `start` and `stop`. `start` will launch a new `lnd` process whilst `stop` will stop it. This class extends the `EventEmitter` class and emits the following events based on activity detected from the lnd log output. - `grpc-proxy-started` - gRPC started - `wallet-opened` Wallet opened - `fully-synced` - lnd is all caught up to the blockchain - `got-block-height` - got updated block height A new `ZapController` class has been created which houses all of the logic for intraprocess communication between the main and renderer processes. Previously we had several `setInterval` loops that were checking every second to see if the application status has changed and trigging the appropriate action if so. This was pretty hard to follow has been replaced here with more extensive use of promises. This enables us to act instantly to relevant changes rather than waiting up to 1 second for the next interval to fire. Now, the only stuff that lives in `main.dev.js` now is the top level `app` listeners, which calls out the other parts mentioned above to bootstrap the application.renovate/lint-staged-8.x
Tom Kirkpatrick
7 years ago
6 changed files with 414 additions and 361 deletions
@ -0,0 +1,85 @@ |
|||
import split2 from 'split2' |
|||
import { spawn } from 'child_process' |
|||
import EventEmitter from 'events' |
|||
import config from '../config' |
|||
import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log' |
|||
|
|||
class Neutrino extends EventEmitter { |
|||
constructor(alias, autopilot) { |
|||
super() |
|||
this.alias = alias |
|||
this.autopilot = autopilot |
|||
this.process = null |
|||
} |
|||
|
|||
start() { |
|||
if (this.process) { |
|||
throw new Error('Neutrino process with PID ${this.process.pid} already exists.') |
|||
} |
|||
|
|||
const lndConfig = config.lnd() |
|||
mainLog.info('Starting lnd in neutrino mode') |
|||
mainLog.debug(' > lndPath', lndConfig.lndPath) |
|||
mainLog.debug(' > lightningRpc:', lndConfig.lightningRpc) |
|||
mainLog.debug(' > lightningHost:', lndConfig.lightningHost) |
|||
mainLog.debug(' > cert:', lndConfig.cert) |
|||
mainLog.debug(' > macaroon:', lndConfig.macaroon) |
|||
|
|||
const neutrinoArgs = [ |
|||
`--configfile=${lndConfig.configPath}`, |
|||
`${this.autopilot ? '--autopilot.active' : ''}`, |
|||
`${this.alias ? `--alias=${this.alias}` : ''}` |
|||
] |
|||
|
|||
this.process = spawn(lndConfig.lndPath, neutrinoArgs) |
|||
.on('error', error => this.emit('error', error)) |
|||
.on('close', code => { |
|||
this.emit('close', code) |
|||
this.process = null |
|||
}) |
|||
|
|||
// Listen for when neutrino prints odata to stderr.
|
|||
this.process.stderr.pipe(split2()).on('data', line => { |
|||
if (process.env.NODE_ENV === 'development') { |
|||
lndLog[lndLogGetLevel(line)](line) |
|||
} |
|||
}) |
|||
|
|||
// Listen for when neutrino prints data to stdout.
|
|||
this.process.stdout.pipe(split2()).on('data', line => { |
|||
if (process.env.NODE_ENV === 'development') { |
|||
lndLog[lndLogGetLevel(line)](line) |
|||
} |
|||
|
|||
// gRPC started.
|
|||
if (line.includes('gRPC proxy started') && line.includes('password')) { |
|||
this.emit('grpc-proxy-started') |
|||
} |
|||
|
|||
// Wallet opened.
|
|||
if (line.includes('gRPC proxy started') && !line.includes('password')) { |
|||
this.emit('wallet-opened') |
|||
} |
|||
|
|||
// LND is all caught up to the blockchain.
|
|||
if (line.includes('Chain backend is fully synced')) { |
|||
this.emit('fully-synced') |
|||
} |
|||
|
|||
// Pass current block height progress to front end for loading state UX
|
|||
if (line.includes('Caught up to height') || line.includes('Catching up block hashes')) { |
|||
this.emit('got-block-height', line) |
|||
} |
|||
}) |
|||
return this.process |
|||
} |
|||
|
|||
stop() { |
|||
if (this.process) { |
|||
this.process.kill() |
|||
this.process = null |
|||
} |
|||
} |
|||
} |
|||
|
|||
export default Neutrino |
@ -0,0 +1,28 @@ |
|||
import { lookup } from 'ps-node' |
|||
import { mainLog } from '../../utils/log' |
|||
|
|||
/** |
|||
* Check to see if an LND process is running. |
|||
* @return {Promise} Boolean indicating wether an existing lnd process was found on the host machine. |
|||
*/ |
|||
const isLndRunning = () => { |
|||
return new Promise((resolve, reject) => { |
|||
mainLog.info('Looking for existing lnd process') |
|||
lookup({ command: 'lnd' }, (err, results) => { |
|||
// There was an error checking for the LND process.
|
|||
if (err) { |
|||
return reject(err) |
|||
} |
|||
|
|||
if (!results.length) { |
|||
// An LND process was found, no need to start our own.
|
|||
mainLog.info('Existing lnd process not found') |
|||
return resolve(false) |
|||
} |
|||
mainLog.info('Found existing lnd process') |
|||
return resolve(true) |
|||
}) |
|||
}) |
|||
} |
|||
|
|||
export default isLndRunning |
@ -0,0 +1,217 @@ |
|||
import { app, ipcMain, dialog } from 'electron' |
|||
import Store from 'electron-store' |
|||
import lnd from './lnd' |
|||
import Neutrino from './lnd/lib/neutrino' |
|||
import { mainLog } from './utils/log' |
|||
|
|||
/** |
|||
* @class ZapController |
|||
* |
|||
* The ZapController class coordinates actions between the the main nand renderer processes. |
|||
*/ |
|||
class ZapController { |
|||
/** |
|||
* Create a new ZapController instance. |
|||
* @param {BrowserWindow} mainWindow BrowserWindow instance to interact with |
|||
* @param {String|Promise} mode String or Promise that resolves to the desired run mode. Valid options are: |
|||
* - 'internal': start a new lnd process. |
|||
* - 'external': connect to an existing lnd process. |
|||
*/ |
|||
constructor(mainWindow, mode) { |
|||
this.mode = mode |
|||
|
|||
// Variable to hold the main window instance.
|
|||
this.mainWindow = mainWindow |
|||
|
|||
// Keep a reference any neutrino process started by us.
|
|||
this.neutrino = null |
|||
|
|||
// Time for the splash screen to remain visible.
|
|||
this.splashScreenTime = 500 |
|||
} |
|||
|
|||
/** |
|||
* Initialize the application. |
|||
*/ |
|||
init() { |
|||
if (process.env.HOT) { |
|||
const port = process.env.PORT || 1212 |
|||
this.mainWindow.loadURL(`http://localhost:${port}/dist/index.html`) |
|||
} else { |
|||
this.mainWindow.loadURL(`file://${__dirname}/dist/index.html`) |
|||
} |
|||
|
|||
// Register IPC listeners so that we can react to instructions coming from the app.
|
|||
this._registerIpcListeners() |
|||
|
|||
// Show the window as soon as the application has finished loading.
|
|||
this.mainWindow.webContents.on('did-finish-load', async () => { |
|||
this.mainWindow.show() |
|||
this.mainWindow.focus() |
|||
mainLog.timeEnd('Time until app is visible') |
|||
mainLog.time('Time until we know the run mode') |
|||
|
|||
Promise.resolve(this.mode) |
|||
.then(mode => { |
|||
const timeUntilWeKnowTheRunMode = mainLog.timeEnd('Time until we know the run mode') |
|||
return setTimeout(() => { |
|||
if (mode === 'external') { |
|||
// If lnd is already running, create and subscribe to the Lightning grpc object.
|
|||
this.startGrpc() |
|||
this.sendMessage('successfullyCreatedWallet') |
|||
} else { |
|||
// Otherwise, start the onboarding process.
|
|||
this.sendMessage('startOnboarding') |
|||
mainLog.timeEnd('Time until onboarding has started') |
|||
} |
|||
}, timeUntilWeKnowTheRunMode < this.splashScreenTime ? this.splashScreenTime : 0) |
|||
}) |
|||
.catch(mainLog.error) |
|||
}) |
|||
|
|||
this.mainWindow.on('closed', () => { |
|||
this.mainWindow = null |
|||
|
|||
// shut down zap when a user closes the window
|
|||
app.quit() |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Send a message to the main window. |
|||
* @param {string} msg message to send. |
|||
* @param {[type]} data additional data to acompany the message. |
|||
*/ |
|||
sendMessage(msg, data) { |
|||
mainLog.info('Sending message to renderer process: %o', { msg, data }) |
|||
this.mainWindow.webContents.send(msg, data) |
|||
} |
|||
|
|||
/** |
|||
* Create and subscribe to the Lightning grpc object. |
|||
*/ |
|||
startGrpc() { |
|||
mainLog.info('Starting gRPC...') |
|||
try { |
|||
const { lndSubscribe, lndMethods } = lnd.initLnd() |
|||
|
|||
// Subscribe to bi-directional streams
|
|||
lndSubscribe(this.mainWindow) |
|||
|
|||
// Listen for all gRPC restful methods
|
|||
ipcMain.on('lnd', (event, { msg, data }) => { |
|||
lndMethods(event, msg, data) |
|||
}) |
|||
|
|||
this.sendMessage('grpcConnected') |
|||
} catch (error) { |
|||
dialog.showMessageBox({ |
|||
type: 'error', |
|||
message: `Unable to connect to lnd. Please check your lnd node and try again: ${error}` |
|||
}) |
|||
app.quit() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create and subscribe to the WalletUnlocker grpc object. |
|||
*/ |
|||
startWalletUnlocker() { |
|||
mainLog.info('Starting wallet unlocker...') |
|||
try { |
|||
const walletUnlockerMethods = lnd.initWalletUnlocker() |
|||
|
|||
// Listen for all gRPC restful methods
|
|||
ipcMain.on('walletUnlocker', (event, { msg, data }) => { |
|||
walletUnlockerMethods(event, msg, data) |
|||
}) |
|||
|
|||
this.sendMessage('walletUnlockerStarted') |
|||
} catch (error) { |
|||
dialog.showMessageBox({ |
|||
type: 'error', |
|||
message: `Unable to start lnd wallet unlocker. Please check your lnd node and try again: ${error}` |
|||
}) |
|||
app.quit() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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. |
|||
*/ |
|||
startLnd(alias, autopilot) { |
|||
this.neutrino = new Neutrino(alias, autopilot) |
|||
|
|||
this.neutrino.on('error', error => { |
|||
mainLog.error(`Got error from lnd process: ${error})`) |
|||
dialog.showMessageBox({ |
|||
type: 'error', |
|||
message: `lnd error: ${error}` |
|||
}) |
|||
}) |
|||
|
|||
this.neutrino.on('close', code => { |
|||
mainLog.info(`Lnd process has shut down (code ${code})`) |
|||
app.quit() |
|||
}) |
|||
|
|||
this.neutrino.on('grpc-proxy-started', () => { |
|||
mainLog.info('gRPC proxy started') |
|||
this.startWalletUnlocker() |
|||
}) |
|||
|
|||
this.neutrino.on('wallet-opened', () => { |
|||
mainLog.info('Wallet opened') |
|||
this.startGrpc() |
|||
this.sendMessage('lndSyncing') |
|||
}) |
|||
|
|||
this.neutrino.on('fully-synced', () => { |
|||
mainLog.info('Neutrino fully synced') |
|||
this.sendMessage('lndSynced') |
|||
}) |
|||
|
|||
this.neutrino.on('got-block-height', line => { |
|||
this.sendMessage('lndStdout', line) |
|||
}) |
|||
|
|||
this.neutrino.start() |
|||
} |
|||
|
|||
/** |
|||
* Add IPC event listeners... |
|||
*/ |
|||
_registerIpcListeners() { |
|||
ipcMain.on('startLnd', (event, options = {}) => { |
|||
const store = new Store({ name: 'connection' }) |
|||
store.store = { |
|||
type: options.connectionType, |
|||
host: options.connectionHost, |
|||
cert: options.connectionCert, |
|||
macaroon: options.connectionMacaroon, |
|||
alias: options.alias, |
|||
autopilot: options.autopilot |
|||
} |
|||
mainLog.info('Saved lnd config to:', store.path) |
|||
|
|||
if (options.connectionType === 'local') { |
|||
mainLog.info('Starting new lnd instance') |
|||
mainLog.debug(' > alias:', options.alias) |
|||
mainLog.debug(' > autopilot:', options.autopilot) |
|||
this.startLnd(options.alias, options.autopilot) |
|||
} else { |
|||
mainLog.info('Connecting to custom lnd instance') |
|||
mainLog.debug(' > connectionHost:', options.connectionHost) |
|||
mainLog.debug(' > connectionCert:', options.connectionCert) |
|||
mainLog.debug(' > connectionMacaroon:', options.connectionMacaroon) |
|||
this.startGrpc() |
|||
this.sendMessage('successfullyCreatedWallet') |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
export default ZapController |
Loading…
Reference in new issue