Merged #2476, URI dialog fixes
authorEdouard Tisserant
Thu, 17 May 2018 09:33:58 +0200
changeset 2010 bb9c28bd204f
parent 2004 28af541d776b (diff)
parent 2009 00faf9279dc0 (current diff)
child 2011 64268e87613e
Merged #2476, URI dialog fixes
connectors/PYRO/__init__.py
--- a/Beremiz.py	Thu May 17 09:33:14 2018 +0200
+++ b/Beremiz.py	Thu May 17 09:33:58 2018 +0200
@@ -48,6 +48,7 @@
         self.splashPath = self.Bpath("images", "splash.png")
         self.modules = ["BeremizIDE"]
         self.debug = os.path.exists("BEREMIZ_DEBUG")
+        self.handle_exception = None
 
     def Bpath(self, *args):
         return os.path.join(self.app_dir, *args)
@@ -115,9 +116,13 @@
 
     def ShowSplashScreen(self):
         class Splash(AdvancedSplash):
+            Painted = False
+
             def OnPaint(_self, event):  # pylint: disable=no-self-argument
                 AdvancedSplash.OnPaint(_self, event)
-                wx.CallAfter(self.AppStart)
+                if not _self.Painted:  # trigger app start only once
+                    _self.Painted = True
+                    wx.CallAfter(self.AppStart)
         bmp = wx.Image(self.splashPath).ConvertToBitmap()
         self.splash = Splash(None,
                              bitmap=bmp,
@@ -198,10 +203,13 @@
             self.CreateUI()
             self.CloseSplash()
             self.ShowUI()
-        # except (KeyboardInterrupt, SystemExit):
-        #     raise
+        except (KeyboardInterrupt, SystemExit):
+            raise
         except Exception:
-            self.handle_exception(*sys.exc_info(), exit=True)
+            if self.handle_exception is not None:
+                self.handle_exception(*sys.exc_info(), exit=True)
+            else:
+                raise
 
     def MainLoop(self):
         self.app.MainLoop()
--- a/Beremiz_service.py	Thu May 17 09:33:14 2018 +0200
+++ b/Beremiz_service.py	Thu May 17 09:33:58 2018 +0200
@@ -30,12 +30,14 @@
 import sys
 import getopt
 import threading
-from threading import Thread, currentThread, Semaphore
+from threading import Thread, currentThread, Semaphore, Lock
 import traceback
 import __builtin__
+import Pyro
 import Pyro.core as pyro
 
-from runtime import PLCObject, ServicePublisher
+from runtime import PLCObject, ServicePublisher, MainWorker
+from runtime.xenomai import TryPreloadXenomai
 import util.paths as paths
 
 
@@ -132,6 +134,8 @@
 
 if __name__ == '__main__':
     __builtin__.__dict__['_'] = lambda x: x
+    # TODO: add a cmdline parameter if Trying Preloading Xenomai makes problem
+    TryPreloadXenomai()
 
 
 def Bpath(*args):
@@ -401,7 +405,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
@@ -411,22 +415,48 @@
         self.port = port
         self.workdir = workdir
         self.argv = argv
-        self.plcobj = None
         self.servicepublisher = None
-        self.autostart = autostart
         self.statuschange = statuschange
         self.evaluator = evaluator
         self.pyruntimevars = pyruntimevars
-
-    def Loop(self):
+        self.plcobj = PLCObject(self)
+
+    def _to_be_published(self):
+        return 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"
+
+    def PrintServerInfo(self):
+        print(_("Pyro port :"), self.port)
+
+        # Beremiz IDE detects LOCAL:// runtime is ready by looking
+        # for self.workdir in the daemon's stdout.
+        print(_("Current working directory :"), self.workdir)
+
+        if self._to_be_published():
+            print(_("Publishing service on local network"))
+
+        sys.stdout.flush()
+
+    def PyroLoop(self, when_ready):
         while self.continueloop:
+            Pyro.config.PYRO_MULTITHREADED = 0
             pyro.initServer()
             self.daemon = pyro.Daemon(host=self.ip_addr, port=self.port)
+
             # pyro never frees memory after connection close if no timeout set
             # taking too small timeout value may cause
             # unwanted diconnection when IDE is kept busy for long periods
             self.daemon.setTimeout(60)
-            self.Start()
+
+            self.daemon.connect(self.plcobj, "PLCObject")
+
+            if self._to_be_published():
+                self.servicepublisher = ServicePublisher.ServicePublisher()
+                self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port)
+
+            when_ready()
             self.daemon.requestLoop()
             self.daemon.sock.close()
 
@@ -440,39 +470,6 @@
             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 _stop(self):
         if self.plcobj is not None:
             self.plcobj.StopPLC()
@@ -481,6 +478,13 @@
             self.servicepublisher = None
         self.daemon.shutdown(True)
 
+    def AutoLoad(self):
+        self.plcobj.AutoLoad()
+        if self.plcobj.GetPLCstatus()[0] == "Stopped":
+            if autostart:
+                self.plcobj.StartPLC()
+        self.plcobj.StatusChange()
+
 
 if enabletwisted:
     import warnings
@@ -529,13 +533,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 +635,46 @@
         except Exception:
             LogMessageAndException(_("WAMP client startup failed. "))
 
+pyro_thread_started = Lock()
+pyro_thread_started.acquire()
+pyro_thread = Thread(target=pyroserver.PyroLoop,
+                     kwargs=dict(when_ready=pyro_thread_started.release))
+pyro_thread.start()
+
+# Wait for pyro thread to be effective
+pyro_thread_started.acquire()
+
+pyroserver.PrintServerInfo()
 
 if havetwisted or havewx:
-    pyro_thread = Thread(target=pyroserver.Loop)
-    pyro_thread.start()
-
+    ui_thread_started = Lock()
+    ui_thread_started.acquire()
     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()
+
+    # This order ui loop to unblock main thread when ready.
+    if havetwisted:
+        reactor.callLater(0, ui_thread_started.release)
+    else:
+        wx.CallAfter(ui_thread_started.release)
+
+    # Wait for ui thread to be effective
+    ui_thread_started.acquire()
+    print("UI thread started successfully.")
+
+try:
+    MainWorker.runloop(pyroserver.AutoLoad)
+except KeyboardInterrupt:
+    pass
+
 pyroserver.Quit()
 sys.exit(0)
--- a/CodeFileTreeNode.py	Thu May 17 09:33:14 2018 +0200
+++ b/CodeFileTreeNode.py	Thu May 17 09:33:58 2018 +0200
@@ -139,11 +139,11 @@
     def GetDataTypes(self, basetypes=False):
         return self.GetCTRoot().GetDataTypes(basetypes=basetypes)
 
-    def GenerateNewName(self, format, start_idx):
+    def GenerateNewName(self, name, format):
         return self.GetCTRoot().GenerateNewName(
-            None, None, format, start_idx,
-            dict([(var.getname().upper(), True)
-                  for var in self.CodeFile.variables.getvariable()]))
+            None, name, format,
+            exclude=dict([(var.getname().upper(), True)
+                          for var in self.CodeFile.variables.getvariable()]))
 
     def SetVariables(self, variables):
         self.CodeFile.variables.setvariable([])
