From 88708388345e02ed7480f09710028ca2dcb7095d Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 7 Apr 2025 15:01:57 +0200 Subject: [PATCH] raise instead of overwriting the config file on syntax error --- electrum/gui/qt/__init__.py | 24 +++++++++++++++++++++++- electrum/simple_config.py | 8 +++----- run_electrum | 25 ++++++++++++++++++++++--- tests/test_simple_config.py | 5 ++--- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index 6eb22f0b7..33cc3af74 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -27,7 +27,7 @@ import os import signal import sys import threading -from typing import Optional, TYPE_CHECKING, List, Sequence +from typing import Optional, TYPE_CHECKING, List, Sequence, Union try: import PyQt6 @@ -582,3 +582,25 @@ class ElectrumGui(BaseElectrumGui, Logger): message = _("Text copied to Clipboard") if title is None else _("{} copied to Clipboard").format(title) # tooltip cannot be displayed immediately when called from a menu; wait 200ms self.timer.singleShot(200, lambda: QToolTip.showText(QCursor.pos(), message, None)) + + +def standalone_exception_dialog(exception: Union[str, BaseException]) -> None: + app = QApplication.instance() + if not app: + app = QApplication([]) + + msg_box = QMessageBox() + msg_box.setWindowTitle(_("Error starting Electrum")) + msg_box.setIcon(QMessageBox.Icon.Critical) + msg_box.setText(_("An error occurred") + ":") + msg_box.setInformativeText(str(exception)) + + # Add detailed traceback if available + if hasattr(exception, "__traceback__"): + import traceback + detailed_text = ''.join(traceback.format_exception( + type(exception), exception, exception.__traceback__) + ) + msg_box.setDetailedText(detailed_text) + + msg_box.exec() diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 18417ccff..bc254c13c 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -900,9 +900,7 @@ def read_user_config(path: Optional[str]) -> Dict[str, Any]: with open(config_path, "r", encoding='utf-8') as f: data = f.read() result = json.loads(data) - except Exception as exc: - _logger.warning(f"Cannot read config file at {config_path}: {exc}") - return {} - if not type(result) is dict: - return {} + assert isinstance(result, dict), "config file is not a dict" + except Exception as e: + raise ValueError(f"Invalid config file at {config_path}: {str(e)}") return result diff --git a/run_electrum b/run_electrum index 4009a1aa2..a13391f10 100755 --- a/run_electrum +++ b/run_electrum @@ -271,6 +271,25 @@ def sys_exit(i): loop_thread.join(timeout=1) sys.exit(i) +def read_config(config_options: dict) -> SimpleConfig: + """ + Reads the config file and returns SimpleConfig, on failure it will potentially + show a GUI error dialog if a gui is available, and then re-raise the exception. + """ + try: + return SimpleConfig(config_options) + except Exception as config_error: + # parse full cmd to find out which UI is being used + full_config_options = parse_command_line(simple_parser=False) + if full_config_options.get("cmd") == 'gui': + gui_name = full_config_options.get(SimpleConfig.GUI_NAME.key(), 'qt') + try: + gui = __import__(f'electrum.gui.{gui_name}', fromlist=['electrum']) + gui.standalone_exception_dialog(config_error) # type: ignore + except Exception as e: + print_stderr(f"Error showing standalone gui dialog: {e}") + raise + def parse_command_line(simple_parser=False) -> Dict: # parse command line from sys.argv if simple_parser: @@ -371,14 +390,14 @@ def main(): sys.argv.remove(x) # parse first without plugins config_options = parse_command_line(simple_parser=True) - tmp_config = SimpleConfig(config_options) + tmp_config = read_config(config_options) # load (only) the commands modules of plugins so their commands are registered - plugin_commands = Plugins(tmp_config, cmd_only=True) + _plugin_commands = Plugins(tmp_config, cmd_only=True) # re-parse command line sys.argv = saved_sys_argv[:] config_options = parse_command_line() - config = SimpleConfig(config_options) + config = read_config(config_options) cmdname = config.get('cmd') # set language as early as possible diff --git a/tests/test_simple_config.py b/tests/test_simple_config.py index 9b77de76a..0a2aa2e7f 100644 --- a/tests/test_simple_config.py +++ b/tests/test_simple_config.py @@ -6,7 +6,6 @@ import shutil from io import StringIO from electrum.simple_config import SimpleConfig, read_user_config -from electrum import constants from . import ElectrumTestCase @@ -250,5 +249,5 @@ class TestUserConfig(ElectrumTestCase): with open(thefile, "w") as f: f.write(repr(payload)) - result = read_user_config(self.user_dir) - self.assertEqual({}, result) + with self.assertRaises(ValueError): + read_user_config(self.user_dir)