Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(app): Collect system USB information #5482

Merged
merged 14 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ jobs:
name: 'JS unit tests; build Protocol Designer, Labware Library, Components Library'
# node version pulled from .nvmrc
language: node_js
before_install:
- sudo apt-get install -y --no-install-recommends libudev-dev
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only a build dep, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but hopefully it's unnecessary now that usb-detection is publishing prebuilt binaries for the correct node versions. Still evaluating but will likely updaate the travis config once more

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there's some continuing native build shenanigans. For now, it appears we need both the prebuilt binaries and these build dependencies in place for everything to work. My hunch is that there's a problem with the Linux prebuilt binaries.

The usb-detection author appears to be in the middle of a node-gyp upgrade, so I'll take a look at removing these deps when that update is released. Keeping in place right now so we can have successful builds.

install:
- make install-js
script:
Expand Down Expand Up @@ -125,6 +127,8 @@ jobs:
- stage: test
name: 'JS E2E tests'
language: node_js
before_install:
- sudo apt-get install -y --no-install-recommends libudev-dev
install:
- make install-js
script:
Expand All @@ -134,6 +138,8 @@ jobs:
- stage: test
name: 'JS type checks'
language: node_js
before_install:
- sudo apt-get install -y --no-install-recommends libudev-dev
install:
- make install-js
script:
Expand All @@ -143,6 +149,8 @@ jobs:
<<: *app_stage_build
name: 'Build/deploy Opentrons App for POSIX (unsigned dev builds)'
os: linux
before_install:
- sudo apt-get install -y --no-install-recommends libudev-dev
script: make -C app-shell dist-posix
# skip app builds entirely if tag is present but does not match ^v
if: tag IS blank
Expand All @@ -151,6 +159,8 @@ jobs:
<<: *app_stage_build
name: 'Build/deploy Opentrons App for Linux'
os: linux
before_install:
- sudo apt-get install -y --no-install-recommends libudev-dev
script: make -C app-shell dist-linux
if: tag =~ ^v

Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ PROTOCOL_DESIGNER_DIR := protocol-designer
SHARED_DATA_DIR := shared-data
UPDATE_SERVER_DIR := update-server
ROBOT_SERVER_DIR := robot-server
APP_SHELL_DIR := app-shell

# this may be set as an environment variable to select the version of
# python to run if pyenv is not available. it should always be set to
Expand Down Expand Up @@ -50,6 +51,7 @@ install-py:
.PHONY: install-js
install-js:
yarn
$(MAKE) -j 1 -C $(APP_SHELL_DIR) setup
$(MAKE) -j 1 -C $(SHARED_DATA_DIR)
$(MAKE) -j 1 -C $(DISCOVERY_CLIENT_DIR)

Expand Down
6 changes: 3 additions & 3 deletions app-shell/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ electron := electron . \
.PHONY: all
all: package

.PHONY: install
install:
yarn
.PHONY: setup
setup:
electron-rebuild

.PHONY: clean
clean:
Expand Down
2 changes: 2 additions & 0 deletions app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"electron-dl": "^1.14.0",
"electron-store": "^4.0.0",
"electron-updater": "^4.1.2",
"execa": "^4.0.0",
"form-data": "^2.5.0",
"fs-extra": "^6.0.1",
"get-stream": "^5.1.0",
Expand All @@ -42,6 +43,7 @@
"pump": "^3.0.0",
"semver": "^5.5.0",
"tempy": "^0.3.0",
"usb-detection": "^4.9.0",
"uuid": "^3.2.1",
"winston": "^3.1.0",
"yargs-parser": "^10.0.0"
Expand Down
2 changes: 2 additions & 0 deletions app-shell/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { registerLabware } from './labware'
import { registerRobotLogs } from './robot-logs'
import { registerUpdate } from './update'
import { registerBuildrootUpdate } from './buildroot'
import { registerSystemInfo } from './system-info'

const config = getConfig()
const log = createLogger('main')
Expand Down Expand Up @@ -58,6 +59,7 @@ function startUp() {
registerUpdate(dispatch),
registerBuildrootUpdate(dispatch),
registerLabware(dispatch, mainWindow),
registerSystemInfo(dispatch),
]