--- a/ConfigTreeNode.py	Thu May 17 09:33:14 2018 +0200
+++ b/ConfigTreeNode.py	Thu May 17 09:33:58 2018 +0200
@@ -274,15 +274,14 @@
             LocationCFilesAndCFLAGS = []
 
         # confnode asks for some LDFLAGS
-        if CTNLDFLAGS:
+        LDFLAGS = []
+        if CTNLDFLAGS is not None:
             # LDFLAGS can be either string
-            if isinstance(CTNLDFLAGS, str):
-                LDFLAGS = [CTNLDFLAGS]
+            if isinstance(CTNLDFLAGS, str) or isinstance(CTNLDFLAGS, unicode):
+                LDFLAGS += [CTNLDFLAGS]
             # or list of strings
             elif isinstance(CTNLDFLAGS, list):
-                LDFLAGS = CTNLDFLAGS[:]
-        else:
-            LDFLAGS = []
+                LDFLAGS += CTNLDFLAGS
 
         # recurse through all children, and stack their results
         for CTNChild in self.IECSortedChildren():
@@ -500,7 +499,10 @@
         # Call the OnCloseMethod
         CTNInstance.OnCTNClose()
         # Delete confnode dir
-        shutil.rmtree(CTNInstance.CTNPath())
+        try:
+            shutil.rmtree(CTNInstance.CTNPath())
+        except:
+            pass
         # Remove child of Children
         self.Children[CTNInstance.CTNType].remove(CTNInstance)
         if len(self.Children[CTNInstance.CTNType]) == 0:
--- a/IDEFrame.py	Thu May 17 09:33:14 2018 +0200
+++ b/IDEFrame.py	Thu May 17 09:33:58 2018 +0200
@@ -1537,6 +1537,7 @@
         self.ProjectTree.SetItemText(root, item_name)
         self.ProjectTree.SetPyData(root, infos)
         highlight_colours = self.Highlights.get(infos.get("tagname", None), (wx.Colour(255, 255, 255, 0), wx.BLACK))
+        self.ProjectTree.SetItemBackgroundColour(root, highlight_colours[0])
         self.ProjectTree.SetItemTextColour(root, highlight_colours[1])
         self.ProjectTree.SetItemExtraImage(root, None)
         if infos["type"] == ITEM_POU:
--- a/PLCControler.py	Thu May 17 09:33:14 2018 +0200
+++ b/PLCControler.py	Thu May 17 09:33:58 2018 +0200
@@ -35,7 +35,6 @@
 import util.paths as paths
 from plcopen import *
 from plcopen.types_enums import *
-from plcopen.XSLTModelQuery import _StringValue, _BoolValue, _translate_args
 from plcopen.InstancesPathCollector import InstancesPathCollector
 from plcopen.POUVariablesCollector import POUVariablesCollector
 from plcopen.InstanceTagnameCollector import InstanceTagnameCollector
@@ -45,11 +44,10 @@
 from PLCGenerator import *
 
 duration_model = re.compile("(?:([0-9]{1,2})h)?(?:([0-9]{1,2})m(?!s))?(?:([0-9]{1,2})s)?(?:([0-9]{1,3}(?:\.[0-9]*)?)ms)?")
+VARIABLE_NAME_SUFFIX_MODEL = re.compile('(\d+)$')
 
 ScriptDirectory = paths.AbsDir(__file__)
 
-
-
 # Length of the buffer
 UNDO_BUFFER_LENGTH = 20
 
@@ -1817,6 +1815,14 @@
         return text
 
     def GenerateNewName(self, tagname, name, format, start_idx=0, exclude=None, debug=False):
+        if name is not None:
+            result = re.search(VARIABLE_NAME_SUFFIX_MODEL, name)
+            if result is not None:
+                format = name[:result.start(1)] + '%d'
+                start_idx = int(result.group(1))
+            else:
+                format = name + '%d'
+
         names = {} if exclude is None else exclude.copy()
         if tagname is not None:
             names.update(dict([(varname.upper(), True)
@@ -1832,6 +1838,14 @@
                                  PLCOpenParser.GetElementClass("connector",    "commonObjects"),
                                  PLCOpenParser.GetElementClass("continuation", "commonObjects"))):
                             names[instance.getname().upper()] = True
+            elif words[0] == 'R':
+                element = self.GetEditedElement(tagname, debug)
+                for task in element.gettask():
+                    names[task.getname().upper()] = True
+                    for instance in task.getpouInstance():
+                        names[instance.getname().upper()] = True
+                for instance in element.getpouInstance():
+                    names[instance.getname().upper()] = True
         else:
             project = self.GetProject(debug)
             if project is not None:
--- a/ProjectController.py	Thu May 17 09:33:14 2018 +0200
+++ b/ProjectController.py	Thu May 17 09:33:58 2018 +0200
@@ -37,7 +37,7 @@
 import re
 import tempfile
 from types import ListType
-from threading import Timer, Lock, Thread
+from threading import Timer
 from datetime import datetime
 from weakref import WeakKeyDictionary
 from itertools import izip
@@ -242,7 +242,6 @@
 
         # Setup debug information
         self.IECdebug_datas = {}
-        self.IECdebug_lock = Lock()
 
         self.DebugTimer = None
         self.ResetIECProgramsAndVariables()
@@ -258,7 +257,6 @@
         # After __init__ root confnode is not valid
         self.ProjectPath = None
         self._setBuildPath(None)
-        self.DebugThread = None
         self.debug_break = False
         self.previous_plcstate = None
         # copy ConfNodeMethods so that it can be later customized
@@ -1420,9 +1418,32 @@
         self.UpdateMethodsFromPLCStatus()
 
     def SnapshotAndResetDebugValuesBuffers(self):
+        if self._connector is not None:
+            plc_status, Traces = self._connector.GetTraceVariables()
+            # print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()]
+            if plc_status == "Started":
+                if len(Traces) > 0:
+                    for debug_tick, debug_buff in Traces:
+                        debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes)
+                        if debug_vars is not None and len(debug_vars) == len(self.TracedIECPath):
+                            for IECPath, values_buffer, value in izip(
+                                    self.TracedIECPath,
+                                    self.DebugValuesBuffers,
+                                    debug_vars):
+                                IECdebug_data = self.IECdebug_datas.get(IECPath, None)
+                                if IECdebug_data is not None and value is not None:
+                                    forced = IECdebug_data[2:4] == ["Forced", value]
+                                    if not IECdebug_data[4] and len(values_buffer) > 0:
+                                        values_buffer[-1] = (value, forced)
+                                    else:
+                                        values_buffer.append((value, forced))
+                            self.DebugTicks.append(debug_tick)
+
         buffers, self.DebugValuesBuffers = (self.DebugValuesBuffers,
                                             [list() for dummy in xrange(len(self.TracedIECPath))])
+
         ticks, self.DebugTicks = self.DebugTicks, []
+
         return ticks, buffers
 
     def RegisterDebugVarToConnector(self):
@@ -1431,7 +1452,6 @@
         self.TracedIECPath = []
         self.TracedIECTypes = []
         if self._connector is not None:
-            self.IECdebug_lock.acquire()
             IECPathsToPop = []
             for IECPath, data_tuple in self.IECdebug_datas.iteritems():
                 WeakCallableDict, _data_log, _status, fvalue, _buffer_list = data_tuple
@@ -1462,7 +1482,6 @@
                 self.TracedIECPath = []
                 self._connector.SetTraceVariablesList([])
             self.SnapshotAndResetDebugValuesBuffers()
-            self.IECdebug_lock.release()
 
     def IsPLCStarted(self):
         return self.previous_plcstate == "Started"
@@ -1494,7 +1513,6 @@
         if IECPath != "__tick__" and IECPath not in self._IECPathToIdx:
             return None
 
-        self.IECdebug_lock.acquire()
         # If no entry exist, create a new one with a fresh WeakKeyDictionary
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         if IECdebug_data is None:
@@ -1510,14 +1528,11 @@
 
         IECdebug_data[0][callableobj] = buffer_list
 
-        self.IECdebug_lock.release()
-
         self.ReArmDebugRegisterTimer()
 
         return IECdebug_data[1]
 
     def UnsubscribeDebugIECVariable(self, IECPath, callableobj):
-        self.IECdebug_lock.acquire()
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         if IECdebug_data is not None:
             IECdebug_data[0].pop(callableobj, None)
@@ -1528,14 +1543,11 @@
                     lambda x, y: x | y,
                     IECdebug_data[0].itervalues(),
                     False)
-        self.IECdebug_lock.release()
 
         self.ReArmDebugRegisterTimer()
 
     def UnsubscribeAllDebugIECVariable(self):
-        self.IECdebug_lock.acquire()
         self.IECdebug_datas = {}
-        self.IECdebug_lock.release()
 
         self.ReArmDebugRegisterTimer()
 
@@ -1543,30 +1555,22 @@
         if IECPath not in self.IECdebug_datas:
             return
 
-        self.IECdebug_lock.acquire()
-
         # If no entry exist, create a new one with a fresh WeakKeyDictionary
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         IECdebug_data[2] = "Forced"
         IECdebug_data[3] = fvalue
 
-        self.IECdebug_lock.release()
-
         self.ReArmDebugRegisterTimer()
 
     def ReleaseDebugIECVariable(self, IECPath):
         if IECPath not in self.IECdebug_datas:
             return
 
-        self.IECdebug_lock.acquire()
-
         # If no entry exist, create a new one with a fresh WeakKeyDictionary
         IECdebug_data = self.IECdebug_datas.get(IECPath, None)
         IECdebug_data[2] = "Registered"
         IECdebug_data[3] = None
 
-        self.IECdebug_lock.release()
-
         self.ReArmDebugRegisterTimer()
 
     def CallWeakcallables(self, IECPath, function_name, *cargs):
@@ -1590,51 +1594,8 @@
             return -1, "No runtime connected!"
         return self._connector.RemoteExec(script, **kwargs)
 
-    def DebugThreadProc(self):
-        """
-        This thread waid PLC debug data, and dispatch them to subscribers
-        """
-        self.debug_break = False
-        debug_getvar_retry = 0
-        while (not self.debug_break) and (self._connector is not None):
-            plc_status, Traces = self._connector.GetTraceVariables()
-            debug_getvar_retry += 1
-            # print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()]
-            if plc_status == "Started":
-                if len(Traces) > 0:
-                    self.IECdebug_lock.acquire()
-                    for debug_tick, debug_buff in Traces:
-                        debug_vars = UnpackDebugBuffer(debug_buff, self.TracedIECTypes)
-                        if debug_vars is not None and len(debug_vars) == len(self.TracedIECPath):
-                            for IECPath, values_buffer, value in izip(
-                                    self.TracedIECPath,
-                                    self.DebugValuesBuffers,
-                                    debug_vars):
-                                IECdebug_data = self.IECdebug_datas.get(IECPath, None)  # FIXME get
-                                if IECdebug_data is not None and value is not None:
-                                    forced = IECdebug_data[2:4] == ["Forced", value]
-                                    if not IECdebug_data[4] and len(values_buffer) > 0:
-                                        values_buffer[-1] = (value, forced)
-                                    else:
-                                        values_buffer.append((value, forced))
-                            self.DebugTicks.append(debug_tick)
-                            debug_getvar_retry = 0
-                    self.IECdebug_lock.release()
-
-                if debug_getvar_retry != 0:
-                    # Be patient, tollerate PLC to come with fresh samples
-                    time.sleep(0.1)
-            else:
-                self.debug_break = True
-        self.logger.write(_("Debugger disabled\n"))
-        self.DebugThread = None
-        if self.DispatchDebugValuesTimer is not None:
-            self.DispatchDebugValuesTimer.Stop()
-
     def DispatchDebugValuesProc(self, event):
-        self.IECdebug_lock.acquire()
         debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
-        self.IECdebug_lock.release()
         start_time = time.time()
         if len(self.TracedIECPath) == len(buffers):
             for IECPath, values in izip(self.TracedIECPath, buffers):
@@ -1645,22 +1606,12 @@
 
         delay = time.time() - start_time
         next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay)
-        if self.DispatchDebugValuesTimer is not None and self.DebugThread is not None:
+        if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Start(
                 int(next_refresh * 1000), oneShot=True)
         event.Skip()
 
     def KillDebugThread(self):
-        tmp_debugthread = self.DebugThread
-        self.debug_break = True
-        if tmp_debugthread is not None:
-            self.logger.writeyield(_("Stopping debugger...\n"))
-            tmp_debugthread.join(timeout=5)
-            if tmp_debugthread.isAlive() and self.logger:
-                self.logger.write_warning(_("Couldn't stop debugger.\n"))
-            else:
-                self.logger.write(_("Debugger stopped.\n"))
-        self.DebugThread = None
         if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Stop()
 
@@ -1672,9 +1623,6 @@
         if self.DispatchDebugValuesTimer is not None:
             self.DispatchDebugValuesTimer.Start(
                 int(REFRESH_PERIOD * 1000), oneShot=True)
