QProcess kill() causes pyQt5 GUI to become slow or crash - python

I have a GUI application which has a start button that slots to a function which will eventually launch a qprocess. This qprocess calls up a long-running console program in the background which spits out a bunch of stdout. The stdout is captured with signals and slots. When there is something new from that qprocess on stdout, it will emit a signal with that stdout line and is slotted by the GUI and displayed in a Text Browser widget. It works great if I just let the detached qprocess run and exit on its own. The GUI remains responsive and everything is great. There is a stop button on the GUI to stop it all too, its important to be able to stop it if needed. I built a slot for the stop button which kills the Qprocess and resets everything to square one. If I press the stop button before the qprocess gets invoked, everything works fine and GUI remains responsive.
But if I try to interrupt the qprocess with a stop button, which sends a .kill() command to the qprocess, it will severely slow down my GUI if not crash it if I try click too many things.
Here is how the qprocess is generated:
def launch_cmd(self):
'''Look for the COM port number the uut/DUT is registered as.'''
self.cli_process = QProcess()
self.cli_process.readyReadStandardOutput.connect(self.handle_stdout)
# self.cli_process.readyReadStandardError.connect(self.handle_stderr)
self.cli_process.start("myprog.exe", ["run", "all", "--debug"])
Here is the stop slot function, which is actually in a thread. I haven't yet tried making this .kill() inside the mainWindow code, its in the Thread class. cli_process was passed into the thread class for some reason.
def stop(self):
'''Stops the thread and does some housekeeping'''
print("Stop detected")
self.abort_flag = True
self.is_running = False
try:
self.cli_process.kill()
#if self.cli_process.processId():
#os.kill(self.cli_process.processId(), signal.SIGINT)
except:
print(f"[{datetime.now()}]: Warning, cli process not killed...continuing headless.")
else: pass
The GUI still works when the stop button is pressed after qprocess is killed, its just really slow. Text fields are slow to respond. Even dragging the GUI window around is choppy and skips around. If I do too many things in the GUI in this condition, it can crash the GUI; like if I mouse over the menu bar items and quickly move the mouse to drag the MainWindow and then go and click some text field, it will freeze and grey out the whole GUI like it is trying to buffer all my functions, and it may follow through with the last action or it may just crash and close.
I narrowed it down to the qprocess being killed.
I wrote a side program to monitor PIDs and confirm that when I press the stop button, the process indeed is killed and the GUI is the only pid running. So somehow a killed off qprocess causes the GUI to jam up. Not sure how to get over this. Thoughts?

Related

QMessageBox does not destroy itself after selection?

