diff --git a/package.json b/package.json index f997d1cc..26b6e59e 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "invariant": "^2.2.4", "lodash": "^4.17.5", "moment": "^2.22.1", - "object-path": "^0.11.4", "qrcode": "^1.2.0", "qrcode-reader": "^1.0.4", "qs": "^6.5.1", diff --git a/src/actions/devices.js b/src/actions/devices.js index 751d4b4c..4f745527 100644 --- a/src/actions/devices.js +++ b/src/actions/devices.js @@ -19,3 +19,7 @@ export const removeDevice: RemoveDevice = payload => ({ type: 'REMOVE_DEVICE', payload, }) + +export const resetDevices = () => ({ + type: 'RESET_DEVICES', +}) diff --git a/src/commands/getAddress.js b/src/commands/getAddress.js index 6029b1e7..2e0ae740 100644 --- a/src/commands/getAddress.js +++ b/src/commands/getAddress.js @@ -20,7 +20,6 @@ type Result = { } const cmd: Command = createCommand( - 'devices', 'getAddress', ({ currencyId, devicePath, path, ...options }) => fromPromise( diff --git a/src/commands/getDeviceInfo.js b/src/commands/getDeviceInfo.js index c2fc4bd2..767f27f9 100644 --- a/src/commands/getDeviceInfo.js +++ b/src/commands/getDeviceInfo.js @@ -17,7 +17,7 @@ type Result = { mcu: boolean, } -const cmd: Command = createCommand('devices', 'getDeviceInfo', ({ devicePath }) => +const cmd: Command = createCommand('getDeviceInfo', ({ devicePath }) => fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))), ) diff --git a/src/commands/getFirmwareInfo.js b/src/commands/getFirmwareInfo.js index d20e7a12..dd1bdd42 100644 --- a/src/commands/getFirmwareInfo.js +++ b/src/commands/getFirmwareInfo.js @@ -12,7 +12,7 @@ type Input = { type Result = * -const cmd: Command = createCommand('devices', 'getFirmwareInfo', data => +const cmd: Command = createCommand('getFirmwareInfo', data => fromPromise(getFirmwareInfo(data)), ) diff --git a/src/commands/getIsGenuine.js b/src/commands/getIsGenuine.js index 9c5a0dbe..25f94d13 100644 --- a/src/commands/getIsGenuine.js +++ b/src/commands/getIsGenuine.js @@ -8,8 +8,6 @@ import getIsGenuine from 'helpers/devices/getIsGenuine' type Input = * type Result = boolean -const cmd: Command = createCommand('devices', 'getIsGenuine', () => - fromPromise(getIsGenuine()), -) +const cmd: Command = createCommand('getIsGenuine', () => fromPromise(getIsGenuine())) export default cmd diff --git a/src/commands/getLatestFirmwareForDevice.js b/src/commands/getLatestFirmwareForDevice.js index 7c8bc414..7a9621c1 100644 --- a/src/commands/getLatestFirmwareForDevice.js +++ b/src/commands/getLatestFirmwareForDevice.js @@ -12,7 +12,7 @@ type Input = { type Result = * -const cmd: Command = createCommand('devices', 'getLatestFirmwareForDevice', data => +const cmd: Command = createCommand('getLatestFirmwareForDevice', data => fromPromise(getLatestFirmwareForDevice(data)), ) diff --git a/src/commands/getMemInfo.js b/src/commands/getMemInfo.js index 8b175181..848fa032 100644 --- a/src/commands/getMemInfo.js +++ b/src/commands/getMemInfo.js @@ -12,7 +12,7 @@ type Input = { type Result = * -const cmd: Command = createCommand('devices', 'getMemInfo', ({ devicePath }) => +const cmd: Command = createCommand('getMemInfo', ({ devicePath }) => fromPromise(withDevice(devicePath)(transport => getMemInfo(transport))), ) diff --git a/src/internals/devices/index.js b/src/commands/index.js similarity index 75% rename from src/internals/devices/index.js rename to src/commands/index.js index 4f830a2e..1b4e3abe 100644 --- a/src/internals/devices/index.js +++ b/src/commands/index.js @@ -1,6 +1,8 @@ // @flow + import type { Command } from 'helpers/ipc' +import getMemInfo from 'commands/getMemInfo' import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import getAddress from 'commands/getAddress' @@ -16,8 +18,13 @@ import installOsuFirmware from 'commands/installOsuFirmware' import installFinalFirmware from 'commands/installFinalFirmware' import installMcu from 'commands/installMcu' import listApps from 'commands/listApps' +import testInterval from 'commands/testInterval' +import testCrash from 'commands/testCrash' -export const commands: Array> = [ +const all: Array> = [ + getMemInfo, + libcoreScanAccounts, + libcoreSignAndBroadcast, getAddress, signTransaction, getDeviceInfo, @@ -25,12 +32,20 @@ export const commands: Array> = [ getIsGenuine, getLatestFirmwareForDevice, installApp, - libcoreScanAccounts, - libcoreSignAndBroadcast, listenDevices, uninstallApp, installOsuFirmware, installFinalFirmware, installMcu, listApps, + testInterval, + testCrash, ] + +all.forEach(cmd => { + if (all.some(c => c !== cmd && c.id === cmd.id)) { + throw new Error(`duplicate command '${cmd.id}'`) + } +}) + +export default all diff --git a/src/commands/installApp.js b/src/commands/installApp.js index 792a5661..c4f1df13 100644 --- a/src/commands/installApp.js +++ b/src/commands/installApp.js @@ -15,11 +15,8 @@ type Input = { type Result = * -const cmd: Command = createCommand( - 'devices', - 'installApp', - ({ devicePath, ...rest }) => - fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))), +const cmd: Command = createCommand('installApp', ({ devicePath, ...rest }) => + fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))), ) export default cmd diff --git a/src/commands/installFinalFirmware.js b/src/commands/installFinalFirmware.js index 775dd743..ff9f677f 100644 --- a/src/commands/installFinalFirmware.js +++ b/src/commands/installFinalFirmware.js @@ -19,7 +19,6 @@ type Result = { } const cmd: Command = createCommand( - 'devices', 'installFinalFirmware', ({ devicePath, firmware }) => fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, firmware))), diff --git a/src/commands/installMcu.js b/src/commands/installMcu.js index b1fa57ed..c16387dc 100644 --- a/src/commands/installMcu.js +++ b/src/commands/installMcu.js @@ -21,8 +21,6 @@ import installMcu from 'helpers/firmware/installMcu' type Input = * type Result = * -const cmd: Command = createCommand('devices', 'installMcu', () => - fromPromise(installMcu()), -) +const cmd: Command = createCommand('installMcu', () => fromPromise(installMcu())) export default cmd diff --git a/src/commands/installOsuFirmware.js b/src/commands/installOsuFirmware.js index 8d4a6f4f..7b397a55 100644 --- a/src/commands/installOsuFirmware.js +++ b/src/commands/installOsuFirmware.js @@ -19,7 +19,6 @@ type Result = { } const cmd: Command = createCommand( - 'devices', 'installOsuFirmware', ({ devicePath, firmware }) => fromPromise(withDevice(devicePath)(transport => installOsuFirmware(transport, firmware))), diff --git a/src/commands/libcoreScanAccounts.js b/src/commands/libcoreScanAccounts.js index 0cb7fb6c..c20ab6d1 100644 --- a/src/commands/libcoreScanAccounts.js +++ b/src/commands/libcoreScanAccounts.js @@ -13,7 +13,6 @@ type Input = { type Result = AccountRaw const cmd: Command = createCommand( - 'devices', 'libcoreScanAccounts', ({ devicePath, currencyId }) => Observable.create(o => { diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index 9e3bea3f..5727b9f7 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -23,7 +23,6 @@ type Input = { type Result = $Exact const cmd: Command = createCommand( - 'devices', 'libcoreSignAndBroadcast', ({ account, transaction, deviceId }) => { // TODO: investigate why importing it on file scope causes trouble diff --git a/src/commands/listApps.js b/src/commands/listApps.js index ecffac64..b600aef4 100644 --- a/src/commands/listApps.js +++ b/src/commands/listApps.js @@ -11,7 +11,7 @@ type Input = { type Result = * -const cmd: Command = createCommand('devices', 'listApps', ({ targetId }) => +const cmd: Command = createCommand('listApps', ({ targetId }) => fromPromise(listApps(targetId)), ) diff --git a/src/commands/listenDevices.js b/src/commands/listenDevices.js index d3ea3aeb..e523750a 100644 --- a/src/commands/listenDevices.js +++ b/src/commands/listenDevices.js @@ -4,6 +4,6 @@ import { createCommand } from 'helpers/ipc' import { Observable } from 'rxjs' import CommNodeHid from '@ledgerhq/hw-transport-node-hid' -const cmd = createCommand('devices', 'listenDevices', () => Observable.create(CommNodeHid.listen)) +const cmd = createCommand('listenDevices', () => Observable.create(CommNodeHid.listen)) export default cmd diff --git a/src/commands/signTransaction.js b/src/commands/signTransaction.js index dfc1cfcb..7ffea26b 100644 --- a/src/commands/signTransaction.js +++ b/src/commands/signTransaction.js @@ -15,7 +15,6 @@ type Input = { type Result = string const cmd: Command = createCommand( - 'devices', 'signTransaction', ({ currencyId, devicePath, path, transaction }) => fromPromise( diff --git a/src/commands/testCrash.js b/src/commands/testCrash.js new file mode 100644 index 00000000..0725988a --- /dev/null +++ b/src/commands/testCrash.js @@ -0,0 +1,17 @@ +// @flow + +// This is a test example for dev testing purpose. + +import { Observable } from 'rxjs' +import { createCommand, Command } from 'helpers/ipc' + +type Input = void +type Result = void + +const cmd: Command = createCommand('testCrash', () => + Observable.create(() => { + process.exit(1) + }), +) + +export default cmd diff --git a/src/commands/testInterval.js b/src/commands/testInterval.js new file mode 100644 index 00000000..d6a8aab9 --- /dev/null +++ b/src/commands/testInterval.js @@ -0,0 +1,13 @@ +// @flow + +// This is a test example for dev testing purpose. + +import { interval } from 'rxjs/observable/interval' +import { createCommand, Command } from 'helpers/ipc' + +type Input = number +type Result = number + +const cmd: Command = createCommand('testInterval', interval) + +export default cmd diff --git a/src/commands/uninstallApp.js b/src/commands/uninstallApp.js index a07a2e8f..e8e3b3f7 100644 --- a/src/commands/uninstallApp.js +++ b/src/commands/uninstallApp.js @@ -15,11 +15,8 @@ type Input = { type Result = * -const cmd: Command = createCommand( - 'devices', - 'uninstallApp', - ({ devicePath, ...rest }) => - fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, rest))), +const cmd: Command = createCommand('uninstallApp', ({ devicePath, ...rest }) => + fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, rest))), ) export default cmd diff --git a/src/components/ManagerPage/DeviceInfos.js b/src/components/ManagerPage/DeviceInfos.js index aae54293..df2c87c2 100644 --- a/src/components/ManagerPage/DeviceInfos.js +++ b/src/components/ManagerPage/DeviceInfos.js @@ -2,8 +2,6 @@ import React, { PureComponent } from 'react' -import runJob from 'renderer/runJob' - import Text from 'components/base/Text' import Box, { Card } from 'components/base/Box' import Button from 'components/base/Button' @@ -30,16 +28,7 @@ class DeviceInfos extends PureComponent { handleGetMemInfos = async () => { try { this.setState({ isLoading: true }) - const { - device: { path: devicePath }, - } = this.props - const memoryInfos = await runJob({ - channel: 'manager', - job: 'getMemInfos', - successResponse: 'manager.getMemInfosSuccess', - errorResponse: 'manager.getMemInfosError', - data: { devicePath }, - }) + const memoryInfos = null // TODO this.setState({ memoryInfos, isLoading: false }) } catch (err) { this.setState({ isLoading: false }) diff --git a/src/components/ManagerPage/FinalFirmwareUpdate.js b/src/components/ManagerPage/FinalFirmwareUpdate.js index 86c128c8..defaac39 100644 --- a/src/components/ManagerPage/FinalFirmwareUpdate.js +++ b/src/components/ManagerPage/FinalFirmwareUpdate.js @@ -4,8 +4,6 @@ import React, { PureComponent } from 'react' import { translate } from 'react-i18next' import type { Device, T } from 'types/common' -// import runJob from 'renderer/runJob' - import Box, { Card } from 'components/base/Box' // import Button from 'components/base/Button' @@ -37,28 +35,6 @@ class FirmwareUpdate extends PureComponent { _unmounting = false - // handleInstallFinalFirmware = async () => { - // try { - // const { latestFirmware } = this.state - // this.setState(state => ({ ...state, installing: true })) - // const { - // device: { path: devicePath }, - // } = this.props - // await runJob({ - // channel: 'manager', - // job: 'installFinalFirmware', - // successResponse: 'device.finalFirmwareInstallSuccess', - // errorResponse: 'device.finalFirmwareInstallError', - // data: { - // devicePath, - // firmware: latestFirmware, - // }, - // }) - // } catch (err) { - // console.log(err) - // } - // } - render() { const { t, ...props } = this.props diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index 84d95ac2..0391e283 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -6,7 +6,6 @@ import isEmpty from 'lodash/isEmpty' import type { Device, T } from 'types/common' -import runJob from 'renderer/runJob' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import Box, { Card } from 'components/base/Box' @@ -74,20 +73,7 @@ class FirmwareUpdate extends PureComponent { installFirmware = async () => { try { - const { latestFirmware } = this.state - const { - device: { path: devicePath }, - } = this.props - await runJob({ - channel: 'manager', - job: 'installOsuFirmware', - successResponse: 'device.osuFirmwareInstallSuccess', - errorResponse: 'device.osuFirmwareInstallError', - data: { - devicePath, - firmware: latestFirmware, - }, - }) + // TODO } catch (err) { console.log(err) } diff --git a/src/components/UpdateNotifier.js b/src/components/UpdateNotifier.js index 04778ee2..92dfd3dc 100644 --- a/src/components/UpdateNotifier.js +++ b/src/components/UpdateNotifier.js @@ -66,7 +66,7 @@ class UpdateNotifier extends PureComponent { sendEvent('msg', 'updater.quitAndInstall')} + onClick={() => sendEvent('updater', 'quitAndInstall')} > {t('update:relaunch')} diff --git a/src/components/modals/Debug.js b/src/components/modals/Debug.js index ae6769e0..98a4386c 100644 --- a/src/components/modals/Debug.js +++ b/src/components/modals/Debug.js @@ -8,12 +8,26 @@ import Box from 'components/base/Box' import EnsureDevice from 'components/ManagerPage/EnsureDevice' import { getDerivations } from 'helpers/derivations' import getAddress from 'commands/getAddress' +import testInterval from 'commands/testInterval' +import testCrash from 'commands/testCrash' class Debug extends Component<*, *> { state = { logs: [], } + onStartPeriod = (period: number) => () => { + this.periodSubs.push( + testInterval.send(period).subscribe(n => this.log(`interval ${n}`), this.error), + ) + } + + onCrash = () => { + testCrash.send().subscribe({ + error: this.error, + }) + } + onClickStressDevice = (device: *) => async () => { try { const currency = getCryptoCurrencyById('bitcoin') @@ -42,6 +56,12 @@ class Debug extends Component<*, *> { this.setState({ logs: [] }) } + cancelAllPeriods = () => { + this.periodSubs.forEach(s => s.unsubscribe()) + this.periodSubs = [] + } + periodSubs = [] + log = (txt: string) => { this.setState(({ logs }) => ({ logs: logs.concat({ txt, type: 'log' }) })) } @@ -60,17 +80,30 @@ class Debug extends Component<*, *> { onHide={this.onHide} render={({ onClose }: *) => ( - DEBUG utils + developer internal tools - - {device => ( - - - - )} - + + + + {device => ( + + )} + + + + + + + + + + { ))} + )} diff --git a/src/helpers/deviceAccess.js b/src/helpers/deviceAccess.js index 637faaa4..a077248e 100644 --- a/src/helpers/deviceAccess.js +++ b/src/helpers/deviceAccess.js @@ -2,7 +2,6 @@ import createSemaphore from 'semaphore' import type Transport from '@ledgerhq/hw-transport' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { retry } from './promise' // all open to device must use openDevice so we can prevent race conditions // and guarantee we do one device access at a time. It also will handle the .close() @@ -13,19 +12,12 @@ type WithDevice = (devicePath: string) => (job: (Transport<*>) => Promise) const semaphorePerDevice = {} export const withDevice: WithDevice = devicePath => { - const { FORK_TYPE } = process.env - if (FORK_TYPE !== 'devices') { - console.warn( - `deviceAccess is only expected to be used in process 'devices'. Any other usage may lead to race conditions. (Got: '${FORK_TYPE}')`, - ) - } - const sem = semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1)) return job => takeSemaphorePromise(sem, async () => { - const t = await retry(() => TransportNodeHid.open(devicePath)) + const t = await TransportNodeHid.open(devicePath) try { const res = await job(t) // $FlowFixMe diff --git a/src/helpers/generic.js b/src/helpers/generic.js deleted file mode 100644 index f3798ba4..00000000 --- a/src/helpers/generic.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable no-bitwise */ - -import bitcoin from 'bitcoinjs-lib' -import bs58 from 'bs58' - -export function toHexDigit(number) { - const digits = '0123456789abcdef' - return digits.charAt(number >> 4) + digits.charAt(number & 0x0f) -} - -export function toHexInt(number) { - return ( - toHexDigit((number >> 24) & 0xff) + - toHexDigit((number >> 16) & 0xff) + - toHexDigit((number >> 8) & 0xff) + - toHexDigit(number & 0xff) - ) -} - -export function encodeBase58Check(vchIn) { - // vchIn = parseHexString(vchIn) - let chksum = bitcoin.crypto.sha256(vchIn) - chksum = bitcoin.crypto.sha256(chksum) - chksum = chksum.slice(0, 4) - const hash = vchIn.concat(Array.from(chksum)) - return bs58.encode(hash) -} - -export function parseHexString(str) { - const result = [] - while (str.length >= 2) { - result.push(parseInt(str.substring(0, 2), 16)) - str = str.substring(2, str.length) - } - return result -} diff --git a/src/helpers/ipc.js b/src/helpers/ipc.js index 3494dabc..f78bb81c 100644 --- a/src/helpers/ipc.js +++ b/src/helpers/ipc.js @@ -2,118 +2,104 @@ import { Observable } from 'rxjs' import uuidv4 from 'uuid/v4' -type Msg = { - type: string, - data?: A, - options?: *, -} - -function send(msg: Msg) { - process.send(msg) +export function createCommand(id: string, impl: In => Observable): Command { + return new Command(id, impl) } export class Command { - channel: string - type: string id: string impl: In => Observable - constructor(channel: string, type: string, impl: In => Observable) { - this.channel = channel - this.type = type - this.id = `${channel}.${type}` - this.impl = impl - } - // ~~~ On exec side we can: - - exec(data: In, requestId: string) { - return this.impl(data).subscribe({ - next: (data: A) => { - send({ - type: `NEXT_${requestId}`, - data, - }) - }, - complete: () => { - send({ - type: `COMPLETE_${requestId}`, - options: { kill: true }, - }) - }, - error: error => { - console.log('exec error:', error) - send({ - type: `ERROR_${requestId}`, - data: { - name: error && error.name, - message: error && error.message, - }, - options: { kill: true }, - }) - }, - }) + constructor(id: string, impl: In => Observable) { + this.id = id + this.impl = impl } - // ~~~ On renderer side we can: - /** * Usage example: - * sub = send(data).subscribe({ next: ... }) + * sub = cmd.send(data).subscribe({ next: ... }) * // or - * const res = await send(data).toPromise() + * const res = await cmd.send(data).toPromise() */ send(data: In): Observable { - const { ipcRenderer } = require('electron') - return Observable.create(o => { - const { channel, type, id } = this - const requestId: string = uuidv4() + return ipcRendererSendCommand(this.id, data) + } +} - const unsubscribe = () => { - ipcRenderer.removeListener('msg', handleMsgEvent) - } +type Msg = { + type: 'NEXT' | 'COMPLETE' | 'ERROR', + requestId: string, + data?: A, +} - function handleMsgEvent(e, msg: Msg) { - switch (msg.type) { - case `NEXT_${requestId}`: - if (msg.data) { - o.next(msg.data) - } - break - - case `COMPLETE_${requestId}`: - o.complete() - unsubscribe() - break - - case `ERROR_${requestId}`: - o.error(msg.data) - unsubscribe() - break - - default: - } +// Implements command message of (Renderer proc -> Main proc) +function ipcRendererSendCommand(id: string, data: In): Observable { + const { ipcRenderer } = require('electron') + return Observable.create(o => { + const requestId: string = uuidv4() + + const unsubscribe = () => { + ipcRenderer.send('command-unsubscribe', { requestId }) + ipcRenderer.removeListener('command-event', handleCommandEvent) + } + + function handleCommandEvent(e, msg: Msg) { + if (requestId !== msg.requestId) return + switch (msg.type) { + case 'NEXT': + if (msg.data) { + o.next(msg.data) + } + break + + case 'COMPLETE': + o.complete() + ipcRenderer.removeListener('command-event', handleCommandEvent) + break + + case 'ERROR': + o.error(msg.data) + ipcRenderer.removeListener('command-event', handleCommandEvent) + break + + default: } + } + + ipcRenderer.on('command-event', handleCommandEvent) - ipcRenderer.on('msg', handleMsgEvent) + ipcRenderer.send('command', { id, data, requestId }) - ipcRenderer.send(channel, { - type, - data: { - id, - data, - requestId, - }, - }) + return unsubscribe + }) +} - return unsubscribe +// Implements command message of (Main proc -> Renderer proc) +// (dual of ipcRendererSendCommand) +export function ipcMainListenReceiveCommands(o: { + onUnsubscribe: (requestId: string) => void, + onCommand: ( + command: { id: string, data: *, requestId: string }, + notifyCommandEvent: (Msg<*>) => void, + ) => void, +}) { + const { ipcMain } = require('electron') + + const onCommandUnsubscribe = (event, { requestId }) => { + o.onUnsubscribe(requestId) + } + + const onCommand = (event, command) => { + o.onCommand(command, payload => { + event.sender.send('command-event', payload) }) } -} -export function createCommand( - channel: string, - type: string, - impl: In => Observable, -): Command { - return new Command(channel, type, impl) + ipcMain.on('command-unsubscribe', onCommandUnsubscribe) + ipcMain.on('command', onCommand) + + return () => { + ipcMain.removeListener('command-unsubscribe', onCommandUnsubscribe) + ipcMain.removeListener('command', onCommand) + } } diff --git a/src/internals/index.js b/src/internals/index.js index 36ec3c28..2e5fe74d 100644 --- a/src/internals/index.js +++ b/src/internals/index.js @@ -1,44 +1,58 @@ // @flow - -import objectPath from 'object-path' -import capitalize from 'lodash/capitalize' +import commands from 'commands' require('../env') require('../init-sentry') -const { FORK_TYPE } = process.env - -process.title = `${require('../../package.json').productName} ${capitalize(FORK_TYPE)}` - -function sendEvent(type: string, data: any, options: Object = { kill: true }) { - process.send({ type, data, options }) -} +process.title = 'Internal' -// $FlowFixMe -let handlers = require(`./${FORK_TYPE}`) // eslint-disable-line import/no-dynamic-require -// handle babel export object syntax -if (handlers.default) { - handlers = handlers.default -} +const subscriptions = {} -process.on('message', payload => { - if (payload.data && payload.data.requestId) { - const { data, requestId, id } = payload.data - // this is the new type of "command" payload! - const cmd = (handlers.commands || []).find(cmd => cmd.id === id) +process.on('message', m => { + console.log(m) + if (m.type === 'command') { + const { data, requestId, id } = m.command + const cmd = commands.find(cmd => cmd.id === id) if (!cmd) { console.warn(`command ${id} not found`) - } else { - cmd.exec(data, requestId) - } - } else { - // this will be deprecated! - const { type, data } = payload - const handler = objectPath.get(handlers, type) - if (!handler) { - console.warn(`No handler found for ${type}`) return } - handler(sendEvent, data) + subscriptions[requestId] = cmd.impl(data).subscribe({ + next: data => { + process.send({ + type: 'NEXT', + requestId, + data, + }) + }, + complete: () => { + delete subscriptions[requestId] + process.send({ + type: 'COMPLETE', + requestId, + }) + }, + error: error => { + console.warn('Command error:', error) + delete subscriptions[requestId] + process.send({ + type: 'ERROR', + requestId, + data: { + name: error && error.name, + message: error && error.message, + }, + }) + }, + }) + } else if (m.type === 'command-unsubscribe') { + const { requestId } = m + const sub = subscriptions[requestId] + if (sub) { + sub.unsubscribe() + delete subscriptions[requestId] + } } }) + +console.log('Internal process is ready!') diff --git a/src/internals/manager/index.js b/src/internals/manager/index.js deleted file mode 100644 index cb51bae3..00000000 --- a/src/internals/manager/index.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import type { Command } from 'helpers/ipc' - -import getMemInfo from 'commands/getMemInfo' - -/** - * Manager - * ------- - * - * xXx - * xXx - * xXx - * xxxXxxx - * xxXxx - * xXx - * xX x Xx - * xX Xx - * xxXXXXXXXxx - * - */ - -export const commands: Array> = [getMemInfo] diff --git a/src/main/autoUpdate.js b/src/main/autoUpdate.js index 76c71ff7..1b50e078 100644 --- a/src/main/autoUpdate.js +++ b/src/main/autoUpdate.js @@ -6,12 +6,12 @@ import { autoUpdater } from 'electron-updater' type SendFunction = (type: string, data: *) => void export default (notify: SendFunction) => { - autoUpdater.on('checking-for-update', () => notify('updater.checking')) - autoUpdater.on('update-available', info => notify('updater.updateAvailable', info)) - autoUpdater.on('update-not-available', () => notify('updater.updateNotAvailable')) - autoUpdater.on('error', err => notify('updater.error', err)) - autoUpdater.on('download-progress', progress => notify('updater.downloadProgress', progress)) - autoUpdater.on('update-downloaded', () => notify('updater.downloaded')) + autoUpdater.on('checking-for-update', () => notify('checking')) + autoUpdater.on('update-available', info => notify('updateAvailable', info)) + autoUpdater.on('update-not-available', () => notify('updateNotAvailable')) + autoUpdater.on('error', err => notify('error', err)) + autoUpdater.on('download-progress', progress => notify('downloadProgress', progress)) + autoUpdater.on('update-downloaded', () => notify('downloaded')) autoUpdater.checkForUpdatesAndNotify() } diff --git a/src/main/bridge.js b/src/main/bridge.js index efe03485..d5aca91d 100644 --- a/src/main/bridge.js +++ b/src/main/bridge.js @@ -1,100 +1,89 @@ // @flow import '@babel/polyfill' +import invariant from 'invariant' import { fork } from 'child_process' -import { BrowserWindow, ipcMain, app } from 'electron' -import objectPath from 'object-path' +import { ipcMain, app } from 'electron' +import { ipcMainListenReceiveCommands } from 'helpers/ipc' import path from 'path' import setupAutoUpdater, { quitAndInstall } from './autoUpdate' -const { DEV_TOOLS } = process.env - // sqlite files will be located in the app local data folder const LEDGER_LIVE_SQLITE_PATH = path.resolve(app.getPath('userData'), 'sqlite') -const processes = [] +let internalProcess -function cleanProcesses() { - processes.forEach(kill => kill()) +const killInternalProcess = () => { + if (internalProcess) { + console.log('killing internal process...') + internalProcess.kill('SIGINT') + internalProcess = null + } } -function sendEventToWindow(name, { type, data }) { - const anotherWindow = BrowserWindow.getAllWindows().find(w => w.name === name) - if (anotherWindow) { - anotherWindow.webContents.send('msg', { type, data }) - } +const forkBundlePath = path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`) + +const bootInternalProcess = () => { + console.log('booting internal process...') + internalProcess = fork(forkBundlePath, { + env: { LEDGER_LIVE_SQLITE_PATH }, + }) + internalProcess.on('exit', code => { + console.log(`Internal process ended with code ${code}`) + internalProcess = null + }) } -function onForkChannel(forkType) { - return (event: any, payload) => { - const { type, data } = payload +process.on('exit', () => { + killInternalProcess() +}) - let compute = fork(path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`), { - env: { - DEV_TOOLS, - FORK_TYPE: forkType, - LEDGER_LIVE_SQLITE_PATH, - }, - }) +ipcMain.on('clean-processes', () => { + killInternalProcess() +}) - const kill = () => { - if (compute) { - compute.kill('SIGINT') - compute = null - } +ipcMainListenReceiveCommands({ + onUnsubscribe: requestId => { + if (!internalProcess) return + internalProcess.send({ type: 'command-unsubscribe', requestId }) + }, + onCommand: (command, notifyCommandEvent) => { + if (!internalProcess) bootInternalProcess() + const p = internalProcess + invariant(p, 'internalProcess not started !?') + + const handleExit = code => { + p.removeListener('message', handleMessage) + p.removeListener('exit', handleExit) + notifyCommandEvent({ + type: 'ERROR', + requestId: command.requestId, + data: { message: `Internal process error (${code})`, name: 'InternalError' }, + }) } - processes.push(kill) - - const onMessage = payload => { - const { type, data, options = {} } = payload - - if (options.window) { - sendEventToWindow(options.window, { type, data }) - } else { - event.sender.send('msg', { type, data }) - } - if (options.kill && compute) { - kill() + const handleMessage = payload => { + if (payload.requestId !== command.requestId) return + notifyCommandEvent(payload) + if (payload.type === 'ERROR' || payload.type === 'COMPLETE') { + p.removeListener('message', handleMessage) + p.removeListener('exit', handleExit) } } - compute.on('message', onMessage) - compute.send({ type, data }) - - process.on('exit', kill) - } -} - -// Forwards every `type` messages to another process -ipcMain.on('devices', onForkChannel('devices')) -ipcMain.on('accounts', onForkChannel('accounts')) -ipcMain.on('manager', onForkChannel('manager')) - -ipcMain.on('clean-processes', cleanProcesses) + p.on('exit', handleExit) + p.on('message', handleMessage) + p.send({ type: 'command', command }) + }, +}) -const handlers = { - updater: { +// TODO move this to "command" pattern +ipcMain.on('updater', (event, { type, data }) => { + const handler = { init: setupAutoUpdater, quitAndInstall, - }, - kill: { - process: (send, { pid }) => { - try { - process.kill(pid, 'SIGINT') - } catch (e) {} // eslint-disable-line no-empty - }, - }, -} - -ipcMain.on('msg', (event: any, payload) => { - const { type, data } = payload - const handler = objectPath.get(handlers, type) - if (!handler) { - console.warn(`No handler found for ${type}`) - return - } - const send = (type: string, data: *) => event.sender.send('msg', { type, data }) + }[type] + const send = (type: string, data: *) => event.sender.send('updater', { type, data }) handler(send, data, type) }) diff --git a/src/reducers/devices.js b/src/reducers/devices.js index ed2154da..430eda36 100644 --- a/src/reducers/devices.js +++ b/src/reducers/devices.js @@ -9,7 +9,7 @@ export type DevicesState = { devices: Device[], } -const state: DevicesState = { +const initialState: DevicesState = { currentDevice: null, devices: [], } @@ -20,6 +20,7 @@ function setCurrentDevice(state) { } const handlers: Object = { + RESET_DEVICES: () => initialState, ADD_DEVICE: (state: DevicesState, { payload: device }: { payload: Device }) => setCurrentDevice({ ...state, @@ -49,4 +50,4 @@ export function getDevices(state: { devices: DevicesState }) { return state.devices.devices } -export default handleActions(handlers, state) +export default handleActions(handlers, initialState) diff --git a/src/renderer/events.js b/src/renderer/events.js index 1833ea72..d5987f7e 100644 --- a/src/renderer/events.js +++ b/src/renderer/events.js @@ -8,20 +8,19 @@ // both of these implementation should have a unique requestId to ensure there is no collision // events should all appear in the promise result / observer msgs as soon as they have this requestId +import 'commands' + import { ipcRenderer } from 'electron' -import objectPath from 'object-path' import debug from 'debug' import { CHECK_UPDATE_DELAY } from 'config/constants' import { setUpdateStatus } from 'reducers/update' -import { addDevice, removeDevice } from 'actions/devices' +import { addDevice, removeDevice, resetDevices } from 'actions/devices' import listenDevices from 'commands/listenDevices' -import i18n from 'renderer/i18n/electron' - const d = { device: debug('lwd:device'), sync: debug('lwd:sync'), @@ -33,6 +32,7 @@ type MsgPayload = { data: any, } +// TODO port remaining to command pattern export function sendEvent(channel: string, msgType: string, data: any) { ipcRenderer.send(channel, { type: msgType, @@ -40,61 +40,71 @@ export function sendEvent(channel: string, msgType: string, data: any) { }) } -export function sendSyncEvent(channel: string, msgType: string, data: any): any { - return ipcRenderer.sendSync(`${channel}:sync`, { - type: msgType, - data, - }) -} - +let syncDeviceSub export default ({ store }: { store: Object, locked: boolean }) => { - const handlers = { - dispatch: ({ type, payload }) => store.dispatch({ type, payload }), - application: { - changeLanguage: lang => i18n.changeLanguage(lang), - }, - updater: { + // Ensure all sub-processes are killed before creating new ones (dev mode...) + ipcRenderer.send('clean-processes') + + if (syncDeviceSub) { + syncDeviceSub.unsubscribe() + syncDeviceSub = null + } + + function syncDevices() { + syncDeviceSub = listenDevices.send().subscribe( + ({ device, type }) => { + if (device) { + if (type === 'add') { + d.device('Device - add') + store.dispatch(addDevice(device)) + } else if (type === 'remove') { + d.device('Device - remove') + store.dispatch(removeDevice(device)) + } + } + }, + error => { + console.warn('listenDevices error', error) + store.dispatch(resetDevices()) + syncDevices() + }, + () => { + console.warn('listenDevices ended unexpectedly. restarting') + store.dispatch(resetDevices()) + syncDevices() + }, + ) + } + + syncDevices() + + if (__PROD__) { + // TODO move this to "command" pattern + const updaterHandlers = { checking: () => store.dispatch(setUpdateStatus('checking')), updateAvailable: info => store.dispatch(setUpdateStatus('available', info)), updateNotAvailable: () => store.dispatch(setUpdateStatus('unavailable')), error: err => store.dispatch(setUpdateStatus('error', err)), downloadProgress: progress => store.dispatch(setUpdateStatus('progress', progress)), downloaded: () => store.dispatch(setUpdateStatus('downloaded')), - }, - } - ipcRenderer.on('msg', (event: any, payload: MsgPayload) => { - const { type, data } = payload - const handler = objectPath.get(handlers, type) - if (!handler) { - return } - handler(data) - }) - - // Ensure all sub-processes are killed before creating new ones (dev mode...) - ipcRenderer.send('clean-processes') + ipcRenderer.on('updater', (event: any, payload: MsgPayload) => { + const { type, data } = payload + updaterHandlers[type](data) + }) - listenDevices.send().subscribe({ - next: ({ device, type }) => { - if (device) { - if (type === 'add') { - d.device('Device - add') - store.dispatch(addDevice(device)) - } else if (type === 'remove') { - d.device('Device - remove') - store.dispatch(removeDevice(device)) - } - } - }, - }) - - if (__PROD__) { // Start check of eventual updates checkUpdates() } } +if (module.hot) { + module.hot.accept('commands', () => { + ipcRenderer.send('clean-processes') + }) +} + export function checkUpdates() { d.update('Update - check') - setTimeout(() => sendEvent('msg', 'updater.init'), CHECK_UPDATE_DELAY) + setTimeout(() => sendEvent('updater', 'init'), CHECK_UPDATE_DELAY) } diff --git a/src/renderer/runJob.js b/src/renderer/runJob.js deleted file mode 100644 index b2fc8e64..00000000 --- a/src/renderer/runJob.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow - -import { ipcRenderer } from 'electron' - -export default function runJob({ - channel, - job, - successResponse, - errorResponse, - data, -}: { - channel: string, - job: string, - successResponse: string, - errorResponse: string, - data?: any, -}): Promise { - return new Promise((resolve, reject) => { - ipcRenderer.send(channel, { type: job, data }) - ipcRenderer.on('msg', handler) - function handler(e, res) { - const { type, data } = res - if (![successResponse, errorResponse].includes(type)) { - return - } - ipcRenderer.removeListener('msg', handler) - if (type === successResponse) { - resolve(data) - } else if (type === errorResponse) { - reject(data) - } - } - }) -}