diff --git a/python/.changelog.d/1026.added b/python/.changelog.d/1026.added new file mode 100644 index 000000000..808696d89 --- /dev/null +++ b/python/.changelog.d/1026.added @@ -0,0 +1 @@ +New exception type `DeviceIsBusy` indicates that the device is in use by another process. diff --git a/python/.changelog.d/1026.fixed b/python/.changelog.d/1026.fixed new file mode 100644 index 000000000..a5b7dcc05 --- /dev/null +++ b/python/.changelog.d/1026.fixed @@ -0,0 +1 @@ +trezorctl will correctly report that device is in use. diff --git a/python/src/trezorlib/cli/__init__.py b/python/src/trezorlib/cli/__init__.py index 5d471c92c..41b03c6fe 100644 --- a/python/src/trezorlib/cli/__init__.py +++ b/python/src/trezorlib/cli/__init__.py @@ -21,9 +21,8 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional import click -from .. import exceptions +from .. import exceptions, transport from ..client import TrezorClient -from ..transport import get_transport from ..ui import ClickUI, ScriptUI if TYPE_CHECKING: @@ -67,14 +66,14 @@ class TrezorConnection: def get_transport(self) -> "Transport": try: # look for transport without prefix search - return get_transport(self.path, prefix_search=False) + return transport.get_transport(self.path, prefix_search=False) except Exception: # most likely not found. try again below. pass # look for transport with prefix search # if this fails, we want the exception to bubble up to the caller - return get_transport(self.path, prefix_search=True) + return transport.get_transport(self.path, prefix_search=True) def get_ui(self) -> "TrezorClientUI": if self.script: @@ -101,6 +100,9 @@ class TrezorConnection: """ try: client = self.get_client() + except transport.DeviceIsBusy: + click.echo("Device is in use by another process.") + sys.exit(1) except Exception: click.echo("Failed to find a Trezor device.") if self.path is not None: diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py index 9db6af67f..1e331194a 100755 --- a/python/src/trezorlib/cli/trezorctl.py +++ b/python/src/trezorlib/cli/trezorctl.py @@ -26,7 +26,7 @@ import click from .. import log, messages, protobuf, ui from ..client import TrezorClient -from ..transport import enumerate_devices +from ..transport import DeviceIsBusy, enumerate_devices from ..transport.udp import UdpTransport from . import ( AliasedGroup, @@ -247,9 +247,15 @@ def list_devices(no_resolve: bool) -> Optional[Iterable["Transport"]]: return enumerate_devices() for transport in enumerate_devices(): - client = TrezorClient(transport, ui=ui.ClickUI()) - click.echo(f"{transport} - {format_device_name(client.features)}") - client.end_session() + try: + client = TrezorClient(transport, ui=ui.ClickUI()) + description = format_device_name(client.features) + client.end_session() + except DeviceIsBusy: + description = "Device is in use by another process" + except Exception: + description = "Failed to read details" + click.echo(f"{transport} - {description}") return None diff --git a/python/src/trezorlib/transport/__init__.py b/python/src/trezorlib/transport/__init__.py index 0828c6ed9..b04876b6b 100644 --- a/python/src/trezorlib/transport/__init__.py +++ b/python/src/trezorlib/transport/__init__.py @@ -48,6 +48,10 @@ class TransportException(TrezorException): pass +class DeviceIsBusy(TransportException): + pass + + class Transport: """Raw connection to a Trezor device. diff --git a/python/src/trezorlib/transport/bridge.py b/python/src/trezorlib/transport/bridge.py index d77e3693d..0066e12d0 100644 --- a/python/src/trezorlib/transport/bridge.py +++ b/python/src/trezorlib/transport/bridge.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional import requests from ..log import DUMP_PACKETS -from . import MessagePayload, Transport, TransportException +from . import DeviceIsBusy, MessagePayload, Transport, TransportException if TYPE_CHECKING: from ..models import TrezorModel @@ -37,14 +37,19 @@ CONNECTION = requests.Session() CONNECTION.headers.update(TREZORD_ORIGIN_HEADER) -def call_bridge(uri: str, data: Optional[str] = None) -> requests.Response: - url = TREZORD_HOST + "/" + uri +class BridgeException(TransportException): + def __init__(self, path: str, status: int, message: str) -> None: + self.path = path + self.status = status + self.message = message + super().__init__(f"trezord: {path} failed with code {status}: {message}") + + +def call_bridge(path: str, data: Optional[str] = None) -> requests.Response: + url = TREZORD_HOST + "/" + path r = CONNECTION.post(url, data=data) if r.status_code != 200: - error_str = ( - f"trezord: {uri} failed with code {r.status_code}: {r.json()['error']}" - ) - raise TransportException(error_str) + raise BridgeException(path, r.status_code, r.json()["error"]) return r @@ -150,7 +155,12 @@ class BridgeTransport(Transport): return [] def begin_session(self) -> None: - data = self._call("acquire/" + self.device["path"]) + try: + data = self._call("acquire/" + self.device["path"]) + except BridgeException as e: + if e.message == "wrong previous session": + raise DeviceIsBusy(self.device["path"]) from e + raise self.session = data.json()["session"] def end_session(self) -> None: diff --git a/python/src/trezorlib/transport/webusb.py b/python/src/trezorlib/transport/webusb.py index cf71f0883..d733ce666 100644 --- a/python/src/trezorlib/transport/webusb.py +++ b/python/src/trezorlib/transport/webusb.py @@ -22,7 +22,7 @@ from typing import Iterable, List, Optional from ..log import DUMP_PACKETS from ..models import TREZORS, TrezorModel -from . import UDEV_RULES_STR, TransportException +from . import UDEV_RULES_STR, DeviceIsBusy, TransportException from .protocol import ProtocolBasedTransport, ProtocolV1 LOG = logging.getLogger(__name__) @@ -57,7 +57,10 @@ class WebUsbHandle: else: args = () raise IOError("Cannot open device", *args) - self.handle.claimInterface(self.interface) + try: + self.handle.claimInterface(self.interface) + except usb1.USBErrorAccess as e: + raise DeviceIsBusy(self.device) from e def close(self) -> None: if self.handle is not None: