merged
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Thu, 12 Apr 2018 22:32:43 +0200
changeset 1986 88048a0dd0c3
parent 1985 ae758ff037dc (current diff)
parent 1984 081265cda5b1 (diff)
child 1987 8d1aca3c9e83
merged
--- a/Beremiz_service.py	Thu Apr 12 22:32:12 2018 +0200
+++ b/Beremiz_service.py	Thu Apr 12 22:32:43 2018 +0200
@@ -35,7 +35,7 @@
 import __builtin__
 import Pyro.core as pyro
 
-from runtime import PLCObject, ServicePublisher
+from runtime import PLCObject, ServicePublisher, MainWorker
 import util.paths as paths
 
 
@@ -401,7 +401,7 @@
 
 class Server(object):
     def __init__(self, servicename, ip_addr, port,
-                 workdir, argv, autostart=False,
+                 workdir, argv,
                  statuschange=None, evaluator=default_evaluator,
                  pyruntimevars=None):
         self.continueloop = True
@@ -413,12 +413,11 @@
         self.argv = argv
         self.plcobj = None
         self.servicepublisher = None
-        self.autostart = autostart
         self.statuschange = statuschange
         self.evaluator = evaluator
         self.pyruntimevars = pyruntimevars
 
-    def Loop(self):
+    def PyroLoop(self):
         while self.continueloop:
             pyro.initServer()
             self.daemon = pyro.Daemon(host=self.ip_addr, port=self.port)
@@ -426,7 +425,27 @@
             # taking too small timeout value may cause
             # unwanted diconnection when IDE is kept busy for long periods
             self.daemon.setTimeout(60)
-            self.Start()
+
+            uri = self.daemon.connect(self.plcobj, "PLCObject")
+
+            print(_("Pyro port :"), self.port)
+            print(_("Pyro object's uri :"), uri)
+
+            # Beremiz IDE detects daemon start by looking
+            # for self.workdir in the daemon's stdout.
+            # Therefore don't delete the following line
+            print(_("Current working directory :"), self.workdir)
+
+            # Configure and publish service
+            # Not publish service if localhost in address params
+            if self.servicename is not None and \
+               self.ip_addr is not None and \
+               self.ip_addr != "localhost" and \
+               self.ip_addr != "127.0.0.1":
+                print(_("Publishing service on local network"))
+                self.servicepublisher = ServicePublisher.ServicePublisher()
+                self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port)
+
             self.daemon.requestLoop()
             self.daemon.sock.close()
 
@@ -440,38 +459,8 @@
             self.plcobj.UnLoadPLC()
         self._stop()
 
-    def Start(self):
-        self.plcobj = PLCObject(self.workdir, self.daemon, self.argv,
-                                self.statuschange, self.evaluator,
-                                self.pyruntimevars)
-
-        uri = self.daemon.connect(self.plcobj, "PLCObject")
-
-        print(_("Pyro port :"), self.port)
-        print(_("Pyro object's uri :"), uri)
-
-        # Beremiz IDE detects daemon start by looking
-        # for self.workdir in the daemon's stdout.
-        # Therefore don't delete the following line
-        print(_("Current working directory :"), self.workdir)
-
-        # Configure and publish service
-        # Not publish service if localhost in address params
-        if self.servicename is not None and \
-           self.ip_addr is not None and \
-           self.ip_addr != "localhost" and \
-           self.ip_addr != "127.0.0.1":
-            print(_("Publishing service on local network"))
-            self.servicepublisher = ServicePublisher.ServicePublisher()
-            self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port)
-
-        self.plcobj.AutoLoad()
-        if self.plcobj.GetPLCstatus()[0] != "Empty":
-            if self.autostart:
-                self.plcobj.StartPLC()
-        self.plcobj.StatusChange()
-
-        sys.stdout.flush()
+    def RegisterPLCObject(self, plcobj):
+        self.plcobj = plcobj
 
     def _stop(self):
         if self.plcobj is not None:
@@ -529,13 +518,13 @@
             return o.res
 
     pyroserver = Server(servicename, given_ip, port,
-                        WorkingDir, argv, autostart,
+                        WorkingDir, argv,
                         statuschange, evaluator, pyruntimevars)
 
     taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx)
 else:
     pyroserver = Server(servicename, given_ip, port,
-                        WorkingDir, argv, autostart,
+                        WorkingDir, argv,
                         statuschange, pyruntimevars=pyruntimevars)
 
 