ipcMain.on('dispatch', (_, action) => {
Expand Down
4 changes: 4 additions & 0 deletions app-shell/src/os.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// @flow
// os helpers

export const isWindows = () => process.platform === 'win32'
156 changes: 156 additions & 0 deletions app-shell/src/system-info/__tests__/dispatch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// @flow
import noop from 'lodash/noop'
import { app } from 'electron'
import * as Fixtures from '@opentrons/app/src/system-info/__fixtures__'
import * as SystemInfo from '@opentrons/app/src/system-info'
import { uiInitialized } from '@opentrons/app/src/shell'
import * as OS from '../../os'
import * as UsbDevices from '../usb-devices'
import { registerSystemInfo } from '..'

import type {
Device,
UsbDeviceMonitor,
UsbDeviceMonitorOptions,
} from '../usb-devices'

jest.mock('../../os')
jest.mock('../usb-devices')

const createUsbDeviceMonitor: JestMockFn<
[UsbDeviceMonitorOptions | void],
UsbDeviceMonitor
> = UsbDevices.createUsbDeviceMonitor

const getWindowsDriverVersion: JestMockFn<[Device], any> =
UsbDevices.getWindowsDriverVersion

const isWindows: JestMockFn<[], boolean> = OS.isWindows

const flush = () => new Promise(resolve => setTimeout(resolve, 0))

describe('app-shell::system-info module action tests', () => {
const dispatch = jest.fn()
const getAllDevices: JestMockFn<[], any> = jest.fn()
const stop = jest.fn()
const monitor: $Shape<UsbDeviceMonitor> = { getAllDevices, stop }
const { windowsDriverVersion: _, ...notRealtek } = Fixtures.mockUsbDevice
const realtek0 = { ...notRealtek, manufacturer: 'Realtek' }
const realtek1 = { ...notRealtek, manufacturer: 'realtek' }
let handler

beforeEach(() => {
handler = registerSystemInfo(dispatch)
isWindows.mockReturnValue(false)
createUsbDeviceMonitor.mockReturnValue(monitor)
getAllDevices.mockResolvedValue([realtek0])
})

afterEach(() => {
jest.resetAllMocks()
})

it('sends initial USB device list on shell:UI_INITIALIZED', () => {
handler(uiInitialized())

return flush().then(() => {
expect(dispatch).toHaveBeenCalledWith(SystemInfo.initialized([realtek0]))
expect(getWindowsDriverVersion).toHaveBeenCalledTimes(0)
})
})

it('will not initialize multiple monitors', () => {
handler(uiInitialized())
handler(uiInitialized())

return flush().then(() => {
expect(createUsbDeviceMonitor).toHaveBeenCalledTimes(1)
expect(dispatch).toHaveBeenCalledTimes(1)
})
})

it('sends systemInfo:USB_DEVICE_ADDED when device added', () => {
handler(uiInitialized())
const monitorOptions = createUsbDeviceMonitor.mock.calls[0][0]

expect(monitorOptions?.onDeviceAdd).toEqual(expect.any(Function))
const onDeviceAdd = monitorOptions?.onDeviceAdd ?? noop
onDeviceAdd(realtek0)

return flush().then(() => {
expect(dispatch).toHaveBeenCalledWith(SystemInfo.usbDeviceAdded(realtek0))
expect(getWindowsDriverVersion).toHaveBeenCalledTimes(0)
})
})

it('sends systemInfo:USB_DEVICE_REMOVED when device removed', () => {
handler(uiInitialized())
const monitorOptions = createUsbDeviceMonitor.mock.calls[0][0]

expect(monitorOptions?.onDeviceRemove).toEqual(expect.any(Function))
const onDeviceRemove = monitorOptions?.onDeviceRemove ?? noop
onDeviceRemove(realtek0)

return flush().then(() => {
expect(dispatch).toHaveBeenCalledWith(
SystemInfo.usbDeviceRemoved(realtek0)
)
})
})

it('stops monitoring on app quit', () => {
handler(uiInitialized())

const appQuitHandler = app.once.mock.calls.find(
([event, handler]) => event === 'will-quit'
)?.[1]

expect(typeof appQuitHandler).toBe('function')
appQuitHandler()
expect(monitor.stop).toHaveBeenCalled()
})

describe('on windows', () => {
beforeEach(() => {
isWindows.mockReturnValue(true)
getWindowsDriverVersion.mockResolvedValue('1.2.3')
})

it('should add Windows driver versions to Realtek devices on initialization', () => {
getAllDevices.mockResolvedValue([realtek0, notRealtek, realtek1])
handler(uiInitialized())

return flush().then(() => {
expect(getWindowsDriverVersion).toHaveBeenCalledWith(realtek0)
expect(getWindowsDriverVersion).toHaveBeenCalledWith(realtek1)

expect(dispatch).toHaveBeenCalledWith(
SystemInfo.initialized([
{ ...realtek0, windowsDriverVersion: '1.2.3' },
notRealtek,
{ ...realtek1, windowsDriverVersion: '1.2.3' },
])
)
})
})

it('should add Windows driver versions to Realtek devices on add', () => {
getAllDevices.mockResolvedValue([])
handler(uiInitialized())
const monitorOptions = createUsbDeviceMonitor.mock.calls[0][0]
const onDeviceAdd = monitorOptions?.onDeviceAdd ?? noop
onDeviceAdd(realtek0)

return flush().then(() => {
expect(getWindowsDriverVersion).toHaveBeenCalledWith(realtek0)

expect(dispatch).toHaveBeenCalledWith(
SystemInfo.usbDeviceAdded({
...realtek0,
windowsDriverVersion: '1.2.3',
})
)
})
})
})
})
101 changes: 101 additions & 0 deletions app-shell/src/system-info/__tests__/usb-devices.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// @flow

import execa from 'execa'
import usbDetection from 'usb-detection'

import * as Fixtures from '@opentrons/app/src/system-info/__fixtures__'
import { createUsbDeviceMonitor, getWindowsDriverVersion } from '../usb-devices'

jest.mock('execa')
jest.mock('usb-detection', () => {
const EventEmitter = require('events')
const detector = new EventEmitter()
detector.startMonitoring = jest.fn()
detector.stopMonitoring = jest.fn()
detector.find = jest.fn()
return detector
})

const usbDetectionFind: JestMockFn<[], any> = (usbDetection.find: any)

describe('app-shell::system-info::usb-devices', () => {
const { windowsDriverVersion: _, ...mockDevice } = Fixtures.mockUsbDevice
afterEach(() => {
jest.resetAllMocks()
})

it('can create a usb device monitor', () => {
expect(usbDetection.startMonitoring).toHaveBeenCalledTimes(0)
createUsbDeviceMonitor()
expect(usbDetection.startMonitoring).toHaveBeenCalledTimes(1)
})

it('usb device monitor can be stopped', () => {
const monitor = createUsbDeviceMonitor()
monitor.stop()
expect(usbDetection.stopMonitoring).toHaveBeenCalledTimes(1)
})

it('can return the list of all devices', () => {
const mockDevices = [
{ ...mockDevice, deviceName: 'foo' },
{ ...mockDevice, deviceName: 'bar' },
{ ...mockDevice, deviceName: 'baz' },
]

usbDetectionFind.mockResolvedValueOnce(mockDevices)

const monitor = createUsbDeviceMonitor()
const result = monitor.getAllDevices()

return expect(result).resolves.toEqual(mockDevices)
})

it('can notify when devices are added', () => {
const onDeviceAdd = jest.fn()
createUsbDeviceMonitor({ onDeviceAdd })

usbDetection.emit('add', mockDevice)

expect(onDeviceAdd).toHaveBeenCalledWith(mockDevice)
})

it('can notify when devices are removed', () => {
const onDeviceRemove = jest.fn()
createUsbDeviceMonitor({ onDeviceRemove })

usbDetection.emit('remove', mockDevice)

expect(onDeviceRemove).toHaveBeenCalledWith(mockDevice)
})

it('can get the Windows driver version of a device', () => {
execa.command.mockResolvedValue({ stdout: '1.2.3' })

const device = {
...mockDevice,
// 291 == 0x0123
vendorId: 291,
// 43981 == 0xABCD
productId: 43981,
// plain string for serial
serialNumber: 'abcdefg',
}

return getWindowsDriverVersion(device).then(version => {
expect(execa.command).toHaveBeenCalledWith(
'Get-PnpDeviceProperty -InstanceID "USB\\VID_0123&PID_ABCD\\abcdefg" -KeyName "DEVPKEY_Device_DriverVersion" | % { $_.Data }',
{ shell: 'PowerShell.exe' }
)
expect(version).toBe('1.2.3')
})
})

it('returns null for unknown if command errors out', () => {
execa.command.mockRejectedValue('AH!')

return getWindowsDriverVersion(mockDevice).then(version => {
expect(version).toBe(null)
})
})
})
Loading