Search code examples
pythonmultithreadingloggingpyside2qrunnable

Python crashes when running a log streamer using Qt


Goal

I have a process that logs on a file (realtime.log) while running and I want to print every new line of that file in my application in realtime. In other words I want to redirect the output from the process to the GUI. This means that I have two different processes running: the "engine" and the GUI.

I have already achieved this by using Tkinter but since I have to make a more complex, professional and good looking GUI I decided to switch to Qt for Python (PySide2).

Problem

Python often crashes when I launch the GUI with the error message: Python has stopped working. The window starts printing the lines and at some point it stops working.

After many attempts and searches I got to a point where the program only crashes if I click on the GUI window. Moreover, the program doesn't crash suddenly but it crashes at the end of the engine's execution.

Environment

  • Windows 10
  • Python 3.6.5
  • PySide2 5.12.6

Code

Note that this is a simplified version.

datalog_path = "realtime.log"


def get_array_from_file(file_path):
    try:
        with open(file_path, 'r') as file:
            lines = file.readlines()
        return lines
    except:
        print('error in file access')


class Streamer(QRunnable):
    def __init__(self, stream, old_array, edit):
        super().__init__()
        self.stream = stream
        self.old_array = old_array
        self.edit = edit

    def run(self):
        try:
            while self.stream:
                array_file = get_array_from_file(datalog_path)
                if len(array_file) != len(self.old_array):
                    for line in array_file[len(self.old_array) - 1:len(array_file)]:
                        self.edit.append(line)
                        # print(line)
                        self.old_array.append(line)
        except:
            print('problem in streaming funct')


class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()

        self.setWindowTitle("DATALOG")
        self.thread_pool = QThreadPool()
        self.edit = QTextEdit()

        self.stream = True
        self.old_array = get_array_from_file(datalog_path)
        self.streamer = Streamer(self.stream, self.old_array, self.edit)
        self.thread_pool.start(self.streamer)

        window = QWidget()
        layout.addWidget(self.edit)
        window.setLayout(layout)
        self.setCentralWidget(window)


    def closeEvent(self, event):
        self.stream = False
        event.accept()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    win = Window()
    win.show()
    app.exec_()

Solution

  • The @hyde answer points out explains the reason for the problem but its solution is not applicable in PySide2 (in PyQt5 a small modification would have to be made, see this), an alternative is to create a QObject that has the signals:

    class Signaller(QtCore.QObject):
        textChanged = Signal(str)
    
    class Streamer(QRunnable):
        def __init__(self, stream, old_array):
            super().__init__()
            self.stream = stream
            self.old_array = old_array
            self.signaller = Signaller()
    
        def run(self):
            try:
                while self.stream:
                    array_file = get_array_from_file(datalog_path)
                    if len(array_file) != len(self.old_array):
                        for line in array_file[len(self.old_array) - 1:len(array_file)]:
                            self.signaller.textChanged.emit(line)
                            # print(line)
                            self.old_array.append(line)
            except:
                print('problem in streaming funct')
    self.stream = True
    self.old_array = get_array_from_file(datalog_path)
    self.streamer = Streamer(self.stream, self.old_array)
    self.streamer.signaller.textChanged.connect(self.edit.append)
    self.thread_pool.start(self.streamer)