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

Transport device disconnected #15615

Merged
merged 10 commits into from
Dec 3, 2024
7 changes: 6 additions & 1 deletion packages/connect/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,8 +714,13 @@ const onCallDevice = async (
const response = messageResponse;

if (response) {
await device.cleanup();
const shouldReleaseSession =
response.success ||
(!response.success &&
// @ts-expect-error
response?.payload?.error !== 'device disconnected during action');

await device.cleanup(shouldReleaseSession);
if (useCoreInPopup) {
// We need to send response before closing popup
sendCoreMessage(response);
Expand Down
6 changes: 4 additions & 2 deletions packages/connect/src/device/Device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,15 @@ export class Device extends TypedEmitter<DeviceEvents> {
this.keepTransportSession = false;
}

async cleanup() {
async cleanup(release = true) {
// remove all listeners
this.eventNames().forEach(e => this.removeAllListeners(e as keyof DeviceEvents));

// make sure that Device_CallInProgress will not be thrown
delete this.runPromise;
await this.release();
if (release) {
await this.release();
}
}

// call only once, right after device creation
Expand Down
4 changes: 3 additions & 1 deletion packages/suite/src/components/suite/ConnectDevicePrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ const getMessageId = ({
return isDesktop() ? 'TR_NO_TRANSPORT_DESKTOP' : 'TR_NO_TRANSPORT';
case 'device-bootloader':
return 'TR_DEVICE_CONNECTED_BOOTLOADER';
case 'device-unacquired':
case 'device-used-elsewhere':
return 'TR_DEVICE_CONNECTED_UNACQUIRED';
case 'device-unacquired':
return 'TR_NEEDS_ATTENTION_UNABLE_TO_CONNECT';
default: {
if (connected) {
return !showWarning ? 'TR_DEVICE_CONNECTED' : 'TR_DEVICE_CONNECTED_WRONG_STATE';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { MouseEventHandler } from 'react';

import { acquireDevice } from '@suite-common/wallet-core';
import { Button } from '@trezor/components';
import { isDesktop } from '@trezor/env-utils';

import { Translation, TroubleshootingTips } from 'src/components/suite';
import { useDevice, useDispatch } from 'src/hooks/suite';
import { TROUBLESHOOTING_TIP_RECONNECT } from 'src/components/suite/troubleshooting/tips';
import {
TROUBLESHOOTING_TIP_RECONNECT,
TROUBLESHOOTING_TIP_CLOSE_ALL_TABS,
} from 'src/components/suite/troubleshooting/tips';

export const DeviceAcquire = () => {
const { isLocked, device } = useDevice();
const { isLocked } = useDevice();
const dispatch = useDispatch();

const isDeviceLocked = isLocked();
Expand All @@ -21,45 +23,15 @@ export const DeviceAcquire = () => {

const ctaButton = (
<Button data-testid="@device-acquire" isLoading={isDeviceLocked} onClick={handleClick}>
<Translation id="TR_ACQUIRE_DEVICE" />
<Translation id="TR_TRY_AGAIN" />
</Button>
);

const tips = [
{
key: 'device-used-elsewhere',
heading: <Translation id="TR_DEVICE_CONNECTED_UNACQUIRED" />,
description: device?.transportSessionOwner ? (
<Translation
id="TR_DEVICE_CONNECTED_UNACQUIRED_DESCRIPTION"
values={{
transportSessionOwner: device.transportSessionOwner,
}}
/>
) : (
// legacy bridge does not share transportSessionOwner information
<Translation id="TR_DEVICE_CONNECTED_UNACQUIRED_DESCRIPTION_UNKNOWN_APP" />
),
},
{
key: 'device-acquire',
heading: <Translation id="TR_TROUBLESHOOTING_CLOSE_TABS" />,
description: (
<Translation
id={
isDesktop()
? 'TR_TROUBLESHOOTING_CLOSE_TABS_DESCRIPTION_DESKTOP'
: 'TR_TROUBLESHOOTING_CLOSE_TABS_DESCRIPTION'
}
/>
),
},
TROUBLESHOOTING_TIP_RECONNECT,
];
const tips = [TROUBLESHOOTING_TIP_CLOSE_ALL_TABS, TROUBLESHOOTING_TIP_RECONNECT];

return (
<TroubleshootingTips
label={<Translation id="TR_ACQUIRE_DEVICE_TITLE" />}
label={<Translation id="TR_NEEDS_ATTENTION_UNABLE_TO_CONNECT" />}
cta={ctaButton}
items={tips}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TROUBLESHOOTING_TIP_UNREADABLE_HID,
TROUBLESHOOTING_TIP_SUITE_DESKTOP_TOGGLE_BRIDGE,
TROUBLESHOOTING_TIP_RECONNECT,
TROUBLESHOOTING_TIP_CLOSE_ALL_TABS,
} from 'src/components/suite/troubleshooting/tips';
import { useSelector, useDispatch } from 'src/hooks/suite';
import type { TrezorDevice } from 'src/types/suite';
Expand Down Expand Up @@ -143,6 +144,11 @@ export const DeviceUnreadable = ({ device }: DeviceUnreadableProps) => {
// you might have a very old device which is no longer supported current bridge
// if on desktop - try toggling between the 2 bridges we have available
items.push(TROUBLESHOOTING_TIP_SUITE_DESKTOP_TOGGLE_BRIDGE);
} else {
// it might also be unreadable because device was acquired on transport layer by another app and never released.
// this should be rather exceptional case that happens only when sessions synchronization is broken or other app
// is not cooperating with us
items.push(TROUBLESHOOTING_TIP_CLOSE_ALL_TABS);
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MouseEventHandler } from 'react';

import { acquireDevice } from '@suite-common/wallet-core';
import { Button } from '@trezor/components';

import { Translation, TroubleshootingTips } from 'src/components/suite';
import { useDevice, useDispatch } from 'src/hooks/suite';
import {
TROUBLESHOOTING_TIP_RECONNECT,
TROUBLESHOOTING_TIP_CLOSE_ALL_TABS,
} from 'src/components/suite/troubleshooting/tips';

export const DeviceUsedElsewhere = () => {
const { isLocked, device } = useDevice();
const dispatch = useDispatch();

const isDeviceLocked = isLocked();

const handleClick: MouseEventHandler = e => {
e.stopPropagation();
dispatch(acquireDevice());
};

const ctaButton = (
<Button
data-testid="@device-used-elsewhere"
isLoading={isDeviceLocked}
onClick={handleClick}
>
<Translation id="TR_ACQUIRE_DEVICE" />
</Button>
);

const tips = [
{
key: 'device-used-elsewhere',
heading: <Translation id="TR_DEVICE_CONNECTED_UNACQUIRED" />,
description: (
<Translation
id="TR_DEVICE_CONNECTED_UNACQUIRED_DESCRIPTION"
values={{
transportSessionOwner: device?.transportSessionOwner || 'unknown',
}}
/>
),
},
TROUBLESHOOTING_TIP_CLOSE_ALL_TABS,
TROUBLESHOOTING_TIP_RECONNECT,
];

return (
<TroubleshootingTips
label={<Translation id="TR_ACQUIRE_DEVICE_TITLE" />}
cta={ctaButton}
items={tips}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DeviceNoFirmware } from './DeviceNoFirmware';
import { DeviceUpdateRequired } from './DeviceUpdateRequired';
import { DeviceDisconnectRequired } from './DeviceDisconnectRequired';
import { MultiShareBackupInProgress } from './MultiShareBackupInProgress';
import { DeviceUsedElsewhere } from './DeviceUsedElsewhere';

const Wrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -68,6 +69,8 @@ export const PrerequisitesGuide = ({ allowSwitchDevice }: PrerequisitesGuideProp
return <DeviceConnect isWebUsbTransport={isWebUsbTransport} />;
case 'device-unacquired':
return <DeviceAcquire />;
case 'device-used-elsewhere':
return <DeviceUsedElsewhere />;
case 'device-unreadable':
return <DeviceUnreadable device={device} />;
case 'device-unknown':
Expand Down
14 changes: 14 additions & 0 deletions packages/suite/src/components/suite/troubleshooting/tips/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,17 @@ export const TROUBLESHOOTING_TIP_RECONNECT = {
/>
),
};

export const TROUBLESHOOTING_TIP_CLOSE_ALL_TABS = {
key: 'device-acquire',
heading: <Translation id="TR_TROUBLESHOOTING_CLOSE_TABS" />,
description: (
<Translation
id={
isDesktop()
? 'TR_TROUBLESHOOTING_CLOSE_TABS_DESCRIPTION_DESKTOP'
: 'TR_TROUBLESHOOTING_CLOSE_TABS_DESCRIPTION'
}
/>
),
};
9 changes: 4 additions & 5 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2087,6 +2087,10 @@ const messages = defineMessagesWithTypeCheck({
defaultMessage: 'Trezor is not readable.',
id: 'TR_NEEDS_ATTENTION_UNREADABLE',
},
TR_NEEDS_ATTENTION_UNABLE_TO_CONNECT: {
defaultMessage: 'Failed to communicate with Trezor',
id: 'TR_NEEDS_ATTENTION_UNABLE_TO_CONNECT',
},
TR_UDEV_DOWNLOAD_TITLE: {
defaultMessage: 'Download udev rules',
id: 'TR_UDEV_DOWNLOAD_TITLE',
Expand Down Expand Up @@ -6790,11 +6794,6 @@ const messages = defineMessagesWithTypeCheck({
defaultMessage:
'The app {transportSessionOwner} may currently be using this device. You can take control of the device if needed.',
},
TR_DEVICE_CONNECTED_UNACQUIRED_DESCRIPTION_UNKNOWN_APP: {
id: 'TR_DEVICE_CONNECTED_UNACQUIRED_DESCRIPTION_UNKNOWN_APP',
defaultMessage:
'Another app may currently be using this device. You can take control of the device if needed.',
},
TR_WIPE_OR_UPDATE: {
id: 'TR_WIPE_OR_UPDATE',
defaultMessage: 'Reset device or update firmware',
Expand Down
3 changes: 3 additions & 0 deletions packages/suite/src/utils/suite/prerequisites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export const getPrerequisiteName = ({ router, device, transport }: GetPrerequisi
return 'device-disconnect-required';
}

if (device.type === 'unacquired' && device?.transportSessionOwner)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

todo: device.used ?

return 'device-used-elsewhere';

// device features cannot be read, device is probably used in another window
if (device.type === 'unacquired') return 'device-unacquired';

Expand Down
40 changes: 25 additions & 15 deletions packages/transport/src/api/usb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ interface TransportInterfaceDevice {
device: USBDevice;
}

/**
* Local error. We cast it to "device disconnected during action" from bridge as it means the same
*/
const INTERFACE_DEVICE_DISCONNECTED = 'The device was disconnected.' as const;

export class UsbApi extends AbstractApi {
chunkSize = 64;

Expand Down Expand Up @@ -218,11 +213,8 @@ export class UsbApi extends AbstractApi {
return this.success(Buffer.from(res.data.buffer));
} catch (err) {
this.logger?.error(`usb: device.transferIn error ${err}`);
if (err.message === INTERFACE_DEVICE_DISCONNECTED) {
return this.error({ error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION });
}

return this.error({ error: ERRORS.INTERFACE_DATA_TRANSFER, message: err.message });
return this.handleReadWriteError(err);
}
}

Expand Down Expand Up @@ -255,12 +247,7 @@ export class UsbApi extends AbstractApi {

return this.success(undefined);
} catch (err) {
this.logger?.error(`usb: device.transferOut error ${err}`);
if (err.message === INTERFACE_DEVICE_DISCONNECTED) {
return this.error({ error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION });
}

return this.error({ error: ERRORS.INTERFACE_DATA_TRANSFER, message: err.message });
return this.handleReadWriteError(err);
}
}

Expand Down Expand Up @@ -517,6 +504,29 @@ export class UsbApi extends AbstractApi {
return [hidDevices, nonHidDevices];
}

// https://github.com/trezor/trezord-go/blob/db03d99230f5b609a354e3586f1dfc0ad6da16f7/usb/libusb.go#L545
private handleReadWriteError(err: Error) {
if (
[
// node usb
'LIBUSB_TRANSFER_ERROR',
'LIBUSB_ERROR_PIPE',
'LIBUSB_ERROR_IO',
'LIBUSB_ERROR_NO_DEVICE',
'LIBUSB_ERROR_OTHER',
// web usb
ERRORS.INTERFACE_DATA_TRANSFER,
'The device was disconnected.',
].some(disconnectedErr => {
return err.message.includes(disconnectedErr);
})
) {
return this.error({ error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION });
}

return this.unknownError(err);
}

public dispose() {
if (this.usbInterface) {
this.usbInterface.onconnect = null;
Expand Down
2 changes: 1 addition & 1 deletion packages/transport/tests/apiUsb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('api/usb', () => {

const result = await promise;
if (result.success) throw new Error('Unexpected success');
expect(result.error).toContain('A transfer error has occurred.');
expect(result.error).toContain('unexpected error');
expect(reset).toHaveBeenCalledTimes(1);
});

Expand Down