Move controlpi.plugins to controlpi-plugins
authorBenjamin Braatz <bb@bbraatz.eu>
Fri, 5 Mar 2021 00:35:46 +0000 (01:35 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Fri, 5 Mar 2021 00:35:46 +0000 (01:35 +0100)
Namespace packages should not be inside regular packages:
https://stackoverflow.com/a/62992832

controlpi-plugins/wsserver.py [new file with mode: 0644]
controlpi/plugins/wsserver.py [deleted file]
setup.py

diff --git a/controlpi-plugins/wsserver.py b/controlpi-plugins/wsserver.py
new file mode 100644 (file)
index 0000000..b431d3e
--- /dev/null
@@ -0,0 +1,96 @@
+"""Provide …
+
+TODO: documentation, doctests, resilient conf-parsing
+"""
+import os
+import json
+import websockets
+from websockets import WebSocketServerProtocol, ConnectionClosedError
+from websockets.http import Headers
+from http import HTTPStatus
+from typing import Union, Optional, Tuple, Iterable, Mapping
+
+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, [{}], [{}], self._receive)
+
+    async def _receive(self, message: Message) -> None:
+        json_message = json.dumps(message)
+        await self._websocket.send(json_message)
+
+    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)
+                self._bus.send(message)
+        except ConnectionClosedError:
+            pass
+        await self._bus.send({'sender': self._name,
+                              'event': 'connection closed'})
+
+
+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'] = 'consolepi-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, b'File not found!'
+        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 = conf['port']
+        self._web_root = os.path.realpath(os.path.join(os.getcwd(),
+                                                       conf['web root']))
+        super()._process_conf(conf)
+
+    async def run(self) -> None:
+        await super().run()
+        await websockets.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
deleted file mode 100644 (file)
index c876b13..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Provide …
-
-TODO: documentation, doctests
-"""
-from controlpi import BasePlugin, Message, PluginConfiguration
-
-
-class WSServer(BasePlugin):
-    async def _receive(self, message: Message) -> None:
-        send_message = {'sender': self._name}
-        await self._bus.send(send_message)
-
-    def _process_conf(self, conf: PluginConfiguration) -> None:
-        self._bus.register(self._name, sends, receives, self._receive)
-        super()._process_conf(conf)
-
-    async def run(self) -> None:
-        await super().run()
-        await self._bus.send({'sender': self._name})
index d4114264eece426acf10aac12a0de359d3ad3aae..6570a5c6742a9b50bbcad847f5175a7fa1c66cbb 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",