-        if self.DebugThread is None:
-            self.DebugThread = Thread(target=self.DebugThreadProc)
-            self.DebugThread.start()
 
     def _Run(self):
         """
--- a/canfestival/canfestival.py	Thu May 17 09:33:14 2018 +0200
+++ b/canfestival/canfestival.py	Thu May 17 09:33:58 2018 +0200
@@ -38,20 +38,16 @@
     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)  # noqa
+CanFestivalPath = os.path.join(base_folder, "CanFestival-3")  # noqa
+sys.path.append(os.path.join(CanFestivalPath, "objdictgen"))  # noqa
+
+# pylint: disable=wrong-import-position
+from nodelist import NodeList
 from nodemanager import NodeManager
 import gen_cfile
 import eds_utils
 import canfestival_config as local_canfestival_config  # pylint: disable=import-error
-
 from commondialogs import CreateNodeDialog
 from subindextable import IECTypeConversion, SizeConversion
 from canfestival import config_utils
--- a/connectors/PYRO/__init__.py	Thu May 17 09:33:14 2018 +0200
+++ b/connectors/PYRO/__init__.py	Thu May 17 09:33:58 2018 +0200
@@ -139,67 +139,24 @@
         confnodesroot.logger.write_error(_("Cannot get PLC status - connection failed.\n"))
         return None
 
+    _special_return_funcs = {
+        "StartPLC": False,
+        "GetTraceVariables": ("Broken", None),
+        "GetPLCstatus": ("Broken", None),
+        "RemoteExec": (-1, "RemoteExec script failed!")
+    }
+
     class PyroProxyProxy(object):
         """
         A proxy proxy class to handle Beremiz Pyro interface specific behavior.
         And to put Pyro exception catcher in between caller and Pyro proxy
         """
-        def __init__(self):
-            # 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)
-        StartPLC = PyroCatcher(_PyroStartPLC, False)
-
-        def _PyroGetTraceVariables(self):
-            """
-            for safe use in from debug thread, must use the copy
-            """
-            if self.RemotePLCObjectProxyCopy is None:
-                self.RemotePLCObjectProxyCopy = copy.copy(confnodesroot._connector.GetPyroProxy())
-            return self.RemotePLCObjectProxyCopy.GetTraceVariables()
-        GetTraceVariables = PyroCatcher(_PyroGetTraceVariables, ("Broken", None))
-
-        def _PyroGetPLCstatus(self):
-            return RemotePLCObjectProxy.GetPLCstatus()
-        GetPLCstatus = PyroCatcher(_PyroGetPLCstatus, ("Broken", None))
-
-        def _PyroRemoteExec(self, script, **kwargs):
-            return RemotePLCObjectProxy.RemoteExec(script, **kwargs)
-        RemoteExec = PyroCatcher(_PyroRemoteExec, (-1, "RemoteExec script failed!"))
-
         def __getattr__(self, attrName):
             member = self.__dict__.get(attrName, None)
             if member is None:
                 def my_local_func(*args, **kwargs):
                     return RemotePLCObjectProxy.__getattr__(attrName)(*args, **kwargs)
-                member = PyroCatcher(my_local_func, None)
+                member = PyroCatcher(my_local_func, _special_return_funcs.get(attrName, None))
                 self.__dict__[attrName] = member
             return member
 
--- a/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Thu May 17 09:33:14 2018 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Thu May 17 09:33:58 2018 +0200
@@ -48,7 +48,7 @@
 # Canvas height
 [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = [0, 100, 200]
 
-CANVAS_BORDER = (20., 10.)  # Border height on at bottom and top of graph
+CANVAS_BORDER = (30., 20.)  # Border height on at bottom and top of graph
 CANVAS_PADDING = 8.5        # Border inside graph where no label is drawn
 VALUE_LABEL_HEIGHT = 17.    # Height of variable label in graph
 AXES_LABEL_HEIGHT = 12.75   # Height of variable value in graph
--- a/controls/DebugVariablePanel/DebugVariableItem.py	Thu May 17 09:33:14 2018 +0200
+++ b/controls/DebugVariablePanel/DebugVariableItem.py	Thu May 17 09:33:58 2018 +0200
@@ -24,6 +24,7 @@
 
 
 from __future__ import absolute_import
+from datetime import timedelta
 import binascii
 import numpy
 from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR
@@ -219,18 +220,20 @@
 
         else:
             self.Data = None
-
+            self.MinValue = None
+            self.MaxValue = None
         # Init variable value
         self.Value = ""
 
     def IsNumVariable(self):
         """
         Return if variable data type is numeric. String variables are
-        considered as numeric (string CRC)
+        considered as numeric (string CRC). Time variables are considered
+        as number of seconds
         @return: True if data type is numeric
         """
         return (self.Parent.IsNumType(self.VariableType) or
-                self.VariableType in ["STRING", "WSTRING"])
+                self.VariableType in ["STRING", "WSTRING", "TIME", "TOD", "DT", "DATE"])
 
     def NewValues(self, ticks, values):
         """
@@ -253,10 +256,15 @@
                 # Translate forced flag to float for storing in Data table
                 forced_value = float(forced)
 
