From 616b78865fa5f9a4f00eee0e6e2c3532bce86d7d Mon Sep 17 00:00:00 2001 From: marioalexis Date: Mon, 4 Nov 2024 13:00:47 -0300 Subject: [PATCH] Fem: Print real-time log messages in mesher task panels - fixes #17594 --- src/Mod/Fem/femmesh/gmshtools.py | 23 +-- src/Mod/Fem/femmesh/netgentools.py | 24 +-- .../femtaskpanels/base_femmeshtaskpanel.py | 174 ++++++++++-------- src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py | 10 +- src/Mod/Fem/femtaskpanels/task_mesh_netgen.py | 12 +- 5 files changed, 130 insertions(+), 113 deletions(-) diff --git a/src/Mod/Fem/femmesh/gmshtools.py b/src/Mod/Fem/femmesh/gmshtools.py index d263b88c8d..6feebb75ff 100644 --- a/src/Mod/Fem/femmesh/gmshtools.py +++ b/src/Mod/Fem/femmesh/gmshtools.py @@ -30,6 +30,7 @@ __url__ = "https://www.freecad.org" import os import re import subprocess +from PySide.QtCore import QProcess import FreeCAD from FreeCAD import Console @@ -54,7 +55,7 @@ class GmshTools: # mesh obj self.mesh_obj = gmsh_mesh_obj - self.process = None + self.process = QProcess() # analysis self.analysis = None if analysis: @@ -210,27 +211,17 @@ class GmshTools: self.write_part_file() self.write_geo() - def compute(self): + def prepare(self): self.load_properties() self.update_mesh_data() self.get_tmp_file_paths() self.get_gmsh_command() self.write_gmsh_input_files() - command_list = [self.gmsh_bin, "-", self.temp_file_geo] - self.process = subprocess.Popen( - command_list, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - startupinfo=femutils.startProgramInfo("hide"), - ) - - out, err = self.process.communicate() - if self.process.returncode != 0: - raise RuntimeError(err.decode("utf-8")) - - return True + def compute(self): + command_list = ["-v", "4", "-", self.temp_file_geo] + self.process.start(self.gmsh_bin, command_list) + return self.process def update_properties(self): self.mesh_obj.FemMesh = Fem.read(self.temp_file_mesh) diff --git a/src/Mod/Fem/femmesh/netgentools.py b/src/Mod/Fem/femmesh/netgentools.py index c4ef9bbd0b..701937ed54 100644 --- a/src/Mod/Fem/femmesh/netgentools.py +++ b/src/Mod/Fem/femmesh/netgentools.py @@ -27,9 +27,9 @@ __url__ = "https://www.freecad.org" import numpy as np import shutil -import subprocess import sys import tempfile +from PySide.QtCore import QProcess import FreeCAD import Fem @@ -81,6 +81,8 @@ class NetgenTools: self.fem_mesh = None self.process = None self.tmpdir = "" + self.process = QProcess() + self.mesh_params = {} def write_geom(self): if not self.tmpdir: @@ -101,9 +103,9 @@ from femmesh.netgentools import NetgenTools NetgenTools.run_netgen(**{params}) """ - def compute(self): + def prepare(self): self.write_geom() - mesh_params = { + self.mesh_params = { "brep_file": self.brep_file, "threads": self.obj.Threads, "heal": self.obj.HealShape, @@ -112,19 +114,11 @@ NetgenTools.run_netgen(**{params}) "result_file": self.result_file, } - code_str = self.code.format(params=mesh_params) + def compute(self): + code_str = self.code.format(params=self.mesh_params) + self.process.start(sys.executable, ["-c", code_str]) - cmd_list = [ - sys.executable, - "-c", - code_str, - ] - self.process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = self.process.communicate() - if self.process.returncode != 0: - raise RuntimeError(err.decode("utf-8")) - - return True + return self.process @staticmethod def run_netgen(brep_file, threads, heal, params, second_order, result_file): diff --git a/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py b/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py index aa4adb613a..5ab36ebdd4 100644 --- a/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py +++ b/src/Mod/Fem/femtaskpanels/base_femmeshtaskpanel.py @@ -30,7 +30,6 @@ __url__ = "https://www.freecad.org" # \brief base task panel for mesh object import time -import threading from abc import ABC, abstractmethod from PySide import QtCore @@ -43,48 +42,19 @@ from femtools.femutils import getOutputWinColor from . import base_femtaskpanel -class _Process(threading.Thread): +class _Thread(QtCore.QThread): """ Class for thread and subprocess manipulation - 'tool' argument must be an object with a 'compute' method - and a 'process' attribute of type Popen object + 'tool' argument must be an object with 'compute' and 'prepare' methods + and a 'process' attribute of type QProcess object """ def __init__(self, tool): + super().__init__() self.tool = tool - self._timer = QtCore.QTimer() - self.success = False - self.update = False - self.error = "" - super().__init__(target=self.tool.compute) - QtCore.QObject.connect(self._timer, QtCore.SIGNAL("timeout()"), self._check) - - def init(self): - self._timer.start(100) - self.start() def run(self): - try: - self.success = self._target(*self._args, **self._kwargs) - except Exception as e: - self.error = str(e) - - def finish(self): - if self.tool.process: - self.tool.process.terminate() - self.join() - - def _check(self): - if not self.is_alive(): - self._timer.stop() - self.join() - if self.success: - try: - self.tool.update_properties() - self.update = True - except Exception as e: - self.error = str(e) - self.success = False + self.tool.prepare() class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): @@ -92,14 +62,79 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): Abstract base class for FemMesh object TaskPanel """ - def __init__(self, obj): + def __init__(self, obj, tool): super().__init__(obj) - - self.tool = None - self.form = None + self.tool = tool self.timer = QtCore.QTimer() - self.process = None - self.console_message = "" + self._thread = _Thread(self.tool) + self.text_log = None + self.text_time = None + + def setup_connections(self): + QtCore.QObject.connect(self._thread, QtCore.SIGNAL("started()"), self.thread_started) + QtCore.QObject.connect(self._thread, QtCore.SIGNAL("finished()"), self.thread_finished) + QtCore.QObject.connect(self.tool.process, QtCore.SIGNAL("started()"), self.process_started) + QtCore.QObject.connect( + self.tool.process, + QtCore.SIGNAL("finished(int,QProcess::ExitStatus)"), + self.process_finished, + ) + QtCore.QObject.connect( + self.tool.process, + QtCore.SIGNAL("readyReadStandardOutput()"), + self.write_output, + ) + QtCore.QObject.connect( + self.tool.process, + QtCore.SIGNAL("readyReadStandardError()"), + self.write_error, + ) + QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.update_timer_text) + + def thread_started(self): + self.text_log.clear() + self.write_log("Prepare meshing...\n", QtGui.QColor(getOutputWinColor("Text"))) + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + + def thread_finished(self): + self.tool.compute() + + def process_finished(self, code, status): + if status == QtCore.QProcess.ExitStatus.NormalExit: + if code != 0: + self.write_log( + "Meshing finished with errors\n", QtGui.QColor(getOutputWinColor("Error")) + ) + self.tool.update_properties() + self.write_log("Process finished\n", QtGui.QColor(getOutputWinColor("Text"))) + else: + self.write_log("Process crashed\n", QtGui.QColor(getOutputWinColor("Error"))) + + def process_started(self): + self.write_log("Start meshing...\n", QtGui.QColor(getOutputWinColor("Text"))) + + def write_output(self): + self.write_log( + self.tool.process.readAllStandardOutput().data().decode("utf-8"), + QtGui.QColor(getOutputWinColor("Logging")), + ) + + def write_error(self): + self.write_log( + self.tool.process.readAllStandardError().data().decode("utf-8"), + QtGui.QColor(getOutputWinColor("Error")), + ) + + def write_log(self, data, color): + cursor = QtGui.QTextCursor(self.text_log.document()) + cursor.beginEditBlock() + cursor.movePosition(QtGui.QTextCursor.End) + fmt = QtGui.QTextCharFormat() + fmt.setForeground(color) + cursor.mergeCharFormat(fmt) + cursor.insertText(data) + cursor.endEditBlock() + self.text_log.ensureCursorVisible() @abstractmethod def set_mesh_params(self): @@ -116,7 +151,10 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): return button_value def accept(self): - if self.process and self.process.is_alive(): + if ( + self._thread.isRunning() + or self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning + ): FreeCAD.Console.PrintWarning("Process still running\n") return None @@ -126,62 +164,46 @@ class _BaseMeshTaskPanel(base_femtaskpanel._BaseTaskPanel, ABC): return super().accept() def reject(self): + # self_thread may be blocking + if self._thread.isRunning(): + return None self.timer.stop() QtGui.QApplication.restoreOverrideCursor() - if self.process and self.process.is_alive(): - self.console_log("Process aborted", "#ff6700") - self.process.finish() + if self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning: + self.tool.process.kill() + self._thread.quit() + FreeCAD.Console.PrintWarning("Process aborted\n") else: return super().reject() def clicked(self, button): if button == QtGui.QDialogButtonBox.Apply: - if self.process and self.process.is_alive(): + if ( + self._thread.isRunning() + or self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning + ): FreeCAD.Console.PrintWarning("Process already running\n") return None self.set_mesh_params() self.run_mesher() - def console_log(self, message="", outputwin_color_type=None): - self.console_message = self.console_message + ( - '{:4.1f}: '.format( - getOutputWinColor("Logging"), time.time() - self.time_start - ) - ) - if outputwin_color_type: - self.console_message += '{}
'.format( - outputwin_color_type, message - ) - else: - self.console_message += message + "
" - self.form.te_output.setText(self.console_message) - self.form.te_output.moveCursor(QtGui.QTextCursor.End) - def update_timer_text(self): - if self.process and self.process.is_alive(): - self.form.l_time.setText(f"Time: {time.time() - self.time_start:4.1f}: ") + if ( + self._thread.isRunning() + or self.tool.process.state() != QtCore.QProcess.ProcessState.NotRunning + ): + self.text_time.setText(f"Time: {time.time() - self.time_start:4.1f}") else: - if self.process: - if self.process.success: - if not self.process.update: - return None - self.console_log("Success!", "#00AA00") - else: - self.console_log(self.process.error, "#AA0000") self.timer.stop() QtGui.QApplication.restoreOverrideCursor() def run_mesher(self): - self.process = _Process(self.tool) self.timer.start(100) self.time_start = time.time() - self.form.l_time.setText(f"Time: {time.time() - self.time_start:4.1f}: ") - self.console_message = "" - self.console_log("Start process...") - QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + self.text_time.setText(f"Time: {time.time() - self.time_start:4.1f}: ") - self.process.init() + self._thread.start() def get_version(self): full_message = self.tool.version() diff --git a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py index 32afdba764..bbdf906d2c 100644 --- a/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py +++ b/src/Mod/Fem/femtaskpanels/task_mesh_gmsh.py @@ -46,13 +46,18 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): """ def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, gmshtools.GmshTools(obj)) self.form = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/MeshGmsh.ui" ) + self.text_log = self.form.te_output + self.text_time = self.form.l_time - self.tool = gmshtools.GmshTools(obj) + self.setup_connections() + + def setup_connections(self): + super().setup_connections() QtCore.QObject.connect( self.form.qsb_max_size, QtCore.SIGNAL("valueChanged(Base::Quantity)"), self.max_changed @@ -69,7 +74,6 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): self.form.cb_dimension.addItems(self.obj.getEnumerationsOfProperty("ElementDimension")) self.form.cb_order.addItems(self.obj.getEnumerationsOfProperty("ElementOrder")) - QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.update_timer_text) QtCore.QObject.connect( self.form.pb_get_gmsh_version, QtCore.SIGNAL("clicked()"), self.get_version ) diff --git a/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py b/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py index 3d9a3b03d1..8d8081170c 100644 --- a/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py +++ b/src/Mod/Fem/femtaskpanels/task_mesh_netgen.py @@ -46,12 +46,19 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): """ def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, netgentools.NetgenTools(obj)) + self.form = FreeCADGui.PySideUic.loadUi( FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/MeshNetgen.ui" ) - self.tool = netgentools.NetgenTools(obj) + self.text_log = self.form.te_output + self.text_time = self.form.l_time + + self.setup_connections() + + def setup_connections(self): + super().setup_connections() QtCore.QObject.connect( self.form.qsb_max_size, @@ -86,7 +93,6 @@ class _TaskPanel(base_femmeshtaskpanel._BaseMeshTaskPanel): QtCore.SIGNAL("currentIndexChanged(int)"), self.fineness_changed, ) - QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.update_timer_text) QtCore.QObject.connect( self.form.pb_get_netgen_version, QtCore.SIGNAL("clicked()"), self.get_version )