# HG changeset patch # User Edouard Tisserant # Date 1701985292 -3600 # Node ID 0b3ac94f494c6ab2426f79628a3a86f6710ca9b8 # Parent 89549813a6c19c8e72706fbd303f980620faa9fa# Parent 7e17f7e02a2bdf21efd0de93f868e52accc2c146 Merge diff -r 89549813a6c1 -r 0b3ac94f494c connectors/ConnectorBase.py --- a/connectors/ConnectorBase.py Sat Nov 25 00:18:05 2023 +0100 +++ b/connectors/ConnectorBase.py Thu Dec 07 22:41:32 2023 +0100 @@ -5,12 +5,21 @@ import hashlib +from runtime import PlcStatus class ConnectorBase(object): chuncksize = 1024*1024 + PLCObjDefaults = { + "StartPLC": False, + "GetTraceVariables": (PlcStatus.Broken, None), + "GetPLCstatus": (PlcStatus.Broken, None), + "RemoteExec": (-1, "RemoteExec script failed!"), + "GetVersions": "*** Unknown ***" + } + def BlobFromFile(self, filepath, seed): s = hashlib.new('md5') s.update(seed.encode()) diff -r 89549813a6c1 -r 0b3ac94f494c connectors/PYRO/__init__.py --- a/connectors/PYRO/__init__.py Sat Nov 25 00:18:05 2023 +0100 +++ b/connectors/PYRO/__init__.py Thu Dec 07 22:41:32 2023 +0100 @@ -34,7 +34,6 @@ # TODO: PSK -from runtime import PlcStatus import importlib @@ -64,6 +63,9 @@ RemotePLCObjectProxy._pyroTimeout = 60 + class MissingCallException(Exception): + pass + def PyroCatcher(func, default=None): """ A function that catch a Pyro exceptions, write error to logger @@ -77,6 +79,8 @@ confnodesroot.logger.write_error(_("Connection lost!\n")) except Pyro5.errors.ProtocolError as e: confnodesroot.logger.write_error(_("Pyro exception: %s\n") % e) + except MissingCallException as e: + confnodesroot.logger.write_warning(_("Remote call not supported: %s\n") % e.message) except Exception as e: errmess = ''.join(Pyro5.errors.get_pyro_traceback()) confnodesroot.logger.write_error(errmess + "\n") @@ -94,13 +98,6 @@ ID, secret = IDPSK PSK.UpdateID(confnodesroot.ProjectPath, ID, secret, uri) - _special_return_funcs = { - "StartPLC": False, - "GetTraceVariables": (PlcStatus.Broken, None), - "GetPLCstatus": (PlcStatus.Broken, None), - "RemoteExec": (-1, "RemoteExec script failed!") - } - class PyroProxyProxy(object): """ A proxy proxy class to handle Beremiz Pyro interface specific behavior. @@ -110,8 +107,12 @@ 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, _special_return_funcs.get(attrName, None)) + call = RemotePLCObjectProxy.__getattr__(attrName) + if call is None: + raise MissingCallException(attrName) + else: + return call(*args, **kwargs) + member = PyroCatcher(my_local_func, self.PLCObjDefaults.get(attrName, None)) self.__dict__[attrName] = member return member diff -r 89549813a6c1 -r 0b3ac94f494c connectors/WAMP/__init__.py --- a/connectors/WAMP/__init__.py Sat Nov 25 00:18:05 2023 +0100 +++ b/connectors/WAMP/__init__.py Thu Dec 07 22:41:32 2023 +0100 @@ -35,7 +35,6 @@ from autobahn.wamp.exception import TransportLost from autobahn.wamp.serializer import MsgPackSerializer -from runtime import PlcStatus _WampSession = None _WampConnection = None @@ -56,14 +55,6 @@ print('WAMP session left') -PLCObjDefaults = { - "StartPLC": False, - "GetTraceVariables": ("Broken", None), - "GetPLCstatus": (PlcStatus.Broken, None), - "RemoteExec": (-1, "RemoteExec script failed!") -} - - def _WAMP_connector_factory(cls, uri, confnodesroot): """ WAMP://127.0.0.1:12345/path#realm#ID @@ -107,26 +98,6 @@ AddToDoBeforeQuit(reactor.stop) reactor.run(installSignalHandlers=False) - def WampSessionProcMapper(funcname): - wampfuncname = str('.'.join((ID, funcname))) - - def catcher_func(*args, **kwargs): - if _WampSession is not None: - try: - return threads.blockingCallFromThread( - reactor, _WampSession.call, wampfuncname, - *args, **kwargs) - except TransportLost: - confnodesroot.logger.write_error(_("Connection lost!\n")) - confnodesroot._SetConnector(None) - except Exception: - errmess = traceback.format_exc() - confnodesroot.logger.write_error(errmess+"\n") - print(errmess) - # confnodesroot._SetConnector(None) - return PLCObjDefaults.get(funcname) - return catcher_func - class WampPLCObjectProxy(object): def __init__(self): global _WampConnection @@ -144,10 +115,30 @@ # # reactor.stop() + def WampSessionProcMapper(self, funcname): + wampfuncname = str('.'.join((ID, funcname))) + + def catcher_func(*args, **kwargs): + if _WampSession is not None: + try: + return threads.blockingCallFromThread( + reactor, _WampSession.call, wampfuncname, + *args, **kwargs) + except TransportLost: + confnodesroot.logger.write_error(_("Connection lost!\n")) + confnodesroot._SetConnector(None) + except Exception: + errmess = traceback.format_exc() + confnodesroot.logger.write_error(errmess+"\n") + print(errmess) + # confnodesroot._SetConnector(None) + return self.PLCObjDefaults.get(funcname) + return catcher_func + def __getattr__(self, attrName): member = self.__dict__.get(attrName, None) if member is None: - member = WampSessionProcMapper(attrName) + member = self.WampSessionProcMapper(attrName) self.__dict__[attrName] = member return member diff -r 89549813a6c1 -r 0b3ac94f494c modbus/mb_runtime.c --- a/modbus/mb_runtime.c Sat Nov 25 00:18:05 2023 +0100 +++ b/modbus/mb_runtime.c Thu Dec 07 22:41:32 2023 +0100 @@ -690,7 +690,7 @@ res |= pthread_attr_init(&attr); res |= pthread_create(&(client_nodes[index].timer_thread_id), &attr, &__mb_client_timer_thread, (void *)((char *)NULL + index)); if (res != 0) { - fprintf(stderr, "Modbus plugin: Error starting timer thread for modbus client node %%s\n", client_nodes[index].location); + fprintf(stderr, "Modbus plugin: Error (%%d) starting timer thread for modbus client node %%s\n", res, client_nodes[index].location); goto error_exit; } } @@ -703,7 +703,7 @@ res |= pthread_attr_init(&attr); res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index)); if (res != 0) { - fprintf(stderr, "Modbus plugin: Error starting thread for modbus client node %%s\n", client_nodes[index].location); + fprintf(stderr, "Modbus plugin: Error (%%d) starting thread for modbus client node %%s\n", res, client_nodes[index].location); goto error_exit; } } @@ -730,7 +730,7 @@ res |= pthread_attr_init(&attr); res |= pthread_create(&(server_nodes[index].thread_id), &attr, &__mb_server_thread, (void *)&(server_nodes[index])); if (res != 0) { - fprintf(stderr, "Modbus plugin: Error starting modbus server thread for node %%s\n", server_nodes[index].location); + fprintf(stderr, "Modbus plugin: Error (%%d) starting modbus server thread for node %%s\n", res, server_nodes[index].location); goto error_exit; } } diff -r 89549813a6c1 -r 0b3ac94f494c runtime/NevowServer.py --- a/runtime/NevowServer.py Sat Nov 25 00:18:05 2023 +0100 +++ b/runtime/NevowServer.py Thu Dec 07 22:41:32 2023 +0100 @@ -28,7 +28,6 @@ import os import collections import shutil -import platform as platform_module from zope.interface import implementer from nevow import appserver, inevow, tags, loaders, athena, url, rend from nevow.page import renderer @@ -99,6 +98,17 @@ self.bindingsNames.append(name) + customSettingsURLs = {} + def addCustomURL(self, segment, func): + self.customSettingsURLs[segment] = func + + def removeCustomURL(self, segment): + del self.customSettingsURLs[segment] + + def customLocateChild(self, ctx, segments): + segment = segments[0] + if segment in self.customSettingsURLs: + return self.customSettingsURLs[segment](ctx, segments) ConfigurableSettings = ConfigurableBindings() @@ -112,13 +122,12 @@ global extensions_settings_od extensions_settings_od.pop(token) + class ISettings(annotate.TypedInterface): platform = annotate.String(label=_("Platform"), - default=platform_module.system() + - " " + platform_module.release(), + default=lambda *a,**k:GetPLCObjectSingleton().GetVersions(), immutable=True) - # TODO version ? # pylint: disable=no-self-argument def sendLogMessage( @@ -159,30 +168,26 @@ "Upload a file to PLC working directory"), action=_("Upload")) -customSettingsURLs = { -} - extensions_settings_od = collections.OrderedDict() + +CSS_tags = [tags.link(rel='stylesheet', + type='text/css', + href=url.here.child("webform_css")), + tags.link(rel='stylesheet', + type='text/css', + href=url.here.child("webinterface_css"))] + @implementer(ISettings) -class SettingsPage(rend.Page): - # We deserve a slash +class StyledSettingsPage(rend.Page): addSlash = True # This makes webform_css url answer some default CSS child_webform_css = webform.defaultCSS child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css') + +class SettingsPage(StyledSettingsPage): - def __getattr__(self, name): - global extensions_settings_od - if name.startswith('configurable_'): - token = name[13:] - def configurable_something(ctx): - settings, _display = extensions_settings_od[token] - return settings - return configurable_something - raise AttributeError - def extensions_settings(self, context, data): """ Project extensions settings Extensions added to Configuration Tree in IDE have their setting rendered here @@ -191,25 +196,22 @@ res = [] for token in extensions_settings_od: _settings, display = extensions_settings_od[token] - res += [tags.h2[display], webform.renderForms(token)] + res += [tags.p[tags.a(href=token)[display]]] return res docFactory = loaders.stan([tags.html[ tags.head[ tags.title[_("Beremiz Runtime Settings")], - tags.link(rel='stylesheet', - type='text/css', - href=url.here.child("webform_css")), - tags.link(rel='stylesheet', - type='text/css', - href=url.here.child("webinterface_css")) + CSS_tags ], tags.body[ + tags.h1["Settings"], tags.a(href='/')['Back'], - tags.h1["Runtime settings:"], + tags.h2["Runtime service"], webform.renderForms('staticSettings'), - tags.h1["Extensions settings:"], + tags.h2["Target specific"], webform.renderForms('dynamicSettings'), + tags.h2["Extensions"], extensions_settings ]]]) @@ -242,10 +244,47 @@ shutil.copyfileobj(fobj,destfd) def locateChild(self, ctx, segments): - if segments[0] in customSettingsURLs: - return customSettingsURLs[segments[0]](ctx, segments) + segment = segments[0] + if segment in extensions_settings_od: + settings, display = extensions_settings_od[segment] + return ExtensionSettingsPage(settings, display), segments[1:] + else: + res = ConfigurableSettings.customLocateChild(ctx, segments) + if res: + return res return super(SettingsPage, self).locateChild(ctx, segments) +class ExtensionSettingsPage(StyledSettingsPage): + + docFactory = loaders.stan([ + tags.html[ + tags.head()[ + tags.title[tags.directive("title")], + CSS_tags + ], + tags.body[ + tags.h1[tags.directive("title")], + tags.a(href='/settings')['Back'], + webform.renderForms('settings') + ]]]) + + def render_title(self, ctx, data): + return self._display_name + + def configurable_settings(self, ctx): + return self._settings + + def __init__(self, settings, display): + self._settings = settings + self._display_name = display + + def locateChild(self, ctx, segments): + res = self._settings.customLocateChild(ctx, segments) + if res: + return res + return super(ExtensionSettingsPage, self).locateChild(ctx, segments) + + def RegisterWebsite(iface, port): website = SettingsPage() site = appserver.NevowSite(website) diff -r 89549813a6c1 -r 0b3ac94f494c runtime/PLCObject.py --- a/runtime/PLCObject.py Sat Nov 25 00:18:05 2023 +0100 +++ b/runtime/PLCObject.py Thu Dec 07 22:41:32 2023 +0100 @@ -28,6 +28,7 @@ import sys import traceback import shutil +import platform as platform_module from time import time import hashlib from tempfile import mkstemp @@ -811,3 +812,7 @@ 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)) + + def GetVersions(self): + return platform_module.system() + " " + platform_module.release() + diff -r 89549813a6c1 -r 0b3ac94f494c runtime/WampClient.py --- a/runtime/WampClient.py Sat Nov 25 00:18:05 2023 +0100 +++ b/runtime/WampClient.py Thu Dec 07 22:41:32 2023 +0100 @@ -490,11 +490,13 @@ def RegisterWebSettings(NS): - NS.ConfigurableSettings.addSettings( + WebSettings = NS.newExtensionSetting("Wamp Extension Settings", "wamp_settings") + WebSettings.addSettings( "wamp", _("Wamp Settings"), webFormInterface, _("Set"), wampConfig) - NS.customSettingsURLs[WAMP_SECRET_URL] = deliverWampSecret + WebSettings.addCustomURL(WAMP_SECRET_URL, deliverWampSecret) + diff -r 89549813a6c1 -r 0b3ac94f494c runtime/spawn_subprocess.py --- a/runtime/spawn_subprocess.py Sat Nov 25 00:18:05 2023 +0100 +++ b/runtime/spawn_subprocess.py Thu Dec 07 22:41:32 2023 +0100 @@ -15,17 +15,24 @@ fsencoding = sys.getfilesystemencoding() class Popen(object): - def __init__(self, args, stdin=None, stdout=None): + def __init__(self, args, stdin=None, stdout=None, stderr=None): self.returncode = None self.stdout = None + self.stderr = None self.stdin = None - # TODO: stderr file_actions = posix_spawn.FileActions() if stdout is not None: # child's stdout, child 2 parent pipe + c1pread, c1pwrite = os.pipe() + # attach child's stdout to writing en of c1p pipe + file_actions.add_dup2(c1pwrite, 1) + # close other end + file_actions.add_close(c1pread) + if stderr is not None: + # child's stderr, child 2 parent pipe c2pread, c2pwrite = os.pipe() - # attach child's stdout to writing en of c2p pipe - file_actions.add_dup2(c2pwrite, 1) + # attach child's stderr to writing en of c2p pipe + file_actions.add_dup2(c2pwrite, 2) # close other end file_actions.add_close(c2pread) if stdin is not None: @@ -38,7 +45,10 @@ args = [s.encode(fsencoding) for s in args if type(s)==str] self.pid = posix_spawn.posix_spawnp(args[0], args, file_actions=file_actions) if stdout is not None: - self.stdout = os.fdopen(c2pread) + self.stdout = os.fdopen(c1pread) + os.close(c1pwrite) + if stderr is not None: + self.stderr = os.fdopen(c2pread) os.close(c2pwrite) if stdin is not None: self.stdin = os.fdopen(p2cwrite, 'w') @@ -52,29 +62,44 @@ if self.stdin is not None: self.stdin.close() self.stdin = None + if self.stdout is not None: stdoutdata = self.stdout.read() else: stdoutdata = "" - # TODO - stderrdata = "" + if self.stderr is not None: + stderrdata = self.stderr.read() + else: + stderrdata = "" self._wait() + if self.stdout is not None: self.stdout.close() self.stdout = None + if self.stderr is not None: + self.stderr.close() + self.stderr = None + return (stdoutdata, stderrdata) def wait(self): if self.stdin is not None: self.stdin.close() self.stdin = None + self._wait() + if self.stdout is not None: self.stdout.close() self.stdout = None + + if self.stderr is not None: + self.stderr.close() + self.stderr = None + return self.returncode def poll(self): @@ -86,10 +111,15 @@ if self.stdin is not None: self.stdin.close() self.stdin = None + if self.stdout is not None: self.stdout.close() self.stdout = None + if self.stderr is not None: + self.stderr.close() + self.stderr = None + return self.returncode def kill(self): @@ -98,10 +128,15 @@ if self.stdin is not None: self.stdin.close() self.stdin = None + if self.stdout is not None: self.stdout.close() self.stdout = None + if self.stderr is not None: + self.stderr.close() + self.stderr = None + def call(*args): cmd = [] diff -r 89549813a6c1 -r 0b3ac94f494c runtime/webinterface.css --- a/runtime/webinterface.css Sat Nov 25 00:18:05 2023 +0100 +++ b/runtime/webinterface.css Thu Dec 07 22:41:32 2023 +0100 @@ -1,6 +1,52 @@ +body { + background-color: #f5faff; +} + +* { + font-family: Tahoma, Verdana, sans-serif; +} + +h1, h2 { + color: darkslateblue; +} + +fieldset { + color: #424242; + border: 1px solid gray; + border-radius: 4px; +} + +input, select, button, a { + font-size: 14px; +} + +button, +input, +select, +textarea { + color: inherit; +} + +legend, .freeform-form-label { + color: darkslateblue; +} .freeform-label { float: left; width: 30%; } +a { + background-color: #e9e9ed; + color: #424242; + padding: 2px 6px; + text-align: center; + text-decoration: none; + display: inline-block; + border: 1px solid gray; + border-radius: 4px; +} + +a:hover, a:active { + background-color: silver; +}