From 02d676bda89c2fb8469ea81f7429c19c1e29df7c Mon Sep 17 00:00:00 2001 From: Sébastien Dailly Date: Sat, 15 Jul 2023 14:44:41 +0200 Subject: Initial commit --- socketserver.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100755 socketserver.py (limited to 'socketserver.py') 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() -- cgit v1.2.3