""" 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 import csv 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) # Read the name to associate the connection with. application_name = configuration.get("name", None) if application_name is not None: # Split the values using `,` as separator reader = csv.reader([application_name]) self.application_name = [name.lower().strip() for name in next(reader)] else: self.application_name = None 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 for title in self.application_name: self.layout_queue.put((js, title)) else: # Associate the layout with the current window title = component.queryUtility(desktopEvent.IDesktop).getForegroundWindowTitle() self.layout_queue.put((js, title)) 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()