home · contact · privacy
Add map navigation key shortcuts.
[plomrogue2-experiments] / plom_socket_io.py
1 class BrokenSocketConnection(Exception):
2     pass
3
4
5 def send(socket, message):
6     """Send message via socket, encoded and delimited the way recv() expects.
7
8     In detail, all \ and $ in message are escaped with prefixed \, and an
9     unescaped $ is appended as a message delimiter. Then, socket.send() is
10     called as often as necessary to ensure message is sent fully, as
11     socket.send() due to buffering may not send all of it right away.
12
13     Assuming socket is blocking, it's rather improbable that socket.send() will
14     be partial / return a positive value less than the (byte) length of msg –
15     but not entirely out of the question. See:
16     - <http://stackoverflow.com/q/19697218>
17     - <http://stackoverflow.com/q/2618736>
18     - <http://stackoverflow.com/q/8900474>
19
20     This also handles a socket.send() return value of 0, which might be
21     possible or not (?) for blocking sockets:
22     - <http://stackoverflow.com/q/34919846>
23     """
24     escaped_message = ''
25     for char in message:
26         if char in ('\\', '$'):
27             escaped_message += '\\'
28         escaped_message += char
29     escaped_message += '$'
30     data = escaped_message.encode()
31     totalsent = 0
32     while totalsent < len(data):
33         socket_broken = False
34         try:
35             sent = socket.send(data[totalsent:])
36             socket_broken = sent == 0
37         except OSError as err:
38             if err.errno == 9:  # "Bad file descriptor", when connection broken
39                 socket_broken = True
40             else:
41                 raise err
42         if socket_broken:
43             raise BrokenSocketConnection
44         totalsent = totalsent + sent
45
46
47 def recv(socket):
48     """Get full send()-prepared message from socket.
49
50     In detail, socket.recv() is looped over for sequences of bytes that can be
51     decoded as a Unicode string delimited by an unescaped $, with \ and $
52     escapable by \. If a sequence of characters that ends in an unescaped $
53     cannot be decoded as Unicode, None is returned as its representation. Stop
54     once socket.recv() returns nothing.
55
56     Under the hood, the TCP stack receives packets that construct the input
57     payload in an internal buffer; socket.recv(BUFSIZE) pops up to BUFSIZE
58     bytes from that buffer, without knowledge either about the input's
59     segmentation into packets, or whether the input is segmented in any other
60     meaningful way; that's why we do our own message segmentation with $ as a
61     delimiter.
62     """
63     esc = False
64     data = b''
65     msg = b''
66     while True:
67         data += socket.recv(1024)
68         if 0 == len(data):
69             return
70         cut_off = 0
71         for c in data:
72             cut_off += 1
73             if esc:
74                 msg += bytes([c])
75                 esc = False
76             elif chr(c) == '\\':
77                 esc = True
78             elif chr(c) == '$':
79                 try:
80                     yield msg.decode()
81                 except UnicodeDecodeError:
82                     yield None
83                 data = data[cut_off:]
84                 msg = b''
85             else:
86                 msg += bytes([c])