# HG changeset patch # User Edouard Tisserant # Date 1526542438 -7200 # Node ID bb9c28bd204fae125a469c94ba81f3c834dea520 # Parent 28af541d776b07557f6870bdcddc3f93e30fcfd0# Parent 00faf9279dc0289a7d512892e766c7892a28b6dc Merged #2476, URI dialog fixes diff -r 00faf9279dc0 -r bb9c28bd204f Beremiz.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() diff -r 00faf9279dc0 -r bb9c28bd204f Beremiz_service.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f CodeFileTreeNode.py --- 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([]) diff -r 00faf9279dc0 -r bb9c28bd204f ConfigTreeNode.py --- 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: diff -r 00faf9279dc0 -r bb9c28bd204f IDEFrame.py --- 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: diff -r 00faf9279dc0 -r bb9c28bd204f PLCControler.py --- 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: diff -r 00faf9279dc0 -r bb9c28bd204f ProjectController.py --- 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): """ diff -r 00faf9279dc0 -r bb9c28bd204f canfestival/canfestival.py --- 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 diff -r 00faf9279dc0 -r bb9c28bd204f connectors/PYRO/__init__.py --- 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 diff -r 00faf9279dc0 -r bb9c28bd204f controls/DebugVariablePanel/DebugVariableGraphicViewer.py --- 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 diff -r 00faf9279dc0 -r bb9c28bd204f controls/DebugVariablePanel/DebugVariableItem.py --- 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( diff -r 00faf9279dc0 -r bb9c28bd204f controls/DebugVariablePanel/DebugVariablePanel.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f controls/LogViewer.py --- 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() diff -r 00faf9279dc0 -r bb9c28bd204f controls/ProjectPropertiesPanel.py --- 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, diff -r 00faf9279dc0 -r bb9c28bd204f controls/VariablePanel.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f editors/CodeFileEditor.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f editors/ResourceEditor.py --- 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 diff -r 00faf9279dc0 -r bb9c28bd204f plcopen/BlockInstanceCollector.py --- 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 diff -r 00faf9279dc0 -r bb9c28bd204f plcopen/plcopen.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f runtime/PLCObject.py --- 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: diff -r 00faf9279dc0 -r bb9c28bd204f runtime/__init__.py --- 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 diff -r 00faf9279dc0 -r bb9c28bd204f runtime/typemapping.py --- 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)) diff -r 00faf9279dc0 -r bb9c28bd204f runtime/xenomai.py --- /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 diff -r 00faf9279dc0 -r bb9c28bd204f targets/Xenomai/__init__.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f targets/Xenomai/plc_Xenomai_main.c --- 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 #include -#include -#include -#include -#include +#include +#include +#include +#include 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; diff -r 00faf9279dc0 -r bb9c28bd204f targets/plc_main_head.c --- 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 **/ diff -r 00faf9279dc0 -r bb9c28bd204f tests/tools/test_application.py --- 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) diff -r 00faf9279dc0 -r bb9c28bd204f xmlclass/xmlclass.py --- 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 )?)")