From c3a6a0fbfb561e892e533ca0027862afa9f5bc9b Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 7 Nov 2023 11:04:12 -0500 Subject: [PATCH] Better handling for surprise disconnects - Fix a state issue where we didn't clear sessions; we now clear them on retry or cancel - Handle surprise unmounts that leave /media populated by also watching /dev - Fix a bug where the initial usb scan used full paths instead of device names; now everything uses full paths --- app-shell-odd/src/system-update/index.ts | 7 ++++ app-shell-odd/src/usb.ts | 39 +++++++++++++++---- app/src/pages/OnDeviceDisplay/UpdateRobot.tsx | 17 +++++--- .../UpdateRobotDuringOnboarding.tsx | 13 +++++-- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/app-shell-odd/src/system-update/index.ts b/app-shell-odd/src/system-update/index.ts index c52e99ddef2..c0ea2f49ee3 100644 --- a/app-shell-odd/src/system-update/index.ts +++ b/app-shell-odd/src/system-update/index.ts @@ -141,8 +141,15 @@ export function registerRobotSystemUpdate(dispatch: Dispatch): Dispatch { massStorageUpdateSet !== null && massStorageUpdateSet.system.startsWith(action.payload.rootPath) ) { + console.log( + `Mass storage device ${action.payload.rootPath} removed, reverting to non-usb updates` + ) massStorageUpdateSet = null getCachedSystemUpdateFiles(dispatch) + } else { + console.log( + `Mass storage device ${action.payload.rootPath} removed but this was not an update source` + ) } break } diff --git a/app-shell-odd/src/usb.ts b/app-shell-odd/src/usb.ts index ac269b78c2e..1d84abb733c 100644 --- a/app-shell-odd/src/usb.ts +++ b/app-shell-odd/src/usb.ts @@ -9,7 +9,8 @@ import { robotMassStorageDeviceRemoved, } from '@opentrons/app/src/redux/shell/actions' const FLEX_USB_MOUNT_DIR = '/media/' -const FLEX_USB_MOUNT_FILTER = /sd[a-z][0-9]$/ +const FLEX_USB_DEVICE_DIR = '/dev/' +const FLEX_USB_MOUNT_FILTER = /sd[a-z]+[0-9]+$/ const MOUNT_ENUMERATION_DELAY_MS = 1000 // These directories are generated by OSX and contain entries for all @@ -89,7 +90,7 @@ export function watchForMassStorage(dispatch: Dispatch): () => void { prevDirs = present.filter((entry): entry is string => entry !== null) }) - const watcher = fs.watch( + const mediaWatcher = fs.watch( FLEX_USB_MOUNT_DIR, { persistent: true }, (event, fileName) => { @@ -107,25 +108,49 @@ export function watchForMassStorage(dispatch: Dispatch): () => void { if (!info.isDirectory) { return } - if (prevDirs.includes(fileName)) { + if (prevDirs.includes(fullPath)) { return } console.log(`New mass storage device ${fileName} detected`) - prevDirs.push(fileName) + prevDirs.push(fullPath) return handleNewlyPresent(fullPath) }) .catch(() => { - if (prevDirs.includes(fileName)) { + if (prevDirs.includes(fullPath)) { console.log(`Mass storage device at ${fileName} removed`) - prevDirs = prevDirs.filter(entry => entry !== fileName) + prevDirs = prevDirs.filter(entry => entry !== fullPath) dispatch(robotMassStorageDeviceRemoved(fullPath)) } }) } ) + const devWatcher = fs.watch( + FLEX_USB_DEVICE_DIR, + { persistent: true }, + (event, fileName) => { + if (!!!fileName) return + if (!fileName.match(FLEX_USB_MOUNT_FILTER)) return + const fullPath = join(FLEX_USB_DEVICE_DIR, fileName) + const mountPath = join(FLEX_USB_MOUNT_DIR, fileName) + fsPromises.stat(fullPath).catch(() => { + if (prevDirs.includes(mountPath)) { + console.log(`Mass storage device at ${fileName} removed`) + prevDirs = prevDirs.filter(entry => entry !== mountPath) + dispatch( + robotMassStorageDeviceRemoved(join(FLEX_USB_MOUNT_DIR, fileName)) + ) + // we don't care if this fails because it's racing the system removing + // the mount dir in the common case + fsPromises.unlink(mountPath).catch(() => {}) + } + }) + } + ) + rescan(dispatch) return () => { - watcher.close() + mediaWatcher.close() + devWatcher.close() } } diff --git a/app/src/pages/OnDeviceDisplay/UpdateRobot.tsx b/app/src/pages/OnDeviceDisplay/UpdateRobot.tsx index 2e8fe867881..810ff22bd40 100644 --- a/app/src/pages/OnDeviceDisplay/UpdateRobot.tsx +++ b/app/src/pages/OnDeviceDisplay/UpdateRobot.tsx @@ -8,8 +8,9 @@ import { Flex, SPACING, DIRECTION_ROW } from '@opentrons/components' import { getLocalRobot } from '../../redux/discovery' import { getRobotUpdateAvailable, - startRobotUpdate, + clearRobotUpdateSession, } from '../../redux/robot-update' +import { useDispatchStartRobotUpdate } from '../../redux/robot-update/hooks' import { UNREACHABLE } from '../../redux/discovery/constants' import { MediumButton } from '../../atoms/buttons' @@ -24,8 +25,6 @@ import type { State, Dispatch } from '../../redux/types' export function UpdateRobot(): JSX.Element { const history = useHistory() const { i18n, t } = useTranslation(['device_settings', 'shared']) - const dispatch = useDispatch() - const localRobot = useSelector(getLocalRobot) const robotUpdateType = useSelector((state: State) => { return localRobot != null && localRobot.status !== UNREACHABLE @@ -33,6 +32,8 @@ export function UpdateRobot(): JSX.Element { : null }) const robotName = localRobot?.name != null ? localRobot.name : 'no name' + const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() + const dispatch = useDispatch() const [errorString, setErrorString] = React.useState(null) @@ -45,11 +46,17 @@ export function UpdateRobot(): JSX.Element { flex="1" buttonType="secondary" buttonText={t('cancel_software_update')} - onClick={() => history.goBack()} + onClick={() => { + dispatch(clearRobotUpdateSession()) + history.goBack() + }} /> dispatch(startRobotUpdate(robotName))} + onClick={() => { + setErrorString(null) + dispatchStartRobotUpdate(robotName) + }} buttonText={i18n.format(t('shared:try_again'), 'capitalize')} /> diff --git a/app/src/pages/OnDeviceDisplay/UpdateRobotDuringOnboarding.tsx b/app/src/pages/OnDeviceDisplay/UpdateRobotDuringOnboarding.tsx index 2a6e79541bf..315e923c731 100644 --- a/app/src/pages/OnDeviceDisplay/UpdateRobotDuringOnboarding.tsx +++ b/app/src/pages/OnDeviceDisplay/UpdateRobotDuringOnboarding.tsx @@ -5,10 +5,12 @@ import { useTranslation } from 'react-i18next' import { Flex, SPACING, DIRECTION_ROW } from '@opentrons/components' +import { useDispatchStartRobotUpdate } from '../../redux/robot-update/hooks' + import { getLocalRobot } from '../../redux/discovery' import { getRobotUpdateAvailable, - startRobotUpdate, + clearRobotUpdateSession, } from '../../redux/robot-update' import { UNREACHABLE } from '../../redux/discovery/constants' import { @@ -34,7 +36,7 @@ export function UpdateRobotDuringOnboarding(): JSX.Element { ] = React.useState(true) const history = useHistory() const { i18n, t } = useTranslation(['device_settings', 'shared']) - + const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() const dispatch = useDispatch() const localRobot = useSelector(getLocalRobot) const robotUpdateType = useSelector((state: State) => { @@ -82,11 +84,14 @@ export function UpdateRobotDuringOnboarding(): JSX.Element { flex="1" buttonType="secondary" buttonText={t('proceed_without_updating')} - onClick={() => history.push('/emergency-stop')} + onClick={() => { + dispatch(clearRobotUpdateSession()) + history.push('/emergency-stop') + }} /> dispatch(startRobotUpdate(robotName))} + onClick={() => dispatchStartRobotUpdate(robotName)} buttonText={i18n.format(t('shared:try_again'), 'capitalize')} />