@@ -631,19 +620,36 @@
         except Exception:
             LogMessageAndException(_("WAMP client startup failed. "))
 
+plcobj = PLCObject(pyroserver)
+
+plcobj.AutoLoad()
+if plcobj.GetPLCstatus()[0] == "Stopped":
+    if autostart:
+        plcobj.StartPLC()
+plcobj.StatusChange()
+
+pyro_thread = Thread(target=pyroserver.PyroLoop)
+pyro_thread.start()
+
+sys.stdout.flush()
 
 if havetwisted or havewx:
-    pyro_thread = Thread(target=pyroserver.Loop)
-    pyro_thread.start()
-
     if havetwisted:
-        reactor.run()
-    elif havewx:
-        app.MainLoop()
-else:
-    try:
-        pyroserver.Loop()
-    except KeyboardInterrupt:
-        pass
+        # reactor._installSignalHandlersAgain()
+        def ui_thread_target():
+            # FIXME: had to disable SignaHandlers install because 
+            # signal not working in non-main thread
+            reactor.run(installSignalHandlers=False)
+    else :
+        ui_thread_target = app.MainLoop
+
+    ui_thread = Thread(target = ui_thread_target)
+    ui_thread.start()
+
+try:
+    MainWorker.runloop()
+except KeyboardInterrupt:
+    pass
+
 pyroserver.Quit()
 sys.exit(0)
--- a/canfestival/canfestival.py	Thu Apr 12 22:32:12 2018 +0200
+++ b/canfestival/canfestival.py	Thu Apr 12 22:32:43 2018 +0200
@@ -38,14 +38,11 @@
     LOCATION_CONFNODE, \
     LOCATION_VAR_MEMORY
 
-try:
-    from nodelist import NodeList
-except ImportError:
-    base_folder = paths.AbsParentDir(__file__, 2)
-    CanFestivalPath = os.path.join(base_folder, "CanFestival-3")
-    sys.path.append(os.path.join(CanFestivalPath, "objdictgen"))
-
-    from nodelist import NodeList
+base_folder = paths.AbsParentDir(__file__, 2)
+CanFestivalPath = os.path.join(base_folder, "CanFestival-3")
+sys.path.append(os.path.join(CanFestivalPath, "objdictgen"))
+
+from nodelist import NodeList
 
 from nodemanager import NodeManager
 import gen_cfile
--- a/connectors/PYRO/__init__.py	Thu Apr 12 22:32:12 2018 +0200
+++ b/connectors/PYRO/__init__.py	Thu Apr 12 22:32:43 2018 +0200
@@ -148,41 +148,17 @@
             # for safe use in from debug thread, must create a copy
             self.RemotePLCObjectProxyCopy = None
 
-        def GetPyroProxy(self):
-            """
-            This func returns the real Pyro Proxy.
-            Use this if you musn't keep reference to it.
-            """
-            return RemotePLCObjectProxy
-
         def _PyroStartPLC(self, *args, **kwargs):
-            """
-            confnodesroot._connector.GetPyroProxy() is used
-            rather than RemotePLCObjectProxy because
-            object is recreated meanwhile,
-            so we must not keep ref to it here
-            """
-            current_status, _log_count = confnodesroot._connector.GetPyroProxy().GetPLCstatus()
-            if current_status == "Dirty":
-                # Some bad libs with static symbols may polute PLC
-                # ask runtime to suicide and come back again
-
-                confnodesroot.logger.write(_("Force runtime reload\n"))
-                confnodesroot._connector.GetPyroProxy().ForceReload()
-                confnodesroot._Disconnect()
-                # let remote PLC time to resurect.(freeze app)
-                sleep(0.5)
-                confnodesroot._Connect()
-            self.RemotePLCObjectProxyCopy = copy.copy(confnodesroot._connector.GetPyroProxy())
-            return confnodesroot._connector.GetPyroProxy().StartPLC(*args, **kwargs)
+            return RemotePLCObjectProxy.StartPLC(*args, **kwargs)
         StartPLC = PyroCatcher(_PyroStartPLC, False)
 
         def _PyroGetTraceVariables(self):
             """
-            for safe use in from debug thread, must use the copy
+            for use from debug thread, use a copy
+            pyro creates a new thread on server end proxy object is copied
             """
             if self.RemotePLCObjectProxyCopy is None:
