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

fix(app-shell,app): Send labware files and runtime parameters over USB #14994

Merged
merged 9 commits into from
Apr 25, 2024
4 changes: 3 additions & 1 deletion api-client/src/protocols/createProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export function createProtocol(
runTimeParameterValues?: RunTimeParameterCreateData
): ResponsePromise<Protocol> {
const formData = new FormData()
files.forEach(file => formData.append('files', file, file.name))
files.forEach(file => {
formData.append('files', file, file.name)
})
if (protocolKey != null) formData.append('key', protocolKey)
if (runTimeParameterValues != null)
formData.append(
Expand Down
13 changes: 0 additions & 13 deletions app-shell/src/protocol-storage/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import fse from 'fs-extra'
import path from 'path'
import { shell } from 'electron'
import first from 'lodash/first'

import {
ADD_PROTOCOL,
Expand Down Expand Up @@ -48,18 +47,6 @@ export const getParsedAnalysisFromPath = (
}
}

export const getProtocolSrcFilePaths = (
protocolKey: string
): Promise<string[]> => {
const protocolDir = `${FileSystem.PROTOCOLS_DIRECTORY_PATH}/${protocolKey}`
return ensureDir(protocolDir)
.then(() => FileSystem.parseProtocolDirs([protocolDir]))
.then(storedProtocols => {
const storedProtocol = first(storedProtocols)
return storedProtocol?.srcFilePaths ?? []
})
}

// Revert a v7.0.0 pre-parity stop-gap solution.
const migrateProtocolsFromTempDirectory = preParityMigrateProtocolsFrom(
FileSystem.PRE_V7_PARITY_DIRECTORY_PATH,
Expand Down
31 changes: 14 additions & 17 deletions app-shell/src/usb.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import axios, { AxiosRequestConfig } from 'axios'
import FormData from 'form-data'
import fs from 'fs'
import path from 'path'

import {
fetchSerialPortList,
Expand All @@ -12,14 +10,14 @@ import {
} from '@opentrons/usb-bridge/node-client'

import { createLogger } from './log'
import { getProtocolSrcFilePaths } from './protocol-storage'
import { usbRequestsStart, usbRequestsStop } from './config/actions'
import {
SYSTEM_INFO_INITIALIZED,
USB_DEVICE_ADDED,
USB_DEVICE_REMOVED,
} from './constants'

import type { IPCSafeFormData } from '@opentrons/app/src/redux/shell/types'
import type { UsbDevice } from '@opentrons/app/src/redux/system-info/types'
import type { PortInfo } from '@opentrons/usb-bridge/node-client'
import type { Action, Dispatch } from './types'
Expand Down Expand Up @@ -83,6 +81,17 @@ function isUsbDeviceOt3(device: UsbDevice): boolean {
device.vendorId === parseInt(DEFAULT_VENDOR_ID, 16)
)
}

function reconstructFormData(ipcSafeFormData: IPCSafeFormData): FormData {
const result = new FormData()
ipcSafeFormData.forEach(entry => {
entry.type === 'file'
? result.append(entry.name, Buffer.from(entry.value), entry.filename)
: result.append(entry.name, entry.value)
})
return result
}

async function usbListener(
_event: IpcMainInvokeEvent,
config: AxiosRequestConfig
Expand All @@ -92,21 +101,9 @@ async function usbListener(
let formHeaders = {}

// check for formDataProxy
if (data?.formDataProxy != null) {
if (data?.proxiedFormData != null) {
// reconstruct FormData
const formData = new FormData()
const { protocolKey } = data.formDataProxy

const srcFilePaths: string[] = await getProtocolSrcFilePaths(protocolKey)

// create readable stream from file
srcFilePaths.forEach(srcFilePath => {
const readStream = fs.createReadStream(srcFilePath)
formData.append('files', readStream, path.basename(srcFilePath))
})

formData.append('key', protocolKey)

const formData = reconstructFormData(data.proxiedFormData)
formHeaders = formData.getHeaders()
data = formData
}
Expand Down
42 changes: 34 additions & 8 deletions app/src/redux/shell/remote.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// access main process remote modules via attachments to `global`
import type { AxiosRequestConfig } from 'axios'
import type { ResponsePromise } from '@opentrons/api-client'
import type { Remote, NotifyTopic, NotifyResponseData } from './types'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import type {
Remote,
NotifyTopic,
NotifyResponseData,
IPCSafeFormData,
} from './types'

const emptyRemote: Remote = {} as any

Expand All @@ -20,18 +24,40 @@ export const remote: Remote = new Proxy(emptyRemote, {
},
})

export function appShellRequestor<Data>(
// FormData and File objects can't be sent through invoke().
// This converts them into simpler objects that can be.
// app-shell will convert them back.
async function proxyFormData(formData: FormData): Promise<IPCSafeFormData> {
const result: IPCSafeFormData = []
for (const [name, value] of formData.entries()) {
if (value instanceof File) {
result.push({
type: 'file',
name,
// todo(mm, 2024-04-24): Send just the (full) filename instead of the file
// contents, to avoid the IPC message ballooning into several MB.
value: await value.arrayBuffer(),
filename: value.name,
})
} else {
result.push({ type: 'string', name, value })
}
}

return result
}

export async function appShellRequestor<Data>(
config: AxiosRequestConfig
): ResponsePromise<Data> {
): Promise<AxiosResponse<Data>> {
const { data } = config
// special case: protocol files and form data cannot be sent through invoke. proxy by protocolKey and handle in app-shell
const formDataProxy =
data instanceof FormData
? { formDataProxy: { protocolKey: data.get('key') } }
? { proxiedFormData: await proxyFormData(data) }
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
: data
const configProxy = { ...config, data: formDataProxy }

return remote.ipcRenderer.invoke('usb:request', configProxy)
return await remote.ipcRenderer.invoke('usb:request', configProxy)
SyntaxColoring marked this conversation as resolved.
Show resolved Hide resolved
}

interface CallbackStore {
Expand Down
15 changes: 15 additions & 0 deletions app/src/redux/shell/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,18 @@ export type ShellAction =
| RobotMassStorageDeviceEnumerated
| RobotMassStorageDeviceRemoved
| NotifySubscribeAction

export type IPCSafeFormDataEntry =
| {
type: 'string'
name: string
value: string
}
| {
type: 'file'
name: string
value: ArrayBuffer
filename: string
}

export type IPCSafeFormData = IPCSafeFormDataEntry[]
Loading