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
30 changes: 17 additions & 13 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 All @@ -349,32 +351,33 @@ export class Device extends TypedEmitter<DeviceEvents> {
try {
await this.run();
} catch (error) {
_log.warn(`device.run error.message: ${error.message}, code: ${error.code}`);

if (
error.code === 'Device_NotFound' ||
error.message === TRANSPORT_ERROR.DEVICE_NOT_FOUND ||
error.message === TRANSPORT_ERROR.DEVICE_DISCONNECTED_DURING_ACTION ||
error.message === TRANSPORT_ERROR.UNEXPECTED_ERROR ||
error.message === TRANSPORT_ERROR.DESCRIPTOR_NOT_FOUND ||
error.message === TRANSPORT_ERROR.HTTP_ERROR // bridge died during device initialization
) {
// disconnected, do nothing
} else if (
// we don't know what really happened
error.message === TRANSPORT_ERROR.UNEXPECTED_ERROR ||
// someone else took the device at the same time
error.message === TRANSPORT_ERROR.SESSION_WRONG_PREVIOUS ||
error.code === 'Device_UsedElsewhere' // most common error - someone else took the device at the same time
// device had some session when first seen -> we do not read it so that we don't interrupt somebody else's flow
error.code === 'Device_UsedElsewhere' ||
// TODO: is this needed? can't I just use transport error?
error.code === 'Device_InitializeFailed'
) {
// TODO needed only for TRANSPORT_ERROR.SESSION_WRONG_PREVIOUS
// this.enumerate(transport);
this.emitLifecycle(DEVICE.CONNECT_UNACQUIRED);
} else if (
// device was claimed by another application on transport api layer (claimInterface in usb nomenclature) but never released (releaseInterface in usb nomenclature)
// the only remedy for this is to reconnect device manually
// or possibly there are 2 applications without common sessions background
error.message === TRANSPORT_ERROR.INTERFACE_UNABLE_TO_OPEN_DEVICE ||
// catch one of trezord LIBUSB_ERRORs
error.message?.indexOf(ERRORS.LIBUSB_ERROR_MESSAGE) >= 0 ||
// we tried to initialize device (either automatically after enumeration or after user click)
// but it did not work out. this device is effectively unreadable and user should do something about it
error.code === 'Device_InitializeFailed'
error.message?.indexOf(ERRORS.LIBUSB_ERROR_MESSAGE) >= 0
) {
this.unreadableError = error?.message;
this.emitLifecycle(DEVICE.CONNECT_UNACQUIRED);
Expand Down Expand Up @@ -553,6 +556,7 @@ export class Device extends TypedEmitter<DeviceEvents> {
]);
}
} catch (error) {
_log.warn('Device._runInner error: ', error.message);
if (
!this.inconsistent &&
(error.message === 'GetFeatures timeout' || error.message === 'Unknown message')
Expand Down Expand Up @@ -750,7 +754,7 @@ export class Device extends TypedEmitter<DeviceEvents> {
this._updateFeatures(message);
}

async checkFirmwareHash(): Promise<FirmwareHashCheckResult | null> {
private async checkFirmwareHash(): Promise<FirmwareHashCheckResult | null> {
const createFailResult = (error: FirmwareHashCheckError, errorPayload?: unknown) => ({
success: false,
error,
Expand Down Expand Up @@ -818,7 +822,7 @@ export class Device extends TypedEmitter<DeviceEvents> {
}
}

async checkFirmwareRevision() {
private async checkFirmwareRevision() {
const firmwareVersion = this.getVersion();

if (!firmwareVersion || !this.features) {
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 @@ -119,16 +120,7 @@ export const DeviceUnreadable = ({ device }: DeviceUnreadableProps) => {
}

// generic troubleshooting tips
const items = [
// closing other apps and reloading should be the first step. Either we might have made a bug and let two apps to talk
// to device at the same time or there might be another application in the wild not really playing according to our rules
TROUBLESHOOTING_TIP_RECONNECT,
// if on web - try installing desktop. this takes you to using bridge which should be more powerful than WebUSB
TROUBLESHOOTING_TIP_SUITE_DESKTOP,
// unfortunately we have seen reports that even old bridge might not be enough for some Windows users. So the only chance
// is using another computer, or maybe it would be better to say another OS
TROUBLESHOOTING_TIP_DIFFERENT_COMPUTER,
];
const items = [];

// only for unreadable HID devices
if (
Expand All @@ -140,9 +132,26 @@ export const DeviceUnreadable = ({ device }: DeviceUnreadableProps) => {
// If even this did not work, go to support or knowledge base
// 'If the last time you updated your device firmware was in 2019 and earlier please follow instructions in <a>the knowledge base</a>',
items.push(TROUBLESHOOTING_TIP_UNREADABLE_HID);
// if on web - try installing desktop. this takes you to using bridge which should be more powerful than WebUSB.
// at the time of writing this, there is still an option to opt-in for legacy bridge in suite-desktop which can
// communicate with this device. see the next troubleshooting point
items.push(TROUBLESHOOTING_TIP_SUITE_DESKTOP);
// 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);
// closing other apps and reloading should be the first step. Either we might have made a bug and let two apps to talk
// to device at the same time or there might be another application in the wild not really playing according to our rules
items.push(TROUBLESHOOTING_TIP_RECONNECT);
// if on web - try installing desktop. this takes you to using bridge which should be more powerful than WebUSB
items.push(TROUBLESHOOTING_TIP_SUITE_DESKTOP);
// unfortunately we have seen reports that even old bridge might not be enough for some Windows users. So the only chance
// is using another computer, or maybe it would be better to say another OS
items.push(TROUBLESHOOTING_TIP_DIFFERENT_COMPUTER);
}

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
1 change: 1 addition & 0 deletions packages/transport/src/api/abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export abstract class AbstractApi extends TypedEmitter<{
| typeof ERRORS.UNEXPECTED_ERROR
| typeof ERRORS.ABORTED_BY_TIMEOUT
| typeof ERRORS.ABORTED_BY_SIGNAL
| typeof ERRORS.LIBUSB_ERROR_ACCESS
>;

/**
Expand Down
Loading
Loading