How to solve the segmentation fault and UI crash issue during the QProcess

143 Views Asked by At

I'm currently developing a MultiThreading window form application using Python that functions by executing QProcess commands via buttons and signaling start and end with a record that is displayed on the window. However, it always crashes due to unresponsiveness or segmentation faults in a Linux environment, whereas it works fine in a Windows environment. My task takes a long build time, about 8 minutes on average, so I use dummy data to simulate the long run time. Where waitForFinished(-1) is to ensure that the next task is executed only after the current task is completed.

I have used -X faulthandler to locate the position of the Segmentation fault.

Environment Version:

OS: Linux, Python: 3.6.9, PyQt5: 5.15.6

Segmentation Fault:

Thread 0x00007fde5fffe700 (most recent call first):
File .../test.py line 27 in run
File .../test.py line 107 in process_single
File .../test.py line 51 in run

Thread 0x00007fde63ffe700 (most recent call first):
File .../test.py line 27 in run
File .../test.py line 107 in process_single
File .../test.py line 51 in run

Current thread 0x00007fdfd245f740(most recent call first):
File .../test.py line 40 in process_output
File .../test.py line 132 in <module> 

My code:

import sys
import threading
from functools import partial
from queue import Queue
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class Job(QObject):
    task_msg = pyqtSignal(str)
    task_started = pyqtSignal()
    task_finished = pyqtSignal()

    def __init__(self, job_build_cmd):
        super().__init__()
        self.job_build_cmd = job_build_cmd  
        self.task_process = None

    def run(self):
        self.task_process = QProcess()
        env = QProcessEnvironment.systemEnvironment()
        env.insert('PATH', '/usr/bin:'+env.value('PATH'))
        self.task_process.setProcessEnvironment(env)
        self.task_process.readyReadStandardOutput.connect(self.process_output)
        self.task_process.finished.connect(lambda exitcode: self.process_finished(exitcode))
        self.task_process.start('bash', ['-c', self.job_build_cmd])
        self.task_process.waitForFinished(-1)

    def process_finished(self, exitCode):
        self.process_output()
        self.task_finished.emit()
        if self.task_process is not None:
            if self.task_process.isOpen():
                self.task_process.close()
            self.task_process.deleteLater()
            self.task_process = None
        
    def process_output(self):
        if self.task_process is not None and self.task_process.isOpen():
            line_data = bytes(self.task_process.readAllStandardOutput()).decode('utf-8', errors = 'replace').strip()
            for line in line_data.splitlines():
                self.task_msg.emit(f'{str(line)}')

class WorkThread(QThread):
    def __init__(self, process_single, t_id):
        super().__init__()
        self.process_single = process_single
        self.t_id = t_id
        
    def run(self):
        self.process_single(self.t_id)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.job_queue = Queue()
        self.work_threads = []
        self.stop_job = False
        self.lock_get_job = threading.Lock()
        self.process = []
        self.initUI()

    def initUI(self):
        self.setGeometry(100, 100, 100, 100)
        layout = QVBoxLayout()

        self.startbtn = QPushButton('Start')
        self.startbtn.clicked.connect(self.run_build)
        layout.addWidget(self.startbtn)

        centralWidget = QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)

    def run_build(self):
        self.startbtn.setEnabled(False)
        tasks = {
            'Job 1': 'for i in $(seq 1$((RANDOM%101+50))); do len=$(($RANDOM % 100 + 30)); echo "Random text $i: $(tr -dc A-Za-z0-9 </dev/urandom | head -c $len)"; sleep 0.$((RANDOM % 10 + 1)); done; exit $((RANDOM % 2))'
        }
        for name, cmd in tasks.items():
            job = Job(cmd)
            job.task_msg.connect(print)
            self.job_queue.put(job)
            self.process.append(job)
            job.task_finished.connect(partial(self.sync_task_finish, job))
        self.create_worker()
            
    def create_worker(self):
        self.num_of_worker = 2
        self.work_threads =[]
        for id in range(self.num_of_worker):
            worker = WorkThread(self.process_single, id)
            self.work_threads.append(worker)
            worker.finished.connect(worker.deleteLater)
            worker.start()
            
    def process_single(self, t_id):
        while not self.stop_job:
            try:
                with self.lock_get_job :
                    if not self.job_queue.empty():
                        job = self.job_queue.get()
                    else:
                        break
                if job is not None:
                    job.task_finished.connect(self.release_process)
                    job.run()
            except Exception as e:
                if job is not None:
                    with self.lock_get_job:
                        self.job_queue.task_done()

    def release_process(self):
        with self.lock_get_job :
            self.job_queue.task_done()

    def sync_task_finish(self, job):
        with self.lock_get_job :
            if job in self.process:
                self.process.remove(job)
            if len(self.process)==0:
                self.job_finish()
    
    def job_finish(self):
        if self.job_queue.empty():
            self.startbtn.setEnabled(True)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

I have attempted to use deleteLater and close to release resources, as well as employing Lock to try to solve the problem, QMetaObject.invokeMethod and @pyqtSlot to replace partial,but none of these solutions have been effective.

I have reduced unnecessary UI displays to ensure the integrity of QProcess and the process of resource release. I hope it can execute all tasks in the queue properly without the UI crashing.

0

There are 0 best solutions below