Browse Source

Rework the process architecture & ipc

master
Gaëtan Renaudeau 7 years ago
parent
commit
231a2c78cb
  1. 1
      package.json
  2. 4
      src/actions/devices.js
  3. 1
      src/commands/getAddress.js
  4. 2
      src/commands/getDeviceInfo.js
  5. 2
      src/commands/getFirmwareInfo.js
  6. 4
      src/commands/getIsGenuine.js
  7. 2
      src/commands/getLatestFirmwareForDevice.js
  8. 2
      src/commands/getMemInfo.js
  9. 21
      src/commands/index.js
  10. 7
      src/commands/installApp.js
  11. 1
      src/commands/installFinalFirmware.js
  12. 4
      src/commands/installMcu.js
  13. 1
      src/commands/installOsuFirmware.js
  14. 1
      src/commands/libcoreScanAccounts.js
  15. 1
      src/commands/libcoreSignAndBroadcast.js
  16. 2
      src/commands/listApps.js
  17. 2
      src/commands/listenDevices.js
  18. 1
      src/commands/signTransaction.js
  19. 17
      src/commands/testCrash.js
  20. 13
      src/commands/testInterval.js
  21. 7
      src/commands/uninstallApp.js
  22. 13
      src/components/ManagerPage/DeviceInfos.js
  23. 24
      src/components/ManagerPage/FinalFirmwareUpdate.js
  24. 16
      src/components/ManagerPage/FirmwareUpdate.js
  25. 2
      src/components/UpdateNotifier.js
  26. 61
      src/components/modals/Debug.js
  27. 10
      src/helpers/deviceAccess.js
  28. 36
      src/helpers/generic.js
  29. 170
      src/helpers/ipc.js
  30. 76
      src/internals/index.js
  31. 22
      src/internals/manager/index.js
  32. 12
      src/main/autoUpdate.js
  33. 133
      src/main/bridge.js
  34. 5
      src/reducers/devices.js
  35. 100
      src/renderer/events.js
  36. 34
      src/renderer/runJob.js

1
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",

4
src/actions/devices.js

@ -19,3 +19,7 @@ export const removeDevice: RemoveDevice = payload => ({
type: 'REMOVE_DEVICE',
payload,
})
export const resetDevices = () => ({
type: 'RESET_DEVICES',
})

1
src/commands/getAddress.js

@ -20,7 +20,6 @@ type Result = {
}
const cmd: Command<Input, Result> = createCommand(
'devices',
'getAddress',
({ currencyId, devicePath, path, ...options }) =>
fromPromise(

2
src/commands/getDeviceInfo.js

@ -17,7 +17,7 @@ type Result = {
mcu: boolean,
}
const cmd: Command<Input, Result> = createCommand('devices', 'getDeviceInfo', ({ devicePath }) =>
const cmd: Command<Input, Result> = createCommand('getDeviceInfo', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))),
)

2
src/commands/getFirmwareInfo.js