-                # String data value is CRC
-                num_value = (binascii.crc32(value) & STRING_CRC_MASK
-                             if self.VariableType in ["STRING", "WSTRING"]
-                             else float(value))
+                if self.VariableType in ["STRING", "WSTRING"]:
+                    # String data value is CRC
+                    num_value = (binascii.crc32(value) & STRING_CRC_MASK)
+                elif self.VariableType in ["TIME", "TOD", "DT", "DATE"]:
+                    # Numeric value of time type variables
+                    # is represented in seconds
+                    num_value = float(value.total_seconds())
+                else:
+                    num_value = float(value)
 
                 # Update variable range values
                 self.MinValue = (min(self.MinValue, num_value)
@@ -341,6 +349,9 @@
                 if self.VariableType in ["STRING", "WSTRING"] \
                 else self.Data[idx, 1:3]
 
+            if self.VariableType in ["TIME", "TOD", "DT", "DATE"]:
+                value = timedelta(seconds=value)
+
             # Get raw value if asked
             if not raw:
                 value = TYPE_TRANSLATOR.get(
--- a/controls/DebugVariablePanel/DebugVariablePanel.py	Thu May 17 09:33:14 2018 +0200
+++ b/controls/DebugVariablePanel/DebugVariablePanel.py	Thu May 17 09:33:58 2018 +0200
@@ -237,7 +237,7 @@
         default_range_idx = 0
         for idx, (text, _value) in enumerate(RANGE_VALUES):
             self.CanvasRange.Append(text)
-            if text == "1s":
+            if _value == 1000000000:
                 default_range_idx = idx
         self.CanvasRange.SetSelection(default_range_idx)
 
--- a/controls/LogViewer.py	Thu May 17 09:33:14 2018 +0200
+++ b/controls/LogViewer.py	Thu May 17 09:33:58 2018 +0200
@@ -396,7 +396,7 @@
         self.HasNewData = False
 
     def SetLogSource(self, log_source):
-        self.LogSource = proxy(log_source) if log_source else None
+        self.LogSource = proxy(log_source) if log_source is not None else None
         self.CleanButton.Enable(self.LogSource is not None)
         if log_source is not None:
             self.ResetLogMessages()
--- a/controls/ProjectPropertiesPanel.py	Thu May 17 09:33:14 2018 +0200
+++ b/controls/ProjectPropertiesPanel.py	Thu May 17 09:33:58 2018 +0200
@@ -27,6 +27,8 @@
 import wx
 from wx.lib.scrolledpanel import ScrolledPanel
 
+from xmlclass.xmlclass import URI_model
+
 # -------------------------------------------------------------------------------
 #                                 Helpers
 # -------------------------------------------------------------------------------
@@ -294,8 +296,16 @@
             if self.Controller is not None and self.Values is not None:
                 old_value = self.Values.get(name)
                 new_value = textctrl.GetValue()
-                if name not in REQUIRED_PARAMS and new_value == "":
+                if name in REQUIRED_PARAMS and new_value == "":
                     new_value = None
+                if name == 'companyURL':
+                    if not URI_model.match(new_value):
+                        new_value = None
+                        dialog = wx.MessageDialog(self, _('Invalid URL!\n'
+                                                          'Please enter correct URL address.'),
+                                                  _("Error"), wx.OK | wx.ICON_ERROR)
+                        dialog.ShowModal()
+                        dialog.Destroy()
                 if old_value != new_value:
                     self.Controller.SetProjectProperties(properties={name: new_value})
                     self.ParentWindow._Refresh(TITLE, FILEMENU, EDITMENU,
--- a/controls/VariablePanel.py	Thu May 17 09:33:14 2018 +0200
+++ b/controls/VariablePanel.py	Thu May 17 09:33:58 2018 +0200
@@ -105,7 +105,6 @@
 }
 
 LOCATION_MODEL = re.compile("((?:%[IQM](?:\*|(?:[XBWLD]?[0-9]+(?:\.[0-9]+)*)))?)$")
-VARIABLE_NAME_SUFFIX_MODEL = re.compile("([0-9]*)$")
 
 
 # -------------------------------------------------------------------------------
@@ -514,7 +513,7 @@
         self.FilterChoices = []
         self.FilterChoiceTransfer = GetFilterChoiceTransfer()
 
-        self.DefaultValue = _VariableInfos("", "", "", "", "", True, "", DefaultType, ([], []), 0)
+        self.DefaultValue = _VariableInfos("LocalVar0", "", "", "", "", True, "", DefaultType, ([], []), 0)
 
         if element_type in ["config", "resource"]:
             self.DefaultTypes = {"All": "Global"}
@@ -590,36 +589,16 @@
         self.VariablesGrid.SetEditable(not self.Debug)
 
         def _AddVariable(new_row):
+            row_content = self.DefaultValue.copy()
             if new_row > 0:
-                row_content = self.Values[new_row - 1].copy()
-
-                result = VARIABLE_NAME_SUFFIX_MODEL.search(row_content.Name)
-                if result is not None:
-                    name = row_content.Name[:result.start(1)]
-                    suffix = result.group(1)
-                    if suffix != "":
-                        start_idx = int(suffix)
-                    else:
-                        start_idx = 0
-                else:
-                    name = row_content.Name
-                    start_idx = 0
-            else:
-                row_content = None
-                start_idx = 0
-                name = "LocalVar"
-
-            if row_content is not None and row_content.Edit:
-                row_content = self.Values[new_row - 1].copy()
-            else:
-                row_content = self.DefaultValue.copy()
-                if self.Filter in self.DefaultTypes:
-                    row_content.Class = self.DefaultTypes[self.Filter]
-                else:
-                    row_content.Class = self.Filter
-
-            row_content.Name = self.Controler.GenerateNewName(
-                self.TagName, None, name + "%d", start_idx)
+                # doesn't copy values of previous var if it's non-editable (like a FB)
+                if self.Values[new_row-1].Edit:
+                    row_content = self.Values[new_row-1].copy()
+                old_name = self.Values[new_row-1].Name
+                row_content.Name = self.Controler.GenerateNewName(
+                    self.TagName, old_name, old_name+'%d')
+            if not row_content.Class:
+                row_content.Class = self.DefaultTypes.get(self.Filter, self.Filter)
 
             if self.Filter == "All" and len(self.Values) > 0:
                 self.Values.insert(new_row, row_content)
--- a/editors/CodeFileEditor.py	Thu May 17 09:33:14 2018 +0200
+++ b/editors/CodeFileEditor.py	Thu May 17 09:33:58 2018 +0200
@@ -35,7 +35,6 @@
 from plcopen.structures import TestIdentifier, IEC_KEYWORDS, DefaultType
 from controls import CustomGrid, CustomTable
 from controls.CustomStyledTextCtrl import CustomStyledTextCtrl, faces, GetCursorPos, NAVIGATION_KEYS
-from controls.VariablePanel import VARIABLE_NAME_SUFFIX_MODEL
 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
 from util.BitmapLibrary import GetBitmap
 from util.TranslationCatalogs import NoTranslate
@@ -674,7 +673,7 @@
         self.Controler = controler
 
         self.VariablesDefaultValue = {
-            "Name":        "",
+            "Name":        "LocalVar0",
             "Type":        DefaultType,
             "Initial":     "",
             "Description": "",
@@ -694,19 +693,9 @@
         def _AddVariable(new_row):
             if new_row > 0:
                 row_content = self.Table.data[new_row - 1].copy()
-                result = VARIABLE_NAME_SUFFIX_MODEL.search(row_content["Name"])
-                if result is not None:
-                    name = row_content["Name"][:result.start(1)]
-                    suffix = result.group(1)
-                    if suffix != "":
-                        start_idx = int(suffix)
-                    else:
-                        start_idx = 0
-                else:
-                    name = row_content["Name"]
-                    start_idx = 0
-                row_content["Name"] = self.Controler.GenerateNewName(
-                    name + "%d", start_idx)
+                old_name = row_content['Name']
+                row_content['Name'] =\
+                    self.Controler.GenerateNewName(old_name, old_name+'%d')
             else:
                 row_content = self.VariablesDefaultValue.copy()
             self.Table.InsertRow(new_row, row_content)
--- a/editors/ResourceEditor.py	Thu May 17 09:33:14 2018 +0200
+++ b/editors/ResourceEditor.py	Thu May 17 09:33:58 2018 +0200
@@ -303,7 +303,8 @@
         self.RefreshHighlightsTimer = wx.Timer(self, -1)
         self.Bind(wx.EVT_TIMER, self.OnRefreshHighlightsTimer, self.RefreshHighlightsTimer)
 
-        self.TasksDefaultValue = {"Name": "", "Triggering": "", "Single": "", "Interval": "", "Priority": 0}
+        self.TasksDefaultValue = {"Name": "task0", "Triggering": "Cyclic",
+                                  "Single": "", "Interval": "T#20ms", "Priority": 0}
         self.TasksTable = ResourceTable(self, [], GetTasksTableColnames())
         self.TasksTable.SetColAlignements([wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, wx.ALIGN_RIGHT])
         self.TasksTable.SetColSizes([200, 100, 100, 150, 100])
@@ -314,7 +315,15 @@
                                    "Down": self.DownTaskButton})
 
         def _AddTask(new_row):
-            self.TasksTable.InsertRow(new_row, self.TasksDefaultValue.copy())
+            if new_row > 0:
+                row_content = self.TasksTable.data[new_row-1].copy()
+                old_name = row_content['Name']
+                row_content['Name'] =\
+                    self.Controler.GenerateNewName(self.TagName, old_name, old_name+'%d')
+            else:
+                row_content = self.TasksDefaultValue.copy()
+
+            self.TasksTable.InsertRow(new_row, row_content)
             self.RefreshModel()
             self.RefreshView()
             return new_row
@@ -338,7 +347,7 @@
         self.TasksTable.ResetView(self.TasksGrid)
         self.TasksGrid.RefreshButtons()
 
-        self.InstancesDefaultValue = {"Name": "", "Type": "", "Task": ""}
+        self.InstancesDefaultValue = {"Name": "instance0", "Type": "", "Task": ""}
         self.InstancesTable = ResourceTable(self, [], GetInstancesTableColnames())
         self.InstancesTable.SetColAlignements([wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT])
         self.InstancesTable.SetColSizes([200, 150, 150])
@@ -349,7 +358,15 @@
                                        "Down": self.DownInstanceButton})
 
         def _AddInstance(new_row):
