Przeglądaj źródła

tweak signal handling

Kamil Sopko 9 lat temu
rodzic
commit
2082446ab0
4 zmienionych plików z 69 dodań i 22 usunięć
  1. 4 0
      FlatCAMApp.py
  2. 17 5
      FlatCAMWorker.py
  3. 31 8
      tclCommands/TclCommand.py
  4. 17 9
      tests/test_tcl_shell.py

+ 4 - 0
FlatCAMApp.py

@@ -108,6 +108,10 @@ class App(QtCore.QObject):
     # Emmited when shell command is finished(one command only)
     # Emmited when shell command is finished(one command only)
     shell_command_finished = QtCore.pyqtSignal(object)
     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)
     message = QtCore.pyqtSignal(str, str, str)
 
 
     def __init__(self, user_defaults=True, post_gui=None):
     def __init__(self, user_defaults=True, post_gui=None):

+ 17 - 5
FlatCAMWorker.py

@@ -48,12 +48,24 @@ class Worker(QtCore.QObject):
 
 
         # 'worker_name' property of task allows to target
         # 'worker_name' property of task allows to target
         # specific worker.
         # 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
             return
 
 
         # FlatCAMApp.App.log.debug("Task ignored.")
         # FlatCAMApp.App.log.debug("Task ignored.")

+ 31 - 8
tclCommands/TclCommand.py

@@ -258,15 +258,17 @@ class TclCommandSignaled(TclCommand):
         it handles  all neccessary stuff about blocking and passing exeptions
         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
     output = None
 
 
     def execute_call(self, args, unnamed_args):
     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):
     def execute_wrapper(self, *args):
         """
         """
@@ -279,11 +281,16 @@ class TclCommandSignaled(TclCommand):
         """
         """
 
 
         @contextmanager
         @contextmanager
-        def wait_signal(signal, timeout=300000):
+        def wait_signal(signal, timeout=10000):
             """Block loop until signal emitted, or timeout (ms) elapses."""
             """Block loop until signal emitted, or timeout (ms) elapses."""
             loop = QtCore.QEventLoop()
             loop = QtCore.QEventLoop()
+
+            # Normal termination
             signal.connect(loop.quit)
             signal.connect(loop.quit)
 
 
+            # Termination by exception in thread
+            self.app.thread_exception.connect(loop.quit)
+
             status = {'timed_out': False}
             status = {'timed_out': False}
 
 
             def report_quit():
             def report_quit():
@@ -292,18 +299,23 @@ class TclCommandSignaled(TclCommand):
 
 
             yield
             yield
 
 
+            # Temporarily change how exceptions are managed.
             oeh = sys.excepthook
             oeh = sys.excepthook
             ex = []
             ex = []
-            def exceptHook(type_, value, traceback):
+
+            def except_hook(type_, value, traceback_):
                 ex.append(value)
                 ex.append(value)
-                oeh(type_, value, traceback)
-            sys.excepthook = exceptHook
+                oeh(type_, value, traceback_)
+            sys.excepthook = except_hook
 
 
+            # Terminate on timeout
             if timeout is not None:
             if timeout is not None:
                 QtCore.QTimer.singleShot(timeout, report_quit)
                 QtCore.QTimer.singleShot(timeout, report_quit)
 
 
+            # Block
             loop.exec_()
             loop.exec_()
 
 
+            # Restore exception management
             sys.excepthook = oeh
             sys.excepthook = oeh
             if ex:
             if ex:
                 self.raise_tcl_error(str(ex[0]))
                 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
             # set detail for processing, it will be there until next open or close
             self.app.shell.open_proccessing(self.get_current_command())
             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):
             with wait_signal(self.app.shell_command_finished, passed_timeout):
                 # every TclCommandNewObject ancestor  support  timeout as parameter,
                 # every TclCommandNewObject ancestor  support  timeout as parameter,
                 # but it does not mean anything for child itself
                 # but it does not mean anything for child itself
                 # when operation  will be  really long is good  to set it higher then defqault 30s
                 # 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]})
                 self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]})
 
 
+            return self.output
+
         except Exception as unknown:
         except Exception as unknown:
             self.log.error("TCL command '%s' failed." % str(self))
             self.log.error("TCL command '%s' failed." % str(self))
             self.app.raise_tcl_unknown_error(unknown)
             self.app.raise_tcl_unknown_error(unknown)

+ 17 - 9
tests/test_tcl_shell.py

@@ -1,6 +1,8 @@
 import sys
 import sys
 import unittest
 import unittest
 from PyQt4 import QtGui
 from PyQt4 import QtGui
+from PyQt4.QtCore import QThread
+
 from FlatCAMApp import App
 from FlatCAMApp import App
 from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon
 from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon
 from ObjectUI import GerberObjectUI, GeometryObjectUI
 from ObjectUI import GerberObjectUI, GeometryObjectUI
@@ -10,6 +12,8 @@ import tempfile
 
 
 class TclShellTest(unittest.TestCase):
 class TclShellTest(unittest.TestCase):
 
 
+    setup = False
+
     gerber_files = 'tests/gerber_files'
     gerber_files = 'tests/gerber_files'
     copper_bottom_filename = 'detector_copper_bottom.gbr'
     copper_bottom_filename = 'detector_copper_bottom.gbr'
     copper_top_filename = 'detector_copper_top.gbr'
     copper_top_filename = 'detector_copper_top.gbr'
@@ -24,20 +28,23 @@ class TclShellTest(unittest.TestCase):
     drill_diameter = 0.8
     drill_diameter = 0.8
 
 
     def setUp(self):
     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)
+
+            # Create App, keep app defaults (do not load
+            # user-defined defaults).
+            self.fc = App(user_defaults=False)
 
 
-        self.fc.shell.show()
+            self.fc.shell.show()
         pass
         pass
 
 
     def tearDown(self):
     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
         pass
 
 
     def test_set_get_units(self):
     def test_set_get_units(self):
@@ -60,6 +67,7 @@ class TclShellTest(unittest.TestCase):
 
 
         # open  gerber files top, bottom and cutout
         # open  gerber files top, bottom and cutout
 
 
+
         self.fc.exec_command_test('set_sys units MM')
         self.fc.exec_command_test('set_sys units MM')
         self.fc.exec_command_test('new')
         self.fc.exec_command_test('new')