From 2082446ab04b6e173d491cd7efe4984039fd6915 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 00:59:02 +0100 Subject: [PATCH] tweak signal handling --- FlatCAMApp.py | 4 ++++ FlatCAMWorker.py | 22 ++++++++++++++++----- tclCommands/TclCommand.py | 41 ++++++++++++++++++++++++++++++--------- tests/test_tcl_shell.py | 26 ++++++++++++++++--------- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index dd917bb7..0ed5b896 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -108,6 +108,10 @@ class App(QtCore.QObject): # Emmited when shell command is finished(one command only) shell_command_finished = QtCore.pyqtSignal(object) + # Emitted when an unhandled exception happens + # in the worker task. + thread_exception = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) def __init__(self, user_defaults=True, post_gui=None): diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 90edaa7f..a1f49a81 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -48,12 +48,24 @@ class Worker(QtCore.QObject): # 'worker_name' property of task allows to target # specific worker. - if 'worker_name' in task and task['worker_name'] == self.name: - task['fcn'](*task['params']) - return + #if 'worker_name' in task and task['worker_name'] == self.name: + # task['fcn'](*task['params']) + # return + + #if 'worker_name' not in task and self.name is None: + # task['fcn'](*task['params']) + # return + + + if ('worker_name' in task and task['worker_name'] == self.name) or \ + ('worker_name' not in task and self.name is None): + + try: + task['fcn'](*task['params']) + except Exception as e: + self.app.thread_exception.emit(e) + raise e - if 'worker_name' not in task and self.name is None: - task['fcn'](*task['params']) return # FlatCAMApp.App.log.debug("Task ignored.") diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index a446a15b..f713b319 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -258,15 +258,17 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 300 sec, but it can be much more - default_timeout = 300000 + # default timeout for operation is 10 sec, but it can be much more + default_timeout = 10000 output = None def execute_call(self, args, unnamed_args): - self.output = self.execute(args, unnamed_args) - self.app.shell_command_finished.emit(self) + try: + self.output = self.execute(args, unnamed_args) + finally: + self.app.shell_command_finished.emit(self) def execute_wrapper(self, *args): """ @@ -279,11 +281,16 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=300000): + def wait_signal(signal, timeout=10000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.app.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -292,18 +299,23 @@ class TclCommandSignaled(TclCommand): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + # Block loop.exec_() + # Restore exception management sys.excepthook = oeh if ex: self.raise_tcl_error(str(ex[0])) @@ -324,12 +336,23 @@ class TclCommandSignaled(TclCommand): # set detail for processing, it will be there until next open or close self.app.shell.open_proccessing(self.get_current_command()) + self.output = None + + def handle_finished(obj): + self.app.shell_command_finished.disconnect(handle_finished) + # TODO: handle output + pass + + self.app.shell_command_finished.connect(handle_finished) + with wait_signal(self.app.shell_command_finished, passed_timeout): # every TclCommandNewObject ancestor support timeout as parameter, # but it does not mean anything for child itself # when operation will be really long is good to set it higher then defqault 30s self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]}) + return self.output + except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 526354fb..ecf60f56 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -1,6 +1,8 @@ import sys import unittest from PyQt4 import QtGui +from PyQt4.QtCore import QThread + from FlatCAMApp import App from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon from ObjectUI import GerberObjectUI, GeometryObjectUI @@ -10,6 +12,8 @@ import tempfile class TclShellTest(unittest.TestCase): + setup = False + gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' copper_top_filename = 'detector_copper_top.gbr' @@ -24,20 +28,23 @@ class TclShellTest(unittest.TestCase): drill_diameter = 0.8 def setUp(self): - self.app = QtGui.QApplication(sys.argv) - # Create App, keep app defaults (do not load - # user-defined defaults). - self.fc = App(user_defaults=False) + if not self.setup: + self.setup=True + self.app = QtGui.QApplication(sys.argv) - self.fc.shell.show() + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + self.fc.shell.show() pass def tearDown(self): - self.app.closeAllWindows() - - del self.fc - del self.app + #self.fc.tcl=None + #self.app.closeAllWindows() + #del self.fc + #del self.app pass def test_set_get_units(self): @@ -60,6 +67,7 @@ class TclShellTest(unittest.TestCase): # open gerber files top, bottom and cutout + self.fc.exec_command_test('set_sys units MM') self.fc.exec_command_test('new')