Merge
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Thu, 07 Dec 2023 22:41:32 +0100
changeset 3881 0b3ac94f494c
parent 3880 89549813a6c1 (current diff)
parent 3861 7e17f7e02a2b (diff)
child 3882 c7ec55cbd35a
Merge
connectors/ConnectorBase.py
connectors/PYRO/__init__.py
connectors/WAMP/__init__.py
runtime/NevowServer.py
runtime/PLCObject.py
runtime/WampClient.py
runtime/spawn_subprocess.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())
--- 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
 
--- 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
 
--- 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;
 			}
 		}
--- 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)
--- 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()
+
--- 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)
+
--- 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 = []
--- 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;
+}