I want to design a program which watches replies from one or more servers and then do something with these replies. This ‘something’ should be implemented as plugins. These plugins just receive the server’s replies and for example display them in the terminal or play a sound file. I also want to write a plugin, which displays this info in a wx window. This is, where the problems start (actually they start in the moment when I want to cleanly shut down these wx windows at program closure).
Let me explain the design of the program before showing the minimal use case at the end of this message:
Since waiting for the server replies is a blocking operation – and I don’t want to renew this every second or so –, I implemented this part in a daemonized thread (the BlockingThread
in my minimal use case). Before watching the replies from the server, it handles the loading of the plugins (MyPlugin
and WxPlugin
). The plugins are responsible to do something with the information received (not implemented in my minimal use case) and different plugins can be used for every thread watching the server. Because these threads are daemonized, they are automatically closed when the program stops. (The server does not mind this.)
All this is implemented with the help of a controlling thread (ControlThread
), which acquires a lock and waits for it to be released with the help of join()
.
Any plugin can request a shutdown of the entire program by calling the exit()
method of the underlying BlockingThread
, which then calls the ControlThread
’s exit()
method. This method releases the acquired lock and the main program proceeds and would delete all blocking threads at its end. But before all daemonized threads get killed, they get the chance to clean up, because their stop()
method is called. Sequentially the BlockingThread
calls the stop()
method of each plugin loaded for it. So that the plugin can also clean up.
This works as expected.
Mind you, maybe all this is overcomplicated and there are better ways to do it?
But the plugin displaying the message in a wx window is somewhat special, because that wx window needs to run a MainLoop
. The plugin WxPlugin
runs this main loop in a separate daemonized thread.
Up to here, everything works as expected and the wx windows can display the information received from the server (not implemented in my minimal use case).
The problem occurs, when I want to close the whole lot e.g. by clicking on any of the wx windows. I bind the mouse click to a method, which calls the underlying plugin’s exit()
method, thus initiated the shutdown procedure described above.
When in return to the shutdown request the plugin’s stop()
method gets called, it Destroy()
s the wx frame first and then calls the app’s ExitMainLoop()
. This is done for all WxPlugin
s. At the very end I sometimes get error messages or segmentation faults and sometimes I don’t get any error. These are the errors I received (always after the second WxPlugin.stop()
method had finished):
Gdk-CRITICAL **: 19:30:55.260: _gdk_frame_clock_freeze: assertion 'GDK_IS_FRAME_CLOCK (clock)' failed
Segmentation fault (core dumped)
I checked with IsMainLoopRunning()
a second after calling ExitMainLoop()
. I noticed, that
- the main loop of the first
BlockingThread
is still running afterExitMainLoop()
and - the main loop of the second
BlockingThread
is never running afterExitMainLoop()
!
I also checked with id()
, if the main loop might be the same for both BlockingThread
s, but they are different objects (as I would have expected).
How can I cleanly shut down all the wx main loops and their associated windows?
Now the minimal use case:
import threading
import time
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, exit_callback, title, id=wx.ID_ANY):
super().__init__(parent, id=id)
self.SetTitle(title)
self.exit_callback = exit_callback
self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeft)
self.Show()
def OnMouseLeft(self, event):
print('MyFrame.OnMouseLeft()')
self.exit_callback()
class MyPlugin:
def __init__(self, exit_callback, title):
print(f'{title} - MyPlugin()')
super().__init__()
self.exit_callback = exit_callback
self.title = title
def stop(self):
print(f'{self.title} - MyPlugin.stop()')
def exit(self):
print(f'{self.title} - MyPlugin.exit()')
self.exit_callback()
class WxPlugin:
def __init__(self, exit_callback, title):
print(f'{title} - WxPlugin()')
super().__init__()
self.exit_callback = exit_callback
self.title = title
self.app = wx.App()
self.frame = MyFrame(None, self.exit, title)
self.app.SetTopWindow(self.frame)
self.t = threading.Thread(target=self.app.MainLoop)
self.t.daemon = True
self.t.start()
def stop(self):
print(f'{self.title} - WxPlugin.stop()')
self.app.ExitMainLoop()
print(f'{self.title} - MainLoop stopped')
time.sleep(1)
print(f'{self.title} - {self.app.IsMainLoopRunning()=}')
def exit(self):
print(f'{self.title} - WxPlugin.exit()')
self.exit_callback()
class BlockingThread(threading.Thread):
def __init__(self, exit_callback, title):
super().__init__()
self.exit_callback = exit_callback
self.title = title
self.plugins = [
MyPlugin(self.exit, title),
WxPlugin(self.exit, title),
]
def run(self):
while True:
time.sleep(30 * 60) # Blocking 30 minutes
def stop(self):
print(f'{self.title} - BlockingThread.stop()')
for plugin in self.plugins:
plugin.stop()
def exit(self):
print(f'{self.title} - BlockingThread.exit()')
self.exit_callback()
class ControlThread(threading.Thread):
def __init__(self):
super().__init__()
self._lock = threading.Lock()
self._lock.acquire()
def run(self):
print('ControlThread.run()')
self._lock.acquire()
def exit(self):
print('ControlThread.exit()')
self._lock.release()
if __name__ == '__main__':
ctrl = ControlThread()
ctrl.start()
threads = [
BlockingThread(ctrl.exit, 'Thread 1'),
BlockingThread(ctrl.exit, 'Thread 2'),
]
for t in threads:
t.daemon = True
t.start()
ctrl.join()
for t in threads:
t.stop()