greg@229: #!/usr/bin/env python
greg@229: # -*- coding: utf-8 -*-
greg@229: 
andrej@1667: # This file is part of Beremiz runtime.
greg@229: #
andrej@1571: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
greg@229: #
andrej@1667: # See COPYING.Runtime file for copyrights details.
greg@229: #
andrej@1667: # This library is free software; you can redistribute it and/or
andrej@1667: # modify it under the terms of the GNU Lesser General Public
andrej@1667: # License as published by the Free Software Foundation; either
andrej@1667: # version 2.1 of the License, or (at your option) any later version.
andrej@1667: 
andrej@1667: # This library is distributed in the hope that it will be useful,
andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1667: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
andrej@1667: # Lesser General Public License for more details.
andrej@1667: 
andrej@1667: # You should have received a copy of the GNU Lesser General Public
andrej@1667: # License along with this library; if not, write to the Free Software
andrej@1667: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
greg@229: 
andrej@1832: 
andrej@1881: from __future__ import absolute_import
Edouard@2582: from threading import Thread, Lock, Event, Condition
andrej@1732: import ctypes
andrej@1732: import os
andrej@1732: import sys
andrej@1783: import traceback
andrej@2537: import shutil
andrej@1832: from time import time
andrej@2540: import hashlib
andrej@2537: from tempfile import mkstemp
andrej@2537: from functools import wraps, partial
edouard@2492: from six.moves import xrange
andrej@2443: from past.builtins import execfile
andrej@2537: import _ctypes
andrej@1832: 
Edouard@1902: from runtime.typemapping import TypeTranslator
Edouard@1902: from runtime.loglevels import LogLevelsDefault, LogLevelsCount
Edouard@2324: from runtime.Stunnel import getPSKID
andrej@2416: from runtime import PlcStatus
Edouard@2270: from runtime import MainWorker
Edouard@2583: from runtime import default_evaluator
Edouard@917: 
greg@229: if os.name in ("nt", "ce"):
Edouard@1919:     dlopen = _ctypes.LoadLibrary
Edouard@1919:     dlclose = _ctypes.FreeLibrary
greg@229: elif os.name == "posix":
Edouard@1919:     dlopen = _ctypes.dlopen
Edouard@1919:     dlclose = _ctypes.dlclose
greg@229: 
andrej@1736: 
laurent@699: def get_last_traceback(tb):
laurent@699:     while tb.tb_next:
laurent@699:         tb = tb.tb_next
laurent@699:     return tb
greg@229: 
andrej@1749: 
andrej@1742: lib_ext = {
andrej@1878:     "linux2": ".so",
andrej@1878:     "win32":  ".dll",
andrej@1878: }.get(sys.platform, "")
greg@229: 
andrej@1736: 
etisserant@291: def PLCprint(message):
etisserant@291:     sys.stdout.write("PLCobject : "+message+"\n")
etisserant@291:     sys.stdout.flush()
etisserant@291: 
andrej@1736: 
Edouard@1984: def RunInMain(func):
Edouard@2486:     @wraps(func)
Edouard@1997:     def func_wrapper(*args, **kwargs):
Edouard@1984:         return MainWorker.call(func, *args, **kwargs)
Edouard@1984:     return func_wrapper
Edouard@1997: 
Edouard@1984: 
Edouard@2270: class PLCObject(object):
Edouard@2270:     def __init__(self, WorkingDir, argv, statuschange, evaluator, pyruntimevars):
edouard@2492:         self.workingdir = WorkingDir  # must exits already
Edouard@2463:         self.tmpdir = os.path.join(WorkingDir, 'tmp')
Edouard@2463:         if os.path.exists(self.tmpdir):
Edouard@2463:             shutil.rmtree(self.tmpdir)
Edouard@2463:         os.mkdir(self.tmpdir)
Edouard@2270:         # FIXME : is argv of any use nowadays ?
Edouard@2270:         self.argv = [WorkingDir] + argv  # force argv[0] to be "path" to exec...
Edouard@2270:         self.statuschange = statuschange
Edouard@2270:         self.evaluator = evaluator
Edouard@2270:         self.pyruntimevars = pyruntimevars
andrej@2416:         self.PLCStatus = PlcStatus.Empty
greg@229:         self.PLClibraryHandle = None
greg@352:         self.PLClibraryLock = Lock()
greg@229:         # Creates fake C funcs proxies
Edouard@1984:         self._InitPLCStubCalls()
Edouard@914:         self._loading_error = None
Edouard@1014:         self.python_runtime_vars = None
Edouard@1434:         self.TraceThread = None
Edouard@1434:         self.TraceLock = Lock()
Edouard@1434:         self.Traces = []
Edouard@2485:         self.DebugToken = 0
Edouard@1433: 
Edouard@2463:         self._init_blobs()
Edouard@2463: 
Edouard@1994:     # First task of worker -> no @RunInMain
Edouard@2270:     def AutoLoad(self, autostart):
Edouard@1997:         # Get the last transfered PLC
greg@229:         try:
andrej@1742:             self.CurrentPLCFilename = open(
andrej@1878:                 self._GetMD5FileName(),
andrej@1878:                 "r").read().strip() + lib_ext
Edouard@2608:             self.PLCStatus = PlcStatus.Stopped
Edouard@2608:             if autostart:
Edouard@2608:                 if self.LoadPLC():
Edouard@2270:                     self.StartPLC()
Edouard@3282:                 else:
Edouard@3282:                     self._fail(_("Problem autostarting PLC : can't load PLC"))
Edouard@3282:                 return
andrej@1846:         except Exception:
andrej@2416:             self.PLCStatus = PlcStatus.Empty
andrej@1742:             self.CurrentPLCFilename = None
greg@229: 
Edouard@2270:         self.StatusChange()
Edouard@2270: 
etisserant@286:     def StatusChange(self):
etisserant@286:         if self.statuschange is not None:
Edouard@1438:             for callee in self.statuschange:
Edouard@1438:                 callee(self.PLCStatus)
etisserant@286: 
Edouard@917:     def LogMessage(self, *args):
Edouard@917:         if len(args) == 2:
Edouard@917:             level, msg = args
Edouard@917:         else:
Edouard@917:             level = LogLevelsDefault
Edouard@917:             msg, = args
Edouard@1906:         PLCprint(msg)
Edouard@1906:         if self._LogMessage is not None:
Edouard@1906:             return self._LogMessage(level, msg, len(msg))
Edouard@1906:         return None
Edouard@917: 
Edouard@1994:     @RunInMain
Laurent@1093:     def ResetLogCount(self):
Laurent@1093:         if self._ResetLogCount is not None:
Laurent@1093:             self._ResetLogCount()
Edouard@917: 
Edouard@1997:     # used internaly
Edouard@917:     def GetLogCount(self, level):
andrej@1739:         if self._GetLogCount is not None:
Edouard@917:             return int(self._GetLogCount(level))
andrej@1742:         elif self._loading_error is not None and level == 0:
Laurent@1093:             return 1
Edouard@914: 
Edouard@1994:     @RunInMain
Edouard@917:     def GetLogMessage(self, level, msgid):
Edouard@921:         tick = ctypes.c_uint32()
Edouard@921:         tv_sec = ctypes.c_uint32()
Edouard@921:         tv_nsec = ctypes.c_uint32()
Edouard@914:         if self._GetLogMessage is not None:
Edouard@914:             maxsz = len(self._log_read_buffer)-1
Edouard@1433:             sz = self._GetLogMessage(level, msgid,
andrej@1768:                                      self._log_read_buffer, maxsz,
andrej@1768:                                      ctypes.byref(tick),
andrej@1768:                                      ctypes.byref(tv_sec),
andrej@1768:                                      ctypes.byref(tv_nsec))
Edouard@914:             if sz and sz <= maxsz:
Edouard@914:                 self._log_read_buffer[sz] = '\x00'
andrej@1740:                 return self._log_read_buffer.value, tick.value, tv_sec.value, tv_nsec.value
andrej@1742:         elif self._loading_error is not None and level == 0:
andrej@1740:             return self._loading_error, 0, 0, 0
Edouard@914:         return None
Edouard@911: 
greg@229:     def _GetMD5FileName(self):
greg@229:         return os.path.join(self.workingdir, "lasttransferedPLC.md5")
greg@229: 
greg@229:     def _GetLibFileName(self):
andrej@1740:         return os.path.join(self.workingdir, self.CurrentPLCFilename)
greg@229: 
Edouard@1994:     def _LoadPLC(self):
greg@229:         """
greg@229:         Load PLC library
greg@229:         Declare all functions, arguments and return values
greg@229:         """
Edouard@1457:         md5 = open(self._GetMD5FileName(), "r").read()
Edouard@1994:         self.PLClibraryLock.acquire()
greg@229:         try:
greg@229:             self._PLClibraryHandle = dlopen(self._GetLibFileName())
greg@229:             self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle)
Edouard@1433: 
Edouard@1463:             self.PLC_ID = ctypes.c_char_p.in_dll(self.PLClibraryHandle, "PLC_ID")
andrej@1739:             if len(md5) == 32:
andrej@1730:                 self.PLC_ID.value = md5
Edouard@1457: 
greg@229:             self._startPLC = self.PLClibraryHandle.startPLC
greg@229:             self._startPLC.restype = ctypes.c_int
greg@229:             self._startPLC.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
Edouard@1433: 
ed@455:             self._stopPLC_real = self.PLClibraryHandle.stopPLC
ed@455:             self._stopPLC_real.restype = None
Edouard@1433: 
laurent@366:             self._PythonIterator = getattr(self.PLClibraryHandle, "PythonIterator", None)
laurent@366:             if self._PythonIterator is not None:
laurent@366:                 self._PythonIterator.restype = ctypes.c_char_p
Edouard@851:                 self._PythonIterator.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)]
Edouard@1433: 
edouard@483:                 self._stopPLC = self._stopPLC_real
laurent@366:             else:
Edouard@717:                 # If python confnode is not enabled, we reuse _PythonIterator
Edouard@1433:                 # as a call that block pythonthread until StopPLC
Edouard@1442:                 self.PlcStopping = Event()
andrej@1750: 
Edouard@868:                 def PythonIterator(res, blkid):
Edouard@1442:                     self.PlcStopping.clear()
Edouard@1442:                     self.PlcStopping.wait()
laurent@366:                     return None
ed@455:                 self._PythonIterator = PythonIterator
Edouard@1433: 
edouard@483:                 def __StopPLC():
ed@455:                     self._stopPLC_real()
Edouard@1442:                     self.PlcStopping.set()
edouard@483:                 self._stopPLC = __StopPLC
Edouard@1433: 
greg@229:             self._ResetDebugVariables = self.PLClibraryHandle.ResetDebugVariables
greg@229:             self._ResetDebugVariables.restype = None
Edouard@1433: 
etisserant@235:             self._RegisterDebugVariable = self.PLClibraryHandle.RegisterDebugVariable
Edouard@3395:             self._RegisterDebugVariable.restype = ctypes.c_int
edouard@477:             self._RegisterDebugVariable.argtypes = [ctypes.c_int, ctypes.c_void_p]
Edouard@1433: 
greg@229:             self._FreeDebugData = self.PLClibraryHandle.FreeDebugData
greg@229:             self._FreeDebugData.restype = None
Edouard@1433: 
edouard@450:             self._GetDebugData = self.PLClibraryHandle.GetDebugData
Edouard@1433:             self._GetDebugData.restype = ctypes.c_int
edouard@450:             self._GetDebugData.argtypes = [ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_void_p)]
etisserant@235: 
etisserant@235:             self._suspendDebug = self.PLClibraryHandle.suspendDebug
Edouard@614:             self._suspendDebug.restype = ctypes.c_int
edouard@462:             self._suspendDebug.argtypes = [ctypes.c_int]
etisserant@235: 
etisserant@235:             self._resumeDebug = self.PLClibraryHandle.resumeDebug
etisserant@235:             self._resumeDebug.restype = None
Edouard@906: 
Laurent@1093:             self._ResetLogCount = self.PLClibraryHandle.ResetLogCount
Laurent@1093:             self._ResetLogCount.restype = None
Laurent@1093: 
Edouard@906:             self._GetLogCount = self.PLClibraryHandle.GetLogCount
Edouard@906:             self._GetLogCount.restype = ctypes.c_uint32
Edouard@917:             self._GetLogCount.argtypes = [ctypes.c_uint8]
Edouard@906: 
Edouard@911:             self._LogMessage = self.PLClibraryHandle.LogMessage
Edouard@911:             self._LogMessage.restype = ctypes.c_int
Edouard@971:             self._LogMessage.argtypes = [ctypes.c_uint8, ctypes.c_char_p, ctypes.c_uint32]
Laurent@1093: 
andrej@1760:             self._log_read_buffer = ctypes.create_string_buffer(1 << 14)  # 16K
Edouard@911:             self._GetLogMessage = self.PLClibraryHandle.GetLogMessage
Edouard@911:             self._GetLogMessage.restype = ctypes.c_uint32
Edouard@921:             self._GetLogMessage.argtypes = [ctypes.c_uint8, ctypes.c_uint32, ctypes.c_char_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32)]
Edouard@911: 
Edouard@914:             self._loading_error = None
Edouard@1035: 
andrej@1780:         except Exception:
Edouard@914:             self._loading_error = traceback.format_exc()
Edouard@914:             PLCprint(self._loading_error)
Edouard@1994:             return False
Edouard@1994:         finally:
Edouard@1994:             self.PLClibraryLock.release()
Edouard@1994: 
Edouard@1994:         return True
Edouard@1994: 
Edouard@1994:     @RunInMain
Edouard@1994:     def LoadPLC(self):
Edouard@1994:         res = self._LoadPLC()
Edouard@1994:         if res:
Edouard@3282:             try:
Edouard@3282:                 self.PythonRuntimeInit()
Edouard@3282:             except Exception:
Edouard@3282:                 self._loading_error = traceback.format_exc()
Edouard@3282:                 PLCprint(self._loading_error)
Edouard@3282:                 return False
Edouard@1994:         else:
Edouard@1984:             self._FreePLC()
Edouard@1994: 
Edouard@1994:         return res
greg@229: 
Edouard@1984:     @RunInMain
Edouard@1045:     def UnLoadPLC(self):
Edouard@1045:         self.PythonRuntimeCleanup()
Edouard@1045:         self._FreePLC()
Edouard@1045: 
Edouard@1984:     def _InitPLCStubCalls(self):
Edouard@1984:         """
Edouard@1984:         create dummy C func proxies
Edouard@1984:         """
andrej@1740:         self._startPLC = lambda x, y: None
andrej@1740:         self._stopPLC = lambda: None
andrej@1740:         self._ResetDebugVariables = lambda: None
Edouard@3395:         self._RegisterDebugVariable = lambda x, y: 0
andrej@1740:         self._IterDebugData = lambda x, y: None
andrej@1740:         self._FreeDebugData = lambda: None
andrej@1740:         self._GetDebugData = lambda: -1
andrej@1740:         self._suspendDebug = lambda x: -1
andrej@1740:         self._resumeDebug = lambda: None
andrej@1740:         self._PythonIterator = lambda: ""
Edouard@1433:         self._GetLogCount = None
Edouard@1906:         self._LogMessage = None
Edouard@914:         self._GetLogMessage = None
Edouard@1984:         self._PLClibraryHandle = None
greg@229:         self.PLClibraryHandle = None
Edouard@1984: 
Edouard@1984:     def _FreePLC(self):
Edouard@1984:         """
Edouard@1984:         Unload PLC library.
Edouard@1984:         This is also called by __init__ to create dummy C func proxies
Edouard@1984:         """
Edouard@1984:         self.PLClibraryLock.acquire()
Edouard@1994:         try:
Edouard@1994:             # Unload library explicitely
Edouard@1994:             if getattr(self, "_PLClibraryHandle", None) is not None:
Edouard@1994:                 dlclose(self._PLClibraryHandle)
Edouard@1994: 
Edouard@1994:             # Forget all refs to library
Edouard@1994:             self._InitPLCStubCalls()
Edouard@1994: 
Edouard@1994:         finally:
Edouard@1994:             self.PLClibraryLock.release()
Edouard@1994: 
greg@229:         return False
greg@229: 
Edouard@2635:     def PythonRuntimeCall(self, methodname, use_evaluator=True, reverse_order=False):
Edouard@1433:         """
Edouard@1433:         Calls init, start, stop or cleanup method provided by
Edouard@1014:         runtime python files, loaded when new PLC uploaded
Edouard@1014:         """
Edouard@2635:         methods = self.python_runtime_vars.get("_runtime_%s" % methodname, [])
Edouard@2635:         if reverse_order:
Edouard@2635:             methods = reversed(methods)
Edouard@2635:         for method in methods:
Edouard@2583:             if use_evaluator:
Edouard@2583:                 _res, exp = self.evaluator(method)
Edouard@2583:             else:
Edouard@2583:                 _res, exp = default_evaluator(method)
Edouard@1433:             if exp is not None:
andrej@1740:                 self.LogMessage(0, '\n'.join(traceback.format_exception(*exp)))
Edouard@1014: 
Edouard@1997:     # used internaly
Edouard@1014:     def PythonRuntimeInit(self):
Edouard@1014:         MethodNames = ["init", "start", "stop", "cleanup"]
Edouard@1014:         self.python_runtime_vars = globals().copy()
Edouard@1438:         self.python_runtime_vars.update(self.pyruntimevars)
andrej@1868:         parent = self
Edouard@1438: 
andrej@1831:         class PLCSafeGlobals(object):
andrej@1868:             def __getattr__(self, name):
andrej@1739:                 try:
andrej@1868:                     t = parent.python_runtime_vars["_"+name+"_ctype"]
Edouard@1156:                 except KeyError:
andrej@1734:                     raise KeyError("Try to get unknown shared global variable : %s" % name)
Edouard@1156:                 v = t()
andrej@1868:                 parent.python_runtime_vars["_PySafeGetPLCGlob_"+name](ctypes.byref(v))
andrej@1868:                 return parent.python_runtime_vars["_"+name+"_unpack"](v)
andrej@1868: 
andrej@1868:             def __setattr__(self, name, value):
andrej@1739:                 try:
andrej@1868:                     t = parent.python_runtime_vars["_"+name+"_ctype"]
Edouard@1156:                 except KeyError:
andrej@1734:                     raise KeyError("Try to set unknown shared global variable : %s" % name)
andrej@1868:                 v = parent.python_runtime_vars["_"+name+"_pack"](t, value)
andrej@1868:                 parent.python_runtime_vars["_PySafeSetPLCGlob_"+name](ctypes.byref(v))
Edouard@1447: 
edouard@2697:         class OnChangeStateClass(object):
edouard@2697:             def __getattr__(self, name):
edouard@2697:                 u = parent.python_runtime_vars["_"+name+"_unpack"]
Edouard@2698:                 return type("changedesc",(),dict(
Edouard@2698:                     count = parent.python_runtime_vars["_PyOnChangeCount_"+name].value,
Edouard@2698:                     first = u(parent.python_runtime_vars["_PyOnChangeFirst_"+name]),
Edouard@2698:                     last = u(parent.python_runtime_vars["_PyOnChangeLast_"+name])))
edouard@2697: 
edouard@2697: 
Edouard@1447:         self.python_runtime_vars.update({
andrej@1739:             "PLCGlobals":     PLCSafeGlobals(),
edouard@2697:             "OnChange":       OnChangeStateClass(),
andrej@1739:             "WorkingDir":     self.workingdir,
andrej@1739:             "PLCObject":      self,
andrej@1739:             "PLCBinary":      self.PLClibraryHandle,
andrej@1739:             "PLCGlobalsDesc": []})
andrej@1739: 
andrej@1739:         for methodname in MethodNames:
andrej@1734:             self.python_runtime_vars["_runtime_%s" % methodname] = []
Edouard@1447: 
Edouard@972:         try:
Edouard@1447:             filenames = os.listdir(self.workingdir)
Edouard@1447:             filenames.sort()
Edouard@1447:             for filename in filenames:
Edouard@972:                 name, ext = os.path.splitext(filename)
Edouard@972:                 if name.upper().startswith("RUNTIME") and ext.upper() == ".PY":
Edouard@1014:                     execfile(os.path.join(self.workingdir, filename), self.python_runtime_vars)
Edouard@1433:                     for methodname in MethodNames:
Edouard@1014:                         method = self.python_runtime_vars.get("_%s_%s" % (name, methodname), None)
Edouard@1014:                         if method is not None:
andrej@1734:                             self.python_runtime_vars["_runtime_%s" % methodname].append(method)
andrej@1780:         except Exception:
andrej@1740:             self.LogMessage(0, traceback.format_exc())
Edouard@972:             raise
Edouard@1433: 
Edouard@2583:         self.PythonRuntimeCall("init", use_evaluator=False)
Edouard@2583: 
Edouard@2582:         self.PythonThreadCondLock = Lock()
Edouard@2720:         self.PythonThreadCmdCond = Condition(self.PythonThreadCondLock)
Edouard@2720:         self.PythonThreadAckCond = Condition(self.PythonThreadCondLock)
Edouard@2720:         self.PythonThreadCmd = None
Edouard@2720:         self.PythonThreadAck = None
Edouard@2600:         self.PythonThread = Thread(target=self.PythonThreadProc, name="PLCPythonThread")
Edouard@2582:         self.PythonThread.start()
Edouard@2582: 
Edouard@1997:     # used internaly
Edouard@1014:     def PythonRuntimeCleanup(self):
Edouard@1014:         if self.python_runtime_vars is not None:
Edouard@2582:             self.PythonThreadCommand("Finish")
Edouard@2582:             self.PythonThread.join()
Edouard@2635:             self.PythonRuntimeCall("cleanup", use_evaluator=False, reverse_order=True)
Edouard@1014: 
Edouard@1014:         self.python_runtime_vars = None
etisserant@291: 
Edouard@2582:     def PythonThreadLoop(self):
andrej@1740:         res, cmd, blkid = "None", "None", ctypes.c_void_p()
andrej@1742:         compile_cache = {}
laurent@798:         while True:
andrej@1740:             cmd = self._PythonIterator(res, blkid)
Edouard@1433:             FBID = blkid.value
laurent@798:             if cmd is None:
laurent@798:                 break
andrej@1739:             try:
andrej@1742:                 self.python_runtime_vars["FBID"] = FBID
andrej@1742:                 ccmd, AST = compile_cache.get(FBID, (None, None))
andrej@1742:                 if ccmd is None or ccmd != cmd:
Edouard@867:                     AST = compile(cmd, '<plc>', 'eval')
andrej@1742:                     compile_cache[FBID] = (cmd, AST)
andrej@1740:                 result, exp = self.evaluator(eval, AST, self.python_runtime_vars)
Edouard@1433:                 if exp is not None:
Edouard@1052:                     res = "#EXCEPTION : "+str(exp[1])
andrej@1768:                     self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (
andrej@1768:                         FBID, cmd, '\n'.join(traceback.format_exception(*exp))))
Edouard@867:                 else:
andrej@1742:                     res = str(result)
andrej@1742:                 self.python_runtime_vars["FBID"] = None
andrej@2418:             except Exception as e:
laurent@798:                 res = "#EXCEPTION : "+str(e)
andrej@1740:                 self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (FBID, cmd, str(e)))
Edouard@1433: 
Edouard@2582:     def PythonThreadProc(self):
Edouard@2582:         while True:
Edouard@2582:             self.PythonThreadCondLock.acquire()
Edouard@2582:             cmd = self.PythonThreadCmd
Edouard@2720:             while cmd is None:
Edouard@2720:                 self.PythonThreadCmdCond.wait()
Edouard@2582:                 cmd = self.PythonThreadCmd
Edouard@2720:             self.PythonThreadCmd = None
Edouard@2582:             self.PythonThreadCondLock.release()
Edouard@2586: 
Edouard@2720:             if cmd == "PreStart":
Edouard@2720:                 self.PreStartPLC()
Edouard@2720:                 # Ack once PreStart done, must be finished before StartPLC
Edouard@2720:                 self.PythonThreadAcknowledge(cmd)
Edouard@2720:             elif cmd == "Start":
Edouard@2720:                 # Ack Immediately, for responsiveness
Edouard@2720:                 self.PythonThreadAcknowledge(cmd)
Edouard@2582:                 self.PythonRuntimeCall("start")
Edouard@2720:                 self.LogMessage("Python extensions started")
Edouard@3578:                 self._PostStartPLC()
Edouard@2582:                 self.PythonThreadLoop()
Edouard@2635:                 self.PythonRuntimeCall("stop", reverse_order=True)
Edouard@2720:             elif cmd == "Finish":
Edouard@2720:                 self.PythonThreadAcknowledge(cmd)
Edouard@2582:                 break
Edouard@2582: 
Edouard@2720:     def PythonThreadAcknowledge(self, ack):
Edouard@2720:         self.PythonThreadCondLock.acquire()
Edouard@2720:         self.PythonThreadAck = ack
Edouard@2720:         self.PythonThreadAckCond.notify()
Edouard@2720:         self.PythonThreadCondLock.release()
Edouard@2720: 
Edouard@2582:     def PythonThreadCommand(self, cmd):
Edouard@2582:         self.PythonThreadCondLock.acquire()
Edouard@2586:         self.PythonThreadCmd = cmd
Edouard@2720:         self.PythonThreadCmdCond.notify()
Edouard@2720:         ack = None
Edouard@2720:         while ack != cmd:
Edouard@2720:             self.PythonThreadAckCond.wait()
Edouard@2720:             ack = self.PythonThreadAck
Edouard@2720:             self.PythonThreadAck = None
Edouard@2720: 
Edouard@2582:         self.PythonThreadCondLock.release()
Edouard@2582: 
Edouard@2682:     def _fail(self, msg):
Edouard@2646:         self.LogMessage(0, msg)
Edouard@2646:         self.PLCStatus = PlcStatus.Broken
Edouard@2646:         self.StatusChange()
Edouard@2646: 
Edouard@2646:     def PreStartPLC(self):
Edouard@2646:         """ 
Edouard@2646:         Here goes actions to be taken just before PLC starts, 
Edouard@2646:         with all libraries and python object already created.
Edouard@2646:         For example : restore saved proprietary parameters
Edouard@2646:         """
Edouard@2646:         pass
Edouard@2646: 
Edouard@3578:     def _PostStartPLC(self):
Edouard@3578:         try:
Edouard@3578:             self.PostStartPLC()
Edouard@3578:         except Exception:
Edouard@3578:             self.LogMessage(0, 'Post Start Exception'+'\n'.join(
Edouard@3578:                 traceback.format_exception(*sys.exc_info())))
Edouard@3578: 
Edouard@2732:     def PostStartPLC(self):
Edouard@2732:         """ 
Edouard@2732:         Here goes actions to be taken after PLC is started, 
Edouard@2732:         with all libraries and python object already created,
Edouard@2732:         and python extensions "Start" methods being called.
Edouard@2732:         This is called before python thread processing py_eval blocks starts.
Edouard@2732:         For example : attach additional ressource to web services
Edouard@2732:         """
Edouard@2732:         pass
Edouard@2732: 
Edouard@1988:     @RunInMain
edouard@462:     def StartPLC(self):
Edouard@2608: 
Edouard@2608:         if self.PLClibraryHandle is None:
Edouard@2608:             if not self.LoadPLC():
Edouard@2646:                 self._fail(_("Problem starting PLC : can't load PLC"))
Edouard@2646: 
andrej@2416:         if self.CurrentPLCFilename is not None and self.PLCStatus == PlcStatus.Stopped:
Edouard@2720:             self.PythonThreadCommand("PreStart")
laurent@798:             c_argv = ctypes.c_char_p * len(self.argv)
andrej@1740:             res = self._startPLC(len(self.argv), c_argv(*self.argv))
Edouard@906:             if res == 0:
Edouard@2720:                 self.LogMessage("PLC started")
andrej@2416:                 self.PLCStatus = PlcStatus.Started
Edouard@1442:                 self.StatusChange()
Edouard@2720:                 self.PythonThreadCommand("Start")
laurent@798:             else:
Edouard@2646:                 self._fail(_("Problem starting PLC : error %d" % res))
Edouard@1433: 
Edouard@1988:     @RunInMain
greg@229:     def StopPLC(self):
andrej@2416:         if self.PLCStatus == PlcStatus.Started:
Edouard@917:             self.LogMessage("PLC stopped")
greg@352:             self._stopPLC()
andrej@2416:             self.PLCStatus = PlcStatus.Stopped
Edouard@1442:             self.StatusChange()
andrej@1739:             if self.TraceThread is not None:
Edouard@1434:                 self.TraceThread.join()
Edouard@1434:                 self.TraceThread = None
greg@229:             return True
greg@229:         return False
greg@229: 
greg@229:     def GetPLCstatus(self):
Edouard@2602:         try:
Edouard@2602:             return self._GetPLCstatus()
Edouard@2602:         except EOFError:
Edouard@2602:             return (PlcStatus.Disconnected, None)
Edouard@2602: 
Edouard@2602:     @RunInMain
Edouard@2602:     def _GetPLCstatus(self):
andrej@1740:         return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
Edouard@1433: 
Edouard@1988:     @RunInMain
Edouard@2324:     def GetPLCID(self):
edouard@2492:         return getPSKID(partial(self.LogMessage, 0))
Edouard@2324: 
Edouard@2463:     def _init_blobs(self):
Edouard@2463:         self.blobs = {}
Edouard@2463:         if os.path.exists(self.tmpdir):
Edouard@2463:             shutil.rmtree(self.tmpdir)
Edouard@2463:         os.mkdir(self.tmpdir)
edouard@2492: 
Edouard@2463:     @RunInMain
Edouard@2487:     def SeedBlob(self, seed):
andrej@2540:         blob = (mkstemp(dir=self.tmpdir) + (hashlib.new('md5'),))
Edouard@2651:         _fd, _path, md5sum = blob
Edouard@2487:         md5sum.update(seed)
Edouard@2487:         newBlobID = md5sum.digest()
Edouard@2487:         self.blobs[newBlobID] = blob
Edouard@2487:         return newBlobID
Edouard@2487: 
Edouard@2487:     @RunInMain
Edouard@2463:     def AppendChunkToBlob(self, data, blobID):
Edouard@2487:         blob = self.blobs.pop(blobID, None)
Edouard@2463: 
Edouard@2463:         if blob is None:
Edouard@2463:             return None
Edouard@2463: 
Edouard@2651:         fd, _path, md5sum = blob
Edouard@2463:         md5sum.update(data)
Edouard@2463:         newBlobID = md5sum.digest()
Edouard@2651:         os.write(fd, data)
Edouard@2487:         self.blobs[newBlobID] = blob
Edouard@2463:         return newBlobID
Edouard@2463: 
Edouard@2463:     @RunInMain
Edouard@2463:     def PurgeBlobs(self):
Edouard@2651:         for fd, _path, _md5sum in self.blobs.values():
Edouard@2651:             os.close(fd)
Edouard@2463:         self._init_blobs()
Edouard@2463: 
Edouard@2702:     def BlobAsFile(self, blobID, newpath):
Edouard@2463:         blob = self.blobs.pop(blobID, None)
Edouard@2463: 
Edouard@2463:         if blob is None:
Edouard@2463:             raise Exception(_("Missing data to create file: {}").format(newpath))
Edouard@2463: 
Edouard@2702:         self._BlobAsFile(blob, newpath)
Edouard@2702: 
Edouard@2702:     def _BlobAsFile(self, blob, newpath):
Edouard@2651:         fd, path, _md5sum = blob
Edouard@2651:         fobj = os.fdopen(fd)
Edouard@2651:         fobj.flush()
Edouard@2651:         os.fsync(fd)
Edouard@2651:         fobj.close()
Edouard@2463:         shutil.move(path, newpath)
edouard@2492: 
Edouard@2594:     def _extra_files_log_path(self):
Edouard@2594:         return os.path.join(self.workingdir, "extra_files.txt")
Edouard@2594: 
Edouard@2596:     def RepairPLC(self):
Edouard@2596:         self.PurgePLC()
Edouard@2596:         MainWorker.quit()
Edouard@2596: 
Edouard@2594:     @RunInMain
Edouard@2594:     def PurgePLC(self):
Edouard@2594: 
Edouard@2594:         extra_files_log = self._extra_files_log_path()
Edouard@2594: 
Edouard@2594:         old_PLC_filename = os.path.join(self.workingdir, self.CurrentPLCFilename) \
Edouard@2594:             if self.CurrentPLCFilename is not None \
Edouard@2594:             else None
Edouard@2594: 
Edouard@2594:         try:
Edouard@2613:             allfiles = open(extra_files_log, "rt").readlines()
Edouard@2613:             allfiles.extend([extra_files_log, old_PLC_filename, self._GetMD5FileName()])
Edouard@2594:         except Exception:
Edouard@2613:             self.LogMessage("No files to purge")
Edouard@2613:             allfiles = []
Edouard@2594: 
Edouard@2596:         for filename in allfiles:
Edouard@2613:             if filename:
Edouard@2613:                 filename = filename.strip()
Edouard@2613:                 try:
Edouard@2613:                     os.remove(os.path.join(self.workingdir, filename))
Edouard@2613:                 except Exception:
Edouard@2613:                     self.LogMessage("Couldn't purge " + filename)
Edouard@2596: 
Edouard@2594:         self.PLCStatus = PlcStatus.Empty
Edouard@2594: 
Edouard@2594:         # TODO: PLCObject restart
Edouard@2594: 
Edouard@2463:     @RunInMain
Edouard@2463:     def NewPLC(self, md5sum, plc_object, extrafiles):
andrej@2416:         if self.PLCStatus in [PlcStatus.Stopped, PlcStatus.Empty, PlcStatus.Broken]:
greg@229:             NewFileName = md5sum + lib_ext
Edouard@2594:             extra_files_log = self._extra_files_log_path()
Edouard@2594: 
Edouard@1994:             new_PLC_filename = os.path.join(self.workingdir, NewFileName)
Edouard@1994: 
Edouard@2463:             self.UnLoadPLC()
Edouard@1045: 
Edouard@2594:             self.PurgePLC()
Edouard@2594: 
andrej@1734:             self.LogMessage("NewPLC (%s)" % md5sum)
Edouard@1433: 
greg@229:             try:
greg@229:                 # Create new PLC file
Edouard@2702:                 self.BlobAsFile(plc_object, new_PLC_filename)
Edouard@1433: 
greg@229:                 # Then write the files
andrej@2442:                 log = open(extra_files_log, "w")
Edouard@2463:                 for fname, blobID in extrafiles:
andrej@1740:                     fpath = os.path.join(self.workingdir, fname)
Edouard@2702:                     self.BlobAsFile(blobID, fpath)
greg@229:                     log.write(fname+'\n')
greg@229: 
Edouard@2653:                 # Store new PLC filename based on md5 key
Edouard@2653:                 with open(self._GetMD5FileName(), "w") as f:
Edouard@2653:                     f.write(md5sum)
Edouard@2653:                     f.flush()
Edouard@2653:                     os.fsync(f.fileno())
Edouard@2653: 
greg@229:                 # Store new PLC filename
greg@229:                 self.CurrentPLCFilename = NewFileName
andrej@1780:             except Exception:
andrej@2416:                 self.PLCStatus = PlcStatus.Broken
Edouard@906:                 self.StatusChange()
etisserant@291:                 PLCprint(traceback.format_exc())
greg@229:                 return False
Edouard@906: 
Edouard@2463:             if self.LoadPLC():
andrej@2416:                 self.PLCStatus = PlcStatus.Stopped
Edouard@3282:                 self.StatusChange()
Edouard@906:             else:
Edouard@3282:                 self._fail(_("Problem installing new PLC : can't load PLC"))
Edouard@906: 
andrej@2416:             return self.PLCStatus == PlcStatus.Stopped
greg@229:         return False
greg@229: 
greg@229:     def MatchMD5(self, MD5):
greg@229:         try:
greg@229:             last_md5 = open(self._GetMD5FileName(), "r").read()
greg@229:             return last_md5 == MD5
andrej@1780:         except Exception:
Edouard@1440:             pass
Edouard@1440:         return False
Edouard@1433: 
Edouard@2485:     @RunInMain
greg@229:     def SetTraceVariablesList(self, idxs):
greg@229:         """
Edouard@1433:         Call ctype imported function to append
greg@229:         these indexes to registred variables in PLC debugger
greg@229:         """
Edouard@2485:         self.DebugToken += 1
edouard@462:         if idxs:
edouard@462:             # suspend but dont disable
Edouard@614:             if self._suspendDebug(False) == 0:
Edouard@614:                 # keep a copy of requested idx
Edouard@614:                 self._ResetDebugVariables()
andrej@1740:                 for idx, iectype, force in idxs:
andrej@1743:                     if force is not None:
andrej@1847:                         c_type, _unpack_func, pack_func = \
Edouard@614:                             TypeTranslator.get(iectype,
andrej@1767:                                                (None, None, None))
andrej@1740:                         force = ctypes.byref(pack_func(c_type, force))
Edouard@3395:                     res = self._RegisterDebugVariable(idx, force)
Edouard@3395:                     if res != 0:
Edouard@3395:                         self._resumeDebug()
Edouard@3395:                         self._suspendDebug(True)
Edouard@3395:                         return -res
Edouard@1434:                 self._TracesSwap()
Edouard@614:                 self._resumeDebug()
Edouard@2485:                 return self.DebugToken
edouard@462:         else:
edouard@462:             self._suspendDebug(True)
Edouard@3577:         return 4 # DEBUG_SUSPENDED
Edouard@1434: 
Edouard@1434:     def _TracesSwap(self):
Edouard@1434:         self.LastSwapTrace = time()
andrej@2416:         if self.TraceThread is None and self.PLCStatus == PlcStatus.Started:
Edouard@2600:             self.TraceThread = Thread(target=self.TraceThreadProc, name="PLCTrace")
Edouard@1434:             self.TraceThread.start()
Edouard@1434:         self.TraceLock.acquire()
Edouard@1434:         Traces = self.Traces
Edouard@1434:         self.Traces = []
Edouard@1434:         self.TraceLock.release()
Edouard@1434:         return Traces
Edouard@1434: 
Edouard@1994:     @RunInMain
Edouard@2485:     def GetTraceVariables(self, DebugToken):
andrej@2546:         if DebugToken is not None and DebugToken == self.DebugToken:
Edouard@2485:             return self.PLCStatus, self._TracesSwap()
Edouard@2485:         return PlcStatus.Broken, []
Edouard@1434: 
Edouard@1434:     def TraceThreadProc(self):
Edouard@1434:         """
Edouard@1434:         Return a list of traces, corresponding to the list of required idx
Edouard@1434:         """
Edouard@1994:         self._resumeDebug()  # Re-enable debugger
andrej@2416:         while self.PLCStatus == PlcStatus.Started:
edouard@450:             tick = ctypes.c_uint32()
edouard@450:             size = ctypes.c_uint32()
Edouard@1075:             buff = ctypes.c_void_p()
Edouard@1433:             TraceBuffer = None
Edouard@1994: 
Edouard@1994:             self.PLClibraryLock.acquire()
Edouard@1994: 
Edouard@1994:             res = self._GetDebugData(ctypes.byref(tick),
Edouard@1994:                                      ctypes.byref(size),
Edouard@1997:                                      ctypes.byref(buff))
Edouard@1994:             if res == 0:
Edouard@1994:                 if size.value:
Edouard@1994:                     TraceBuffer = ctypes.string_at(buff.value, size.value)
Edouard@1994:                 self._FreeDebugData()
Edouard@1994: 
Edouard@1994:             self.PLClibraryLock.release()
Edouard@1997: 
Edouard@1994:             # leave thread if GetDebugData isn't happy.
Edouard@1994:             if res != 0:
Edouard@1994:                 break
Edouard@1990: 
Edouard@1433:             if TraceBuffer is not None:
Edouard@1994:                 self.TraceLock.acquire()
Edouard@1994:                 lT = len(self.Traces)
Edouard@1994:                 if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024:
Edouard@1994:                     self.Traces.pop(0)
Edouard@1994:                 self.Traces.append((tick.value, TraceBuffer))
Edouard@1994:                 self.TraceLock.release()
Edouard@1994: 
Edouard@1994:             # TraceProc stops here if Traces not polled for 3 seconds
Edouard@1994:             traces_age = time() - self.LastSwapTrace
Edouard@1994:             if traces_age > 3:
Edouard@1994:                 self.TraceLock.acquire()
Edouard@1994:                 self.Traces = []
Edouard@1994:                 self.TraceLock.release()
Edouard@1994:                 self._suspendDebug(True)  # Disable debugger
Edouard@1994:                 break
Edouard@1994: 
Edouard@1994:         self.TraceThread = None
Edouard@1434: 
Edouard@1440:     def RemoteExec(self, script, *kwargs):
laurent@699:         try:
andrej@2419:             exec(script, kwargs)
andrej@1780:         except Exception:
andrej@1847:             _e_type, e_value, e_traceback = sys.exc_info()
laurent@699:             line_no = traceback.tb_lineno(get_last_traceback(e_traceback))
Edouard@1433:             return (-1, "RemoteExec script failed!\n\nLine %d: %s\n\t%s" %
andrej@1878:                     (line_no, e_value, script.splitlines()[line_no - 1]))
laurent@699:         return (0, kwargs.get("returnVal", None))