4

I have a main pyqt program that needs to run external program with arguments. I would like to use a QDialog as a sort of a status monitor that would capture the external program's stdout while it is executing and display them in a textbox inside the QDialog. I have the following status monitor code:

class ProgressInfo(QtGui.QDialog):
    def __init__(self, cmd, args, parent=None):
        #super(self).__init__(parent)
        QDialog.__init__(self)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        self.cmd = cmd
        self.args = args
        self.keepGoing = True
        layout = QFormLayout()
        layout.setContentsMargins(10, 10, 10, 10)
        self.output = QtGui.QTextEdit()
        layout.addRow(self.output)
        layout.addRow(self.ui.buttonBox)
        self.setLayout(layout)

        self.ext_process = QtCore.QProcess(self)
        #self.ext_process.waitForFinished(-1)
        #self.ext_process.waitForStarted()
        #self.ext_process.readyRead.connect(self.dataReady)
        self.ext_process.started.connect(self.open)
        self.ext_process.readyReadStandardOutput.connect(self.dataReady)
        self.ext_process.finished.connect(self.onProcessFinished)
        self.ext_process.start(self.cmd, self.args)

    def dataReady(self):
        cursor = self.output.textCursor()
        cursor.movePosition(cursor.End)
        cursor.insertText(str(self.ext_process.readAll()))
        self.output.ensureCursorVisible()

    def onProcessFinished(self):
        cursor = self.output.textCursor()
        cursor.movePosition(cursor.End)
        #cursor.insertText(str(self.ext_process.readAll()))
        cursor.insertText(str(self.ext_process.readAllStandardOutput()))
        self.output.ensureCursorVisible()

Then I would instantiate this with the following command:

prog='C:/Program Files (x86)/My Program/Execute.exe'
margs=['D:/Data/Input1.txt', 'D:/Data/Input2.txt']
status = ProgressInfo(prog, margs, self)

So far this hasn't worked yet.

Problem 1: the external program will run only after I uncommented out the waitForFinished(-1) line.

Problem 2. the QDialog box only open in a flash, then disappears.

Problem 3. obviously, no standout from the running program is displayed.

Lastly, the code I put together draws on many people's ideas and lessons, but I look at it, it seems that it can only print out all of the standout after the program finished, but I was hoping it will display line by line as the program is writing them out at runtime.

My tool chains: Python 64-bit version 2.7.5 and I am developing on Windows 7 box

('Qt version:', '4.8.5')
('SIP version:', '4.14.7')
('PyQt version:', '4.10.2')

Thanks for any help.

For Comment
  • 1,139
  • 4
  • 13
  • 25
  • 1
    why do you wait for it to finish before it has event started? – user3528438 Feb 18 '16 at 22:56
  • If you want to monitor something it is generally a good practice to do that in a separate thread. This allows the UI to remain interactive while the monitoring is done in the background. You can use the `QProcess.readyRead` signal to connect it to a slot in your UI which will be triggered every time the subprocess has produced something. – rbaleksandar Feb 18 '16 at 23:39
  • @user3528438, not sure what you mean, but as I was saying, if I don't use that waitForFinished line, the external program would not even execute... could you elaborate what you mean? – For Comment Feb 19 '16 at 00:10
  • @rbaleksandar, yes, i agree, according to python documentation, the QProcess IS executing asynchronously, is it not? so, could you elaborate? – For Comment Feb 19 '16 at 00:12
  • @ForComment. On (2): this is most likely because you aren't keeping a reference to the dialog, and so it is being garbage-collected immediately after it is shown. – ekhumoro Feb 19 '16 at 00:30
  • I've just tried adding waitForReadyRead, then hook up readyReadStandardOutput and readyReadStandardError. The external program doesn't event run. Only when I use the waitForFinished will the program actually run. – For Comment Feb 19 '16 at 03:19
  • when i am not using the 'waitForFinished', everytime I tried to run the program, it will say: QProcess: Destroyed while process is still running. – For Comment Feb 19 '16 at 03:27
  • Can you show us the code where you instantiate `ProgressInfo` (this is done either in a `main()` or at the top-level of your script? From what you've just commented on I have the feeling that you are executing your `QApplication` incorrectly. – rbaleksandar Feb 19 '16 at 08:06
  • Don't forget to mark an answer that you think fits you best or leave a comment if none of the suggested solutions works for you so that we know that everything is okay/there is still work to be done. – rbaleksandar Feb 19 '16 at 12:28

2 Answers2

6

Here is an example how you can do it (I use QWidget but you can also use QDialog or whatever). I don't use a separate thread because the UI doesn't need to be interactive. If you want to add buttons etc. then you should consider going for the good old QThread running a QObject model provided by Qt.

#!/usr/bin/python
from PyQt4.QtGui import * 
from PyQt4.QtCore import * 
import sys


class MyQProcess(QWidget):     
  def __init__(self):    
   super(QWidget, self).__init__()

   # Add the UI components (here we use a QTextEdit to display the stdout from the process)
   layout = QVBoxLayout()
   self.edit = QTextEdit()
   self.edit.setWindowTitle("QTextEdit Standard Output Redirection")
   layout.addWidget(self.edit)
   self.setLayout(layout)

   # Add the process and start it
   self.process = QProcess()
   self.setupProcess()   

   # Show the widget
   self.show()

  def setupProcess(self):
    # Set the channels
    self.process.setProcessChannelMode(QProcess.MergedChannels)
    # Connect the signal readyReadStandardOutput to the slot of the widget
    self.process.readyReadStandardOutput.connect(self.readStdOutput)
    # Run the process with a given command
    self.process.start("df -h")

  def __del__(self):
    # If QApplication is closed attempt to kill the process
    self.process.terminate()
    # Wait for Xms and then elevate the situation to terminate
    if not self.process.waitForFinished(10000):
      self.process.kill()

  @pyqtSlot()
  def readStdOutput(self):
    # Every time the process has something to output we attach it to the QTextEdit
    self.edit.append(QString(self.process.readAllStandardOutput()))


def main():  
    app = QApplication(sys.argv)
    w   = MyQProcess()

    return app.exec_()

if __name__ == '__main__':
    main()

Notice that the command I'm using (df -h) runs once (it's a Linux command which displays the disk usage on your hard drives) and then it's over. You can replace it also with your Execute.exe which can run indefinitely. I have tested it with htop (a terminal-based advanced task manager), which once started doesn't stop unless the user wants it to or the system stops (crash, shutdown etc.).

