aboutsummaryrefslogtreecommitdiff
path: root/socketserver.py
blob: 471b8be1ba97a4546f1a198b7a55a8af9b1b8606 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
""" 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"])))
        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).strip()
        if data == bytes("", "ascii"):
            # A socket ready but sending garbage is a dead socket.
            self.close(conn)
            return
        json_data = str(data, "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:
            title = self.application_name
        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()