-                self.RemotePLCObjectProxyCopy = copy.copy(confnodesroot._connector.GetPyroProxy())
+                self.RemotePLCObjectProxyCopy = copy.copy(RemotePLCObjectProxy)
             return self.RemotePLCObjectProxyCopy.GetTraceVariables()
         GetTraceVariables = PyroCatcher(_PyroGetTraceVariables, ("Broken", None))
 
--- a/runtime/PLCObject.py	Thu Apr 12 22:32:12 2018 +0200
+++ b/runtime/PLCObject.py	Thu Apr 12 22:32:43 2018 +0200
@@ -23,7 +23,8 @@
 
 
 from __future__ import absolute_import
-from threading import Timer, Thread, Lock, Semaphore, Event
+import thread
+from threading import Timer, Thread, Lock, Semaphore, Event, Condition
 import ctypes
 import os
 import sys
@@ -60,28 +61,133 @@
     sys.stdout.flush()
 
 
+class job(object):
+    """
+    job to be executed by a worker
+    """
+    def __init__(self,call,*args,**kwargs):
+        self.job = (call,args,kwargs)
+        self.result = None
+        self.success = False
+        self.exc_info = None
+
+    def do(self):
+        """
+        do the job by executing the call, and deal with exceptions 
+        """
+        try :
+            call, args, kwargs = self.job
+            self.result = call(*args,**kwargs)
+            self.success = True
+        except Exception:
+            self.success = False
+            self.exc_info = sys.exc_info()
+
+
+class worker(object):
+    """
+    serialize main thread load/unload of PLC shared objects
+    """
+    def __init__(self):
+        # Only one job at a time
+        self._finish = False
+        self._threadID = None
+        self.mutex = Lock()
+        self.todo = Condition(self.mutex)
+        self.done = Condition(self.mutex)
+        self.job = None
+
+    def runloop(self):
+        """
+        meant to be called by worker thread (blocking)
+        """
+        self._threadID = thread.get_ident()
+        self.mutex.acquire()
+        while not self._finish:
+            self.todo.wait()
+            if self.job is not None:
+                self.job.do()
+            self.done.notify_all()
+        self.mutex.release()
+    
+    def call(self, *args, **kwargs):
+        print("call", args, kwargs)
+        """
+        creates a job, execute it in worker thread, and deliver result.
+        if job execution raise exception, re-raise same exception 
+        meant to be called by non-worker threads, but this is accepted.
+        blocking until job done
+        """
+
+        _job = job(*args,**kwargs)
+
+        if self._threadID == thread.get_ident():
+            # if caller is worker thread execute immediately
+            _job.do()
+        else:
+            # otherwise notify and wait for completion
+            self.mutex.acquire()
+
+            while self.job is not None:
+                self.done.wait()
+
+            self.job = _job
+            self.todo.notify()
+            self.done.wait()
+            _job = self.job
+            self.job = None
+            self.mutex.release()
+
+        if _job.success:
+            return _job.result
+        else:
+            raise _job.exc_info[0], _job.exc_info[1], _job.exc_info[2]
+        
+    def quit(self):
+        """
+        unblocks main thread, and terminate execution of runloop()
+        """
+        # mark queue
+        self._finish = True
+        self.mutex.acquire()
+        self.job = None
+        self.todo.notify()
+        self.mutex.release()
+
+
+MainWorker = worker()
+
+
+def RunInMain(func):
+    def func_wrapper(*args,**kwargs):
+        return MainWorker.call(func, *args, **kwargs)
+    return func_wrapper
+    
+
 class PLCObject(pyro.ObjBase):
-    def __init__(self, workingdir, daemon, argv, statuschange, evaluator, pyruntimevars):
+    def __init__(self, server):
+
         pyro.ObjBase.__init__(self)
-        self.evaluator = evaluator
-        self.argv = [workingdir] + argv  # force argv[0] to be "path" to exec...
-        self.workingdir = workingdir
+        self.evaluator = server.evaluator
+        self.argv = [server.workdir] + server.argv  # force argv[0] to be "path" to exec...
+        self.workingdir = server.workdir
         self.PLCStatus = "Empty"
         self.PLClibraryHandle = None
         self.PLClibraryLock = Lock()
         self.DummyIteratorLock = None
         # Creates fake C funcs proxies
-        self._FreePLC()
-        self.daemon = daemon
-        self.statuschange = statuschange
+        self._InitPLCStubCalls()
+        self.daemon = server.daemon
+        self.statuschange = server.statuschange
         self.hmi_frame = None