Note that you have to ensure that the external process is stopped in a clean manner. This can be done inside __del__ (destructor) or another function invoked at the end of the life of a given widget. What I've done is basically send a SIGTERM (terminate) to the external process and once a given amount of time has passed but the process is still running I elevate the situation to SIGKILL (kill).

The code needs more work obviously but it should be enough to give you an idea how things work.

Here is the same version of the code above but with an extra thread. Note that I am redirecting the output from the external process to a slot in my worker. You don't have to do that unless you want to maybe work on that output. So you can skip this and connect your process signal to the slot in your widget that receives it and outputs its content. The processing of the output will be done again inside the separate thread so you can go the distance instead of freezing your UI (which will happen if you follow the

from PyQt4.QtGui import * 
from PyQt4.QtCore import * 
import sys

class Worker(QObject):
  sendOutput = pyqtSignal(QString)  

  def __init__(self):
    super(Worker, self).__init__()
    self.process = QProcess()
    self.setupProcess()

  def __del__(self):
    self.process.terminate()
    if not self.process.waitForFinished(10000):
      self.process.kill()

  def setupProcess(self):
    self.process.setProcessChannelMode(QProcess.MergedChannels)
    self.process.readyReadStandardOutput.connect(self.readStdOutput)
    self.process.start("htop")

  @pyqtSlot()
  def readStdOutput(self):
    output = QString(self.process.readAllStandardOutput())
    # Do some extra processing of the output here if required
    # ...
    self.sendOutput.emit(output)



class MyQProcess(QWidget):     
  def __init__(self):    
   super(QWidget, self).__init__()
   layout = QVBoxLayout()
   self.edit = QTextEdit()
   self.thread = QThread()

   self.setupConnections()

   self.edit.setWindowTitle("QTextEdit Standard Output Redirection")
   layout.addWidget(self.edit)
   self.setLayout(layout)
   self.show()

  def setupConnections(self):
    self.worker = Worker()
    self.thread.finished.connect(self.worker.deleteLater)
    self.worker.sendOutput.connect(self.showOutput)

    self.worker.moveToThread(self.thread)
    self.thread.start()

  def __del__(self):
    if self.thread.isRunning():
      self.thread.quit()
      # Do some extra checking if thread has finished or not here if you want to

  #Define Slot Here 
  @pyqtSlot(QString)
  def showOutput(self, output):
    #self.edit.clear()
    self.edit.append(output)


def main():  
    app = QApplication(sys.argv)
    w   = MyQProcess()

    return app.exec_()

if __name__ == '__main__':
    main()

