""" 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()