-        self.pyruntimevars = pyruntimevars
+        self.pyruntimevars = server.pyruntimevars
         self._loading_error = None
         self.python_runtime_vars = None
         self.TraceThread = None
         self.TraceLock = Lock()
         self.TraceWakeup = Event()
         self.Traces = []
+        server.RegisterPLCObject(self)
 
     def AutoLoad(self):
         # Get the last transfered PLC if connector must be restart
@@ -145,6 +251,7 @@
     def _GetLibFileName(self):
         return os.path.join(self.workingdir, self.CurrentPLCFilename)
 
+    @RunInMain
     def LoadPLC(self):
         """
         Load PLC library
@@ -233,19 +340,18 @@
         except Exception:
             self._loading_error = traceback.format_exc()
             PLCprint(self._loading_error)
+            self._FreePLC()
             return False
 
+    @RunInMain
     def UnLoadPLC(self):
         self.PythonRuntimeCleanup()
         self._FreePLC()
 
-    def _FreePLC(self):
-        """
-        Unload PLC library.
-        This is also called by __init__ to create dummy C func proxies
-        """
-        self.PLClibraryLock.acquire()
-        # Forget all refs to library
+    def _InitPLCStubCalls(self):
+        """
+        create dummy C func proxies
+        """
         self._startPLC = lambda x, y: None
         self._stopPLC = lambda: None
         self._ResetDebugVariables = lambda: None
@@ -259,11 +365,22 @@
         self._GetLogCount = None
         self._LogMessage = None
         self._GetLogMessage = None
+        self._PLClibraryHandle = None
         self.PLClibraryHandle = None
+
+    def _FreePLC(self):
+        """
+        Unload PLC library.
+        This is also called by __init__ to create dummy C func proxies
+        """
+        self.PLClibraryLock.acquire()
+
         # Unload library explicitely
         if getattr(self, "_PLClibraryHandle", None) is not None:
             dlclose(self._PLClibraryHandle)
-            self._PLClibraryHandle = None
+
+        # Forget all refs to library
+        self._InitPLCStubCalls()
 
         self.PLClibraryLock.release()
         return False
@@ -397,18 +514,6 @@
             return True
         return False
 
-    def _Reload(self):
-        self.daemon.shutdown(True)
-        self.daemon.sock.close()
-        os.execv(sys.executable, [sys.executable]+sys.argv[:])
-        # never reached
-        return 0
-
-    def ForceReload(self):
-        # respawn python interpreter
-        Timer(0.1, self._Reload).start()
-        return True
-
     def GetPLCstatus(self):
         return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
 
@@ -460,7 +565,6 @@
                 self.PLCStatus = "Stopped"
             else:
                 self.PLCStatus = "Broken"
-                self._FreePLC()
             self.StatusChange()
 
             return self.PLCStatus == "Stopped"
--- a/runtime/__init__.py	Thu Apr 12 22:32:12 2018 +0200
+++ b/runtime/__init__.py	Thu Apr 12 22:32:43 2018 +0200
@@ -24,5 +24,5 @@
 from __future__ import absolute_import
 import os
 
-from runtime.PLCObject import PLCObject, PLCprint
+from runtime.PLCObject import PLCObject, PLCprint, MainWorker
 import runtime.ServicePublisher
--- a/targets/Xenomai/plc_Xenomai_main.c	Thu Apr 12 22:32:12 2018 +0200
+++ b/targets/Xenomai/plc_Xenomai_main.c	Thu Apr 12 22:32:43 2018 +0200
@@ -159,6 +159,15 @@
     exit(0);
 }
 
+#define _startPLCLog(text) \
+    {\
+    	char mstr[] = text;\
+        LogMessage(LOG_CRITICAL, mstr, sizeof(mstr));\
+        goto error;\
+    }
+
+#define FO "Failed opening "
+
 #define max_val(a,b) ((a>b)?a:b)
 int startPLC(int argc,char **argv)
 {
@@ -171,49 +180,55 @@
 
     /*** RT Pipes creation and opening ***/
     /* create Debug_pipe */
-    if(rt_pipe_create(&Debug_pipe, "Debug_pipe", DEBUG_PIPE_MINOR, PIPE_SIZE)) 
-        goto error;
+    if(rt_pipe_create(&Debug_pipe, "Debug_pipe", DEBUG_PIPE_MINOR, PIPE_SIZE) < 0) 
+        _startPLCLog(FO "Debug_pipe real-time end");
     PLC_state |= PLC_STATE_DEBUG_PIPE_CREATED;
 
     /* open Debug_pipe*/
-    if((Debug_pipe_fd = open(DEBUG_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((Debug_pipe_fd = open(DEBUG_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO DEBUG_PIPE_DEVICE);
     PLC_state |= PLC_STATE_DEBUG_FILE_OPENED;
 
     /* create Python_pipe */
-    if(rt_pipe_create(&Python_pipe, "Python_pipe", PYTHON_PIPE_MINOR, PIPE_SIZE)) 
-        goto error;
+    if(rt_pipe_create(&Python_pipe, "Python_pipe", PYTHON_PIPE_MINOR, PIPE_SIZE) < 0) 
+        _startPLCLog(FO "Python_pipe real-time end");
     PLC_state |= PLC_STATE_PYTHON_PIPE_CREATED;
 
     /* open Python_pipe*/
-    if((Python_pipe_fd = open(PYTHON_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((Python_pipe_fd = open(PYTHON_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO PYTHON_PIPE_DEVICE);
     PLC_state |= PLC_STATE_PYTHON_FILE_OPENED;
 
     /* create WaitDebug_pipe */
-    if(rt_pipe_create(&WaitDebug_pipe, "WaitDebug_pipe", WAITDEBUG_PIPE_MINOR, PIPE_SIZE))
-        goto error;
+    if(rt_pipe_create(&WaitDebug_pipe, "WaitDebug_pipe", WAITDEBUG_PIPE_MINOR, PIPE_SIZE) < 0)
+        _startPLCLog(FO "WaitDebug_pipe real-time end");
     PLC_state |= PLC_STATE_WAITDEBUG_PIPE_CREATED;
 
     /* open WaitDebug_pipe*/
-    if((WaitDebug_pipe_fd = open(WAITDEBUG_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((WaitDebug_pipe_fd = open(WAITDEBUG_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO WAITDEBUG_PIPE_DEVICE);
     PLC_state |= PLC_STATE_WAITDEBUG_FILE_OPENED;
 
     /* create WaitPython_pipe */
-    if(rt_pipe_create(&WaitPython_pipe, "WaitPython_pipe", WAITPYTHON_PIPE_MINOR, PIPE_SIZE))
-        goto error;
+    if(rt_pipe_create(&WaitPython_pipe, "WaitPython_pipe", WAITPYTHON_PIPE_MINOR, PIPE_SIZE) < 0)
+        _startPLCLog(FO "WaitPython_pipe real-time end");
     PLC_state |= PLC_STATE_WAITPYTHON_PIPE_CREATED;
 
     /* open WaitPython_pipe*/
-    if((WaitPython_pipe_fd = open(WAITPYTHON_PIPE_DEVICE, O_RDWR)) == -1) goto error;
+    if((WaitPython_pipe_fd = open(WAITPYTHON_PIPE_DEVICE, O_RDWR)) == -1)
+        _startPLCLog(FO WAITPYTHON_PIPE_DEVICE);
     PLC_state |= PLC_STATE_WAITPYTHON_FILE_OPENED;
 
     /*** create PLC task ***/
-    if(rt_task_create(&PLC_task, "PLC_task", 0, 50, T_JOINABLE)) goto error;
+    if(rt_task_create(&PLC_task, "PLC_task", 0, 50, T_JOINABLE))
+        _startPLCLog("Failed creating PLC task");
     PLC_state |= PLC_STATE_TASK_CREATED;
 
     if(__init(argc,argv)) goto error;
 
     /* start PLC task */
-    if(rt_task_start(&PLC_task, &PLC_task_proc, NULL)) goto error;
+    if(rt_task_start(&PLC_task, &PLC_task_proc, NULL))
+        _startPLCLog("Failed starting PLC task");
 
     return 0;
 
--- a/targets/plc_main_head.c	Thu Apr 12 22:32:12 2018 +0200
+++ b/targets/plc_main_head.c	Thu Apr 12 22:32:43 2018 +0200
@@ -35,6 +35,9 @@
 /* Help to quit cleanly when init fail at a certain level */
 static int init_level = 0;
 
+/* Prototype for Logging to help spotting errors at init */
+int LogMessage(uint8_t level, char* buf, uint32_t size);
+
 /*
  * Prototypes of functions exported by plugins
  **/