# This file is part of Beremiz runtime.
# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
# See COPYING.Runtime file for copyrights details.
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import absolute_import
from threading import Thread, Lock, Event, Condition
import platform as platform_module
from tempfile import mkstemp
from functools import wraps, partial
from six.moves import xrange
from past.builtins import execfile
from runtime.typemapping import TypeTranslator
from runtime.loglevels import LogLevelsDefault, LogLevelsCount
from runtime.Stunnel import getPSKID
from runtime import PlcStatus
from runtime import MainWorker
from runtime import default_evaluator
if os.name in ("nt", "ce"):
dlopen = _ctypes.LoadLibrary
dlclose = _ctypes.FreeLibrary
dlclose = _ctypes.dlclose
def get_last_traceback(tb):
sys.stdout.write("PLCobject : "+message+"\n")
def func_wrapper(*args, **kwargs):
return MainWorker.call(func, *args, **kwargs)
def __init__(self, WorkingDir, argv, statuschange, evaluator, pyruntimevars):
self.workingdir = WorkingDir # must exits already
self.tmpdir = os.path.join(WorkingDir, 'tmp')
if os.path.exists(self.tmpdir):
shutil.rmtree(self.tmpdir)
# FIXME : is argv of any use nowadays ?
self.argv = [WorkingDir] + argv # force argv[0] to be "path" to exec...
self.statuschange = statuschange
self.evaluator = evaluator
self.pyruntimevars = pyruntimevars
self.PLCStatus = PlcStatus.Empty
self.PLClibraryHandle = None
self.PLClibraryLock = Lock()
# Creates fake C funcs proxies
self._loading_error = None
self.python_runtime_vars = None
# First task of worker -> no @RunInMain
def AutoLoad(self, autostart):
# Get the last transfered PLC
self.CurrentPLCFilename = open(
"r").read().strip() + lib_ext
self.PLCStatus = PlcStatus.Stopped
self._fail(_("Problem autostarting PLC : can't load PLC"))
self.PLCStatus = PlcStatus.Empty
self.CurrentPLCFilename = None
if self.statuschange is not None:
for callee in self.statuschange:
def LogMessage(self, *args):
if self._LogMessage is not None:
return self._LogMessage(level, msg, len(msg))
if self._ResetLogCount is not None:
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:
def GetLogMessage(self, level, msgid):
tv_sec = ctypes.c_uint32()
tv_nsec = ctypes.c_uint32()
if self._GetLogMessage is not None:
maxsz = len(self._log_read_buffer)-1
sz = self._GetLogMessage(level, msgid,
self._log_read_buffer, maxsz,
self._log_read_buffer[sz] = '\x00'
return self._log_read_buffer.value, tick.value, tv_sec.value, tv_nsec.value
elif self._loading_error is not None and level == 0:
return self._loading_error, 0, 0, 0
def _GetMD5FileName(self):
return os.path.join(self.workingdir, "lasttransferedPLC.md5")
def _GetLibFileName(self):
return os.path.join(self.workingdir, self.CurrentPLCFilename)
Declare all functions, arguments and return values
md5 = open(self._GetMD5FileName(), "r").read()
self.PLClibraryLock.acquire()
self._PLClibraryHandle = dlopen(self._GetLibFileName())
self.PLClibraryHandle = ctypes.CDLL(self.CurrentPLCFilename, handle=self._PLClibraryHandle)
self.PLC_ID = ctypes.c_char_p.in_dll(self.PLClibraryHandle, "PLC_ID")
self._startPLC = self.PLClibraryHandle.startPLC
self._startPLC.restype = ctypes.c_int
self._startPLC.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)]
self._stopPLC_real = self.PLClibraryHandle.stopPLC
self._stopPLC_real.restype = None
self._PythonIterator = getattr(self.PLClibraryHandle, "PythonIterator", None)
if self._PythonIterator is not None:
self._PythonIterator.restype = ctypes.c_char_p
self._PythonIterator.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int)]
self._stopPLC = self._stopPLC_real
# If python confnode is not enabled, we reuse _PythonIterator
# as a call that block pythonthread until StopPLC
self.PlcStopping = Event()
def PythonIterator(res, blkid, is_last):
self._PythonIterator = PythonIterator
self._stopPLC = __StopPLC
self._ResetDebugVariables = self.PLClibraryHandle.ResetDebugVariables
self._ResetDebugVariables.restype = None
self._RegisterDebugVariable = self.PLClibraryHandle.RegisterDebugVariable
self._RegisterDebugVariable.restype = ctypes.c_int
self._RegisterDebugVariable.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_uint32]
self._FreeDebugData = self.PLClibraryHandle.FreeDebugData
self._FreeDebugData.restype = None
self._GetDebugData = self.PLClibraryHandle.GetDebugData
self._GetDebugData.restype = ctypes.c_int
self._GetDebugData.argtypes = [ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_void_p)]
self._suspendDebug = self.PLClibraryHandle.suspendDebug
self._suspendDebug.restype = ctypes.c_int
self._suspendDebug.argtypes = [ctypes.c_int]
self._resumeDebug = self.PLClibraryHandle.resumeDebug
self._resumeDebug.restype = None
self._ResetLogCount = self.PLClibraryHandle.ResetLogCount
self._ResetLogCount.restype = None
self._GetLogCount = self.PLClibraryHandle.GetLogCount
self._GetLogCount.restype = ctypes.c_uint32
self._GetLogCount.argtypes = [ctypes.c_uint8]
self._LogMessage = self.PLClibraryHandle.LogMessage
self._LogMessage.restype = ctypes.c_int
self._LogMessage.argtypes = [ctypes.c_uint8, ctypes.c_char_p, ctypes.c_uint32]
self._log_read_buffer = ctypes.create_string_buffer(1 << 14) # 16K
self._GetLogMessage = self.PLClibraryHandle.GetLogMessage
self._GetLogMessage.restype = ctypes.c_uint32
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)]
self._loading_error = None
self._loading_error = traceback.format_exc()
PLCprint(self._loading_error)
self.PLClibraryLock.release()
self._loading_error = traceback.format_exc()
PLCprint(self._loading_error)
self.PythonRuntimeCleanup()
def _InitPLCStubCalls(self):
create dummy C func proxies
self._startPLC = lambda x, y: None
self._stopPLC = lambda: None
self._ResetDebugVariables = lambda: None
self._RegisterDebugVariable = lambda x, y: 0
self._IterDebugData = lambda x, y: None
self._FreeDebugData = lambda: None
self._GetDebugData = lambda: -1
self._suspendDebug = lambda x: -1
self._resumeDebug = lambda: None
self._PythonIterator = lambda *a: ""
self._GetLogMessage = None
self._PLClibraryHandle = None
self.PLClibraryHandle = None
This is also called by __init__ to create dummy C func proxies
self.PLClibraryLock.acquire()
# Unload library explicitely
if getattr(self, "_PLClibraryHandle", None) is not None:
dlclose(self._PLClibraryHandle)
# Forget all refs to library
self.PLClibraryLock.release()
def PythonRuntimeCall(self, methodname, use_evaluator=True, reverse_order=False):
Calls init, start, stop or cleanup method provided by
runtime python files, loaded when new PLC uploaded
methods = self.python_runtime_vars.get("_runtime_%s" % methodname, [])
methods = reversed(methods)
_res, exp = self.evaluator(method)
_res, exp = default_evaluator(method)
self.LogMessage(0, '\n'.join(traceback.format_exception(*exp)))
def PythonRuntimeInit(self):
MethodNames = ["init", "start", "stop", "cleanup"]
self.python_runtime_vars = globals().copy()
self.python_runtime_vars.update(self.pyruntimevars)
class PLCSafeGlobals(object):
def __getattr__(self, name):
t = parent.python_runtime_vars["_"+name+"_ctype"]
raise KeyError("Try to get unknown shared global variable : %s" % name)
parent.python_runtime_vars["_PySafeGetPLCGlob_"+name](ctypes.byref(v))
return parent.python_runtime_vars["_"+name+"_unpack"](v)
def __setattr__(self, name, value):
t = parent.python_runtime_vars["_"+name+"_ctype"]
raise KeyError("Try to set unknown shared global variable : %s" % name)
v = parent.python_runtime_vars["_"+name+"_pack"](t, value)
parent.python_runtime_vars["_PySafeSetPLCGlob_"+name](ctypes.byref(v))
class OnChangeStateClass(object):
def __getattr__(self, name):
u = parent.python_runtime_vars["_"+name+"_unpack"]
return type("changedesc",(),dict(
count = parent.python_runtime_vars["_PyOnChangeCount_"+name].value,
first = u(parent.python_runtime_vars["_PyOnChangeFirst_"+name]),
last = u(parent.python_runtime_vars["_PyOnChangeLast_"+name])))
self.python_runtime_vars.update({
"PLCGlobals": PLCSafeGlobals(),
"OnChange": OnChangeStateClass(),
"WorkingDir": self.workingdir,
"PLCBinary": self.PLClibraryHandle,
for methodname in MethodNames:
self.python_runtime_vars["_runtime_%s" % methodname] = []
filenames = os.listdir(self.workingdir)
for filename in filenames:
name, ext = os.path.splitext(filename)
if name.upper().startswith("RUNTIME") and ext.upper() == ".PY":
execfile(os.path.join(self.workingdir, filename), self.python_runtime_vars)
for methodname in MethodNames:
method = self.python_runtime_vars.get("_%s_%s" % (name, methodname), None)
self.python_runtime_vars["_runtime_%s" % methodname].append(method)
self.LogMessage(0, traceback.format_exc())
self.PythonRuntimeCall("init", use_evaluator=False)
self.PythonThreadCondLock = Lock()
self.PythonThreadCmdCond = Condition(self.PythonThreadCondLock)
self.PythonThreadAckCond = Condition(self.PythonThreadCondLock)
self.PythonThreadCmd = None
self.PythonThreadAck = None
self.PythonThread = Thread(target=self.PythonThreadProc, name="PLCPythonThread")
self.PythonThread.start()
def PythonRuntimeCleanup(self):
if self.python_runtime_vars is not None:
self.PythonThreadCommand("Finish")
self.PythonRuntimeCall("cleanup", use_evaluator=False, reverse_order=True)
self.python_runtime_vars = None
def PythonThreadLoop(self):
res, cmd, blkid, is_last = "None", "None", ctypes.c_void_p(), ctypes.c_int()
cmd = self._PythonIterator(res, blkid, ctypes.byref(is_last))
GOING_IDLE = is_last.value != 0
self.python_runtime_vars["FBID"] = FBID
ccmd, AST = compile_cache.get(FBID, (None, None))
if ccmd is None or ccmd != cmd:
AST = compile(cmd, '<plc>', 'eval')
compile_cache[FBID] = (cmd, AST)
result, exp = self.evaluator(eval, AST, self.python_runtime_vars)
res = "#EXCEPTION : "+str(exp[1])
self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (
FBID, cmd, '\n'.join(traceback.format_exception(*exp))))
self.python_runtime_vars["FBID"] = None
res = "#EXCEPTION : "+str(e)
self.LogMessage(1, ('PyEval@0x%x(Code="%s") Exception "%s"') % (FBID, cmd, str(e)))
todo = self.python_runtime_vars["OnIdle"]
def PythonThreadProc(self):
self.PythonThreadCondLock.acquire()
cmd = self.PythonThreadCmd
self.PythonThreadCmdCond.wait()
cmd = self.PythonThreadCmd
self.PythonThreadCmd = None
self.PythonThreadCondLock.release()
# Ack once PreStart done, must be finished before StartPLC
self.PythonThreadAcknowledge(cmd)
# Ack Immediately, for responsiveness
self.PythonThreadAcknowledge(cmd)
self.PythonRuntimeCall("start")
self.LogMessage("Python extensions started")
self.PythonRuntimeCall("stop", reverse_order=True)
self.PythonThreadAcknowledge(cmd)
def PythonThreadAcknowledge(self, ack):
self.PythonThreadCondLock.acquire()
self.PythonThreadAck = ack
self.PythonThreadAckCond.notify()
self.PythonThreadCondLock.release()
def PythonThreadCommand(self, cmd):
self.PythonThreadCondLock.acquire()
self.PythonThreadCmd = cmd
self.PythonThreadCmdCond.notify()
self.PythonThreadAckCond.wait()
ack = self.PythonThreadAck
self.PythonThreadAck = None
self.PythonThreadCondLock.release()
self.PLCStatus = PlcStatus.Broken
Here goes actions to be taken just before PLC starts,
with all libraries and python object already created.
For example : restore saved proprietary parameters
self.LogMessage(0, 'Post Start Exception'+'\n'.join(
traceback.format_exception(*sys.exc_info())))
Here goes actions to be taken after PLC is started,
with all libraries and python object already created,
and python extensions "Start" methods being called.
This is called before python thread processing py_eval blocks starts.
For example : attach additional ressource to web services
if self.PLClibraryHandle is None:
self._fail(_("Problem starting PLC : can't load PLC"))
if self.CurrentPLCFilename is not None and self.PLCStatus == PlcStatus.Stopped:
self.PythonThreadCommand("PreStart")
c_argv = ctypes.c_char_p * len(self.argv)
res = self._startPLC(len(self.argv), c_argv(*self.argv))
self.LogMessage("PLC started")
self.PLCStatus = PlcStatus.Started
self.PythonThreadCommand("Start")
self._fail(_("Problem starting PLC : error %d" % res))
if self.PLCStatus == PlcStatus.Started:
self.LogMessage("PLC stopped")
self.PLCStatus = PlcStatus.Stopped
if self.TraceThread is not None:
return self._GetPLCstatus()
return (PlcStatus.Disconnected, None)
return self.PLCStatus, map(self.GetLogCount, xrange(LogLevelsCount))
return getPSKID(partial(self.LogMessage, 0))
self.blobs = {} # dict of list
if os.path.exists(self.tmpdir):
shutil.rmtree(self.tmpdir)
def _append_blob(self, blob, newBlobID):
self.blobs.setdefault(newBlobID,[]).append(blob)
def _pop_blob(self, blobID):
blobs = self.blobs.pop(blobID, None)
# insert same blob list back if not empty
blobs = self.blobs[blobID] = blobs
def SeedBlob(self, seed):
blob = (mkstemp(dir=self.tmpdir) + (hashlib.new('md5'),))
_fd, _path, md5sum = blob
newBlobID = md5sum.digest()
self._append_blob(blob, newBlobID)
def AppendChunkToBlob(self, data, blobID):
blob = self._pop_blob(blobID)
newBlobID = md5sum.digest()
self._append_blob(blob, newBlobID)
for blobs in list(self.blobs.values()):
for fd, _path, _md5sum in blobs:
def BlobAsFile(self, blobID, newpath):
blob = self._pop_blob(blobID)
raise Exception(_("Missing data to create file: {}").format(newpath))
self._BlobAsFile(blob, newpath)
def _BlobAsFile(self, blob, newpath):
shutil.move(path, newpath)
def _extra_files_log_path(self):
return os.path.join(self.workingdir, "extra_files.txt")
extra_files_log = self._extra_files_log_path()
old_PLC_filename = os.path.join(self.workingdir, self.CurrentPLCFilename) \
if self.CurrentPLCFilename is not None \
allfiles = open(extra_files_log, "rt").readlines()
allfiles.extend([extra_files_log, old_PLC_filename, self._GetMD5FileName()])
self.LogMessage("No files to purge")
for filename in allfiles:
filename = filename.strip()
os.remove(os.path.join(self.workingdir, filename))
self.LogMessage("Couldn't purge " + filename)
self.PLCStatus = PlcStatus.Empty
# TODO: PLCObject restart
def NewPLC(self, md5sum, plc_object, extrafiles):
if self.PLCStatus in [PlcStatus.Stopped, PlcStatus.Empty, PlcStatus.Broken]:
NewFileName = md5sum + lib_ext
extra_files_log = self._extra_files_log_path()
new_PLC_filename = os.path.join(self.workingdir, NewFileName)
self.LogMessage("NewPLC (%s)" % md5sum)
self.BlobAsFile(plc_object, new_PLC_filename)
log = open(extra_files_log, "w")
for fname, blobID in extrafiles:
fpath = os.path.join(self.workingdir, fname)
self.BlobAsFile(blobID, fpath)
# Store new PLC filename based on md5 key
with open(self._GetMD5FileName(), "w") as f:
self.CurrentPLCFilename = NewFileName
self.PLCStatus = PlcStatus.Broken
PLCprint(traceback.format_exc())
self.PLCStatus = PlcStatus.Stopped
self._fail(_("Problem installing new PLC : can't load PLC"))
return self.PLCStatus == PlcStatus.Stopped
last_md5 = open(self._GetMD5FileName(), "r").read()
def SetTraceVariablesList(self, idxs):
Call ctype imported function to append
these indexes to registred variables in PLC debugger
# suspend but dont disable
if self._suspendDebug(False) == 0:
# keep a copy of requested idx
self._ResetDebugVariables()
for idx, iectype, force in idxs:
c_type, _unpack_func, pack_func = \
TypeTranslator.get(iectype,
force = ctypes.byref(pack_func(c_type, force))
res = self._RegisterDebugVariable(idx, force)
return 4 # DEBUG_SUSPENDED
self.LastSwapTrace = time()
if self.TraceThread is None and self.PLCStatus == PlcStatus.Started:
self.TraceThread = Thread(target=self.TraceThreadProc, name="PLCTrace")
def GetTraceVariables(self, DebugToken):
if DebugToken is not None and DebugToken == self.DebugToken:
return self.PLCStatus, self._TracesSwap()
return PlcStatus.Broken, []
def TraceThreadProc(self):
Return a list of traces, corresponding to the list of required idx
self._resumeDebug() # Re-enable debugger
while self.PLCStatus == PlcStatus.Started:
self.PLClibraryLock.acquire()
res = self._GetDebugData(ctypes.byref(tick),
TraceBuffer = ctypes.string_at(buff.value, size.value)
self.PLClibraryLock.release()
# leave thread if GetDebugData isn't happy.
if TraceBuffer is not None:
if lT != 0 and lT * len(self.Traces[0]) > 1024 * 1024:
self.Traces.append((tick.value, TraceBuffer))
# TraceProc stops here if Traces not polled for 3 seconds
traces_age = time() - self.LastSwapTrace
self._suspendDebug(True) # Disable debugger
def RemoteExec(self, script, *kwargs):
_e_type, e_value, e_traceback = sys.exc_info()
line_no = traceback.tb_lineno(get_last_traceback(e_traceback))
return (-1, "RemoteExec script failed!\n\nLine %d: %s\n\t%s" %
(line_no, e_value, script.splitlines()[line_no - 1]))
return (0, kwargs.get("returnVal", None))
return platform_module.system() + " " + platform_module.release()