""" 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 import json 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"]))) print("Listening on", 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.get("name", None) if self.application_name is not None: self.application_name = self.application_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) if data == bytes("", "ascii"): # A socket ready but sending garbage is a dead socket. self.close(conn) return json_data = str(data.strip(), "utf-8") try: js = json.loads(json_data) for key in js.keys(): component.handle(Debug("Received %s from the socket" % (key))) except Exception as e: print("Can’t read", json_data, e) return if self.application_name is not None: # As we have a name in the configuration to associate with, use # this name as a reference title = self.application_name else: # Associate the layout with the current window title = component.queryUtility(desktopEvent.IDesktop).getForegroundWindowTitle() self.layout_queue.put((js, title)) # 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. self.layout_queue.put((js, 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()