@ -12,7 +12,7 @@ type Input = {
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'getFirmwareInfo', data =>
const cmd: Command<Input, Result> = createCommand('getFirmwareInfo', data =>
fromPromise(getFirmwareInfo(data)),
)

4
src/commands/getIsGenuine.js

@ -8,8 +8,6 @@ import getIsGenuine from 'helpers/devices/getIsGenuine'
type Input = *
type Result = boolean
const cmd: Command<Input, Result> = createCommand('devices', 'getIsGenuine', () =>
fromPromise(getIsGenuine()),
)
const cmd: Command<Input, Result> = createCommand('getIsGenuine', () => fromPromise(getIsGenuine()))
export default cmd

2
src/commands/getLatestFirmwareForDevice.js

@ -12,7 +12,7 @@ type Input = {
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'getLatestFirmwareForDevice', data =>
const cmd: Command<Input, Result> = createCommand('getLatestFirmwareForDevice', data =>
fromPromise(getLatestFirmwareForDevice(data)),
)

2
src/commands/getMemInfo.js

@ -12,7 +12,7 @@ type Input = {
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'getMemInfo', ({ devicePath }) =>
const cmd: Command<Input, Result> = createCommand('getMemInfo', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => getMemInfo(transport))),
)

21
src/internals/devices/index.js → 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<Command<any, any>> = [
const all: Array<Command<any, any>> = [
getMemInfo,
libcoreScanAccounts,
libcoreSignAndBroadcast,
getAddress,
signTransaction,
getDeviceInfo,
@ -25,12 +32,20 @@ export const commands: Array<Command<any, any>> = [
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

7
src/commands/installApp.js

@ -15,11 +15,8 @@ type Input = {
type Result = *
const cmd: Command<Input, Result> = createCommand(
'devices',
'installApp',
({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))),
const cmd: Command<Input, Result> = createCommand('installApp', ({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => installApp(transport, rest))),
)
export default cmd

1
src/commands/installFinalFirmware.js

@ -19,7 +19,6 @@ type Result = {
}
const cmd: Command<Input, Result> = createCommand(
'devices',
'installFinalFirmware',
({ devicePath, firmware }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, firmware))),

4
src/commands/installMcu.js

@ -21,8 +21,6 @@ import installMcu from 'helpers/firmware/installMcu'
type Input = *
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'installMcu', () =>
fromPromise(installMcu()),
)
const cmd: Command<Input, Result> = createCommand('installMcu', () => fromPromise(installMcu()))
export default cmd

1
src/commands/installOsuFirmware.js

@ -19,7 +19,6 @@ type Result = {
}
const cmd: Command<Input, Result> = createCommand(
'devices',
'installOsuFirmware',
({ devicePath, firmware }) =>
fromPromise(withDevice(devicePath)(transport => installOsuFirmware(transport, firmware))),

1
src/commands/libcoreScanAccounts.js

@ -13,7 +13,6 @@ type Input = {
type Result = AccountRaw
const cmd: Command<Input, Result> = createCommand(
'devices',
'libcoreScanAccounts',
({ devicePath, currencyId }) =>
Observable.create(o => {

1
src/commands/libcoreSignAndBroadcast.js

@ -23,7 +23,6 @@ type Input = {
type Result = $Exact<OperationRaw>
const cmd: Command<Input, Result> = createCommand(
'devices',
'libcoreSignAndBroadcast',
({ account, transaction, deviceId }) => {
// TODO: investigate why importing it on file scope causes trouble

2
src/commands/listApps.js

@ -11,7 +11,7 @@ type Input = {
type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'listApps', ({ targetId }) =>
const cmd: Command<Input, Result> = createCommand('listApps', ({ targetId }) =>
fromPromise(listApps(targetId)),
)

2
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

1
src/commands/signTransaction.js

@ -15,7 +15,6 @@ type Input = {
type Result = string
const cmd: Command<Input, Result> = createCommand(
'devices',
'signTransaction',
({ currencyId, devicePath, path, transaction }) =>
fromPromise(

17
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<Input, Result> = createCommand('testCrash', () =>
Observable.create(() => {
process.exit(1)
}),
)
export default cmd

13
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<Input, Result> = createCommand('testInterval', interval)
export default cmd

7
src/commands/uninstallApp.js

@ -15,11 +15,8 @@ type Input = {
type Result = *
const cmd: Command<Input, Result> = createCommand(
'devices',
'uninstallApp',
({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, rest))),
const cmd: Command<Input, Result> = createCommand('uninstallApp', ({ devicePath, ...rest }) =>
fromPromise(withDevice(devicePath)(transport => uninstallApp(transport, rest))),
)
export default cmd

13
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<Props, State> {
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 })

24
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<Props, State> {
_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

16
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<Props, State> {
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)
}

2
src/components/UpdateNotifier.js

@ -66,7 +66,7 @@ class UpdateNotifier extends PureComponent<Props> {
<Box ml="auto">
<NotifText
style={{ cursor: 'pointer', textDecoration: 'underline' }}
onClick={() => sendEvent('msg', 'updater.quitAndInstall')}
onClick={() => sendEvent('updater', 'quitAndInstall')}
>
{t('update:relaunch')}
</NotifText>

61
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 }: *) => (
<ModalBody onClose={onClose}>
<ModalTitle>DEBUG utils</ModalTitle>
<ModalTitle>developer internal tools</ModalTitle>
<ModalContent>
<EnsureDevice>
{device => (
<Box horizontal style={{ padding: 20 }}>
<Button onClick={this.onClickStressDevice(device)} primary>
Stress getAddress (BTC)
</Button>
</Box>
)}
</EnsureDevice>
<Box style={{ height: 60, overflow: 'auto' }}>
<Box horizontal style={{ padding: 10 }}>
<EnsureDevice>
{device => (
<Button onClick={this.onClickStressDevice(device)} primary>
Stress getAddress (BTC)
</Button>
)}
</EnsureDevice>
</Box>
<Box horizontal style={{ padding: 10 }}>
<Button onClick={this.onCrash} danger>
crash process
</Button>
</Box>
<Box horizontal style={{ padding: 10 }}>
<Button onClick={this.onStartPeriod(1000)} primary>
interval(1s)
</Button>
<Button onClick={this.cancelAllPeriods}>Cancel</Button>
</Box>
</Box>
<Box
style={{
padding: '20px 10px',
@ -92,6 +125,14 @@ class Debug extends Component<*, *> {
</Box>
))}
</Box>
<Button
style={{ position: 'absolute', right: 30, bottom: 28 }}
onClick={() => {
this.setState({ logs: [] })
}}
>
Clear
</Button>
</ModalContent>
</ModalBody>
)}

10
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) => <T>(job: (Transport<*>) => Promise<T>)
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

36
src/helpers/generic.js

@ -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
}

170
src/helpers/ipc.js

@ -2,118 +2,104 @@
import { Observable } from 'rxjs'
import uuidv4 from 'uuid/v4'
type Msg<A> = {
type: string,
data?: A,
options?: *,
}
function send<A>(msg: Msg<A>) {
process.send(msg)
export function createCommand<In, A>(id: string, impl: In => Observable<A>): Command<In, A> {
return new Command(id, impl)
}
export class Command<In, A> {
channel: string
type: string
id: string
impl: In => Observable<A>
constructor(channel: string, type: string, impl: In => Observable<A>) {
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<A>) {
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<A> {
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<A> = {
type: 'NEXT' | 'COMPLETE' | 'ERROR',
requestId: string,
data?: A,
}
function handleMsgEvent(e, msg: Msg<A>) {
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<In, A>(id: string, data: In): Observable<A> {
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<A>) {
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<In, A>(
channel: string,
type: string,
impl: In => Observable<A>,
): Command<In, A> {
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)
}
}

76
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!')

22
src/internals/manager/index.js

@ -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<Command<any, any>> = [getMemInfo]

12
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()
}

133
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)
})

5
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)

100
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)
}

34
src/renderer/runJob.js

@ -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<any> {
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)
}
}
})
}
Loading…
Cancel
Save