Using QRunnable in python script causes crash

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Post Reply
maxwang531
Posts: 21
Joined: Sun Sep 19, 2021 5:25 pm

Using QRunnable in python script causes crash

Post by maxwang531 »

I currently use FreeCAD to create a CNC path for my model. The path content is composed of a Profile and a Pocket processing. Since I need to call the CAM simulator to generate a processed model file, I also call FreeCAD's internal simulator.
Since each simulation needs to be completed, I used the QRunnable function to let the simulation be carried out in it and export the simulation results. But the FreeCAD program will crash every time at the end of the script.
Below is part of my code.

Code: Select all

FREECADPATH = "D:\\FreeCAD\\bin"
SIMULATORPATH="D:\\FreeCAD\\lib"
BINVOXPATH="E:\\111Masterarbeit\\Github\\Ausarbeitung\\utils"
import os
import sys
sys.path.append(FREECADPATH)
sys.path.append(SIMULATORPATH)
sys.path.append(BINVOXPATH)

import math
import numpy as np
import csv
import pandas as pd
#import PathSimulator

import FreeCAD as App
import FreeCADGui as Gui

from FreeCAD import Base, Rotation, Vector
import Part
import Path
import Draft

from math import pi, sin, cos

from PathScripts import PathJob
from PathScripts import PathJobGui

from PathScripts import PathProfile
from PathScripts import PathAdaptive
from PathScripts import PathPocket

import PathScripts.PathDressupDogbone as PathDressupDogbone

import PathScripts.PathDressupHoldingTags as PathDressupHoldingTags

from PathScripts import PathGeom
from PathScripts import PathPostProcessor
from PathScripts import PathUtil

DOC=FreeCAD.openDocument('E:/111Masterarbeit/Github/Ausarbeitung/Model mit binvox/Rechteckpocket2.FCStd')
DOC.recompute()
DOC = FreeCAD.activeDocument()
DOC.recompute()

Part1 = DOC.getObject('Part__Feature')

result = pd.read_csv('E:/111Masterarbeit/Github/Ausarbeitung/CSV/Pocket-Result.csv') 
x=result.shape[1] 
y=result.shape[0] 
print(result.iloc[0][1])
print(int(result.iloc[0][2]))

if int(result.iloc[0][1]) == 10:
    facName = 'Face{:d}'.format(int(result.iloc[0][2]))
print(facName)

Gui.activateWorkbench("PathWorkbench")
job = PathJob.Create('Job', [Part1], None)
job.ViewObject.Proxy = PathJobGui.ViewProvider(job.ViewObject)

#Profile Creat
profile = PathProfile.Create('Profile')
profile.setExpression('StepDown', None)
profile.StepDown = 10
profile.processHoles = True
profile.processPerimeter = True
profile.recompute()
DOC.recompute()

#Pocket
pocket = PathPocket.Create('Pocket')
pocket.Base = (Part1 , facName)
pocket.setExpression('StepDown', None)
pocket.StepDown = 5
pocket.recompute()
DOC.recompute()

#Simulation
from PathScripts import PathSimulatorGui as a
from time import sleep
from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread,QThreadPool, pyqtSignal)
class Runnable(QRunnable):

    def run(self):
        count = 0
        app = QCoreApplication.instance()
        while a.pathSimulation.iprogress < a.pathSimulation.numCommands :
            sleep(1)
        print("Tasks are finished")
        a.pathSimulation.accept()
        #app.quit()
        DOC.recompute()
        FreeCADGui.Control.closeDialog()

        # export

        Gui.Selection.addSelection('Rechteckpocket2', 'CutMaterial')
        __objs__ = []
        __objs__.append(FreeCAD.getDocument("Rechteckpocket2").getObject("CutMaterial"))
        import Mesh
        Mesh.export(__objs__, u"E:/111Masterarbeit/Github/Ausarbeitung/ModelCutmaterial/Rechteckpocket-CutMaterial.stl")
        print("export finished")

        del __objs__

        DOC.recompute()

        DOC.removeObject('CutMaterial')
        DOC.recompute()