-            self.InstancesTable.InsertRow(new_row, self.InstancesDefaultValue.copy())
+            if new_row > 0:
+                row_content = self.InstancesTable.data[new_row - 1].copy()
+                old_name = row_content['Name']
+                row_content['Name'] =\
+                    self.Controler.GenerateNewName(self.TagName, old_name, old_name+'%d')
+            else:
+                row_content = self.InstancesDefaultValue.copy()
+
+            self.InstancesTable.InsertRow(new_row, row_content)
             self.RefreshModel()
             self.RefreshView()
             return new_row
--- a/plcopen/BlockInstanceCollector.py	Thu May 17 09:33:14 2018 +0200
+++ b/plcopen/BlockInstanceCollector.py	Thu May 17 09:33:58 2018 +0200
@@ -4,8 +4,8 @@
 # See COPYING file for copyrights details.
 
 from __future__ import absolute_import
+from collections import OrderedDict, namedtuple
 from plcopen.XSLTModelQuery import XSLTModelQuery, _StringValue, _BoolValue, _translate_args
-from collections import OrderedDict, namedtuple
 
 # -------------------------------------------------------------------------------
 #           Helpers object for generating pou block instances list
--- a/plcopen/plcopen.py	Thu May 17 09:33:14 2018 +0200
+++ b/plcopen/plcopen.py	Thu May 17 09:33:58 2018 +0200
@@ -152,12 +152,12 @@
     test_result = []
     result = criteria["pattern"].search(text)
     while result is not None:
-        prev_pos = result.endpos
+        prev_pos = result.span()[1]
         start = TextLenInRowColumn(text[:result.start()])
         end = TextLenInRowColumn(text[:result.end() - 1])
         test_result.append((start, end, "\n".join(lines[start[0]:end[0] + 1])))
         result = criteria["pattern"].search(text, result.end())
-        if result is not None and prev_pos == result.endpos:
+        if result is not None and prev_pos == result.end():
             break
     return test_result
 
@@ -441,7 +441,7 @@
                     "authorName": contentheader_obj.setauthor,
                     "pageSize": lambda v: contentheader_obj.setpageSize(*v),
                     "scaling": contentheader_obj.setscaling}.get(attr)
-            if func is not None:
+            if func is not None and value is not None:
                 func(value)
             elif attr in ["modificationDateTime", "organization", "language"]:
                 setattr(contentheader_obj, attr, value)
--- a/runtime/PLCObject.py	Thu May 17 09:33:14 2018 +0200
+++ b/runtime/PLCObject.py	Thu May 17 09:33:58 2018 +0200
@@ -23,7 +23,8 @@
 
 
 from __future__ import absolute_import
-from threading import Timer, Thread, Lock, Semaphore, Event
+import thread
+from threading import Thread, Lock, Semaphore, Event, Condition
 import ctypes
 import os
 import sys
@@ -60,31 +61,139 @@
     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.free = Condition(self.mutex)
+        self.job = None
+
+    def runloop(self, *args, **kwargs):
+        """
+        meant to be called by worker thread (blocking)
+        """
+        self._threadID = thread.get_ident()
+        if args or kwargs:
+            job(*args, **kwargs).do()
+            # result is ignored
+        self.mutex.acquire()
+        while not self._finish:
+            self.todo.wait()
+            if self.job is not None:
+                self.job.do()
+                self.done.notify()
+            else:
+                self.free.notify()
+        self.mutex.release()
+
+    def call(self, *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() or self._threadID is None:
+            # 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.free.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 = []
 
+    # First task of worker -> no @RunInMain
     def AutoLoad(self):
-        # Get the last transfered PLC if connector must be restart
+        # Get the last transfered PLC
         try:
             self.CurrentPLCFilename = open(
                 self._GetMD5FileName(),
@@ -100,6 +209,7 @@
             for callee in self.statuschange:
                 callee(self.PLCStatus)
 
+    @RunInMain
     def LogMessage(self, *args):
         if len(args) == 2:
             level, msg = args
@@ -111,16 +221,19 @@
             return self._LogMessage(level, msg, len(msg))
         return None
 
+    @RunInMain
     def ResetLogCount(self):
         if self._ResetLogCount is not None:
             self._ResetLogCount()
 
+    # used internaly
     def GetLogCount(self, level):
         if self._GetLogCount is not None:
             return int(self._GetLogCount(level))
         elif self._loading_error is not None and level == 0:
             return 1
 
+    @RunInMain
     def GetLogMessage(self, level, msgid):
         tick = ctypes.c_uint32()
         tv_sec = ctypes.c_uint32()
@@ -145,12 +258,13 @@
     def _GetLibFileName(self):
         return os.path.join(self.workingdir, self.CurrentPLCFilename)
 
-    def LoadPLC(self):
+    def _LoadPLC(self):
         """
         Load PLC library
         Declare all functions, arguments and return values
         """
         md5 = open(self._GetMD5FileName(), "r").read()
+        self.PLClibraryLock.acquire()
         try:
             self._PLClibraryHandle = dlopen(self._GetLibFileName())
             self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle)
@@ -227,25 +341,34 @@
 
             self._loading_error = None
 
-            self.PythonRuntimeInit()
-
-            return True
         except Exception:
             self._loading_error = traceback.format_exc()
             PLCprint(self._loading_error)
             return False
-
+        finally:
+            self.PLClibraryLock.release()
+
+        return True
+
+    @RunInMain
+    def LoadPLC(self):
+        res = self._LoadPLC()
+        if res:
+            self.PythonRuntimeInit()
+        else:
+            self._FreePLC()
+
+        return res
+
+    @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,13 +382,26 @@
         self._GetLogCount = None
         self._LogMessage = None
         self._GetLogMessage = None
+        self._PLClibraryHandle = None
         self.PLClibraryHandle = None
-        # Unload library explicitely
-        if getattr(self, "_PLClibraryHandle", None) is not None:
-            dlclose(self._PLClibraryHandle)
-            self._PLClibraryHandle = None
-
-        self.PLClibraryLock.release()
+
+    def _FreePLC(self):
+        """
+        Unload PLC library.
+        This is also called by __init__ to create dummy C func proxies
+        """
+        self.PLClibraryLock.acquire()
+        try:
+            # Unload library explicitely
+            if getattr(self, "_PLClibraryHandle", None) is not None:
+                dlclose(self._PLClibraryHandle)
+
+            # Forget all refs to library
+            self._InitPLCStubCalls()
+
+        finally:
+            self.PLClibraryLock.release()
+
         return False
 
     def PythonRuntimeCall(self, methodname):
@@ -278,6 +414,7 @@
             if exp is not None:
                 self.LogMessage(0, '\n'.join(traceback.format_exception(*exp)))
 
+    # used internaly
     def PythonRuntimeInit(self):
         MethodNames = ["init", "start", "stop", "cleanup"]
         self.python_runtime_vars = globals().copy()
@@ -329,6 +466,7 @@
 
         self.PythonRuntimeCall("init")
 
+    # used internaly
     def PythonRuntimeCleanup(self):
         if self.python_runtime_vars is not None:
             self.PythonRuntimeCall("cleanup")
@@ -340,10 +478,8 @@
         res, cmd, blkid = "None", "None", ctypes.c_void_p()
         compile_cache = {}
         while True:
-            # print "_PythonIterator(", res, ")",
             cmd = self._PythonIterator(res, blkid)
             FBID = blkid.value
-            # print " -> ", cmd, blkid
             if cmd is None:
                 break
             try:
@@ -364,6 +500,7 @@
                 res = "#EXCEPTION : "+str(e)
                 self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (FBID, cmd, str(e)))
 
