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