From 1f6bd9a694e16c82438329803b2f376252688bb0 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 29 Jun 2022 17:47:24 +0200 Subject: [PATCH] kivy: fix threading issue for window.show_error related: https://github.com/spesmilo/electrum/commit/5e0df77df6b06330284e2c15e188fbb4a9865fdb kivy 2.1 seemingly became more sensitive to threading issues. This used to work on kivy 2.0 and older, but 2.1 is complaining. ``` E | lnworker.LNWallet.[default_wallet] | Exception in pay_invoice: TypeError('Cannot create graphics instruction outside the main Kivy thread') Traceback (most recent call last): File "...\electrum\util.py", line 1184, in wrapper return await func(*args, **kwargs) File "...\electrum\lnworker.py", line 1178, in pay_invoice util.trigger_callback('payment_succeeded', self.wallet, key) File "...\electrum\util.py", line 1633, in trigger_callback callback(*args) File "...\electrum\gui\kivy\main_window.py", line 309, in on_event_payment_succeeded self.show_info(_('Payment succeeded') + '\n\n' + description) File "...\electrum\gui\kivy\main_window.py", line 1105, in show_info self.show_error(error, icon=f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/important', File "...\electrum\gui\kivy\main_window.py", line 1097, in show_error self.show_info_bubble(text=error, icon=icon, width=width, File "...\electrum\gui\kivy\main_window.py", line 1123, in show_info_bubble info_bubble = self.info_bubble = Factory.InfoBubble() File "...\Python310\site-packages\kivy\uix\bubble.py", line 199, in __init__ self._arrow_layout = BoxLayout() File "...\Python310\site-packages\kivy\uix\boxlayout.py", line 145, in __init__ super(BoxLayout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\layout.py", line 76, in __init__ super(Layout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\widget.py", line 361, in __init__ self.canvas = Canvas(opacity=self.opacity) File "kivy\graphics\instructions.pyx", line 608, in kivy.graphics.instructions.Canvas.__init__ File "kivy\graphics\instructions.pyx", line 154, in kivy.graphics.instructions.InstructionGroup.__init__ File "kivy\graphics\instructions.pyx", line 60, in kivy.graphics.instructions.Instruction.__init__ TypeError: Cannot create graphics instruction outside the main Kivy thread E | gui.kivy.uix.dialogs.crash_reporter.ExceptionHook | exception caught by crash reporter Traceback (most recent call last): File "...\electrum\gui\kivy\uix\screens.py", line 419, in pay_thread fut.result() File "...\Python310\lib\concurrent\futures\_base.py", line 446, in result return self.__get_result() File "...\Python310\lib\concurrent\futures\_base.py", line 391, in __get_result raise self._exception File "...\electrum\util.py", line 1184, in wrapper return await func(*args, **kwargs) File "...\electrum\lnworker.py", line 1178, in pay_invoice util.trigger_callback('payment_succeeded', self.wallet, key) File "...\electrum\util.py", line 1633, in trigger_callback callback(*args) File "...\electrum\gui\kivy\main_window.py", line 309, in on_event_payment_succeeded self.show_info(_('Payment succeeded') + '\n\n' + description) File "...\electrum\gui\kivy\main_window.py", line 1105, in show_info self.show_error(error, icon=f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/important', File "...\electrum\gui\kivy\main_window.py", line 1097, in show_error self.show_info_bubble(text=error, icon=icon, width=width, File "...\electrum\gui\kivy\main_window.py", line 1123, in show_info_bubble info_bubble = self.info_bubble = Factory.InfoBubble() File "...\Python310\site-packages\kivy\uix\bubble.py", line 199, in __init__ self._arrow_layout = BoxLayout() File "...\Python310\site-packages\kivy\uix\boxlayout.py", line 145, in __init__ super(BoxLayout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\layout.py", line 76, in __init__ super(Layout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\widget.py", line 361, in __init__ self.canvas = Canvas(opacity=self.opacity) File "kivy\graphics\instructions.pyx", line 608, in kivy.graphics.instructions.Canvas.__init__ File "kivy\graphics\instructions.pyx", line 154, in kivy.graphics.instructions.InstructionGroup.__init__ File "kivy\graphics\instructions.pyx", line 60, in kivy.graphics.instructions.Instruction.__init__ TypeError: Cannot create graphics instruction outside the main Kivy thread During handling of the above exception, another exception occurred: Traceback (most recent call last): File "...\electrum\util.py", line 1128, in run_with_except_hook run_original(*args2, **kwargs2) File "...\Python310\lib\threading.py", line 946, in run self._target(*self._args, **self._kwargs) File "...\electrum\gui\kivy\uix\screens.py", line 421, in pay_thread self.app.show_error(repr(e)) File "...\electrum\gui\kivy\main_window.py", line 1097, in show_error self.show_info_bubble(text=error, icon=icon, width=width, File "...\electrum\gui\kivy\main_window.py", line 1123, in show_info_bubble info_bubble = self.info_bubble = Factory.InfoBubble() File "...\Python310\site-packages\kivy\uix\bubble.py", line 199, in __init__ self._arrow_layout = BoxLayout() File "...\Python310\site-packages\kivy\uix\boxlayout.py", line 145, in __init__ super(BoxLayout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\layout.py", line 76, in __init__ super(Layout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\widget.py", line 361, in __init__ self.canvas = Canvas(opacity=self.opacity) File "kivy\graphics\instructions.pyx", line 608, in kivy.graphics.instructions.Canvas.__init__ File "kivy\graphics\instructions.pyx", line 154, in kivy.graphics.instructions.InstructionGroup.__init__ File "kivy\graphics\instructions.pyx", line 60, in kivy.graphics.instructions.Instruction.__init__ TypeError: Cannot create graphics instruction outside the main Kivy thread Exception in thread Thread-2 (pay_thread): Traceback (most recent call last): File "...\electrum\gui\kivy\uix\screens.py", line 419, in pay_thread fut.result() File "...\Python310\lib\concurrent\futures\_base.py", line 446, in result return self.__get_result() File "...\Python310\lib\concurrent\futures\_base.py", line 391, in __get_result raise self._exception File "...\electrum\util.py", line 1184, in wrapper return await func(*args, **kwargs) File "...\electrum\lnworker.py", line 1178, in pay_invoice util.trigger_callback('payment_succeeded', self.wallet, key) File "...\electrum\util.py", line 1633, in trigger_callback callback(*args) File "...\electrum\gui\kivy\main_window.py", line 309, in on_event_payment_succeeded self.show_info(_('Payment succeeded') + '\n\n' + description) File "...\electrum\gui\kivy\main_window.py", line 1105, in show_info self.show_error(error, icon=f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/important', File "...\electrum\gui\kivy\main_window.py", line 1097, in show_error self.show_info_bubble(text=error, icon=icon, width=width, File "...\electrum\gui\kivy\main_window.py", line 1123, in show_info_bubble info_bubble = self.info_bubble = Factory.InfoBubble() File "...\Python310\site-packages\kivy\uix\bubble.py", line 199, in __init__ self._arrow_layout = BoxLayout() File "...\Python310\site-packages\kivy\uix\boxlayout.py", line 145, in __init__ super(BoxLayout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\layout.py", line 76, in __init__ super(Layout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\widget.py", line 361, in __init__ self.canvas = Canvas(opacity=self.opacity) File "kivy\graphics\instructions.pyx", line 608, in kivy.graphics.instructions.Canvas.__init__ File "kivy\graphics\instructions.pyx", line 154, in kivy.graphics.instructions.InstructionGroup.__init__ File "kivy\graphics\instructions.pyx", line 60, in kivy.graphics.instructions.Instruction.__init__ TypeError: Cannot create graphics instruction outside the main Kivy thread During handling of the above exception, another exception occurred: Traceback (most recent call last): File "...\electrum\util.py", line 1128, in run_with_except_hook run_original(*args2, **kwargs2) File "...\Python310\lib\threading.py", line 946, in run self._target(*self._args, **self._kwargs) File "...\electrum\gui\kivy\uix\screens.py", line 421, in pay_thread self.app.show_error(repr(e)) File "...\electrum\gui\kivy\main_window.py", line 1097, in show_error self.show_info_bubble(text=error, icon=icon, width=width, File "...\electrum\gui\kivy\main_window.py", line 1123, in show_info_bubble info_bubble = self.info_bubble = Factory.InfoBubble() File "...\Python310\site-packages\kivy\uix\bubble.py", line 199, in __init__ self._arrow_layout = BoxLayout() File "...\Python310\site-packages\kivy\uix\boxlayout.py", line 145, in __init__ super(BoxLayout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\layout.py", line 76, in __init__ super(Layout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\widget.py", line 361, in __init__ self.canvas = Canvas(opacity=self.opacity) File "kivy\graphics\instructions.pyx", line 608, in kivy.graphics.instructions.Canvas.__init__ File "kivy\graphics\instructions.pyx", line 154, in kivy.graphics.instructions.InstructionGroup.__init__ File "kivy\graphics\instructions.pyx", line 60, in kivy.graphics.instructions.Instruction.__init__ TypeError: Cannot create graphics instruction outside the main Kivy thread During handling of the above exception, another exception occurred: Traceback (most recent call last): File "...\Python310\lib\threading.py", line 1009, in _bootstrap_inner self.run() File "...\electrum\util.py", line 1130, in run_with_except_hook sys.excepthook(*sys.exc_info()) File "...\electrum\gui\kivy\uix\dialogs\crash_reporter.py", line 188, in sys.excepthook = lambda exctype, value, tb: self.handle_exception(value) File "...\electrum\gui\kivy\uix\dialogs\crash_reporter.py", line 199, in handle_exception e = CrashReporter(self.main_window, *exc_info) File "...\electrum\gui\kivy\uix\dialogs\crash_reporter.py", line 100, in __init__ Factory.Popup.__init__(self) File "...\Python310\site-packages\kivy\uix\modalview.py", line 195, in __init__ super(ModalView, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\anchorlayout.py", line 68, in __init__ super(AnchorLayout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\layout.py", line 76, in __init__ super(Layout, self).__init__(**kwargs) File "...\Python310\site-packages\kivy\uix\widget.py", line 361, in __init__ self.canvas = Canvas(opacity=self.opacity) File "kivy\graphics\instructions.pyx", line 608, in kivy.graphics.instructions.Canvas.__init__ File "kivy\graphics\instructions.pyx", line 154, in kivy.graphics.instructions.InstructionGroup.__init__ File "kivy\graphics\instructions.pyx", line 60, in kivy.graphics.instructions.Instruction.__init__ TypeError: Cannot create graphics instruction outside the main Kivy thread ``` --- electrum/gui/kivy/main_window.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index ce9fe054c..8e1fbfd42 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -414,6 +414,7 @@ class ElectrumWindow(App, Logger, EventListener): self.password = None self._use_single_password = False self.resume_dialog = None + self.gui_thread = threading.current_thread() App.__init__(self)#, **kwargs) Logger.__init__(self) @@ -1100,6 +1101,17 @@ class ElectrumWindow(App, Logger, EventListener): return self.qr_dialog(label.name, label.data, show_text_with_qr) + def scheduled_in_gui_thread(func): + """Decorator to ensure that func runs in the GUI thread. + Note: the return value is swallowed! + """ + def wrapper(self: 'ElectrumWindow', *args, **kwargs): + if threading.current_thread() == self.gui_thread: + func(self, *args, **kwargs) + else: + Clock.schedule_once(lambda dt: func(self, *args, **kwargs)) + return wrapper + def show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon=f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/error', duration=0, modal=False): @@ -1117,6 +1129,7 @@ class ElectrumWindow(App, Logger, EventListener): duration=duration, modal=modal, exit=exit, pos=pos, arrow_pos=arrow_pos) + @scheduled_in_gui_thread def show_info_bubble(self, text=_('Hello World'), pos=None, duration=0, arrow_pos='bottom_mid', width=None, icon='', modal=False, exit=False): '''Method to show an Information Bubble