+    @RunInMain
     def StartPLC(self):
         if self.CurrentPLCFilename is not None and self.PLCStatus == "Stopped":
             c_argv = ctypes.c_char_p * len(self.argv)
@@ -382,6 +519,7 @@
                 self.PLCStatus = "Broken"
                 self.StatusChange()
 
+    @RunInMain
     def StopPLC(self):
         if self.PLCStatus == "Started":
             self.LogMessage("PLC stopped")
@@ -391,40 +529,38 @@
             self.StatusChange()
             self.PythonRuntimeCall("stop")
             if self.TraceThread is not None:
-                self.TraceWakeup.set()
                 self.TraceThread.join()
                 self.TraceThread = None
             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
-
+    @RunInMain
     def GetPLCstatus(self):
         return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
 
+    @RunInMain
     def NewPLC(self, md5sum, data, extrafiles):
         if self.PLCStatus in ["Stopped", "Empty", "Broken"]:
             NewFileName = md5sum + lib_ext
             extra_files_log = os.path.join(self.workingdir, "extra_files.txt")
 
-            self.UnLoadPLC()
+            old_PLC_filename = os.path.join(self.workingdir, self.CurrentPLCFilename) \
+                if self.CurrentPLCFilename is not None \
+                else None
+            new_PLC_filename = os.path.join(self.workingdir, NewFileName)
+
+            # Some platform (i.e. Xenomai) don't like reloading same .so file
+            replace_PLC_shared_object = new_PLC_filename != old_PLC_filename
+
+            if replace_PLC_shared_object:
+                self.UnLoadPLC()
 
             self.LogMessage("NewPLC (%s)" % md5sum)
             self.PLCStatus = "Empty"
 
             try:
-                os.remove(os.path.join(self.workingdir,
-                                       self.CurrentPLCFilename))
+                if replace_PLC_shared_object:
+                    os.remove(old_PLC_filename)
                 for filename in file(extra_files_log, "r").readlines() + [extra_files_log]:
                     try:
                         os.remove(os.path.join(self.workingdir, filename.strip()))
@@ -435,8 +571,8 @@
 
             try:
                 # Create new PLC file
-                open(os.path.join(self.workingdir, NewFileName),
-                     'wb').write(data)
+                if replace_PLC_shared_object:
+                    open(new_PLC_filename, 'wb').write(data)
 
                 # Store new PLC filename based on md5 key
                 open(self._GetMD5FileName(), "w").write(md5sum)
@@ -456,11 +592,12 @@
                 PLCprint(traceback.format_exc())
                 return False
 
-            if self.LoadPLC():
+            if not replace_PLC_shared_object:
+                self.PLCStatus = "Stopped"
+            elif self.LoadPLC():
                 self.PLCStatus = "Stopped"
             else:
                 self.PLCStatus = "Broken"
-                self._FreePLC()
             self.StatusChange()
 
             return self.PLCStatus == "Stopped"
@@ -496,14 +633,6 @@
         else:
             self._suspendDebug(True)
 
-    def _TracesPush(self, trace):
-        self.TraceLock.acquire()
-        lT = len(self.Traces)
-        if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024:
-            self.Traces.pop(0)
-        self.Traces.append(trace)
-        self.TraceLock.release()
-
     def _TracesSwap(self):
         self.LastSwapTrace = time()
         if self.TraceThread is None and self.PLCStatus == "Started":
@@ -513,26 +642,9 @@
         Traces = self.Traces
         self.Traces = []
         self.TraceLock.release()
-        self.TraceWakeup.set()
         return Traces
 
-    def _TracesAutoSuspend(self):
-        # TraceProc stops here if Traces not polled for 3 seconds
-        traces_age = time() - self.LastSwapTrace
-        if traces_age > 3:
-            self.TraceLock.acquire()
-            self.Traces = []
-            self.TraceLock.release()
-            self._suspendDebug(True)  # Disable debugger
-            self.TraceWakeup.clear()
-            self.TraceWakeup.wait()
-            self._resumeDebug()  # Re-enable debugger
-
-    def _TracesFlush(self):
-        self.TraceLock.acquire()
-        self.Traces = []
-        self.TraceLock.release()
-
+    @RunInMain
     def GetTraceVariables(self):
         return self.PLCStatus, self._TracesSwap()
 
@@ -540,23 +652,47 @@
         """
         Return a list of traces, corresponding to the list of required idx
         """
+        self._resumeDebug()  # Re-enable debugger
         while self.PLCStatus == "Started":
             tick = ctypes.c_uint32()
             size = ctypes.c_uint32()
             buff = ctypes.c_void_p()
             TraceBuffer = None
-            if self.PLClibraryLock.acquire(False):
-                if self._GetDebugData(ctypes.byref(tick),
-                                      ctypes.byref(size),
-                                      ctypes.byref(buff)) == 0:
-                    if size.value:
-                        TraceBuffer = ctypes.string_at(buff.value, size.value)
-                    self._FreeDebugData()
-                self.PLClibraryLock.release()
+
+            self.PLClibraryLock.acquire()
+
+            res = self._GetDebugData(ctypes.byref(tick),
+                                     ctypes.byref(size),
+                                     ctypes.byref(buff))
+            if res == 0:
+                if size.value:
+                    TraceBuffer = ctypes.string_at(buff.value, size.value)
+                self._FreeDebugData()
+
+            self.PLClibraryLock.release()
+
+            # leave thread if GetDebugData isn't happy.
+            if res != 0:
+                break
+
             if TraceBuffer is not None:
-                self._TracesPush((tick.value, TraceBuffer))
-            self._TracesAutoSuspend()
-        self._TracesFlush()
+                self.TraceLock.acquire()
+                lT = len(self.Traces)
+                if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024:
+                    self.Traces.pop(0)
+                self.Traces.append((tick.value, TraceBuffer))
+                self.TraceLock.release()
+
+            # TraceProc stops here if Traces not polled for 3 seconds
+            traces_age = time() - self.LastSwapTrace
+            if traces_age > 3:
+                self.TraceLock.acquire()
+                self.Traces = []
+                self.TraceLock.release()
+                self._suspendDebug(True)  # Disable debugger
+                break
+
+        self.TraceThread = None
 
     def RemoteExec(self, script, *kwargs):
         try:
