aboutsummaryrefslogtreecommitdiff
path: root/socketserver.py
diff options
context:
space:
mode:
authorSébastien Dailly <sebastien@dailly.me>2023-07-15 14:44:41 +0200
committerSébastien Dailly <sebastien@dailly.me>2023-07-15 14:59:43 +0200
commit02d676bda89c2fb8469ea81f7429c19c1e29df7c (patch)
tree11dae86a561a4800661e6c174b47611a704f857e /socketserver.py
Initial commit
Diffstat (limited to 'socketserver.py')
-rwxr-xr-xsocketserver.py138
1 files changed, 138 insertions, 0 deletions
diff --git a/socketserver.py b/socketserver.py
new file mode 100755
index 0000000..fb71d6d
--- /dev/null
+++ b/socketserver.py
@@ -0,0 +1,138 @@
+""" The module manage the socket server which allow to send new commands to
+ apply to the macropad.
+
+ Any client connected to the server will also be informed from the manual
+ changes in the macropad by the user.
+
+ The module is not blocking, and run in the main thread.
+"""
+
+import socket
+import selectors
+
+from dataclasses import dataclass
+from typing import Callable
+
+from zope import component
+from interfaces import desktopEvent
+
+from interfaces.message import Debug, ISocketMessage
+
+@dataclass
+class waitEvent:
+ callback = Callable[[object, socket, int], None]
+
+@dataclass
+class sendEvent:
+ callback = Callable[[object, socket, int], None]
+ message: str
+
+actions = {
+ "stack" : lambda x : x,
+ "layout" : lambda x : x,
+ }
+
+class Handler(object):
+ """ Listen the incomming connexions and dispatch them into the connexion
+ object """
+
+ def __init__(self, configuration, layout_queue):
+ """ Initialize the the socket server
+
+ The layout_queue will be populated with all the elements received from the socket.
+ """
+ super().__init__()
+ self.sel = selectors.DefaultSelector()
+ component.provideHandler(self.sendMessage)
+
+ self.socket = socket.socket()
+ self.socket.bind((configuration["host"], int(configuration["port"])))
+ self.socket.listen(100)
+ self.socket.setblocking(False)
+ c = waitEvent()
+ c.callback = self.accept
+ self.sel.register(self.socket, selectors.EVENT_READ, c)
+ self.application_name = configuration["name"].lower()
+
+ self.connexions = []
+ self.layout_queue = layout_queue
+
+ @component.adapter(ISocketMessage)
+ def sendMessage(self, content:ISocketMessage) -> None:
+ """ Send a message to all the sockets connected to the server
+ """
+ c = sendEvent(message = content.content)
+ c.callback = self._send
+ map = self.sel.get_map()
+ for value in list(map.values())[:]:
+ if value.fileobj == self.socket:
+ # Ignore the main connexion, keep it only in accept mode.
+ continue
+ self.sel.modify(value.fileobj, selectors.EVENT_WRITE, c)
+
+ def update(self):
+ """ Read the status for all the sockets, if they are available.
+ """
+ events = self.sel.select(timeout=0)
+ for key, mask in events:
+ c = key.data
+
+ if isinstance(c, waitEvent):
+ c.callback(key.fileobj, mask)
+ elif isinstance(c, sendEvent):
+ c.callback(key.fileobj, mask, c.message)
+
+ def _switch_read(self, conn):
+ """ Send the socket back into read mode
+ """
+ c = waitEvent()
+ c.callback = self._read
+ try:
+ self.sel.register(conn, selectors.EVENT_READ, c)
+ except KeyError:
+ self.sel.modify(conn, selectors.EVENT_READ, c)
+
+ def accept(self, sock, mask):
+ conn, addr = sock.accept() # Should be ready
+ conn.setblocking(False)
+ self._switch_read(conn)
+ component.handle(Debug("Received new connection"))
+
+ def _read(self:object, conn:socket, mask:int):
+ """ Internal method used to retreive data from the socket
+ """
+ data = conn.recv(1024).strip()
+ if data == bytes("", "ascii"):
+ # A socket ready but sending garbage is a dead socket.
+ self.close(conn)
+ return
+ last_layout = str(data, "utf-8")
+ try:
+ js = json.loads(last_layout)[-1]
+ for key, value in js.items():
+ last_layout = actions[key](value)
+ except:
+ print(last_layout)
+
+ #title = component.queryUtility(desktopEvent.IDesktop).getForegroundWindowTitle()
+ # The application name is hardcoded in the code, and should be reported
+ # in the configuration or somewhere else.
+ # The name of the current application is not reliable because the
+ # message can be send with some lattency.
+ component.handle(Debug("Received %s from the socket" % (last_layout)))
+
+ # Associate the layout with the current window
+ self.layout_queue.put((last_layout, self.application_name))
+
+ def _send(self:object, conn:socket, mask:int, text) -> None:
+ """ Internal method used to dispatch the message to the socket. """
+ try:
+ conn.sendall(bytes(text , "utf-8"))
+ except:
+ self.close(conn)
+ return
+ self._switch_read(conn)
+
+ def close(self, conn):
+ self.sel.unregister(conn)
+ conn.close()