So basically I created a script called worker which contain 2 classes:
Worker
CreateThread
this script is supposed to handle unexpected exception occurs, while also provide threading solution to running heavy function to prevent GUI freezing.
My problem is that I get unexpected behavior sometimes, for example one time I can run function with the worker class and I will get a good response and everything works fine, and second time things go wrong and I get odd errors and also GUI crashes.
I tried to change the order I manage the thread handling, and I tried addepting the code to the errors but there is a hidden problem that keeps hunting me...
that is my worker code:
# Create a worker class #
class Worker(QObject):
my_started = pyqtSignal()
my_finished = pyqtSignal()
my_error = pyqtSignal(str, str)
my_success = pyqtSignal()
def __init__(self):
super(Worker, self).__init__()
def run(self, func):
try:
self.my_started.emit()
time.sleep(0.1)
x = func()
if x is None:
self.my_success.emit()
self.my_finished.emit()
return
if x[0] == -1:
function_error = x[1]
trace_back = x[2]
print(f'\nFunction error: {function_error}')
print(trace_back)
self.my_error.emit(str(function_error), trace_back,
str(func.__name__))
self.my_finished.emit()
return
except Exception as e:
self.trace_back = str(traceback.format_exc())
print(f'\nFunction error: {e}')
print(self.trace_back)
self.my_error.emit(str(e), self.trace_back)
self.my_finished.emit()
# Creating thread manger class #
class CreateThread(Worker, QObject):
def __init__(self, atp_name):
super(CreateThread, self).__init__()
self.stop_flag = False
self.atp_name = atp_name
self.thread = QThread()
self.worker = Worker()
def start_thread(self, func, **kwargs):
self.kwargs = kwargs
self.func_name = func.__name__
if self.stop_flag:
return
if self.func_name == '<lambda>':
self.func_name = self.kwargs['func_name']
# Thread and Worker init #
self.thread = QThread()
self.worker = Worker()
act = partial(self.worker.run, func)
self.worker.moveToThread(self.thread)
self.thread.started.connect(act)
# Signal to Function connection #
self.worker.my_started.connect(self.started_signal_connection)
self.worker.my_finished.connect(self.finished_signal_connection)
self.worker.my_error.connect(self.error_signal_connection)
self.worker.my_success.connect(self.success_signal_connection)
# Handling the worker and the thread deletion #
self.worker.my_finished.connect(self.thread.quit)
self.worker.my_finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
# Connect signals from worker #
def started_signal_connection(self):
self.stop_flag = True
print(f"\nFunction {self.func_name} started\n")
def finished_signal_connection(self):
self.stop_flag = False
print("\nWorker finished\n")
def error_signal_connection(self, error_name, trace_back):
self.today = date.today()
self.now = datetime.now()
self.current_date = str(
self.now.strftime("%d/%m/%Y %H:%M:%S")).replace(' ',
'_').replace(
'/', '_').replace(':', '_')
self.header_name = f'{self.atp_name}_{self.func_name}_Date_{self.current_date}'
self.target_folder = 'target_folder'
if os.path.isfile(self.header_name) == False:
file_name = f'{self.target_folder}{self.header_name}.txt'
try:
with open(file_name, 'w', encoding='utf-8') as f:
f.write(f'''ATP: {self.atp_name}\nFunction name: {self.func_name}\nError: {error_name}\nDate: {self.current_date}\n\n{trace_back}''')
except Exception as e:
print(e)
def success_signal_connection(self):
self.stop_flag = False
print("\nFunction success")
Another thing I noticed is that every time I use the worker with function that use Tk - askopenfilename()
I get troubles, here is a traceback example:
line 57, in run
**x = func()**
line 64, in open_main_terminal
**self.path1, self.file_name = upload_item()**
line 62, in upload_item
**filename = askopenfilename()**
line 375, in askopenfilename
**return Open(**options).show()**
line 39, in show
**w = Frame(self.master)**
line 2741, in __init__
**Widget.__init__(self, master, 'frame', cnf, {}, extra)**
line 2296, in __init__ (widgetName, self._w) + extra + self._options(cnf))
**RuntimeError: main thread is not in main loop**
Related
in the code posted below, i am subclassing Process. And as stated in some tutorials, .start must be called to start the worked process.
the problem I am facing, is that i can not invoke the .start() method because it causes the app to crash and generate the error message posted below. instead, i invoke .run() method.
please let me know why i can not use .start() and whey it generates that error message ans how to fix it so i can use .start()
Note: the code posted below is in a webservice
error msg:
File "C:\Python310\lib\multiprocessing\process.py", line 100, in _check_closed
if self._closed:
AttributeError: 'KeyGridCellsProcessing' object has no attribute '_closed'
code:
class KeyGridCellsProcessing(Process):
#staticmethod
def newInstance(pNameSuffix,row,col,_pixelsValuesSatisfyThresholdInWindowedSegment,pixelsValuesSatisfyThresholdInTIFFImageDatasetCnt):
keyGridCellsProcessing = KeyGridCellsProcessing(pNameSuffix,row,col,_pixelsValuesSatisfyThresholdInWindowedSegment,pixelsValuesSatisfyThresholdInTIFFImageDatasetCnt)
KeyGridCellsProcessing.processesCollector.append(KeyGridCellsProcessing)
return keyGridCellsProcessing
#staticmethod
def setDatasetElevationsTIFFInEPSG25832(datasetElevationsTIFFInEPSG25832):
KeyGridCellsProcessing.datasetElevationsTIFFInEPSG25832 = datasetElevationsTIFFInEPSG25832
#staticmethod
def setNDVIsTIFFWindowedSegmentContentsInEPSG25832(NDVIsTIFFWindowedSegmentContentsInEPSG25832):
KeyGridCellsProcessing.NDVIsTIFFWindowedSegmentContentsInEPSG25832 = NDVIsTIFFWindowedSegmentContentsInEPSG25832
#staticmethod
def setMainTIFFImageDatasetContents(mainTIFFImageDatasetContents):
KeyGridCellsProcessing.mainTIFFImageDatasetContents = mainTIFFImageDatasetContents
#staticmethod
def waitForProcessToFinish():
for p in KeyGridCellsProcessing.processesCollector:
p.join()
# logger.debug(f"process:{p}")
# exit()
def __init__(self,pNameSuffix,row,col,_pixelsValuesSatisfyThresholdInWindowedSegment,pixelsValuesSatisfyThresholdInTIFFImageDatasetCnt):
# self.queue = queue
self.pNameSuffix = pNameSuffix
self.row = row
self.col = col
self._pixelsValuesSatisfyThresholdInWindowedSegment = _pixelsValuesSatisfyThresholdInWindowedSegment
self.pixelsValuesSatisfyThresholdInTIFFImageDatasetCnt = pixelsValuesSatisfyThresholdInTIFFImageDatasetCnt
self.fourCornersOfKeyWindowInEPSG4326 = []
def run(self):
runStartTime = time.time()
#logic
...
...
runEndTime = time.time() - runStartTime
KeyGridCellsProcessing.runMethodForKeyGridCellsExecutionTimeAccumulator+=runEndTime
#staticmethod
def enqueue():
KeyGridCellsProcessing.keyGridCellsQueue.put([KeyGridCellsProcessing.NDVIsPer10mX10mForKeyWindow,
......,
......,
......,
......,
......,
], block=True, timeout=None)
#staticmethod
def dequeue():
deqeueStartTime = time.time()
item = KeyGridCellsProcessing.keyGridCellsQueue.get(block=True, timeout=None)
KeyGridCellsProcessing.executionTimeOfDequeueProcess = time.time() - deqeueStartTime
return item
#staticmethod
def shutdownQueue():
KeyGridCellsProcessing.keyGridCellsQueue.close()
KeyGridCellsProcessing.keyGridCellsQueue.join_thread()
def startProcessing(self):
#self.start()#<=========== causes the app to crash
self.run() #works
start() first action is to call self._check_closed() in BaseProcess (base class for Process)
def _check_closed(self):
if self._closed:
raise ValueError("process object is closed")
self._closed is initialized in BaseProcess __init__, but since you don't have super().__init__() in your class it's never initialized. Adding it should solve the issue
class KeyGridCellsProcessing(Process):
def __init__(self, ...):
super().__init__()
...
I'm trying to extend this example on multithreading in PyQt5:
Multithreading PyQt applications with QThreadPool (MWE below)
to allow for two different threaded functions, one that uses a callback and the other that does not. In the example above, the progress_callback is hard-coded in the Worker() class __init__, which means that any threaded function must have a signature that accommodates that callback:
def execute_this_fn(self, progress_callback):
which means that if there is a second threaded process, that function's signature would also be required to accommodate the callback in its signature even though the callback is not used.
So instead of hard-coding progress_callback into the __init__ of Worker(), I'd like to pass in the callback when instantiating Worker().
My MWE is below -- I include two commented-out two lines from the original example for reference. When I run it, and press the "DANGER!" button on the GUI, I get:
$ python threadtest.py
Multithreading with maximum 16 threads
progress_callback = <bound PYQT_SIGNAL progress of WorkerSignals object at 0x113bbd160>
n = 0
n = 1
n = 2
n = 3
n = 4
Done.
THREAD COMPLETE!
So the code runs, but the callback function (slot) progress_fn is never called, and I'm not sure why...
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
''' Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
`tuple` (exctype, value, traceback.format_exc() )
result
`object` data returned from processing, anything
progress
`int` indicating % progress
'''
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
''' Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# The old way (hard-code the callback to kwargs)
#self.kwargs['progress_callback'] = self.signals.progress
#pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
self.signals = WorkerSignals()
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n) # this does not execute...
def execute_this_fn(self, progress_callback):
print(f'progress_callback = {progress_callback}')
for n in range(0, 5):
time.sleep(1)
print(f'n = {n}')
progress_callback.emit(int(n*100/4))
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
# Pass the function to execute
# the old way (callback hardcoded in __init__ of Worker class)
#worker = Worker(self.execute_this_fn)
# the desired new way
worker = Worker(self.execute_this_fn, progress_callback=self.signals.progress)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
worker.signals.progress.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
The problem is simple: You have 2 WorkerSignals objects, and in one of them you make the connection and with the other you emit the signal. The solution is to use the same object for the connection and for the emission:
worker = Worker(self.execute_this_fn, progress_callback=self.signals.progress)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
self.signals.progress.connect(self.progress_fn)
Although I prefer to create a QObject that implements that logic:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
"""Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
`tuple` (exctype, value, traceback.format_exc() )
result
`object` data returned from processing, anything
progress
`int` indicating % progress
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
"""Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
"""
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# The old way (hard-code the callback to kwargs)
# self.kwargs['progress_callback'] = self.signals.progress
#pyqtSlot()
def run(self):
"""
Initialise the runner function with passed args, kwargs.
"""
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class ProgressCallback(QObject):
progressChanged = pyqtSignal(int)
def __call__(self, value):
self.progressChanged.emit(value)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print(
"Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
)
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n) # this does not execute...
def execute_this_fn(self, progress_callback):
print(f"progress_callback = {progress_callback}")
for n in range(0, 5):
time.sleep(1)
print(f"n = {n}")
progress_callback(int(n * 100 / 4))
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
callback = ProgressCallback()
worker = Worker(self.execute_this_fn, progress_callback=callback)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
callback.progressChanged.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter += 1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
I have a problem with an application on CentOs / RH to show download statuses from the API. Everything works fine using the PyCharm IDE, but after compiling with PyInstaller (one folder) the application is very unstable and I can't find an error. I run the thread and download API statuses every 10 seconds and if there is a change, I update the icon and send a notification. After left-clicking on the icon, the statuses are displayed in Gtk.ApplicationWindow.
Sometimes Gtk.StatusIcon can not change status or be inactive - left / right clicking doesn't work (but notifications of status changes come)
the application may end unexpectedly
I suspect the problem is in threads, but I can't find a proper solution.
Code (main.py)
class ExampleSystemTrayInit(Gtk.StatusIcon):
def __init__(self):
super().__init__()
app.tray = Gtk.StatusIcon()
app.tray.set_from_file(ico_start)
app.tray.connect('popup-menu', self.on_right_click)
app.tray.connect('activate', self.on_left_click, app)
def on_right_click(self, icon, event_button, event_time):
self.menu = Gtk.Menu()
quit = Gtk.MenuItem("Quit")
quit.connect('activate', self.quitApp)
self.menu.append(quit)
self.menu.show_all()
self.menu.popup(None, None, Gtk.StatusIcon.position_menu, app.tray, event_button, event_time)
def on_left_click(self, icon, app):
try:
self.app = app
if app.all_status:
data = self.app.all_status
Controller.show_window(self, data, self.app, refresh=False)
#call to class MainWindow(Gtk.ApplicationWindow) and show some data
else:
pass;
except Exception as e:
print(e)
app.tray.set_from_file(ico_disconnect)
def quitApp(self, par):
app.quit()
class ExampleSystemTray(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(
application_id="example-system-tray2.app"
)
self.tray = None
self.mainWindow = None
def do_activate(self):
if not hasattr(self, "my_app"):
self.hold()
self.my_app_settings = "Primary application instance."
self.systray = ExampleSystemTrayInit()
TrayController(app)
else:
print("Already running!")
def do_startup(self):
Gdk.threads_init()
Gdk.threads_enter()
Gtk.Application.do_startup(self)
Gdk.threads_leave()
if __name__ == "__main__":
GObject.threads_init()
app = ExampleSystemTray()
app.run()
TrayController (traycontroller.py)
class TrayController(threading.Thread):
def __init__(self, app):
self.app = app
self.cache = None
threading.Thread.__init__(self, name='TrayController', )
self.interval = 10
self.finished = threading.Event()
self.daemon = True
self.start()
def run(self):
while True:
try:
self.finished.wait(self.interval)
if not self.finished.is_set():
self.connect(self.app)
except Exception as e:
print(e)
def cancel(self):
"""Stop the timer if it hasn't finished yet."""
self.finished.set()
def connect(self, app):
try:
self.result = GetJson.get_json(self, api_view)
if self.result == None:
self.cache = None
self.show_status(2)
app.all_status = None
elif (self.cache != self.result):
self.cache = self.result
app.all_status = AllStatusInstance(self.result)
self.show_status(app.all_status.status)
Controller.show_main_window(self, app.all_staus, app, refresh=True)
#call to class MainWindow(Gtk.ApplicationWindow) if window is visble, then refresh
except Exception as e:
print(e)
self.show_status(2)
app.all_status = None
def show_status(self, status):
self.status = status
if self.status == 0:
return self.change_tray_icon(ico_red, notify_red, tag_red, widget=Gtk.StatusIcon)
elif self.status == 1:
return self.change_tray_icon(ico_gray, notify_gray, tag_gray, widget=Gtk.StatusIcon)
elif self.status == 2:
return self.change_tray_icon(ico_disconnect, notify_disconnect, tag_disconnect,
widget=Gtk.StatusIcon)
def change_tray_icon(self, icon, notification, tag, widget):
if self.app.tray.get_title() != tag:
self.app.tray.set_from_file(icon)
self.app.tray.set_title(tag)
self.notify("Notification:", notification, icon)
def notify(self, title, body, link):
notify2.init("Alert", mainloop=None)
icon = GdkPixbuf.Pixbuf.new_from_file(link)
n = notify2.Notification(title, body)
n.set_icon_from_pixbuf(icon)
n.set_urgency(notify2.URGENCY_CRITICAL)
n.show()
class AllStatusInstance(object):
__instance = None
def __new__(cls, val):
if AllStatusInstance.__instance is None:
AllStatusInstance.__instance = object.__new__(cls)
AllStatusInstance.__instance.val = val
return AllStatusInstance.__instance
Indeed, the problem is threads, in the sense that you cannot use GTK API from multiple threads, ever, as the documentation clearly states.
You should use Gio.Task if you have a blocking, synchronous operation and you wish to update some UI state at the end of it; or, if you're using a thread object in Python, always use GLib.MainContext.invoke_full() to invoke a function within the same thread that is running the GTK main loop.
What you should not do, is this:
def do_startup(self):
Gdk.threads_init()
Gdk.threads_enter()
Gtk.Application.do_startup(self)
Gdk.threads_leave()
and then call GTK API from a separate thread than the one that is running GTK's event loop; that is entirely undefined, non-portable behaviour.
I am trying to create a price ticker which refreshes every 2 seconds.
how to cancel the previous thread after a new one was created (a new one is created after the run_worker() was called)
I was trying to access somehow the self._flag from outside to close the thread, but how to access the currently running one and close it ?
def run_worker(self):
print("run_worker")
symbol = str(self.ui.pair_combo.currentText())
worker = Worker(self.execute_this_fn, symbol)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
self.threadpool.start(worker)
class WorkerSignals(QObject):
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
class Worker(QRunnable):
def __init__(self, fn, *argv, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.argv = argv
self.kwargs = kwargs
self.signals = WorkerSignals()
self.c = Client(api_key="account-xxx", api_secret="xxx", sandbox=True)
#pyqtSlot()
def run(self):
asset = self.argv[0]
self._flag = True
try:
while self._flag:
# The price of the LAST executed trade on the exchange
result = list(self.c.get_ticker(asset).values())[3]
print("result " + result)
time.sleep(2)
if not self._flag:
break
else:
self._flag = False
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() #done
everytime the run_worker function is called (depending on the currentText in the combo box) a new thread is produced.
I am trying to loop an alarm (in the file beep.wav) and show an alert.
Once the alert is closed, I want to instantly stop the alarm.
I am attempting a solution which uses threading to control the alarm's playback.
However, it throws an error:
Traceback (most recent call last):
File "test.py", line 28, in <module>
thread.stop()
File "test.py", line 21, in stop
self.process.kill()
AttributeError: 'AlarmThread' object has no attribute 'process'
I don't really know why this error would get thrown, but it looks like self.process is, for some reason, not assigned when AlarmThread.stop is called.
This makes no sense to me, as from my code it looks like thread.stop is only called after thread.start:
import subprocess
import threading
class AlarmThread(threading.Thread):
def __init__(self, file_name="beep.wav"):
super(AlarmThread, self).__init__()
self.file_name = file_name
self.ongoing = None
def run(self):
self.ongoing = True
while self.ongoing:
self.process = subprocess.Popen(["afplay", self.file_name])
self.process.wait()
def stop(self):
if self.ongoing is not None:
self.ongoing = False
self.process.kill()
thread = AlarmThread()
thread.start()
# show_alert is synchronous, an alert must be closed before the script continues
show_alert("1 second timer")
thread.stop()
thread.join()
You have a race condition. The thread hasn't had time to start, create the process and assign self.process by the time you call thread.stop(). You could initialize self.process in __init__ and use that to see if the process is really there
import subprocess
import threading
class AlarmThread(threading.Thread):
def __init__(self, file_name="beep.wav"):
super(AlarmThread, self).__init__()
self.lock = threading.Lock()
self.file_name = file_name
self.ongoing = False
self.process = None
def run(self):
self.ongoing = True
while True:
with self.lock:
if not self.ongoing:
break
self.process = subprocess.Popen(["afplayer", self.file_name])
self.process.wait()
def stop(self):
with self.lock:
if self.ongoing:
self.ongoing = False
if self.process:
self.process.kill()
thread = AlarmThread()
thread.start()
# show_alert is synchronous, an alert must be closed before the script continues
show_alert("1 second timer")
thread.stop()
thread.join()
Yes, it was caused by a race condition.
The thread hasn't had time to start, create the process and assign self.process by the time you call thread.stop()
However, I found a fix that relied upon simply waiting until thread.process was assigned:
thread = AlarmThread()
thread.start()
while not thread.process:
time.sleep(0.1)
show_alert(message)
thread.stop()
thread.join()
My class also changed slightly to ensure thread.process is always assigned:
class AlarmThread(threading.Thread):
def __init__(self, file_name="beep.wav"):
super(AlarmThread, self).__init__()
self.file_name = file_name
self.ongoing = None
self.process = None
def run(self):
self.ongoing = True
while self.ongoing:
self.process = subprocess.Popen(["afplay", self.file_name])
self.process.wait()
self.process = None
def stop(self):
if self.ongoing is not None:
self.ongoing = False
self.process.kill()