Adapt to changes in controlpi.
authorBenjamin Braatz <bb@bbraatz.eu>
Mon, 22 Mar 2021 14:35:13 +0000 (15:35 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Mon, 22 Mar 2021 14:35:13 +0000 (15:35 +0100)
conf.json
controlpi-plugins/wsserver.py [deleted file]
controlpi_plugins/wsserver.py [new file with mode: 0644]
setup.py

index 8e4c584a7040b69798b47ccf0f74cda57e82041a..bb7087aee68096893dbad34c99df0611ada2f031 100644 (file)
--- a/conf.json
+++ b/conf.json
             "command": "set state",
             "new state": false
         }
-    },
-    "Debug Logger": {
-        "plugin": "Log",
-        "filter": [
-            {}
-        ]
     }
 }
diff --git a/controlpi-plugins/wsserver.py b/controlpi-plugins/wsserver.py
deleted file mode 100644 (file)
index c19126b..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-"""Provide …
-
-TODO: documentation, doctests, resilient conf-parsing
-"""
-import os
-import json
-from websockets import (WebSocketServerProtocol, ConnectionClosedOK,
-                        ConnectionClosedError, serve)
-from websockets.http import Headers
-from http import HTTPStatus
-from typing import Optional, Tuple
-
-from controlpi import BasePlugin, MessageBus, Message, PluginConfiguration
-
-
-class Connection:
-    def __init__(self, bus: MessageBus, websocket: WebSocketServerProtocol,
-                 path: str) -> None:
-        self._bus = bus
-        self._websocket = websocket
-        address = self._websocket.remote_address
-        self._address = address[0]
-        self._port = address[1]
-        self._name = f"{self._address}:{self._port}"
-        if path != '/':
-            self._name = path[1:]
-        self._bus.register(self._name, 'WSServer-Connection',
-                           [{}], [{}], self._receive)
-
-    async def _receive(self, message: Message) -> None:
-        json_message = json.dumps(message)
-        try:
-            await self._websocket.send(json_message)
-        except (ConnectionClosedOK, ConnectionClosedError):
-            pass
-
-    async def run(self):
-        await self._bus.send({'sender': self._name,
-                              'event': 'connection opened',
-                              'address': self._address,
-                              'port': self._port})
-        try:
-            async for json_message in self._websocket:
-                original_message = json.loads(json_message)
-                message = {'sender': self._name}
-                message.update(original_message)
-                await self._bus.send(message)
-        except (ConnectionClosedOK, ConnectionClosedError):
-            pass
-        await self._bus.send({'sender': self._name,
-                              'event': 'connection closed'})
-        self._bus.unregister(self._name)
-
-
-Response = Optional[Tuple[HTTPStatus, Headers, bytes]]
-
-
-class WSServer(BasePlugin):
-    async def _handler(self, websocket: WebSocketServerProtocol,
-                       path: str) -> None:
-        connection = Connection(self._bus, websocket, path)
-        await connection.run()
-
-    async def _process_request(self, path: str,
-                               request_headers: Headers) -> Response:
-        if 'Upgrade' in request_headers:
-            return None
-        if path == '/':
-            path = '/index.html'
-        response_headers = Headers()
-        response_headers['Server'] = 'controlpi-wsserver websocket server'
-        response_headers['Connection'] = 'close'
-        file_path = os.path.realpath(os.path.join(self._web_root, path[1:]))
-        if os.path.commonpath((self._web_root, file_path)) != self._web_root \
-                or not os.path.exists(file_path) \
-                or not os.path.isfile(file_path):
-            return (HTTPStatus.NOT_FOUND, response_headers,
-                    f"File '{path}' not found!".encode())
-        mime_type = 'application/octet-stream'
-        extension = file_path.split('.')[-1]
-        if extension == 'html':
-            mime_type = 'text/html'
-        elif extension == 'js':
-            mime_type = 'text/javascript'
-        elif extension == 'css':
-            mime_type = 'text/css'
-        response_headers['Content-Type'] = mime_type
-        body = open(file_path, 'rb').read()
-        response_headers['Content-Length'] = str(len(body))
-        return HTTPStatus.OK, response_headers, body
-
-    def _process_conf(self, conf: PluginConfiguration) -> None:
-        self._port = 80
-        if 'port' in conf:
-            self._port = conf['port']
-        else:
-            print(f"'port' not configured for WSServer '{self._name}'."
-                  " Using 80.")
-        web_root = 'web'
-        if 'web root' in conf:
-            web_root = conf['web root']
-        else:
-            print(f"'web root' not configured for WSServer '{self._name}'."
-                  " Using 'web'.")
-        self._web_root = os.path.realpath(os.path.join(os.getcwd(),
-                                                       web_root))
-        super()._process_conf(conf)
-
-    async def run(self) -> None:
-        await super().run()
-        await serve(self._handler, port=self._port,
-                    process_request=self._process_request)
-        print(f"WSServer '{self._name}' serving on port {self._port}.")
diff --git a/controlpi_plugins/wsserver.py b/controlpi_plugins/wsserver.py
new file mode 100644 (file)
index 0000000..2a8d625
--- /dev/null
@@ -0,0 +1,148 @@
+"""Provide websocket server plugin.
+
+…
+
+TODO: documentation, doctests
+TODO: Mount multiple web apps from packages and file paths
+TODO: Let Debug web app collapse/expand nested structures
+TODO: Make Debug web app work with nested structures in commands
+"""
+import os
+import json
+from websockets import (WebSocketServerProtocol, ConnectionClosedOK,
+                        ConnectionClosedError, serve)
+from websockets.http import Headers
+from http import HTTPStatus
+from typing import Optional, Tuple
+
+from controlpi import BasePlugin, MessageBus, Message, MessageTemplate
+
+
+class Connection:
+    """Connection to websocket.
+
+    Instances are created on external connections to websocket.
+    """
+
+    def __init__(self, bus: MessageBus,
+                 websocket: WebSocketServerProtocol) -> None:
+        """Initialise conncection.
+
+        Message bus and websocket are set by websocket server when creating
+        the connection.
+        """
+        self._bus = bus
+        self._websocket = websocket
+        address = self._websocket.remote_address
+        self._address = address[0]
+        self._port = address[1]
+        self._name = f"{self._address}:{self._port}"
+        self._bus.register(self._name, 'WSServer',
+                           [MessageTemplate()], [MessageTemplate()],
+                           self._receive)
+
+    async def _receive(self, message: Message) -> None:
+        """Receive messages from bus and relay to websocket."""
+        json_message = json.dumps(message)
+        try:
+            await self._websocket.send(json_message)
+        except (ConnectionClosedOK, ConnectionClosedError):
+            pass
+
+    async def run(self):
+        """Listen on websocket and relay messages to bus."""
+        await self._bus.send({'sender': self._name,
+                              'event': 'connection opened',
+                              'address': self._address,
+                              'port': self._port})
+        try:
+            async for json_message in self._websocket:
+                original_message = json.loads(json_message)
+                message = {'sender': self._name}
+                message.update(original_message)
+                await self._bus.send(message)
+        except (ConnectionClosedOK, ConnectionClosedError):
+            pass
+        await self._bus.send({'sender': self._name,
+                              'event': 'connection closed'})
+        self._bus.unregister(self._name)
+
+
+Response = Optional[Tuple[HTTPStatus, Headers, bytes]]
+
+
+class WSServer(BasePlugin):
+    """Websocket server as ControlPi plugin.
+
+    Run websocket server on host and port given in configuration, serving
+    the contents in given web root.
+    """
+
+    CONF_SCHEMA = {'properties': {'host': {'type': 'string'},
+                                  'port': {'type': 'integer'},
+                                  'web root': {'type': 'string'}}}
+    """Schema for WServer plugin configuration.
+
+    Optional configuration keys:
+
+    - 'host': network interfaces to listen on (default: None, meaning all
+      interfaces)
+    - 'port': port to connect to (default: 80)
+    - 'web root': root of files to serve (default: 'web')
+    """
+
+    async def _handler(self, websocket: WebSocketServerProtocol,
+                       path: str) -> None:
+        connection = Connection(self.bus, websocket)
+        await connection.run()
+
+    async def _process_request(self, path: str,
+                               request_headers: Headers) -> Response:
+        if 'Upgrade' in request_headers:
+            return None
+        if path == '/':
+            path = '/index.html'
+        response_headers = Headers()
+        response_headers['Server'] = 'controlpi-wsserver websocket server'
+        response_headers['Connection'] = 'close'
+        file_path = os.path.realpath(os.path.join(self._web_root, path[1:]))
+        if os.path.commonpath((self._web_root, file_path)) != self._web_root \
+                or not os.path.exists(file_path) \
+                or not os.path.isfile(file_path):
+            return (HTTPStatus.NOT_FOUND, response_headers,
+                    f"File '{path}' not found!".encode())
+        mime_type = 'application/octet-stream'
+        extension = file_path.split('.')[-1]
+        if extension == 'html':
+            mime_type = 'text/html'
+        elif extension == 'js':
+            mime_type = 'text/javascript'
+        elif extension == 'css':
+            mime_type = 'text/css'
+        response_headers['Content-Type'] = mime_type
+        body = open(file_path, 'rb').read()
+        response_headers['Content-Length'] = str(len(body))
+        return HTTPStatus.OK, response_headers, body
+
+    def process_conf(self) -> None:
+        """Get host, port and path settings from configuration."""
+        self._port = 80
+        if 'port' in self.conf:
+            self._port = self.conf['port']
+        else:
+            print(f"'port' not configured for WSServer '{self.name}'."
+                  " Using 80.")
+        web_root = 'web'
+        if 'web root' in self.conf:
+            web_root = self.conf['web root']
+        else:
+            print(f"'web root' not configured for WSServer '{self.name}'."
+                  " Using 'web'.")
+        self._web_root = os.path.realpath(os.path.join(os.getcwd(),
+                                                       web_root))
+
+    async def run(self) -> None:
+        """Set up websocket server."""
+        await serve(self._handler, port=self._port,
+                    process_request=self._process_request)
+        print(f"WSServer '{self.name}' serving on port {self._port}.")
index a62d71e6f4a22fa7fc503da4df7a58a4de786d44..76aca3aa1c1978cc7e2084fe0adbf5e10b013ada 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ setuptools.setup(
     long_description=long_description,
     long_description_content_type="text/markdown",
     url="http://docs.graph-it.com/graphit/controlpi-wsserver",
-    packages=["controlpi-plugins"],
+    packages=["controlpi_plugins"],
     install_requires=[
         "websockets",
         "controlpi @ git+git://git.graph-it.com/graphit/controlpi.git",