# HG changeset patch # User Edouard Tisserant # Date 1534860662 -7200 # Node ID d9175daf6522426cb2c8ebcd689edda3ae32aeb3 # Parent 2e38b5ec4753c2c6bd24d1371f870fec75805550 Refactoring. Separated PLC Object, PYRO Server and MainWorker : - PLC Object is now a Singleton, instantiated through runtime.CreatePLCObjectSingleton(...) - Pyro server doesn't hold any reference to PLCObject, and doesn't create it anymore - PLC Object class doesn't inherit from Pyro.ObjBase anymore - Pyro related code moved to runtime.PyroServer.py - MainWorker class moved to runtime/Worker.py - Both PLC Object and MainWorker creation happens in runtime/__init__.py diff -r 2e38b5ec4753 -r d9175daf6522 Beremiz_service.py --- a/Beremiz_service.py Thu Aug 16 11:22:40 2018 +0200 +++ b/Beremiz_service.py Tue Aug 21 16:11:02 2018 +0200 @@ -31,13 +31,12 @@ import getopt import threading from threading import Thread, Semaphore, Lock -import traceback import __builtin__ -import Pyro -import Pyro.core as pyro - -from runtime import PLCObject, ServicePublisher, MainWorker + +import runtime +from runtime.PyroServer import Server from runtime.xenomai import TryPreloadXenomai +from runtime import LogMessageAndException import util.paths as paths @@ -242,12 +241,11 @@ TBMENU_CHANGE_WD = wx.NewId() TBMENU_QUIT = wx.NewId() - def __init__(self, pyroserver, level): + def __init__(self, pyroserver): wx.TaskBarIcon.__init__(self) self.pyroserver = pyroserver # Set the image self.UpdateIcon(None) - self.level = level # bind some events self.Bind(wx.EVT_MENU, self.OnTaskBarStartPLC, id=self.TBMENU_START) @@ -270,15 +268,14 @@ menu = wx.Menu() menu.Append(self.TBMENU_START, _("Start PLC")) menu.Append(self.TBMENU_STOP, _("Stop PLC")) - if self.level == 1: - menu.AppendSeparator() - menu.Append(self.TBMENU_CHANGE_NAME, _("Change Name")) - menu.Append(self.TBMENU_CHANGE_INTERFACE, _("Change IP of interface to bind")) - menu.Append(self.TBMENU_CHANGE_PORT, _("Change Port Number")) - menu.Append(self.TBMENU_CHANGE_WD, _("Change working directory")) - menu.AppendSeparator() - menu.Append(self.TBMENU_LIVE_SHELL, _("Launch a live Python shell")) - menu.Append(self.TBMENU_WXINSPECTOR, _("Launch WX GUI inspector")) + menu.AppendSeparator() + menu.Append(self.TBMENU_CHANGE_NAME, _("Change Name")) + menu.Append(self.TBMENU_CHANGE_INTERFACE, _("Change IP of interface to bind")) + menu.Append(self.TBMENU_CHANGE_PORT, _("Change Port Number")) + menu.Append(self.TBMENU_CHANGE_WD, _("Change working directory")) + menu.AppendSeparator() + menu.Append(self.TBMENU_LIVE_SHELL, _("Launch a live Python shell")) + menu.Append(self.TBMENU_WXINSPECTOR, _("Launch WX GUI inspector")) menu.AppendSeparator() menu.Append(self.TBMENU_QUIT, _("Quit")) return menu @@ -297,19 +294,10 @@ return icon def OnTaskBarStartPLC(self, evt): - if self.pyroserver.plcobj is not None: - plcstatus = self.pyroserver.plcobj.GetPLCstatus()[0] - if plcstatus is "Stopped": - self.pyroserver.plcobj.StartPLC() - else: - print(_("PLC is empty or already started.")) + runtime.GetPLCObjectSingleton().StartPLC() def OnTaskBarStopPLC(self, evt): - if self.pyroserver.plcobj is not None: - if self.pyroserver.plcobj.GetPLCstatus()[0] == "Started": - Thread(target=self.pyroserver.plcobj.StopPLC).start() - else: - print(_("PLC is not started.")) + runtime.GetPLCObjectSingleton().StopPLC() def OnTaskBarChangeInterface(self, evt): ip_addr = self.pyroserver.ip_addr @@ -345,10 +333,7 @@ self.pyroserver.Restart() def _LiveShellLocals(self): - if self.pyroserver.plcobj is not None: - return {"locals": self.pyroserver.plcobj.python_runtime_vars} - else: - return {} + return {"locals": runtime.GetPLCObjectSingleton().python_runtime_vars} def OnTaskBarLiveShell(self, evt): from wx import py @@ -391,90 +376,6 @@ res = (None, sys.exc_info()) return res - -class Server(object): - def __init__(self, servicename, ip_addr, port, - workdir, argv, - statuschange=None, evaluator=default_evaluator, - pyruntimevars=None): - self.continueloop = True - self.daemon = None - self.servicename = servicename - self.ip_addr = ip_addr - self.port = port - self.workdir = workdir - self.argv = argv - self.servicepublisher = None - self.statuschange = statuschange - self.evaluator = evaluator - self.pyruntimevars = pyruntimevars - 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.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() - - def Restart(self): - self._stop() - - def Quit(self): - self.continueloop = False - if self.plcobj is not None: - self.plcobj.StopPLC() - self.plcobj.UnLoadPLC() - self._stop() - - def _stop(self): - if self.plcobj is not None: - self.plcobj.StopPLC() - if self.servicepublisher is not None: - self.servicepublisher.UnRegisterService() - 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 with warnings.catch_warnings(): @@ -522,31 +423,11 @@ wx.CallAfter(wx_evaluator, o) wx_eval_lock.acquire() return o.res - - pyroserver = Server(servicename, given_ip, port, - WorkingDir, argv, - statuschange, evaluator, pyruntimevars) - - taskbar_instance = BeremizTaskBarIcon(pyroserver, enablewx) else: - pyroserver = Server(servicename, given_ip, port, - WorkingDir, argv, - statuschange, pyruntimevars=pyruntimevars) - + evaluator = default_evaluator # Exception hooks - -def LogMessageAndException(msg, exp=None): - if exp is None: - exp = sys.exc_info() - if pyroserver.plcobj is not None: - pyroserver.plcobj.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp))) - else: - print(msg) - traceback.print_exception(*exp) - - def LogException(*exp): LogMessageAndException("", exp) @@ -596,6 +477,15 @@ sys.path.append(extension_folder) execfile(os.path.join(extension_folder, extention_file), locals()) + +runtime.CreatePLCObjectSingleton( + WorkingDir, argv, statuschange, evaluator, pyruntimevars) + +pyroserver = Server(servicename, given_ip, port) + +if havewx: + taskbar_instance = BeremizTaskBarIcon(pyroserver) + if havetwisted: if webport is not None: try: @@ -625,6 +515,11 @@ pyroserver.PrintServerInfo() +# Beremiz IDE detects LOCAL:// runtime is ready by looking +# for self.workdir in the daemon's stdout. +print(_("Current working directory :"), WorkingDir) + + if havetwisted or havewx: ui_thread_started = Lock() ui_thread_started.acquire() @@ -651,9 +546,15 @@ print("UI thread started successfully.") try: - MainWorker.runloop(pyroserver.AutoLoad) + runtime.MainWorker.runloop( + runtime.GetPLCObjectSingleton().AutoLoad, autostart) except KeyboardInterrupt: pass pyroserver.Quit() + +plcobj = runtime.GetPLCObjectSingleton() +plcobj.StopPLC() +plcobj.UnLoadPLC() + sys.exit(0) diff -r 2e38b5ec4753 -r d9175daf6522 runtime/PLCObject.py --- a/runtime/PLCObject.py Thu Aug 16 11:22:40 2018 +0200 +++ b/runtime/PLCObject.py Tue Aug 21 16:11:02 2018 +0200 @@ -23,7 +23,6 @@ from __future__ import absolute_import -import thread from threading import Thread, Lock, Semaphore, Event, Condition import ctypes import os @@ -31,10 +30,10 @@ import traceback from time import time import _ctypes # pylint: disable=wrong-import-order -import Pyro.core as pyro from runtime.typemapping import TypeTranslator from runtime.loglevels import LogLevelsDefault, LogLevelsCount +from runtime import MainWorker if os.name in ("nt", "ce"): dlopen = _ctypes.LoadLibrary @@ -61,107 +60,6 @@ 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): @@ -169,22 +67,19 @@ return func_wrapper -class PLCObject(pyro.ObjBase): - def __init__(self, server): - pyro.ObjBase.__init__(self) - self.evaluator = server.evaluator - self.argv = [server.workdir] + server.argv # force argv[0] to be "path" to exec... - self.workingdir = server.workdir +class PLCObject(object): + def __init__(self, WorkingDir, argv, statuschange, evaluator, pyruntimevars): + self.workingdir = WorkingDir + # 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 = "Empty" self.PLClibraryHandle = None self.PLClibraryLock = Lock() - self.DummyIteratorLock = None # Creates fake C funcs proxies self._InitPLCStubCalls() - self.daemon = server.daemon - self.statuschange = server.statuschange - self.hmi_frame = None - self.pyruntimevars = server.pyruntimevars self._loading_error = None self.python_runtime_vars = None self.TraceThread = None @@ -192,7 +87,7 @@ self.Traces = [] # First task of worker -> no @RunInMain - def AutoLoad(self): + def AutoLoad(self, autostart): # Get the last transfered PLC try: self.CurrentPLCFilename = open( @@ -200,10 +95,15 @@ "r").read().strip() + lib_ext if self.LoadPLC(): self.PLCStatus = "Stopped" + if autostart: + self.StartPLC() + return except Exception: self.PLCStatus = "Empty" self.CurrentPLCFilename = None + self.StatusChange() + def StatusChange(self): if self.statuschange is not None: for callee in self.statuschange: diff -r 2e38b5ec4753 -r d9175daf6522 runtime/PyroServer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/PyroServer.py Tue Aug 21 16:11:02 2018 +0200 @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz runtime. + +# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD +# Copyright (C) 2017: Andrey Skvortsov +# Copyright (C) 2018: Edouard TISSERANT + +# See COPYING file for copyrights details. + +import sys + +import Pyro +import Pyro.core as pyro +import runtime +from runtime.ServicePublisher import ServicePublisher + +class Server(object): + def __init__(self, servicename, ip_addr, port): + self.continueloop = True + self.daemon = None + self.servicename = servicename + self.ip_addr = ip_addr + self.port = port + self.servicepublisher = None + + 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) + + 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) + + pyro_obj=Pyro.core.ObjBase() + pyro_obj.delegateTo(runtime.GetPLCObjectSingleton()) + + self.daemon.connect(pyro_obj, "PLCObject") + + if self._to_be_published(): + self.servicepublisher = ServicePublisher() + self.servicepublisher.RegisterService(self.servicename, self.ip_addr, self.port) + + when_ready() + self.daemon.requestLoop() + self.daemon.sock.close() + + def Restart(self): + self._stop() + + def Quit(self): + self.continueloop = False + self._stop() + + def _stop(self): + if self.servicepublisher is not None: + self.servicepublisher.UnRegisterService() + self.servicepublisher = None + self.daemon.shutdown(True) + + + diff -r 2e38b5ec4753 -r d9175daf6522 runtime/Worker.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/Worker.py Tue Aug 21 16:11:02 2018 +0200 @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz runtime. +# +# Copyright (C) 2018: Edouard TISSERANT +# +# See COPYING.Runtime file for copyrights details. + +from __future__ import absolute_import +import thread +from threading import Lock, Condition + +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() diff -r 2e38b5ec4753 -r d9175daf6522 runtime/__init__.py --- a/runtime/__init__.py Thu Aug 16 11:22:40 2018 +0200 +++ b/runtime/__init__.py Tue Aug 21 16:11:02 2018 +0200 @@ -1,28 +1,32 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# 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. +from __future__ import absolute_import +import traceback -# 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. +from runtime.Worker import worker +MainWorker = worker() -# 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 runtime.PLCObject import PLCObject -from __future__ import absolute_import -import os +_PLCObjectSingleton = None -from runtime.PLCObject import PLCObject, PLCprint, MainWorker -import runtime.ServicePublisher +def GetPLCObjectSingleton(): + global _PLCObjectSingleton + assert(_PLCObjectSingleton is not None) + return _PLCObjectSingleton + + +def LogMessageAndException(msg, exp=None): + global _PLCObjectSingleton + if exp is None: + exp = sys.exc_info() + if _PLCObjectSingleton is not None: + _PLCObjectSingleton.LogMessage(0, msg + '\n'.join(traceback.format_exception(*exp))) + else: + print(msg) + traceback.print_exception(*exp) + +def CreatePLCObjectSingleton(*args): + global _PLCObjectSingleton + _PLCObjectSingleton = PLCObject(*args)