--- a/runtime/__init__.py	Thu May 17 09:33:14 2018 +0200
+++ b/runtime/__init__.py	Thu May 17 09:33:58 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/runtime/typemapping.py	Thu May 17 09:33:14 2018 +0200
+++ b/runtime/typemapping.py	Thu May 17 09:33:58 2018 +0200
@@ -35,7 +35,7 @@
 
 def _ttime():
     return (IEC_TIME,
-            lambda x: td(0, x.s, x.ns/1000),
+            lambda x: td(0, x.s, x.ns/1000.0),
             lambda t, x: t(x.days * 24 * 3600 + x.seconds, x.microseconds*1000))
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/xenomai.py	Thu May 17 09:33:58 2018 +0200
@@ -0,0 +1,16 @@
+from ctypes import CDLL, RTLD_GLOBAL, pointer, c_int, POINTER, c_char, create_string_buffer
+def TryPreloadXenomai():
+    """
+    Xenomai 3 (at least for version <= 3.0.6) do not handle properly dlclose
+    of shared objects whose dlopen did trigger xenomai_init.
+    As a workaround, this pre-loads xenomai libraries that need to be 
+    initialized and call xenomai_init once for all.
+    
+    Xenomai auto init of libs MUST be disabled (see --auto-init-solib in xeno-config)
+    """
+    try:
+        for name in ["cobalt", "modechk", "copperplate", "alchemy"]:
+            globals()[name] = CDLL("lib"+name+".so", mode=RTLD_GLOBAL)
+        cobalt.xenomai_init(pointer(c_int(0)), pointer((POINTER(c_char)*2)(create_string_buffer("prog_name"), None)))  
+    except:
+        pass
--- a/targets/Xenomai/__init__.py	Thu May 17 09:33:14 2018 +0200
+++ b/targets/Xenomai/__init__.py	Thu May 17 09:33:58 2018 +0200
@@ -37,7 +37,7 @@
         if xeno_config:
             from util.ProcessLogger import ProcessLogger
             status, result, _err_result = ProcessLogger(self.CTRInstance.logger,
-                                                        xeno_config + " --skin=native --"+flagsname,
+                                                        xeno_config + " --skin=posix --skin=alchemy --no-auto-init --"+flagsname,
                                                         no_stdout=True).spin()
             if status:
                 self.CTRInstance.logger.write_error(_("Unable to get Xenomai's %s \n") % flagsname)
--- a/targets/Xenomai/plc_Xenomai_main.c	Thu May 17 09:33:14 2018 +0200
+++ b/targets/Xenomai/plc_Xenomai_main.c	Thu May 17 09:33:58 2018 +0200
@@ -11,10 +11,10 @@
 #include <sys/mman.h>
 #include <sys/fcntl.h>
 
-#include <native/task.h>
-#include <native/timer.h>
-#include <native/sem.h>
-#include <native/pipe.h>
+#include <alchemy/task.h>
+#include <alchemy/timer.h>
+#include <alchemy/sem.h>
+#include <alchemy/pipe.h>
 
 unsigned int PLC_state = 0;
 #define PLC_STATE_TASK_CREATED                 1
@@ -37,6 +37,15 @@
 #define PYTHON_PIPE_MINOR            3
 #define PIPE_SIZE                    1 
 
+// rt-pipes commands
+
+#define PYTHON_PENDING_COMMAND 1
+#define PYTHON_FINISH 2
+
+#define DEBUG_FINISH 2
+
+#define DEBUG_PENDING_DATA 1
+#define DEBUG_UNLOCK 1
 
 long AtomicCompareExchange(long* atomicvar,long compared, long exchange)
 {
@@ -82,6 +91,18 @@
         if (PLC_shutdown) break;
         rt_task_wait_period(NULL);
     }
+    /* since xenomai 3 it is not enough to close() 
+       file descriptor to unblock read()... */
+    {
+        /* explicitely finish python thread */
+        char msg = PYTHON_FINISH;
+        rt_pipe_write(&WaitPython_pipe, &msg, sizeof(msg), P_NORMAL);
+    }
+    {
+        /* explicitely finish debug thread */
+        char msg = DEBUG_FINISH;
+        rt_pipe_write(&WaitDebug_pipe, &msg, sizeof(msg), P_NORMAL);
+    }
 }
 
 static unsigned long __debug_tick;
@@ -159,6 +180,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 +201,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;
 
@@ -241,7 +277,6 @@
     return 0;
 }
 
-#define DEBUG_UNLOCK 1
 void LeaveDebugSection(void)
 {
     if(AtomicCompareExchange( &debug_state, 
@@ -254,7 +289,6 @@
 
 extern unsigned long __tick;
 
-#define DEBUG_PENDING_DATA 1
 int WaitDebugData(unsigned long *tick)
 {
     char cmd;
@@ -304,8 +338,6 @@
     AtomicCompareExchange( &debug_state, DEBUG_BUSY, DEBUG_FREE);
 }
 
-#define PYTHON_PENDING_COMMAND 1
-
 #define PYTHON_FREE 0
 #define PYTHON_BUSY 1
 static long python_state = PYTHON_FREE;
--- a/targets/plc_main_head.c	Thu May 17 09:33:14 2018 +0200
+++ b/targets/plc_main_head.c	Thu May 17 09:33:58 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
  **/
--- a/tests/tools/test_application.py	Thu May 17 09:33:14 2018 +0200
+++ b/tests/tools/test_application.py	Thu May 17 09:33:58 2018 +0200
@@ -41,7 +41,7 @@
 
 class UserApplicationTest(unittest.TestCase):
     def InstallExceptionHandler(self):
-        def handle_exception(e_type, e_value, e_traceback):
+        def handle_exception(e_type, e_value, e_traceback, exit=False):
             # traceback.print_exception(e_type, e_value, e_traceback)
             self.exc_info = [e_type, e_value, e_traceback]
         self.exc_info = None
@@ -89,7 +89,9 @@
         # disable default exception handler in Beremiz
         self.app.InstallExceptionHandler = lambda: None
         self.InstallExceptionHandler()
+        self.app.handle_exception = sys.excepthook
         self.app.PreStart()
+        self.ProcessEvents()
         self.app.frame.Show()
         self.ProcessEvents()
         self.app.frame.ShowFullScreen(True)
--- a/xmlclass/xmlclass.py	Thu May 17 09:33:14 2018 +0200
+++ b/xmlclass/xmlclass.py	Thu May 17 09:33:58 2018 +0200
@@ -67,7 +67,7 @@
 QName_model = re.compile('((?:[a-zA-Z_][\w]*:)?[a-zA-Z_][\w]*)$')
 QNames_model = re.compile('((?:[a-zA-Z_][\w]*:)?[a-zA-Z_][\w]*(?: (?:[a-zA-Z_][\w]*:)?[a-zA-Z_][\w]*)*)$')
 NCName_model = re.compile('([a-zA-Z_][\w]*)$')
-URI_model = re.compile('((?:http://|/)?(?:[\w.-]*/?)*)$')
+URI_model = re.compile('((?:htt(p|ps)://|/)?(?:[\w.-]*/?)*)$')
 LANGUAGE_model = re.compile('([a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*)$')
 
 ONLY_ANNOTATION = re.compile("((?:annotation )?)")