Correctly terminating QThreadPools - python

I use a QThreadPool to run an Background task which controls a camera which takes images.
However, as soon as I run this thread the application crashes after everything is done. That means in the moment where the thread is cleaned up it comes to a segfault.
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV).
Here is the complete gdb log: https://pastebin.com/CEy215AJ
Can anyone help me figure out which resource is causing the main thread to crash, when the threadpool is terminated?
I tried already:
gdb
gdb with python debug build
As an emergency solution I currently control the camera in a python subprocess and then save the image to the hard drive and read it again. It's slow but it works.
What else could I investigate to find out the cause of the segfault?
Code of a minimal (not) working example:
camera.py
from dataclasses import dataclass
from pathlib import Path
import cvb
import numpy as np
#dataclass
class Camera:
camera_path = Path(cvb.install_path()) / "drivers" / "GenICam.vin"
def take_image(self) -> np.array:
try:
with cvb.DeviceFactory.open(str(self.camera_path), port=0) as device:
stream = device.stream()
stream.start()
image, status = stream.wait()
stream.close()
# stream.try_abort()
if status == cvb.WaitStatus.Ok:
image.unlock()
return cvb.as_array(image, copy=True)
else:
print(f"Image acquisition status was not ok. It was {status}")
except Exception as e:
print(f"Exception while taking picture! {e}")
finally:
pass
# del device
# del stream
# del status
runner.py
import time
from PySide6.QtCore import QRunnable, Slot
from cvb_bug.camera import Camera
class Runner(QRunnable):
#Slot()
def run(self) -> None:
try:
camera = Camera()
for x in range(5):
image = camera.take_image()
print(image.shape)
time.sleep(0.5)
except Exception as e:
print(e)
main_threaded.py
import sys
from PySide6.QtCore import QThreadPool
from PySide6.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, QWidget, QApplication
from cvb_bug.runner import Runner
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.threadpool = None
self.runner = None
layout = QVBoxLayout()
btn = QPushButton("Start")
btn.pressed.connect(self.start)
layout.addWidget(btn)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
def start(self):
print("START")
self.threadpool = QThreadPool()
self.runner = Runner()
self.threadpool.start(self.runner)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
app.exec()

Related

QtWebPageRenderer SIGILL issue