Further clarification: As I've told @BrendanAbel in the comment section of his answer the issue with using slots with QThread is that the slots have the same thread affinity (=the thread they belong to) as the QThread instance itself, which is the same thread where the QThread was created from. The only - I repeat the only - thing that runs in a separate thread when it comes to a QThread is its event loop represented by QThread.run(). If you look on the Internet you will find out that this way of doing things is discouraged (unless you really, really know that you have to subclass QThread) because since the early versions of Qt 4 run() was abstract and you had to subclass QThread in order to use a QThread. Later the abstract run() got a concrete implementation hence the need of subclassing QThread was removed. About thread-safety and signals what @BrendanAbel wrote is only partially true. It comes down to the connection type (default is AutoConnection). If you manually specify the connection type you may actually render the signals thread-unsafe. Read more about this in the Qt documentation.

rbaleksandar
  • 8,713
  • 7
  • 76
  • 161
  • I just tried this 'extra thread' approach, it flashed opening the MyQProcess widget, then it almost immediately goes to __del__ to terminate itself, hence bringing down the external process as well because they are tied together with the 'finished.connect' hook. So, this doesn't work. – For Comment Feb 19 '16 at 20:20
  • It works and I can provide you with a screenshot if you like. Please post your `main()` or wherever you are creating your widget and showing it. As I've mentioned in my comment under your question I do believe that you are doing something wrong with the whole execution of your application. – rbaleksandar Feb 19 '16 at 20:54
  • I don't doubt your demo program is working at all. I have a QMainWindow as main form, which has a button I press to go to a function where I just instantiate your `MyQProcess` while passing in the external program (an windows exe) and its 3 parameters that the instance of `MyQProcess` will pass these to the `Worker` instance as parameters that are used to start the `QProcess` instance. I think there might be something problematic with `QProcess` on windows or it might be that that windows exe just can't be run like this using QProcess... – For Comment Feb 20 '16 at 02:45
  • You can execute EXEs on Windows with `QProcess` so don't worry about that. :) I just wanted to see more of your code. The fact that you go right inside the destructor of your worker means that the thread it belongs to also gets terminated, which happens only if the widget that has spawned the instance of `QThread` also gets closed. So there is a high chance that you are doing something wrong with the widget itself and not with the `QProcess` part. Question: do you have `show()` somewhere outside the widget you have posted that displays it on the screen? – rbaleksandar Feb 20 '16 at 08:37
  • Also where exactly do you add your `QDialog` widget to your main window? Do you set it as a central widget? – rbaleksandar Feb 20 '16 at 08:39
  • the QDialog is a pop-up dialog and it is called upon at runtime upon a button click to start execution. So, no, it is not part of the main window. I'll organize my code a bit to show just the part that this is called. – For Comment Feb 20 '16 at 10:15
  • Ok, I just accepted @rbaleksandar's worker approach as the answer. The reason for my QDialog to die quickly is because I didn't give it a more persistent parent. – For Comment Feb 25 '16 at 17:15
  • If you are interested in the parent-child model in Qt you can check my post [here](http://stackoverflow.com/a/35523786/1559401). – rbaleksandar Feb 25 '16 at 18:50
0

You can't wait on a process and update the GUI in the same thread. The GUI only updates on each iteration of the event loop. If the event loop is stuck waiting on a process, it can't update the GUI.

The solution is to monitor the process in a separate thread, freeing up the main thread to continue updating the GUI. Most GUI elements aren't thread-safe, so you can't write the output to the QTextEdit directly from the monitoring thread. But Signals are thread-safe, so you can send output from the monitoring thread back to the main thread using a signal, which the QDialog in the main thread can handle and print the output to the QTextEdit

import subprocess
import sys

from PyQt4.QtCore import QObject, QThread, pyqtSignal
from PyQt4.QtGui import QDialog, QTextEdit, QVBoxLayout, QPushButton, QApplication


class MyDialog(QDialog):

    def __init__(self):
        super(MyDialog, self).__init__()
        self.ui_lay = QVBoxLayout()
        self.setLayout(self.ui_lay)
        self.ui_txt = QTextEdit(self)
        self.ui_lay.addWidget(self.ui_txt)
        self.ui_btn = QPushButton('Ping', self)
        self.ui_lay.addWidget(self.ui_btn)

        self.thread = MyThread(self)
        self.thread.line_printed.connect(self.handle_line)

        self.ui_btn.clicked.connect(self.run_thread)

    def run_thread(self):
        self.thread.start_command('ping google.com')

    def handle_line(self, line):
        cursor = self.ui_txt.textCursor()
        cursor.movePosition(cursor.End)
        cursor.insertText(line)
        self.ui_txt.ensureCursorVisible()


class MyThread(QThread):

    line_printed = pyqtSignal(str)

    def __init__(self, parent):
        super(MyThread, self).__init__(parent)
        self.cmd = None

    def start_command(self, cmd):
        self.cmd = cmd
        self.start()

    def run(self):
        if self.cmd:
            popen = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, shell=True)
            lines_iterator = iter(popen.stdout.readline, b"")
            for line in lines_iterator:
                self.line_printed.emit(line)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    dlg = MyDialog()
    dlg.show()
    app.exec_()
