Permalink
Browse files

feat(ux): keep app running on window close

The standard approach on Darwin is to keen applications running when the
window is closed. Clicking the apps icon again in the doc should bring
back the application window.

This changeset refactors things to handle this. It will keep Zap and lnd
running in the background when the main application window is closed.
Quitting the app will stop both the app and lnd.

Fix #586
Fix #601
  • Loading branch information...
mrfelton committed Aug 1, 2018
1 parent 32399cf commit b180287f6ca72bf883e1183f262c3c334f599e6c
@@ -1,24 +1,9 @@
import config from './config'
import lightning from './lib/lightning'
import walletUnlocker from './lib/walletUnlocker'
import subscribe from './subscribe'
import methods from './methods'
import walletUnlockerMethods from './walletUnlockerMethods'
// use mainLog because lndLog is reserved for the lnd binary itself
import { mainLog } from '../utils/log'
const initLnd = async () => {
const lnd = await lightning()
const lndSubscribe = mainWindow => subscribe(mainWindow, lnd, mainLog)
const lndMethods = (event, msg, data) => methods(lnd, mainLog, event, msg, data)
return Promise.resolve({
lndSubscribe,
lndMethods
})
}
const initWalletUnlocker = () => {
const lndConfig = config.lnd()
const walletUnlockerObj = walletUnlocker(lndConfig.rpcProtoPath, lndConfig.host)
@@ -29,6 +14,5 @@ const initWalletUnlocker = () => {
}
export default {
initLnd,
initWalletUnlocker
}
@@ -2,52 +2,112 @@ import grpc from 'grpc'
import { loadSync } from '@grpc/proto-loader'
import config from '../config'
import { getDeadline, validateHost, createSslCreds, createMacaroonCreds } from './util'
import methods from '../methods'
import { mainLog } from '../../utils/log'
import subscribeToTransactions from '../subscribe/transactions'
import subscribeToInvoices from '../subscribe/invoices'
import subscribeToChannelGraph from '../subscribe/channelgraph'
/**
* Creates an LND grpc client lightning service.
* @returns {rpc.lnrpc.Lightning}
* @returns {Lightning}
*/
const lightning = async () => {
const lndConfig = config.lnd()
const { host, rpcProtoPath, cert, macaroon } = lndConfig
// Verify that the host is valid before creating a gRPC client that is connected to it.
return await validateHost(host).then(async () => {
// Load the gRPC proto file.
// The following options object closely approximates the existing behavior of grpc.load.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
const options = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
class Lightning {
constructor() {
this.mainWindow = null
this.lnd = null
this.subscriptions = {
channelGraph: null,
invoices: null,
transactions: null
}
const packageDefinition = loadSync(rpcProtoPath, options)
// Load gRPC package definition as a gRPC object hierarchy.
const rpc = grpc.loadPackageDefinition(packageDefinition)
// Create ssl and macaroon credentials to use with the gRPC client.
const [sslCreds, macaroonCreds] = await Promise.all([
createSslCreds(cert),
createMacaroonCreds(macaroon)
])
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds)
// Instantiate a new connection to the Lightning interface.
const lnd = new rpc.lnrpc.Lightning(host, credentials)
// Call the getInfo method to ensure that we can make successful calls to the gRPC interface.
return new Promise((resolve, reject) => {
lnd.getInfo({}, { deadline: getDeadline(2) }, err => {
if (err) {
return reject(err)
}
return resolve(lnd)
}
/**
* Connect to the gRPC interface and verify it is functional.
* @return {Promise<rpc.lnrpc.Lightning>}
*/
async connect() {
const lndConfig = config.lnd()
const { host, rpcProtoPath, cert, macaroon } = lndConfig
// Verify that the host is valid before creating a gRPC client that is connected to it.
return await validateHost(host).then(async () => {
// Load the gRPC proto file.
// The following options object closely approximates the existing behavior of grpc.load.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
const options = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
}
const packageDefinition = loadSync(rpcProtoPath, options)
// Load gRPC package definition as a gRPC object hierarchy.
const rpc = grpc.loadPackageDefinition(packageDefinition)
// Create ssl and macaroon credentials to use with the gRPC client.
const [sslCreds, macaroonCreds] = await Promise.all([
createSslCreds(cert),
createMacaroonCreds(macaroon)
])
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds)
// Create a new gRPC client instance.
const lnd = new rpc.lnrpc.Lightning(host, credentials)
// Call the getInfo method to ensure that we can make successful calls to the gRPC interface.
return new Promise((resolve, reject) => {
lnd.getInfo({}, { deadline: getDeadline(2) }, err => {
if (err) {
return reject(err)
}
this.lnd = lnd
return resolve(lnd)
})
})
})
})
}
/**
* Discomnnect the gRPC service.
*/
disconnect() {
this.unsubscribe()
this.lnd.close()
}
/**
* Hook up lnd restful methods.
*/
lndMethods(event, msg, data) {
return methods(this.lnd, mainLog, event, msg, data)
}
/**
* Subscribe to all bi-directional streams.
*/
subscribe(mainWindow) {
this.mainWindow = mainWindow
this.subscriptions.channelGraph = subscribeToChannelGraph(this.mainWindow, this.lnd, mainLog)
this.subscriptions.invoices = subscribeToInvoices(this.mainWindow, this.lnd, mainLog)
this.subscriptions.transactions = subscribeToTransactions(this.mainWindow, this.lnd, mainLog)
}
/**
* Unsubscribe from all bi-directional streams.
*/
unsubscribe() {
Object.keys(this.subscriptions).forEach(subscription => {
if (this.subscriptions[subscription]) {
this.subscriptions[subscription].cancel()
this.subscriptions[subscription] = null
}
})
this.mainWindow = null
}
}
export default lightning
export default Lightning
@@ -2,6 +2,7 @@ import dns from 'dns'
import fs from 'fs'
import axios from 'axios'
import { promisify } from 'util'
import { lookup } from 'ps-node'
import path from 'path'
import grpc from 'grpc'
import isIP from 'validator/lib/isIP'
@@ -137,3 +138,27 @@ export const createMacaroonCreds = async macaroonPath => {
callback(null, metadata)
)
}
/**
* Check to see if an LND process is running.
* @return {Promise} Boolean indicating wether an existing lnd process was found on the host machine.
*/
export 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)
})
})
}
@@ -1,8 +1,14 @@
export default function subscribeToChannelGraph(mainWindow, lnd) {
import { status } from 'grpc'
export default function subscribeToChannelGraph(mainWindow, lnd, log) {
const call = lnd.subscribeChannelGraph({})
call.on('data', channelGraphData => mainWindow.send('channelGraphData', { channelGraphData }))
call.on('end', () => log.info('end'))
call.on('error', error => error.code !== status.CANCELLED && log.error(error))
call.on('status', channelGraphStatus =>
mainWindow.send('channelGraphStatus', { channelGraphStatus })
)
return call
}

This file was deleted.

Oops, something went wrong.
@@ -1,8 +1,15 @@
import { status } from 'grpc'
export default function subscribeToInvoices(mainWindow, lnd, log) {
const call = lnd.subscribeInvoices({})
call.on('data', invoice => mainWindow.send('invoiceUpdate', { invoice }))
call.on('data', invoice => {
log.info('INVOICE:', invoice)
mainWindow.send('invoiceUpdate', { invoice })
})
call.on('end', () => log.info('end'))
call.on('error', error => log.error(error))
call.on('status', status => log.info('status:', status))
call.on('error', error => error.code !== status.CANCELLED && log.error(error))
call.on('status', status => log.info('INVOICE STATUS:', status))
return call
}
@@ -1,10 +1,15 @@
import { status } from 'grpc'
export default function subscribeToTransactions(mainWindow, lnd, log) {
const call = lnd.subscribeTransactions({})
call.on('data', transaction => {
log.info('TRANSACTION:', transaction)
mainWindow.send('newTransaction', { transaction })
})
call.on('end', () => log.info('end'))
call.on('error', error => log.error('error: ', error))
call.on('error', error => error.code !== status.CANCELLED && log.error(error))
call.on('status', status => log.info('TRANSACTION STATUS: ', status))
return call
}
@@ -33,7 +33,7 @@ export default function(walletUnlocker, log, event, msg, data) {
case 'initWallet':
walletController
.initWallet(walletUnlocker, data)
.then(() => event.sender.send('successfullyCreatedWallet'))
.then(() => event.sender.send('finishOnboarding'))
.catch(error => log.error('initWallet:', error))
break
default:
@@ -14,19 +14,14 @@ import ZapUpdater from './updater'
// Set up a couple of timers to track the app startup progress.
mainLog.time('Time until app is ready')
mainLog.time('Time until lnd process lookup finished')
/**
* Initialize Zap as soon as electron is ready.
*/
app.on('ready', () => {
mainLog.timeEnd('Time until app is ready')
// Start a couple more timers to track the app loading time.
mainLog.time('Time until app is visible')
mainLog.time('Time until onboarding has started')
// Create the electron browser window.
// Create a new browser window.
const mainWindow = new BrowserWindow({
show: false,
titleBarStyle: 'hidden',
@@ -71,22 +66,11 @@ app.on('ready', () => {
mainLog.error
)
mainWindow.webContents.once('dom-ready', () => {
mainWindow.openDevTools()
zap.mainWindow.webContents.once('dom-ready', () => {
zap.mainWindow.openDevTools()
})
}
/**
* Add application event listener:
* - Kill lnd process is killed when the app quits.
*/
app.on('quit', () => {
mainLog.debug('app.quit')
if (zap.neutrino) {
zap.neutrino.stop()
}
})
/**
* Add application event listener:
* - Open zap payment form when lightning url is opened
@@ -97,7 +81,7 @@ app.on('ready', () => {
event.preventDefault()
const payreq = url.split(':')[1]
zap.sendMessage('lightningPaymentUri', { payreq })
mainWindow.show()
zap.mainWindow.show()
})
// HACK: patch webrequest to fix devtools incompatibility with electron 2.x.
@@ -126,4 +110,24 @@ app.on('ready', () => {
app.quit()
}
})
/**
* Add application event listener:
* - Stop gRPC and kill lnd process before the app windows are closed and the app quits.
*/
app.on('before-quit', async event => {
if (zap.state !== 'terminated') {
event.preventDefault()
zap.terminate()
} else {
zap.mainWindow.forceClose = true
}
})
/**
* On OS X it's common to re-open a window in the app when the dock icon is clicked.
*/
app.on('activate', () => {
zap.mainWindow.show()
})
})
@@ -1,7 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import { btc } from 'utils'
import { showNotification } from 'notifications'
import { showNotification } from '../notifications'
import { btc } from '../utils'
import { requestSuggestedNodes } from '../api'
import { setError } from './error'
// ------------------------------------
@@ -44,7 +44,7 @@ import {
walletUnlockerGrpcActive,
receiveSeed,
receiveSeedError,
successfullyCreatedWallet,
finishOnboarding,
walletUnlocked,
unlockWalletError
} from './onboarding'
@@ -111,7 +111,7 @@ const ipc = createIpc({
walletUnlockerGrpcActive,
receiveSeed,
receiveSeedError,
successfullyCreatedWallet,
finishOnboarding,
walletUnlocked,
unlockWalletError
})
Oops, something went wrong.

0 comments on commit b180287

Please sign in to comment.