Gui.runCommand('Path_Simulator',0)
a.pathSimulation.SetupSimulation()   #Simulation Reset
a.pathSimulation.SimFF()
runnable = Runnable()
QThreadPool.globalInstance().start(runnable)
Is there any solution to this? Is this a conflict between the QRunnable function and FreeCAD's internal program?
My FreeCAD version information is as follows.

Code: Select all

OS: Windows 10 Version 2009
Word size of OS: 64-bit
Word size of FreeCAD: 64-bit
Version: 0.19.24291 (Git)
Build type: Release
Branch: releases/FreeCAD-0-19
Hash: 7b5e18a0759de778b74d3a5c17eba9cb815035ac
Python version: 3.8.6+
Qt version: 5.15.2
Coin version: 4.0.1
OCC version: 7.5.0
wmayer
Founder
Posts: 20243
Joined: Thu Feb 19, 2009 10:32 am
Contact:

Re: Using QRunnable in python script causes crash

Post by wmayer »

The problem is that Qt's GUI is not thread-safe. This means from within a thread you are not allowed to directly manipulate the GUI.
Now QRunnable is managed by the thread pool and each instance is executed in its own thread. So, if you work with QThread or QRunnable doesn't make a big difference at the end.

The following function calls in the run() method are not allowed:

[/code]
a.pathSimulation.accept()
DOC.recompute()
FreeCADGui.Control.closeDialog()
Gui.Selection.addSelection('Rechteckpocket2', 'CutMaterial')
DOC.removeObject('CutMaterial')
[/code]

One way to (indirectly) modify the GUI from within a thread is to post events or with signals/slots where the connection type between sender and receiver is "queued". In this case the actual functions are executed in the GUI thread.
maxwang531
Posts: 21
Joined: Sun Sep 19, 2021 5:25 pm

Re: Using QRunnable in python script causes crash

Post by maxwang531 »

wmayer wrote: Sat Nov 27, 2021 2:14 pm The problem is that Qt's GUI is not thread-safe. This means from within a thread you are not allowed to directly manipulate the GUI.
Now QRunnable is managed by the thread pool and each instance is executed in its own thread. So, if you work with QThread or QRunnable doesn't make a big difference at the end.

The following function calls in the run() method are not allowed:

[/code]
a.pathSimulation.accept()
DOC.recompute()
FreeCADGui.Control.closeDialog()
Gui.Selection.addSelection('Rechteckpocket2', 'CutMaterial')
DOC.removeObject('CutMaterial')
[/code]

One way to (indirectly) modify the GUI from within a thread is to post events or with signals/slots where the connection type between sender and receiver is "queued". In this case the actual functions are executed in the GUI thread.
Thank you for pointing out the key to the problem. After receiving your answer, I carefully studied the operation of the Qt framework, but unfortunately there is no progress.
If I want to use the path simulator through scripts, the worker threads must be used. In fact, I have tried putting instructions with GUI in the main thread, FreeCAD will go directly into the unresponsive state. The code is shown below.

Code: Select all

Gui.runCommand('Path_Simulator', 0)
a.pathSimulation.SetupSimulation()  # Simulation Reset
a.pathSimulation.SimFF()
while a.pathSimulation.iprogress < a.pathSimulation.numCommands:
    sleep(1)
print("Tasks are finished")
a.pathSimulation.accept()
DOC.recompute()
FreeCADGui.Control.closeDialog()
At present, I try to use signals and slots to complete the call of the path simulator. I try to write a QObject and put the operations that I need to perform in it. I am not sure whether GUI commands can be used in it.

Code: Select all

class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal()

    def run(self):
        count = 0
        app = QCoreApplication.instance()
        while a.pathSimulation.iprogress < a.pathSimulation.numCommands:
            sleep(1)
            self.progress.emit()
        print("Tasks are finished")
        a.pathSimulation.accept()

        # app.quit
        DOC.recompute()
        FreeCADGui.Control.closeDialog()

        # export
        Gui.Selection.addSelection('Rechteckpocket2', 'CutMaterial')
        __objs__ = []
        __objs__.append(FreeCAD.getDocument("Rechteckpocket2").getObject("CutMaterial"))
        import Mesh
        Mesh.export(__objs__,
                    u"E:/111Masterarbeit/Github/Ausarbeitung/ModelCutmaterial/Rechteckpocket1-CutMaterial.stl")
        print("export finished")
        del __objs__
        DOC.recompute()
        self.finished.emit()
