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. 5
      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. 5
      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. 47
      src/components/modals/Debug.js
  27. 10
      src/helpers/deviceAccess.js
  28. 36
      src/helpers/generic.js
  29. 130
      src/helpers/ipc.js
  30. 76
      src/internals/index.js
  31. 22
      src/internals/manager/index.js
  32. 12
      src/main/autoUpdate.js
  33. 131
      src/main/bridge.js
  34. 5
      src/reducers/devices.js
  35. 86
      src/renderer/events.js
  36. 34
      src/renderer/runJob.js

1
package.json

@ -64,7 +64,6 @@
"invariant": "^2.2.4", "invariant": "^2.2.4",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"moment": "^2.22.1", "moment": "^2.22.1",
"object-path": "^0.11.4",
"qrcode": "^1.2.0", "qrcode": "^1.2.0",
"qrcode-reader": "^1.0.4", "qrcode-reader": "^1.0.4",
"qs": "^6.5.1", "qs": "^6.5.1",

4
src/actions/devices.js

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

1
src/commands/getAddress.js

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

2
src/commands/getDeviceInfo.js

@ -17,7 +17,7 @@ type Result = {
mcu: boolean, 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))), fromPromise(withDevice(devicePath)(transport => getDeviceInfo(transport))),
) )

2
src/commands/getFirmwareInfo.js

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

4
src/commands/getIsGenuine.js

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

2
src/commands/getLatestFirmwareForDevice.js

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

2
src/commands/getMemInfo.js

@ -12,7 +12,7 @@ type Input = {
type Result = * 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))), fromPromise(withDevice(devicePath)(transport => getMemInfo(transport))),
) )

21
src/internals/devices/index.js → src/commands/index.js