I have a thread that is running and a 'stop thread' button which when clicked calls a method that calls the terminate() of the thread. I have a QMessage Box that confirms the stop button before it terminates. The functionality is as expected but if I then start the thread again and click the close button then I get the confirmation MessageBox, I then select confirm and the thread stops but another confirmation box appears. It continues in this manner - if I run it 3 times I have to confirm three boxes, 4 I get 4 etc. The first box will terminate the thread in all cases. It seems like the MessageBox is not being destroyed properly each time it appears so they just build up? Not sure what's going on.
See below code. I have a thread 'my_thread' and a button connected to the function 'stop_thread' outlined below.
def stop_thread(self):
prompt=QWidgets.QMessageBox.question(self, 'Stop!', 'Are you sure you want to stop thread?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
if prompt == QtWidgets.QMessageBox.Yes:
self.my_thread.terminate()
else:
pass
I would expect each time I start and then stop the thread using the button I get one confirmation box appearing and upon selecting 'Yes' the thread will terminate. In reality the thread terminates but I get multiple MessageBoxes appearing one after the other - the number of them corresponds to how many times I have started the thread.
Solved it - It was because I was connecting the stop_button to the function outlined above in the function that I used to start the thread. So every time I started a thread I connected it again and called the function an extra time. What I didn't know was you could connect buttons to a function more than once (or multiple functions) but you learn something new everyday.
To fix I moved the connection of the stop button to the stop_thread function to the main class and now works great.

PyQt5 opening and closing windows

I have two UI windows created with QT Designer. I have two separate python scripts for each UI. What I'm trying to do is the first script opens a window, creates a thread that looks for a certain condition, then when found, opens the second UI. Then the second UI creates a thread, and when done, opens the first UI.
This seems to work fine, here's the partial code that is fired when the signal is called:
def run_fuel(self):
self.st_window = L79Fuel.FuelWindow(self)
self.thread.exit()
self.st_window.show()
self.destroy()
So that appears to work fine. I am still unsure of the proper way to kill the thread, the docs seem to state exit() or quit(). But...the new window from the other script (L79Fuel.py) is shown and the old window destroyed.
Then the new window does some things, and again when a signal is called, it triggers an similar function that I'd like to close that window, and reopen the first window.
def start_first(self):
self.r_window = L79Tools.FirstWindow(self)
self.thread.exit()
self.r_window.show()
self.destroy()
And this just exits with a code 0. I stepped through it with a debugger, and what seems to be happening is it runs through start_first, does everything in the function, and then goes back to the first window's sys.exit(app.exec_()), does that line, and then loops back to the start_first function (the second window) and executes that code again, in a loop, over and over.
I'm stumped. I've read as much as I could find, but nothing seems to address this. I'm guessing there's something I'm doing wrong with the threading (both windows have a thread going) and I'm not killing the threads correctly, or something along those lines.
Only the main thread can create GUI elements. You won't be able to create a second window in another thread. It's hard to see exactly how your program is set up based on the two callback slots in your question.
Generally, you kill threads with quit
self.thread.quit()
If you want, you can wait until it full quits and then delete the reference
self.thread.wait()
self.thread = Nonee
The answer? self.hide()
I was doing self.destroy on the windows to get rid of them, but what I didn't realize was that also was causing the program to start the process to exit completely. So it seems the answer is to kill the thread, and then simply .hide() the window. Now, each window opens and closes, multiple times if needed, while starting and stopping threads. New code (each function is essentially the same) below:
def run_race(self):
self.thread.stop()
self.thread.wait()
self.thread = None
self.r_window = L79Race.RaceWindow(self)
self.r_window.show()
self.hide()
While the threading wasn't the problem, I thought it was better to completely kill the thread before creating the next window. The self.thread.stop() is shown below. I realize .terminate() is not recommended, but I'm not really writing or trying to save anything in the thread, so I'm not worried about when it closes, only that it does.
def stop(self):
self.terminate()

How to stop a running python script

I created a GUI with PyQt which implements the buttons "Start" and "Stop".
When I click on "Start" a huge python Script is started. The function of "Stop" has to end this python script, but when I start the script it runs and I can't stop it. I even can't activate anything else on the GUI and I get no reaction from it. So i have to wait the long time until the python script ends.
How can I implement the methods so that I can interrupt the script with the "Stop" button even when I want?
Since you do everything in the QButton.clicked signal, your GUI locks up until you exit that function.
My solution i used in a small project was to seperate it into a GUI and worker process.
Use multiprocessing.Process to do your processing and have it send the results over a multiprocessing.Pipe.
The worker also has a second Pipe to recieve commands (my project just uses one command - exit)
In the GUI, you create 2 Pipes: one for results, one for commands.
Initialize the worker with both pipes and start the process.
The next step would be to have a QTimer poll the pipe for results and display them.
By doing so, your UI stays responsive while the calculations happen in the background.

How to suspend the execution of a QThread upon a QPushButton press?

I'm writing a simple GUI for the Lights Out Board (Wiki) using PyQt4. The GUI has a 'Solve' Button which uses Iterative Deepening Depth First Search. This being time consuming, I spawn a new QThread to solve for the puzzle and update the board when it finishes while the GUI remains responsive. This I'm able to do.
But, I also have a 'Stop' Button which should stop the Search thread if it is currently running and I'm unable to stop the QThread using the exit(). Here's the code for the three functions.
class LightsOut(QWidget):
def __init__(self, parent=None):
# Whole other initialization stuff
self.pbStart.clicked.connect(self.puzzleSolver) # The 'Start' Button
self.pbStop.clicked.connect(self.searchStop) # The 'Stop' Button
def searchStop(self):
if self.searchThread.isRunning():
self.searchThread.exit() # This isn't working
self.tbLogWindow.append('Stopped the search !') # This is being printed
else:
self.tbLogWindow.append('Search is not running')
def searchFinish(self):
self.loBoard.setBoard() # Redraw the lights out board with solution
def puzzleSolver(self):
maxDepth = self.sbMaxDepth.value() # Get the depth from Spin Box
self.searchThread = SearchThread(self.loBoard, self.tbLogWindow, maxDepth)
self.searchThread.finished.connect(self.searchFinish)
self.tbLogWindow.append('Search started')
self.searchThread.start()
When I click the 'Stop' Button, in the Log Window (QTextBrowser), I can see 'Stopped the search' message, but my CPU is still running at 100% and when the search finishes, the solution is being displayed (searchFinish is being called). Obviously, I'm missing something very simple, and I haven't used terminate() as it is frowned upon in the documentation.
Use terminate() instead of quit and call wait(). wait() will block until the QThread has finished.
Another thing you could do is to set a quit condition outside the thread which you will check for inside the thread (Probably the best solution). Additionally you can connect a slot to the finished signal.

On-Demand Python Thread Start/Join Freezing Up from wxPython GUI

I'm attempting to build a very simple wxPython GUI that monitors and displays external data. There is a button that turns the monitoring on/off. When monitoring is turned on, the GUI updates a couple of wx StaticLabels with real-time data. When monitoring is turned off, the GUI idles.
The way I tried to build it was with a fairly simple Python Thread layout. When the 'Start Monitoring' button is clicked, the program spawns a thread that updates the labels with real-time information. When the 'Stop Monitoring' button is clicked, thread.join() is called, and it should stop.
The start function works and the real-time data updating works great, but when I click 'Stop', the whole program freezes. I'm running this on Windows 7 64-bit, so I get the usual "This Program has Stopped Responding" Windows dialog.
Here is the relevant code:
class MonGUI(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
...
... other code for the GUI here ...
...
# Create the thread that will update the VFO information
self.monThread = Thread(None, target=self.monThreadWork)
self.monThread.daemon = True
self.runThread = False
def monThreadWork(self):
while self.runThread:
...
... Update the StaticLabels with info
... (This part working)
...
# Turn monitoring on/off when the button is pressed.
def OnClick(self, event):
if self.isMonitoring:
self.button.SetLabel("Start Monitoring")
self.isMonitoring = False
self.runThread = False
self.monThread.join()
else:
self.button.SetLabel("Stop Monitoring")
self.isMonitoring = True
# Start the monitor thread!
self.runThread = True
self.monThread.start()
I'm sure there is a better way to do this, but I'm fairly new to GUI programming and Python threads, and this was the first thing I came up with.
So, why does clicking the button to stop the thread make the whole thing freeze up?
In wxPython, GUI operations need to take place in the main thread. At places in your code you are calling the GUI from a different thread.
The easiest solution is to use wx.CallAfter(). A line of code would look like
wx.CallAfter(self.button.SetLabel, “Start Monitoring”)
which will then call self.button.SetLabel(“Start Monitoring”) from the main thread after the function completes.
There are other ways around this as well, such as using a Python threading Queue or wx.PostEvent, but start with CallAfter because it's easiest.
Other issues are also relevant, like you can't restart the same thread, but using CallAfter will stop the crashing.
It's likely hanging on join([timeout]), which blocks the calling thread until the thread whose join() method is called terminates – either normally or through an unhandled exception – or until the optional timeout occurs.
Do you have some inner loop in your thread, or a blocking call that waits for some source of data that may never come? When I wrote a basic serial program that grabbed COM port data, it would sometimes hang because a read function in my thread would block until it got something.
I would sprinkle in a few debugging print statements to see whats happening.
Edit:
I'd also use a threading.Event() instead of a Boolean flag, e.g.:
# in the init code...
self.runThread = threading.Event()
# when starting thread...
self.runThread.set()
self.monThread.start()
# in the thread...
while self.runThread.isSet():
pass # do stuff
# killing the thread...
self.runThread.clear()
self.monThread.join()
This shouldn't make it work differently, but it's a slightly safer way to do it.
tom10 has the right idea with avoiding UI updates from the monitor thread.
Also, it is probably not a good idea to have the blocking call self.monThread.join() in your UI thread. If you want the UI to give some feedback that the monitor thread has actually ended, have monThreadWorker issue a wx.CallAfter() or wx.PostEvent() just before it closes.
Avoid anything that blocks in your UI thread, and you will avoid deadlocking the UI

Categories

Resources