I am still a beginner with the Qt framework. If possible, can you help to write a simple example for calling the path simulator? Or are there other ways to call the path simulator in the script?
wmayer
Founder
Posts: 20243
Joined: Thu Feb 19, 2009 10:32 am
Contact:

Re: Using QRunnable in python script causes crash

Post by wmayer »

I am still a beginner with the Qt framework. If possible, can you help to write a simple example for calling the path simulator? Or are there other ways to call the path simulator in the script?
Sorry but I don't know much about the Path code and can't really tell what can be used in a thread. It's best that you ask one of its maintainers.
TheMarkster
Veteran
Posts: 5505
Joined: Thu Apr 05, 2018 1:53 am

Re: Using QRunnable in python script causes crash

Post by TheMarkster »

Try adding a call to FreeCADGui.updateGui() inside your loop. This will keep the Gui from becoming unresponsive in many cases.
maxwang531
Posts: 21
Joined: Sun Sep 19, 2021 5:25 pm

Re: Using QRunnable in python script causes crash

Post by maxwang531 »

TheMarkster wrote: Sat Nov 27, 2021 10:24 pm Try adding a call to FreeCADGui.updateGui() inside your loop. This will keep the Gui from becoming unresponsive in many cases.
Thank you for this solution. After experimenting, the code that I previously ran in the sub-thread can indeed be run in the main thread, and the GUI will not appear unresponsive. This is really amazing.

Code: Select all

while a.pathSimulation.iprogress < a.pathSimulation.numCommands:
    FreeCADGui.updateGui()
    sleep(0.05)
print("Tasks are finished")
a.pathSimulation.accept()
DOC.recompute()
FreeCADGui.Control.closeDialog()
User avatar
ebrahim raeyat
Posts: 619
Joined: Sun Sep 09, 2018 7:00 pm
Location: Iran
Contact:

Re: Using QRunnable in python script causes crash

Post by ebrahim raeyat »

I also have a problem with using QRunnable:

I have a dialog and want when user accept (click ok), does 2 time consuming tasks, when I use simple task, it works, but with my real task, it did not work, minimal code that working:

Code: Select all


from PySide2.QtCore import QRunnable, QThreadPool


class Runnable(QRunnable):

    def __init__(self, fn, *args, **kwargs):
        super().__init__()
        self.fn = fn
        self.args = args
        self.kwargs = kwargs

    def run(self):
        self.fn(*self.args, **self.kwargs)


class EtabsTaskPanel:

    def __init__(self):
        self.form = Gui.PySideUic.loadUi(PATH_TO_UI_FILE)

    def accept(self):
        pool = QThreadPool.globalInstance()
        runnable = Runnable(self.time_consuming_func)
        pool.start(runnable)
        
    def time_consuming_func(self):
        import time
        for i in range(20):
            print(i)
            time.sleep(1)
        print('finished')

output:

Code: Select all


16:36:22  0
16:36:23  1
16:36:24  2
16:36:25  3
16:36:26  4
16:36:27  5
16:36:28  6
16:36:29  7
16:36:30  8
16:36:31  9
16:36:32  10
16:36:33  11
16:36:34  12
16:36:35  13
16:36:36  14
16:36:37  15
16:36:38  16
16:36:39  17
16:36:40  18
16:36:41  19
16:36:42  finished

I set it to 20 seconds, because my real task, take some seconds to freezes the FreeCAD.

but when it come to my real task, it crashes, the difference is in time consuming function, when i import my module, it freezes. my program works without threading:

Code: Select all

    def time_consuming_func(self):
    	from safe.punch import etabs_punch
    	print('finished')
    	
FreeCAD freezes at this point. as I said my import without threading works.

I also implemented yorik's statuswidget, it works in my civiltools WB the way that yorik use, but when I want to apply this method in my dialog, FreeCAD show a tiny windows with FreeCAD icon and then freezes.

https://github.com/yorikvanhavre/BIM_Wo ... ar.py#L122

https://github.com/ebrahimraeyat/civilT ... Bar.py#L40
Post Reply