diff --git a/electrum/__init__.py b/electrum/__init__.py index 8bafda636..cea6ac59b 100644 --- a/electrum/__init__.py +++ b/electrum/__init__.py @@ -11,6 +11,10 @@ if is_local and os.name == 'nt': os.add_dll_directory(os.path.dirname(__file__)) +class GuiImportError(ImportError): + pass + + from .version import ELECTRUM_VERSION from .util import format_satoshis from .wallet import Wallet diff --git a/electrum/commands.py b/electrum/commands.py index 2107171ef..dd04f770c 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -38,6 +38,7 @@ from functools import wraps, partial from itertools import repeat from decimal import Decimal from typing import Optional, TYPE_CHECKING, Dict, List +import os from .import util, ecc from .util import (bfh, bh2u, format_satoshis, json_decode, json_normalize, @@ -57,11 +58,13 @@ from .lnutil import SENT, RECEIVED from .lnutil import LnFeatures from .lnutil import extract_nodeid from .lnpeer import channel_id_from_funding_tx -from .plugin import run_hook +from .plugin import run_hook, DeviceMgr from .version import ELECTRUM_VERSION from .simple_config import SimpleConfig from .invoices import Invoice from . import submarine_swaps +from . import GuiImportError +from . import crypto if TYPE_CHECKING: @@ -546,9 +549,45 @@ class Commands: @command('') async def version(self): """Return the version of Electrum.""" - from .version import ELECTRUM_VERSION return ELECTRUM_VERSION + @command('') + async def version_info(self): + """Return information about dependencies, such as their version and path.""" + ret = { + "electrum.version": ELECTRUM_VERSION, + "electrum.path": os.path.dirname(os.path.realpath(__file__)), + } + # add currently running GUI + if self.daemon and self.daemon.gui_object: + ret.update(self.daemon.gui_object.version_info()) + # always add Qt GUI, so we get info even when running this from CLI + try: + from .gui.qt import ElectrumGui as QtElectrumGui + ret.update(QtElectrumGui.version_info()) + except GuiImportError: + pass + # Add shared libs (.so/.dll), and non-pure-python dependencies. + # Such deps can be installed in various ways - often via the Linux distro's pkg manager, + # instead of using pip, hence it is useful to list them for debugging. + from . import ecc_fast + ret.update(ecc_fast.version_info()) + from . import qrscanner + ret.update(qrscanner.version_info()) + ret.update(DeviceMgr.version_info()) + ret.update(crypto.version_info()) + # add some special cases + import aiohttp + ret["aiohttp.version"] = aiohttp.__version__ + import aiorpcx + ret["aiorpcx.version"] = aiorpcx._version_str + import certifi + ret["certifi.version"] = certifi.__version__ + import dns + ret["dnspython.version"] = dns.__version__ + + return ret + @command('w') async def getmpk(self, wallet: Abstract_Wallet = None): """Get master public key. Return your wallet\'s master public key""" diff --git a/electrum/crypto.py b/electrum/crypto.py index 8fc02a8d8..fd255c8f0 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -28,7 +28,7 @@ import os import sys import hashlib import hmac -from typing import Union +from typing import Union, Mapping, Optional from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException, versiontuple from .i18n import _ @@ -84,6 +84,27 @@ if not (HAS_CRYPTODOME or HAS_CRYPTOGRAPHY): sys.exit(f"Error: at least one of ('pycryptodomex', 'cryptography') needs to be installed.") +def version_info() -> Mapping[str, Optional[str]]: + ret = {} + if HAS_PYAES: + ret["pyaes.version"] = ".".join(map(str, pyaes.VERSION[:3])) + else: + ret["pyaes.version"] = None + if HAS_CRYPTODOME: + ret["cryptodome.version"] = Cryptodome.__version__ + if hasattr(Cryptodome, "__path__"): + ret["cryptodome.path"] = ", ".join(Cryptodome.__path__ or []) + else: + ret["cryptodome.version"] = None + if HAS_CRYPTOGRAPHY: + ret["cryptography.version"] = cryptography.__version__ + if hasattr(cryptography, "__path__"): + ret["cryptography.path"] = ", ".join(cryptography.__path__ or []) + else: + ret["cryptography.version"] = None + return ret + + class InvalidPadding(Exception): pass diff --git a/electrum/daemon.py b/electrum/daemon.py index 6c26d6b7e..8bb8fc6b9 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -51,6 +51,7 @@ from .commands import known_commands, Commands from .simple_config import SimpleConfig from .exchange_rate import FxThread from .logging import get_logger, Logger +from . import GuiImportError if TYPE_CHECKING: from electrum import gui @@ -622,7 +623,10 @@ class Daemon(Logger): gui_name = 'qt' self.logger.info(f'launching GUI: {gui_name}') try: - gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) + try: + gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) + except GuiImportError as e: + sys.exit(str(e)) self.gui_object = gui.ElectrumGui(config=config, daemon=self, plugins=plugins) if not self._stop_entered: self.gui_object.main() diff --git a/electrum/ecc_fast.py b/electrum/ecc_fast.py index 3a6e706e4..d95234aa0 100644 --- a/electrum/ecc_fast.py +++ b/electrum/ecc_fast.py @@ -138,3 +138,9 @@ except BaseException as e: if _libsecp256k1 is None: # hard fail: sys.exit(f"Error: Failed to load libsecp256k1.") + + +def version_info() -> dict: + return { + "libsecp256k1.path": _libsecp256k1._name if _libsecp256k1 else None, + } diff --git a/electrum/gui/__init__.py b/electrum/gui/__init__.py index 4d4ccdcdd..4d366879c 100644 --- a/electrum/gui/__init__.py +++ b/electrum/gui/__init__.py @@ -4,7 +4,7 @@ # Notifications about network events are sent to the GUI by using network.register_callback() -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Mapping, Optional if TYPE_CHECKING: from . import qt @@ -28,3 +28,7 @@ class BaseElectrumGui: This method must be thread-safe. """ pass + + @classmethod + def version_info(cls) -> Mapping[str, Optional[str]]: + return {} diff --git a/electrum/gui/kivy/__init__.py b/electrum/gui/kivy/__init__.py index ab722d4c9..b7c9249a6 100644 --- a/electrum/gui/kivy/__init__.py +++ b/electrum/gui/kivy/__init__.py @@ -29,16 +29,19 @@ import sys import os from typing import TYPE_CHECKING +from electrum import GuiImportError + KIVY_GUI_PATH = os.path.abspath(os.path.dirname(__file__)) os.environ['KIVY_DATA_DIR'] = os.path.join(KIVY_GUI_PATH, 'data') try: sys.argv = [''] import kivy -except ImportError: +except ImportError as e: # This error ideally shouldn't be raised with pre-built packages - sys.exit("Error: Could not import kivy. Please install it using the " - "instructions mentioned here `https://kivy.org/#download` .") + raise GuiImportError( + "Error: Could not import kivy. Please install it using the " + "instructions mentioned here `https://kivy.org/#download` .") from e # minimum required version for kivy kivy.require('1.8.0') @@ -77,3 +80,12 @@ class ElectrumGui(BaseElectrumGui, Logger): if not app: return Clock.schedule_once(lambda dt: app.stop()) + + @classmethod + def version_info(cls): + ret = { + "kivy.version": kivy.__version__, + } + if hasattr(kivy, "__path__"): + ret["kivy.path"] = ", ".join(kivy.__path__ or []) + return ret diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index bf4780b3a..9ff817b8f 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -30,12 +30,15 @@ import traceback import threading from typing import Optional, TYPE_CHECKING, List +from electrum import GuiImportError try: import PyQt5 import PyQt5.QtGui -except Exception: - sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'") +except Exception as e: + raise GuiImportError( + "Error: Could not import PyQt5 on Linux systems, " + "you may try 'sudo apt-get install python3-pyqt5'") from e from PyQt5.QtGui import QGuiApplication from PyQt5.QtWidgets import (QApplication, QSystemTrayIcon, QWidget, QMenu, @@ -469,3 +472,13 @@ class ElectrumGui(BaseElectrumGui, Logger): def stop(self): self.logger.info('closing GUI') self.app.quit_signal.emit() + + @classmethod + def version_info(cls): + ret = { + "qt.version": QtCore.QT_VERSION_STR, + "pyqt.version": QtCore.PYQT_VERSION_STR, + } + if hasattr(PyQt5, "__path__"): + ret["pyqt.path"] = ", ".join(PyQt5.__path__ or []) + return ret diff --git a/electrum/plugin.py b/electrum/plugin.py index 2d5592d16..fe7b93932 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -29,7 +29,7 @@ import time import threading import sys from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, - Dict, Iterable, List, Sequence, Callable, TypeVar) + Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping) import concurrent from concurrent import futures from functools import wraps, partial @@ -749,3 +749,25 @@ class DeviceMgr(ThreadJob): client.handler.update_status(False) return devices + + @classmethod + def version_info(cls) -> Mapping[str, Optional[str]]: + ret = {} + # add libusb + try: + import usb1 + except Exception as e: + ret["libusb.version"] = None + else: + ret["libusb.version"] = ".".join(map(str, usb1.getVersion()[:4])) + try: + ret["libusb.path"] = usb1.libusb1.libusb._name + except AttributeError: + ret["libusb.path"] = None + # add hidapi + from importlib.metadata import version + try: + ret["hidapi.version"] = version("hidapi") + except ImportError: + ret["hidapi.version"] = None + return ret diff --git a/electrum/qrscanner.py b/electrum/qrscanner.py index dab4e87de..424dd4186 100644 --- a/electrum/qrscanner.py +++ b/electrum/qrscanner.py @@ -102,5 +102,11 @@ def find_system_cameras() -> Mapping[str, str]: return devices +def version_info() -> Mapping[str, Optional[str]]: + return { + "libzbar.path": libzbar._name if libzbar else None, + } + + if __name__ == "__main__": print(scan_barcode())