My problem is summed in title. When I call method setHtml on instance of QtWebPageRenderer, SIGILL signal is emitted and my application goes down.
I'm aware that this issue is caused by bad Qt5 dynamic library but I installed it with:
sudo pip install PyQt5 --only-binary PyQt5
sudo pip install PyQtWebEngine --only-binary PyQtWebEngine
so I thought I will get correct precompiled library. When I tried to install PyQt5 without --only-binary, I always ended with some strange compilation error. Something like qmake is not in PATH even though it is and I'm able to call qmake from shell.
So my question is, how to make PyQt5 running on Fedora 31 without any SIGILLs.
EDIT:
Following code can replicate the issue. That information about SIGILL is little inaccurate because first signal is actually SIGTRAP, after I hit continue in gdb, I got SIGILL. This hints that Qt is actually trying to say something to me, although in not very intuitive way.
After some playing around with it, I found that without thread, its ok. Does this mean that Qt forces user to use QThread and not python threads? Or it means that I can't call methods of Qt objects outside of thread where event loop is running?
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
class Runner(threading.Thread):
def __init__(self, web_view):
self.web_view = web_view
threading.Thread.__init__(self)
self.daemon = True
def run(self):
self.web_view.load(QtCore.QUrl("https://www.worldometers.info/coronavirus/"))
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
web_view = WebView()
runner = Runner(web_view)
runner.start()
app.exec_()
if __name__ == "__main__":
main()
You have to have several restrictions:
A QObject is not thread-safe so when creating "web_view" in the main thread then it is not safe to modify it in the secondary thread
Since the QWebEnginePage tasks run asynchronously then you need a Qt eventloop.
So if you want to use python's Thread class then you must implement both conditions:
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
class Runner(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
def run(self):
# The QWebEnginePage was created in a new thread and
# that thread has an eventloop
loop = QtCore.QEventLoop()
web_view = WebView()
web_view.load(QtCore.QUrl("https://www.worldometers.info/coronavirus/"))
loop.exec_()
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
runner = Runner()
runner.start()
app.exec_()
if __name__ == "__main__":
main()
In reality QThread and threading.Thread() are native thread handlers of the OS, so in practical terms it can be said that QThread is a threading.Thread() + QObject with an eventloop running on the secondary thread.
On the other hand, if your objective is to call a function from a thread to which it does not belong, then you should use asynchronous methods as pointed out in this answer.
In this case the simplest is to use pyqtSlot + QMetaObject:
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
#QtCore.pyqtSlot(QtCore.QUrl)
def load(self, url):
QWebEnginePage.load(self, url)
class Runner(threading.Thread):
def __init__(self, web_view):
self.web_view = web_view
threading.Thread.__init__(self)
self.daemon = True
def run(self):
url = QtCore.QUrl("https://www.worldometers.info/coronavirus/")
QtCore.QMetaObject.invokeMethod(
self.web_view,
"load",
QtCore.Qt.QueuedConnection,
QtCore.Q_ARG(QtCore.QUrl, url),
)
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
web_view = WebView()
runner = Runner(web_view)
runner.start()
app.exec_()
if __name__ == "__main__":
main()
Or functools.partial() + QTimer
from functools import partial
import signal
import sys
import threading
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtWebEngineWidgets import QWebEnginePage
class WebView(QWebEnginePage):
def __init__(self):
QWebEnginePage.__init__(self)
self.loadFinished.connect(self.on_load_finish)
def print_result(self, data):
print("-" * 30)
print(data)
with open("temp.html", "wb") as hndl:
hndl.write(data.encode("utf-8"))
def on_load_finish(self):
self.toHtml(self.print_result)
class Runner(threading.Thread):
def __init__(self, web_view):
self.web_view = web_view
threading.Thread.__init__(self)
self.daemon = True
def run(self):
wrapper = partial(
self.web_view.load,
QtCore.QUrl("https://www.worldometers.info/coronavirus/"),
)
QtCore.QTimer.singleShot(0, wrapper)
def main():
signal.signal(signal.SIGINT, signal.SIG_DFL)
app = QtWidgets.QApplication(sys.argv)
web_view = WebView()
runner = Runner(web_view)
runner.start()
app.exec_()
if __name__ == "__main__":
main()

Ignore KeyboardInterrupt after closing a QProcess with a CTRL_C_EVENT

How can I ignore a KeyboardInterrupt after I used a CTRL_C_EVENT in Python? I only need CTRL_C_EVENT to close the Qprocess and not the PyQt5 window. I tried making an exception for KeyboardInterrupt but I still get the error.
I understand that CTRL_C_EVENT isn't the best option, if you want to keep PyQt5 running, however, the QProcess .exe only responds to that command. Can you help me? I made a sample code below:
import sys
import os
from PyQt5.QtWidgets import QMainWindow, QApplication, QHBoxLayout, QPushButton
from PyQt5.QtCore import QProcess
from signal import CTRL_C_EVENT
class main(QMainWindow):
def __init__(self):
super(main, self).__init__()
self.resize(1000, 800)
self.hLayout = QHBoxLayout(self)
self.process = QProcess()
self.stop_btn = QPushButton('stop process')
self.stop_btn.clicked.connect(self.finishProcess)
self.hLayout.addWidget(self.stop_btn )
self.setLayout(self.hLayout)
path = 'C:\...\.exe'
self.processOffline.start(path)
self.pid = self.processOffline.processId()
def finishProcess(self):
os.kill(self.pid, CTRL_C_EVENT)
try:
self.processOffline.waitForFinished()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
app = QApplication(sys.argv)
w = main()
w.show()
sys.exit(app.exec_())

create multiple thread for read and write with PyQt5

I write a code with PyQt5 for send and receive data via serial port. when a button pressed, data sends without any problem. in receive, data packet length is different in every time and time between any send is random. I create a thread for read serial port buffer in while loop and if data was received, then put that in a global queue. in other thread , queue will be checked and if it is not empty, some process will be done on data.
but when I test program and send data to application , the bellow error display and application crash.
the error:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTextDocument(0x1d1166f7620), parent's thread is QThread(0x1d116712ff0), current thread is
QThread(0x1d1166f7560)
here is the main part of code:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *
import errordialog as D1
import serial
import serial.tools.list_ports
import threading
import queue
class Ui_Form(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def setupUi(self, Form):
self.q = queue.Queue()
.
.
.
def open_port(self):
if self.comboBoxPORT.count()==0:
dialog.exec_()
else:
self.ser.port = self.comboBoxPORT.currentText()
b=int(self.lineEditBaudrate.text())
if b==0:
self.ser.baudrate = 9600
else:
self.ser.baudrate = int(self.lineEditBaudrate.text())
try:
self.ser.open()
self.pushButtonClose.setEnabled(True)
self.pushButtonOpen.setEnabled(False)
self.pushButtonOpen.setStyleSheet("background-color: lime")
self.pushButtonSend.setEnabled(True)
self.comboBoxPORT.setEnabled(False)
self.lineEditBaudrate.setEnabled(False)
self.textEditReceived.setReadOnly(False)
g=threading.Thread(target=ui.read_data)
e=threading.Thread(target=ui.printData)
g.start()
e.start()
except:
dialog.exec_()
def read_data(self):
while True:
t = self.ser.read().decode()
if t:
self.q.put(t)
def printData(self):
while True:
while not self.q.empty():
print(self.q.get())
self.textEditReceived.setText(self.q.get())
def close_port(self):
try:
self.ser.close()
self.pushButtonClose.setEnabled(False)
self.pushButtonOpen.setEnabled(True)
self.pushButtonOpen.setStyleSheet("background-color: azure")
self.pushButtonSend.setEnabled(False)
self.comboBoxPORT.setEnabled(True)
self.lineEditBaudrate.setEnabled(True)
except:
dialog.exec_()
def send(self):
self.ser.write(str.encode(self.textEditSent.toPlainText()))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Form = QtWidgets.QWidget()
ui = Ui_Form()
ui.setupUi(Form)
Form.show()
##============= add error dialog
d1 = D1.Ui_DialogError()
dialog = QtWidgets.QDialog()
d1.setupUi(dialog)
##---======================----------
ui.pushButtonOpen.clicked.connect(lambda : ui.open_port())
d1.pushButtonCanceError.clicked.connect(lambda: dialog.close())
ui.pushButtonExit.clicked.connect(lambda: Form.close())
ui.pushButtonClose.clicked.connect(lambda: ui.close_port())
ui.pushButtonSend.clicked.connect(lambda: ui.send())
##=============-----------------
##----------====================
sys.exit(app.exec_())

How to exit properly a Queue and Qthread for tests with pytest?

I created a GUI with PyQt5, and I wand to test it through pytest.
I my GUI requires to redirect the standard output, so I use a Qthread to create a listener.
This listener put the stdout in a Queue and send a signal which exploited by the GUI.
Until here, there is no problem. My issue appears when I what to quit; when I quit using python interpreter I have no problem, but when I use pytest I get EOFError or a message saying that I kill a running thread.
I tried to quit correctly but the problem persist, so I came for help.
Here is an example of the GUI.py :
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from functools import partial
import multiprocessing
from PyQt5 import QtGui, QtCore, QtWidgets, QtTest
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject, pyqtSignal, pyqtSlot, QThread
from PyQt5.QtGui import QIcon, QTextCursor
class MyReceiver(QObject):
mysignal = pyqtSignal(str)
def __init__(self,queue,*args,**kwargs):
QObject.__init__(self,*args,**kwargs)
self.queue = queue
self.runCondition=True
#pyqtSlot(str)
def run(self):
while self.runCondition:
text = self.queue.get()
self.mysignal.emit(text)
def QueueStreamSetup():
queue = multiprocessing.Queue(-1)
sys.stdout = WriteStream(queue)
#sys.stderr = WriteStream(queue)
return queue
class WriteStream(object):
def __init__(self,queue):
self.queue = queue
def write(self, text):
self.queue.put(text)
def flush(self):
self.queue.put('FLUSH ')
QtTest.QTest.qWait(2 * 1000)
pass
def threadConnect(view, queue):
qthread = QThread()
my_receiver = MyReceiver(queue)
my_receiver.mysignal.connect(view.append_text)
my_receiver.moveToThread(qthread)
#
qthread.started.connect(partial(my_receiver.run,))
qthread.start()
return(qthread, my_receiver)
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI(self)
def restore(self):
# Restore sys.stdout
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
#pyqtSlot(str)
def append_text(self,text):
self.textEdit.moveCursor(QTextCursor.End)
self.textEdit.insertPlainText( text )
self.textEdit.moveCursor(QTextCursor.End)
def initUI(self, MainWindow):
# centralwidget
MainWindow.resize(346, 193)
self.centralwidget = QtWidgets.QWidget(MainWindow)
# The Action to quit
self.exitAction = QAction(QIcon('exit24.png'), 'Exit', self)
self.exitAction.setShortcut('Ctrl+Q')
self.exitAction.triggered.connect(self.close)
# The bar
self.statusBar()
self.menubar = self.menuBar()
self.fileMenu = self.menubar.addMenu('&File')
self.exitMenu=self.fileMenu.addAction(self.exitAction)
# tThe Button
self.btn_quit = QtWidgets.QPushButton(self.centralwidget)
self.btn_quit.setGeometry(QtCore.QRect(120, 20, 89, 25))
self.btn_quit.clicked.connect(lambda: self.doPrint() )
# The textEdit
self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
self.textEdit.setGeometry(QtCore.QRect(10, 60, 321, 81))
# Show the frame
MainWindow.setCentralWidget(self.centralwidget)
self.show()
def doPrint(self):
# Test to print something.
print('TEST doPrint')
def closeEvent(self, event):
# Ask a question before to quit.
reply = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
# Treat the answer.
if reply == QMessageBox.Yes:
self.restore()
event.accept()
else:
event.ignore()
def main():
queue = QueueStreamSetup()
app = QApplication(sys.argv)
ex = Example()
qthread, my_receiver = threadConnect(ex, queue)
return app, ex, queue, qthread, my_receiver
def finish(queue, qthread, my_receiver):
print('Finish')
my_receiver.runCondition=False
queue.close()
queue.join_thread()
qthread.terminate()
qthread.wait()
qthread.exit()
print('Finish Done')
if __name__ == '__main__':
app, ex, queue, qthread, my_receiver =main()
rc= app.exec_()
finish(queue, qthread, my_receiver)
print('the application ends with exit code {}'.format(rc))
sys.exit(rc)
Then the pytest file named test_GUI.py :
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os, sys
import pytest
from PyQt5 import QtGui, QtCore, QtWidgets, QtTest
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject
import GUI
#pytest.fixture(scope="module")
def Viewer(request):
print(" SETUP GUI")
yield GUI.main()
print(" TEARDOWN GUI")
class Test_GUI_CXS() :
def test_launching(self, Viewer, qtbot, mocker, caplog):
# open the window and add the qtbot.
print(" SETUP Window")
app, window, queue, qthread, my_receiver = Viewer
qtbot.addWidget(window)
qtbot.wait_for_window_shown(window)
QtTest.QTest.qWait(0.5 *1000)
# Test
qtbot.mouseClick( window.btn_quit, QtCore.Qt.LeftButton )
QtTest.QTest.qWait(0.5 *1000)
# EXIT
mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
window.exitAction.trigger()
QtTest.QTest.qWait(1 *1000)
assert window.close()
# Finish the processes.
print( "App END")
GUI.finish(queue, qthread, my_receiver)
print( "END TEST.")
So if I run the command: pytest -v -s ./test_GUI.py , I get the following element in the messages:
Qt exceptions in virtual methods:
________________________________________________________________________________
Traceback (most recent call last):
File "xxx/GUI.py", line 25, in run
text = self.queue.get()
File "xxx/lib/python3.7/multiprocessing/connection.py", line 383, in _recv
raise EOFError
EOFError
I do not understand why there is this end of file error, but I assume that it is linked with the termination of the Queue and the Qthread.
Because until the Queue is not empty, the my_receiver can not stop running and so the Qthread can not be terminate.
Unfortunately I found nothing about this kind of problem on the Internet.
Any suggestions or help on this case would be greatly appreciate.
The problem is that when you close the Queue the self.queue.get() is still running, preventing the thread from finishing executing as it blocks while self.runCondition: from executing. Considering the above a possible solution is to send a None and then just close the Queue:
class MyReceiver(QObject):
mysignal = pyqtSignal(str)
def __init__(self, queue, *args, **kwargs):
super(MyReceiver, self).__init__(*args, **kwargs)
self.queue = queue
self.runCondition = True
def run(self):
while self.runCondition:
text = self.queue.get()
if isinstance(text, str):
self.mysignal.emit(text)
def finish(queue, qthread, my_receiver):
print('Finish')
my_receiver.runCondition = False
queue.put(None)
qthread.quit()
qthread.wait()
qthread.exit()
queue.close()
queue.join_thread()
print('Finish Done')

Pyqt5 threading can't work (Fatal Python error: could not acquire lock)

I have read bunch of QThread tutorials and SO Answers about this. But I can't make my threading works. Most of the time, it just run once or it print error message. Threading works if I started it immediately after the app start, but I want the thread to run after certain function, cause I want to set the directory location first.
The file structure is arranged like this:
App.py
Controllers/
main_controller.py
recorder.py
Model/
model.py
Views/
main_view.py
App.py
import sys
from PyQt5.QtWidgets import QApplication
from Model.model import Model
from Controllers.main_controller import MainController
from Views.main_view import MainView
class App(QApplication):
def __init__(self, sys_argv):
super().__init__(sys_argv)
self.model = Model()
self.main_controller = MainController(self.model)
self.main_view = MainView(self.model, self.main_controller)
if __name__ == "__main__":
app = App(sys.argv)
sys.exit(app.exec_())
model.py
from PyQt5.QtCore import QObject
class Model(QObject):
def __init__(self):
super().__init__()
self.directory = ""
def get_directory(self):
return self.directory
def set_directory(self, directory):
self.directory = directory
main_view.py
from PyQt5.QtWidgets import QMenu, QSystemTrayIcon, QMainWindow, QFileDialog
from PyQt5 import QtGui
from PyQt5.QtWidgets import QApplication
from Controllers.recorder import Recorder
class MainView(QMainWindow):
def __init__(self, model, main_controller):
super().__init__()
self._model = model
self._main_controller = main_controller
# UI
icon = QtGui.QIcon("icon24x24.png")
menu = QMenu()
start_action = menu.addAction("Start Recording")
stop_action = menu.addAction("Stop Recording")
self.tray = QSystemTrayIcon()
self.tray.setIcon(icon)
self.tray.setContextMenu(menu)
self.tray.show()
start_action.triggered.connect(self.start_app)
stop_action.triggered.connect(self.stop_app)
self.recordThread = Recorder()
def start_app(self):
directory = QFileDialog.getExistingDirectory(self, "Select Directory")
self._main_controller.set_directory(directory)
self.start_thread()
def start_thread(self):
self.recordThread.start()
def stop_app(self):
self.recordThread.terminate()
QApplication.instance().quit()
print("app stopped")
main_controller.py
from PyQt5.QtCore import QObject
class MainController(QObject):
def __init__(self, model):
super().__init__()
self._model = model
def set_directory(self, directory):
self._model.set_directory(directory)
recorder.py
import time
from PyQt5.QtCore import QThread, QTimer, pyqtSignal
from Model.model import Model
class Recorder(QThread):
job_done = pyqtSignal()
def __init__(self):
QThread.__init__(self)
self._model = Model()
def __del__(self):
self.wait()
def run(self):
while True:
print("I am the loop")
print(self._model.get_directory())
# time.sleep(4 - time.time() % 4)
QThread.sleep(4)
print("now is {}".format(time.time()))
self.job_done.emit()
I've tried using various style, including Qthread, QObject, pyqtsignal according to various tutorial. but nothing works for me. It either just print "I am the loop" then exit. or print
I am the loop
Fatal Python error: could not acquire lock for <_io.BufferedWriter name='<stdout>'> at interpreter shutdown, possibly due to daemon threads
Thread 0x00007f5d1bfff700 (most recent call first):
File "/App/Controllers/recorder.py", line 20 in run
Current thread 0x00007f5d3458f700 (most recent call first):
Aborted
Thank you
Nothing wrong with your code. The app closed immediately after calling QFileDialog.getExistingDirectory because it's the nature of Qt.
Qt is made to exit when all Windows are closed
The app closed because you no longer have any windows. Your app doesn't have any windows but QSystemTrayIcon(). Setting setQuitOnLastWindowClosed() to False solved the problem.
yourApp.setQuitOnLastWindowClosed(False)
Source

Categories