@ -1,6 +1,8 @@
// @flow // @flow
import type { Command } from 'helpers/ipc' import type { Command } from 'helpers/ipc'
import getMemInfo from 'commands/getMemInfo'
import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreScanAccounts from 'commands/libcoreScanAccounts'
import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
@ -16,8 +18,13 @@ import installOsuFirmware from 'commands/installOsuFirmware'
import installFinalFirmware from 'commands/installFinalFirmware' import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu' import installMcu from 'commands/installMcu'
import listApps from 'commands/listApps' 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, getAddress,
signTransaction, signTransaction,
getDeviceInfo, getDeviceInfo,
@ -25,12 +32,20 @@ export const commands: Array<Command<any, any>> = [
getIsGenuine, getIsGenuine,
getLatestFirmwareForDevice, getLatestFirmwareForDevice,
installApp, installApp,
libcoreScanAccounts,
libcoreSignAndBroadcast,
listenDevices, listenDevices,
uninstallApp, uninstallApp,
installOsuFirmware, installOsuFirmware,
installFinalFirmware, installFinalFirmware,
installMcu, installMcu,
listApps, 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

5
src/commands/installApp.js

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

1
src/commands/installFinalFirmware.js

@ -19,7 +19,6 @@ type Result = {
} }
const cmd: Command<Input, Result> = createCommand( const cmd: Command<Input, Result> = createCommand(
'devices',
'installFinalFirmware', 'installFinalFirmware',
({ devicePath, firmware }) => ({ devicePath, firmware }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, 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 Input = *
type Result = * type Result = *
const cmd: Command<Input, Result> = createCommand('devices', 'installMcu', () => const cmd: Command<Input, Result> = createCommand('installMcu', () => fromPromise(installMcu()))
fromPromise(installMcu()),
)
export default cmd export default cmd

1
src/commands/installOsuFirmware.js

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

1
src/commands/libcoreScanAccounts.js

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

1
src/commands/libcoreSignAndBroadcast.js

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

2
src/commands/listApps.js

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

2
src/commands/listenDevices.js

@ -4,6 +4,6 @@ import { createCommand } from 'helpers/ipc'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import CommNodeHid from '@ledgerhq/hw-transport-node-hid' 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 export default cmd

1
src/commands/signTransaction.js

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

5
src/commands/uninstallApp.js

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

13
src/components/ManagerPage/DeviceInfos.js

@ -2,8 +2,6 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import runJob from 'renderer/runJob'
import Text from 'components/base/Text' import Text from 'components/base/Text'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
import Button from 'components/base/Button' import Button from 'components/base/Button'
@ -30,16 +28,7 @@ class DeviceInfos extends PureComponent<Props, State> {
handleGetMemInfos = async () => { handleGetMemInfos = async () => {
try { try {
this.setState({ isLoading: true }) this.setState({ isLoading: true })
const { const memoryInfos = null // TODO
device: { path: devicePath },
} = this.props
const memoryInfos = await runJob({
channel: 'manager',
job: 'getMemInfos',
successResponse: 'manager.getMemInfosSuccess',
errorResponse: 'manager.getMemInfosError',
data: { devicePath },
})
this.setState({ memoryInfos, isLoading: false }) this.setState({ memoryInfos, isLoading: false })
} catch (err) { } catch (err) {
this.setState({ isLoading: false }) 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 { translate } from 'react-i18next'
import type { Device, T } from 'types/common' import type { Device, T } from 'types/common'
// import runJob from 'renderer/runJob'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
// import Button from 'components/base/Button' // import Button from 'components/base/Button'
@ -37,28 +35,6 @@ class FirmwareUpdate extends PureComponent<Props, State> {
_unmounting = false _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() { render() {
const { t, ...props } = this.props 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 type { Device, T } from 'types/common'
import runJob from 'renderer/runJob'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import Box, { Card } from 'components/base/Box' import Box, { Card } from 'components/base/Box'
@ -74,20 +73,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
installFirmware = async () => { installFirmware = async () => {
try { try {
const { latestFirmware } = this.state // TODO
const {
device: { path: devicePath },
} = this.props
await runJob({
channel: 'manager',
job: 'installOsuFirmware',
successResponse: 'device.osuFirmwareInstallSuccess',
errorResponse: 'device.osuFirmwareInstallError',
data: {
devicePath,
firmware: latestFirmware,
},
})
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }

2
src/components/UpdateNotifier.js

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

47
src/components/modals/Debug.js

@ -8,12 +8,26 @@ import Box from 'components/base/Box'
import EnsureDevice from 'components/ManagerPage/EnsureDevice' import EnsureDevice from 'components/ManagerPage/EnsureDevice'
import { getDerivations } from 'helpers/derivations' import { getDerivations } from 'helpers/derivations'
import getAddress from 'commands/getAddress' import getAddress from 'commands/getAddress'
import testInterval from 'commands/testInterval'
import testCrash from 'commands/testCrash'
class Debug extends Component<*, *> { class Debug extends Component<*, *> {
state = { state = {
logs: [], 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 () => { onClickStressDevice = (device: *) => async () => {
try { try {
const currency = getCryptoCurrencyById('bitcoin') const currency = getCryptoCurrencyById('bitcoin')
@ -42,6 +56,12 @@ class Debug extends Component<*, *> {
this.setState({ logs: [] }) this.setState({ logs: [] })
} }
cancelAllPeriods = () => {
this.periodSubs.forEach(s => s.unsubscribe())
this.periodSubs = []
}
periodSubs = []
log = (txt: string) => { log = (txt: string) => {
this.setState(({ logs }) => ({ logs: logs.concat({ txt, type: 'log' }) })) this.setState(({ logs }) => ({ logs: logs.concat({ txt, type: 'log' }) }))
} }
@ -60,17 +80,30 @@ class Debug extends Component<*, *> {
onHide={this.onHide} onHide={this.onHide}
render={({ onClose }: *) => ( render={({ onClose }: *) => (
<ModalBody onClose={onClose}> <ModalBody onClose={onClose}>
<ModalTitle>DEBUG utils</ModalTitle> <ModalTitle>developer internal tools</ModalTitle>
<ModalContent> <ModalContent>
<Box style={{ height: 60, overflow: 'auto' }}>
<Box horizontal style={{ padding: 10 }}>
<EnsureDevice> <EnsureDevice>
{device => ( {device => (
<Box horizontal style={{ padding: 20 }}>
<Button onClick={this.onClickStressDevice(device)} primary> <Button onClick={this.onClickStressDevice(device)} primary>
Stress getAddress (BTC) Stress getAddress (BTC)
</Button> </Button>
</Box>
)} )}
</EnsureDevice> </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 <Box
style={{ style={{
padding: '20px 10px', padding: '20px 10px',
@ -92,6 +125,14 @@ class Debug extends Component<*, *> {
</Box> </Box>
))} ))}
</Box> </Box>
<Button
style={{ position: 'absolute', right: 30, bottom: 28 }}
onClick={() => {
this.setState({ logs: [] })
}}
>
Clear
</Button>
</ModalContent> </ModalContent>
</ModalBody> </ModalBody>
)} )}

10
src/helpers/deviceAccess.js

@ -2,7 +2,6 @@
import createSemaphore from 'semaphore' import createSemaphore from 'semaphore'
import type Transport from '@ledgerhq/hw-transport' import type Transport from '@ledgerhq/hw-transport'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' 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 // 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() // 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 = {} const semaphorePerDevice = {}
export const withDevice: WithDevice = devicePath => { 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 = const sem =
semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1)) semaphorePerDevice[devicePath] || (semaphorePerDevice[devicePath] = createSemaphore(1))
return job => return job =>
takeSemaphorePromise(sem, async () => { takeSemaphorePromise(sem, async () => {
const t = await retry(() => TransportNodeHid.open(devicePath)) const t = await TransportNodeHid.open(devicePath)
try { try {
const res = await job(t) const res = await job(t)
// $FlowFixMe // $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
}

130
src/helpers/ipc.js

@ -2,118 +2,104 @@
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import uuidv4 from 'uuid/v4' import uuidv4 from 'uuid/v4'
type Msg<A> = { export function createCommand<In, A>(id: string, impl: In => Observable<A>): Command<In, A> {
type: string, return new Command(id, impl)
data?: A,
options?: *,
}
function send<A>(msg: Msg<A>) {
process.send(msg)
} }
export class Command<In, A> { export class Command<In, A> {
channel: string
type: string
id: string id: string
impl: In => Observable<A> 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: constructor(id: string, impl: In => Observable<A>) {
this.id = id
exec(data: In, requestId: string) { this.impl = impl
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 },
})
},
})
} }
// ~~~ On renderer side we can:
/** /**
* Usage example: * Usage example:
* sub = send(data).subscribe({ next: ... }) * sub = cmd.send(data).subscribe({ next: ... })
* // or * // or
* const res = await send(data).toPromise() * const res = await cmd.send(data).toPromise()
*/ */
send(data: In): Observable<A> { send(data: In): Observable<A> {
return ipcRendererSendCommand(this.id, data)
}
}
type Msg<A> = {
type: 'NEXT' | 'COMPLETE' | 'ERROR',
requestId: string,
data?: A,
}
// Implements command message of (Renderer proc -> Main proc)
function ipcRendererSendCommand<In, A>(id: string, data: In): Observable<A> {
const { ipcRenderer } = require('electron') const { ipcRenderer } = require('electron')
return Observable.create(o => { return Observable.create(o => {
const { channel, type, id } = this
const requestId: string = uuidv4() const requestId: string = uuidv4()
const unsubscribe = () => { const unsubscribe = () => {
ipcRenderer.removeListener('msg', handleMsgEvent) ipcRenderer.send('command-unsubscribe', { requestId })
ipcRenderer.removeListener('command-event', handleCommandEvent)
} }
function handleMsgEvent(e, msg: Msg<A>) { function handleCommandEvent(e, msg: Msg<A>) {
if (requestId !== msg.requestId) return
switch (msg.type) { switch (msg.type) {
case `NEXT_${requestId}`: case 'NEXT':
if (msg.data) { if (msg.data) {
o.next(msg.data) o.next(msg.data)
} }
break break
case `COMPLETE_${requestId}`: case 'COMPLETE':
o.complete() o.complete()
unsubscribe() ipcRenderer.removeListener('command-event', handleCommandEvent)
break break
case `ERROR_${requestId}`: case 'ERROR':
o.error(msg.data) o.error(msg.data)
unsubscribe() ipcRenderer.removeListener('command-event', handleCommandEvent)
break break
default: default:
} }
} }
ipcRenderer.on('msg', handleMsgEvent) ipcRenderer.on('command-event', handleCommandEvent)
ipcRenderer.send(channel, { ipcRenderer.send('command', { id, data, requestId })
type,
data: {
id,
data,
requestId,
},
})
return unsubscribe return unsubscribe
}) })
}
} }
export function createCommand<In, A>( // Implements command message of (Main proc -> Renderer proc)
channel: string, // (dual of ipcRendererSendCommand)
type: string, export function ipcMainListenReceiveCommands(o: {
impl: In => Observable<A>, onUnsubscribe: (requestId: string) => void,
): Command<In, A> { onCommand: (
return new Command(channel, type, impl) 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)
})
}
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 // @flow
import commands from 'commands'
import objectPath from 'object-path'
import capitalize from 'lodash/capitalize'
require('../env') require('../env')
require('../init-sentry') require('../init-sentry')
const { FORK_TYPE } = process.env process.title = 'Internal'
process.title = `${require('../../package.json').productName} ${capitalize(FORK_TYPE)}`
function sendEvent(type: string, data: any, options: Object = { kill: true }) {
process.send({ type, data, options })
}
// $FlowFixMe const subscriptions = {}
let handlers = require(`./${FORK_TYPE}`) // eslint-disable-line import/no-dynamic-require
// handle babel export object syntax
if (handlers.default) {
handlers = handlers.default
}
process.on('message', payload => { process.on('message', m => {
if (payload.data && payload.data.requestId) { console.log(m)
const { data, requestId, id } = payload.data if (m.type === 'command') {
// this is the new type of "command" payload! const { data, requestId, id } = m.command
const cmd = (handlers.commands || []).find(cmd => cmd.id === id) const cmd = commands.find(cmd => cmd.id === id)
if (!cmd) { if (!cmd) {
console.warn(`command ${id} not found`) 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 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 type SendFunction = (type: string, data: *) => void
export default (notify: SendFunction) => { export default (notify: SendFunction) => {
autoUpdater.on('checking-for-update', () => notify('updater.checking')) autoUpdater.on('checking-for-update', () => notify('checking'))
autoUpdater.on('update-available', info => notify('updater.updateAvailable', info)) autoUpdater.on('update-available', info => notify('updateAvailable', info))
autoUpdater.on('update-not-available', () => notify('updater.updateNotAvailable')) autoUpdater.on('update-not-available', () => notify('updateNotAvailable'))
autoUpdater.on('error', err => notify('updater.error', err)) autoUpdater.on('error', err => notify('error', err))
autoUpdater.on('download-progress', progress => notify('updater.downloadProgress', progress)) autoUpdater.on('download-progress', progress => notify('downloadProgress', progress))
autoUpdater.on('update-downloaded', () => notify('updater.downloaded')) autoUpdater.on('update-downloaded', () => notify('downloaded'))
autoUpdater.checkForUpdatesAndNotify() autoUpdater.checkForUpdatesAndNotify()
} }

131
src/main/bridge.js

@ -1,100 +1,89 @@
// @flow // @flow
import '@babel/polyfill' import '@babel/polyfill'
import invariant from 'invariant'
import { fork } from 'child_process' import { fork } from 'child_process'
import { BrowserWindow, ipcMain, app } from 'electron' import { ipcMain, app } from 'electron'
import objectPath from 'object-path' import { ipcMainListenReceiveCommands } from 'helpers/ipc'
import path from 'path' import path from 'path'
import setupAutoUpdater, { quitAndInstall } from './autoUpdate' import setupAutoUpdater, { quitAndInstall } from './autoUpdate'
const { DEV_TOOLS } = process.env
// sqlite files will be located in the app local data folder // sqlite files will be located in the app local data folder
const LEDGER_LIVE_SQLITE_PATH = path.resolve(app.getPath('userData'), 'sqlite') const LEDGER_LIVE_SQLITE_PATH = path.resolve(app.getPath('userData'), 'sqlite')
const processes = [] let internalProcess
function cleanProcesses() {
processes.forEach(kill => kill())
}
function sendEventToWindow(name, { type, data }) { const killInternalProcess = () => {
const anotherWindow = BrowserWindow.getAllWindows().find(w => w.name === name) if (internalProcess) {
if (anotherWindow) { console.log('killing internal process...')
anotherWindow.webContents.send('msg', { type, data }) internalProcess.kill('SIGINT')
internalProcess = null
} }
} }
function onForkChannel(forkType) { const forkBundlePath = path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`)
return (event: any, payload) => {
const { type, data } = payload
let compute = fork(path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist/internals`), { const bootInternalProcess = () => {
env: { console.log('booting internal process...')
DEV_TOOLS, internalProcess = fork(forkBundlePath, {
FORK_TYPE: forkType, env: { LEDGER_LIVE_SQLITE_PATH },
LEDGER_LIVE_SQLITE_PATH,
},
}) })
internalProcess.on('exit', code => {
console.log(`Internal process ended with code ${code}`)
internalProcess = null
})
}
const kill = () => { process.on('exit', () => {
if (compute) { killInternalProcess()
compute.kill('SIGINT') })
compute = null
}
}
processes.push(kill)
const onMessage = payload => { ipcMain.on('clean-processes', () => {
const { type, data, options = {} } = payload killInternalProcess()
})
if (options.window) { ipcMainListenReceiveCommands({
sendEventToWindow(options.window, { type, data }) onUnsubscribe: requestId => {
} else { if (!internalProcess) return
event.sender.send('msg', { type, data }) internalProcess.send({ type: 'command-unsubscribe', requestId })
} },
if (options.kill && compute) { onCommand: (command, notifyCommandEvent) => {
kill() 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' },
})
} }
compute.on('message', onMessage) const handleMessage = payload => {
compute.send({ type, data }) if (payload.requestId !== command.requestId) return
notifyCommandEvent(payload)
process.on('exit', kill) if (payload.type === 'ERROR' || payload.type === 'COMPLETE') {
p.removeListener('message', handleMessage)
p.removeListener('exit', handleExit)
}
} }
}
// 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 = { // TODO move this to "command" pattern
updater: { ipcMain.on('updater', (event, { type, data }) => {
const handler = {
init: setupAutoUpdater, init: setupAutoUpdater,
quitAndInstall, quitAndInstall,
}, }[type]
kill: { const send = (type: string, data: *) => event.sender.send('updater', { type, data })
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 })
handler(send, data, type) handler(send, data, type)
}) })

5
src/reducers/devices.js

@ -9,7 +9,7 @@ export type DevicesState = {
devices: Device[], devices: Device[],
} }
const state: DevicesState = { const initialState: DevicesState = {
currentDevice: null, currentDevice: null,
devices: [], devices: [],
} }
@ -20,6 +20,7 @@ function setCurrentDevice(state) {
} }
const handlers: Object = { const handlers: Object = {
RESET_DEVICES: () => initialState,
ADD_DEVICE: (state: DevicesState, { payload: device }: { payload: Device }) => ADD_DEVICE: (state: DevicesState, { payload: device }: { payload: Device }) =>
setCurrentDevice({ setCurrentDevice({
...state, ...state,
@ -49,4 +50,4 @@ export function getDevices(state: { devices: DevicesState }) {
return state.devices.devices return state.devices.devices
} }
export default handleActions(handlers, state) export default handleActions(handlers, initialState)

86
src/renderer/events.js

@ -8,20 +8,19 @@
// both of these implementation should have a unique requestId to ensure there is no collision // 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 // events should all appear in the promise result / observer msgs as soon as they have this requestId
import 'commands'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import objectPath from 'object-path'
import debug from 'debug' import debug from 'debug'
import { CHECK_UPDATE_DELAY } from 'config/constants' import { CHECK_UPDATE_DELAY } from 'config/constants'
import { setUpdateStatus } from 'reducers/update' import { setUpdateStatus } from 'reducers/update'
import { addDevice, removeDevice } from 'actions/devices' import { addDevice, removeDevice, resetDevices } from 'actions/devices'
import listenDevices from 'commands/listenDevices' import listenDevices from 'commands/listenDevices'
import i18n from 'renderer/i18n/electron'
const d = { const d = {
device: debug('lwd:device'), device: debug('lwd:device'),
sync: debug('lwd:sync'), sync: debug('lwd:sync'),
@ -33,6 +32,7 @@ type MsgPayload = {
data: any, data: any,
} }
// TODO port remaining to command pattern
export function sendEvent(channel: string, msgType: string, data: any) { export function sendEvent(channel: string, msgType: string, data: any) {
ipcRenderer.send(channel, { ipcRenderer.send(channel, {
type: msgType, type: msgType,
@ -40,42 +40,19 @@ export function sendEvent(channel: string, msgType: string, data: any) {
}) })
} }
export function sendSyncEvent(channel: string, msgType: string, data: any): any { let syncDeviceSub
return ipcRenderer.sendSync(`${channel}:sync`, {
type: msgType,
data,
})
}
export default ({ store }: { store: Object, locked: boolean }) => { export default ({ store }: { store: Object, locked: boolean }) => {
const handlers = {
dispatch: ({ type, payload }) => store.dispatch({ type, payload }),
application: {
changeLanguage: lang => i18n.changeLanguage(lang),
},
updater: {
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...) // Ensure all sub-processes are killed before creating new ones (dev mode...)
ipcRenderer.send('clean-processes') ipcRenderer.send('clean-processes')
listenDevices.send().subscribe({ if (syncDeviceSub) {
next: ({ device, type }) => { syncDeviceSub.unsubscribe()
syncDeviceSub = null
}
function syncDevices() {
syncDeviceSub = listenDevices.send().subscribe(
({ device, type }) => {
if (device) { if (device) {
if (type === 'add') { if (type === 'add') {
d.device('Device - add') d.device('Device - add')
@ -86,15 +63,48 @@ export default ({ store }: { store: Object, locked: boolean }) => {
} }
} }
}, },
}) error => {
console.warn('listenDevices error', error)
store.dispatch(resetDevices())
syncDevices()
},
() => {
console.warn('listenDevices ended unexpectedly. restarting')
store.dispatch(resetDevices())
syncDevices()
},
)
}
syncDevices()
if (__PROD__) { 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('updater', (event: any, payload: MsgPayload) => {
const { type, data } = payload
updaterHandlers[type](data)
})
// Start check of eventual updates // Start check of eventual updates
checkUpdates() checkUpdates()
} }
} }
if (module.hot) {
module.hot.accept('commands', () => {
ipcRenderer.send('clean-processes')
})
}
export function checkUpdates() { export function checkUpdates() {
d.update('Update - check') 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