Brendan Abel
  • 35,343
  • 14
  • 88
  • 118
  • Please avoid adding slots and signal to a subclassed `QThread`. This is not how it's done in modern Qt. The only part that actually runs in a separate loop is the `run()` event loop. The rest (slots and signals) is part of the `QThread` instance, which resides in the same thread where it was created. In your case - the main thread. If you want slots-signal functionality use `QObject` and then move that `QObject` to a separate standard thread. – rbaleksandar Feb 19 '16 at 08:39
  • Signals are thread-safe, it's perfectly fine to emit them from `run`. It really doesn't matter what thread they're in. True, you could create a separate `QObject` with signals and pass that as the target to a regular `QThread`, but it's not necessary. – Brendan Abel Feb 19 '16 at 08:45
  • The **slots** are the main problem here. The slots RUN inside the same thread your `QThread` is instantiated in. So if you add some heavy processing in there your UI will freeze. Please refer to the Qt documentation to see more why addings slot-signal functionality to a subclassed `QThread` is bad. – rbaleksandar Feb 19 '16 at 08:52
  • I know the slots run in the main thread, that's what I want, because you can only update the GUI from the main thread. If I had to do heavy processing, I'd do it in the run method of the thread. – Brendan Abel Feb 19 '16 at 08:55
  • Please see my updated answer. This is how it should be done when you work with multiple threads and want slots and signals. – rbaleksandar Feb 19 '16 at 08:56
  • Thanks guys, let me try out these solutions right now. – For Comment Feb 19 '16 at 16:37
  • Yes, I've seen that you've implemented the *Worker Object* pattern. That is one of the two patterns that the Qt docs recommends, the other being the one that I've implemented. Directly from the Qt Docs -- *Another way to make code run in a separate thread, is to subclass `QThread` and reimplement run().* It's true that if you want slots to run in a separate thread, then you don't want to define them on the `QThread`, also in the Qt docs -- *a developer who wishes to invoke slots in the new thread must use the worker-object approach*. None of those caveats apply to this question. – Brendan Abel Feb 19 '16 at 17:35
  • @BrendanAbel, I just tried your solution, first, there should be a few changes. One being that you can't use `pyqtSignal` with a thread, but instead, you should use emit directly on the thread itself, like `self.emit`. Second, your solution and @rbaleksandar solution both have the same problem in that the external program thread pointer is almost immediately destroyed while the actual thread is still running (as evidenced by the `QThread: Destroyed while thread is still running` message. Once it is destroyed, all those signal-slot calls are causing python.exe to crash. – For Comment Feb 19 '16 at 21:48
  • as a continuation from the comment above, I meant to say that the newly created thread variable seems to be destroyed right after the thread is `start`ed. This is the same symptom in my original posting in that only when I call the `thread.waitForFinished(-1)` on the thread will it persist in memory, but then all other GUI interactions are halted. I am almost ready to give up... – For Comment Feb 19 '16 at 21:53
  • @ForComment What do you mean you can't use `pyqtSignal` with a `QThread`? You can use `pyqtSignals` on any `QObject`, and `QThreads` are `QObjects`. – Brendan Abel Feb 19 '16 at 21:53
  • Also, for what it's worth, under the hood all signals use `QObject.emit`. The new-style syntax of `pyqtSignal.emit` is just syntactic sugar. – Brendan Abel Feb 19 '16 at 22:00
  • @BrendanAbel, I am using PyCharm developing the app, for your solution above, it gave the following error message: `self.line_printed.emit(line) TypeError: pyqtSignal must be bound to a QObject, not 'MyThread'` – For Comment Feb 20 '16 at 02:35
  • @Brendan Abel, your thread approach is also working in terms of starting the process and then give stdout feedback, except it doesn't handle the influx of stdout text very well, basically, the subprocess created buffer that delayed output of those lines to the point of no output at all after the first couple of lines, I tried to put bufsize=1 to no avail. But the QProcess seems to handle this ok. – For Comment Feb 25 '16 at 17:16
  • I think it's a python 2 limitation. See this answer for another way to structure the `readline` loop that should prevent that problem -- http://stackoverflow.com/a/1085100/1547004 – Brendan Abel Feb 25 '16 at 17:23
  • Actually, I've already tried that exact solution you pointed out. I think it will work in Python3, but right now I am stuck with python2.7 as a key part of my program will require 2.7, sigh. – For Comment Feb 25 '16 at 17:27
  • Is it possible that your process is printing one giant line? As in no newlines? If so, `readlines` may not be appropriate. You might be able to switch it to use `read` – Brendan Abel Feb 25 '16 at 17:33