blob: 4084f300df3f78dc61dcad297b8f48c277157188 [file] [log] [blame]
Andrew Geisslerc926e172021-05-07 16:11:35 -05001#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4
5import abc
6import asyncio
7import json
8import os
9import signal
10import socket
11import sys
Patrick Williams213cb262021-08-07 19:21:33 -050012import multiprocessing
Andrew Geisslerc926e172021-05-07 16:11:35 -050013from . import chunkify, DEFAULT_MAX_CHUNK
14
15
16class ClientError(Exception):
17 pass
18
19
20class ServerError(Exception):
21 pass
22
23
24class AsyncServerConnection(object):
25 def __init__(self, reader, writer, proto_name, logger):
26 self.reader = reader
27 self.writer = writer
28 self.proto_name = proto_name
29 self.max_chunk = DEFAULT_MAX_CHUNK
30 self.handlers = {
31 'chunk-stream': self.handle_chunk,
Andrew Geissler09036742021-06-25 14:25:14 -050032 'ping': self.handle_ping,
Andrew Geisslerc926e172021-05-07 16:11:35 -050033 }
34 self.logger = logger
35
36 async def process_requests(self):
37 try:
38 self.addr = self.writer.get_extra_info('peername')
39 self.logger.debug('Client %r connected' % (self.addr,))
40
41 # Read protocol and version
42 client_protocol = await self.reader.readline()
43 if client_protocol is None:
44 return
45
46 (client_proto_name, client_proto_version) = client_protocol.decode('utf-8').rstrip().split()
47 if client_proto_name != self.proto_name:
48 self.logger.debug('Rejecting invalid protocol %s' % (self.proto_name))
49 return
50
51 self.proto_version = tuple(int(v) for v in client_proto_version.split('.'))
52 if not self.validate_proto_version():
53 self.logger.debug('Rejecting invalid protocol version %s' % (client_proto_version))
54 return
55
56 # Read headers. Currently, no headers are implemented, so look for
57 # an empty line to signal the end of the headers
58 while True:
59 line = await self.reader.readline()
60 if line is None:
61 return
62
63 line = line.decode('utf-8').rstrip()
64 if not line:
65 break
66
67 # Handle messages
68 while True:
69 d = await self.read_message()
70 if d is None:
71 break
72 await self.dispatch_message(d)
73 await self.writer.drain()
74 except ClientError as e:
75 self.logger.error(str(e))
76 finally:
77 self.writer.close()
78
79 async def dispatch_message(self, msg):
80 for k in self.handlers.keys():
81 if k in msg:
82 self.logger.debug('Handling %s' % k)
83 await self.handlers[k](msg[k])
84 return
85
86 raise ClientError("Unrecognized command %r" % msg)
87
88 def write_message(self, msg):
89 for c in chunkify(json.dumps(msg), self.max_chunk):
90 self.writer.write(c.encode('utf-8'))
91
92 async def read_message(self):
93 l = await self.reader.readline()
94 if not l:
95 return None
96
97 try:
98 message = l.decode('utf-8')
99
100 if not message.endswith('\n'):
101 return None
102
103 return json.loads(message)
104 except (json.JSONDecodeError, UnicodeDecodeError) as e:
105 self.logger.error('Bad message from client: %r' % message)
106 raise e
107
108 async def handle_chunk(self, request):
109 lines = []
110 try:
111 while True:
112 l = await self.reader.readline()
113 l = l.rstrip(b"\n").decode("utf-8")
114 if not l:
115 break
116 lines.append(l)
117
118 msg = json.loads(''.join(lines))
119 except (json.JSONDecodeError, UnicodeDecodeError) as e:
120 self.logger.error('Bad message from client: %r' % lines)
121 raise e
122
123 if 'chunk-stream' in msg:
124 raise ClientError("Nested chunks are not allowed")
125
126 await self.dispatch_message(msg)
127
Andrew Geissler09036742021-06-25 14:25:14 -0500128 async def handle_ping(self, request):
129 response = {'alive': True}
130 self.write_message(response)
131
Andrew Geisslerc926e172021-05-07 16:11:35 -0500132
133class AsyncServer(object):
134 def __init__(self, logger, loop=None):
135 if loop is None:
136 self.loop = asyncio.new_event_loop()
137 self.close_loop = True
138 else:
139 self.loop = loop
140 self.close_loop = False
141
142 self._cleanup_socket = None
143 self.logger = logger
144
145 def start_tcp_server(self, host, port):
146 self.server = self.loop.run_until_complete(
147 asyncio.start_server(self.handle_client, host, port, loop=self.loop)
148 )
149
150 for s in self.server.sockets:
Andrew Geissler09036742021-06-25 14:25:14 -0500151 self.logger.debug('Listening on %r' % (s.getsockname(),))
Andrew Geisslerc926e172021-05-07 16:11:35 -0500152 # Newer python does this automatically. Do it manually here for
153 # maximum compatibility
154 s.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
155 s.setsockopt(socket.SOL_TCP, socket.TCP_QUICKACK, 1)
156
157 name = self.server.sockets[0].getsockname()
158 if self.server.sockets[0].family == socket.AF_INET6:
159 self.address = "[%s]:%d" % (name[0], name[1])
160 else:
161 self.address = "%s:%d" % (name[0], name[1])
162
163 def start_unix_server(self, path):
164 def cleanup():
165 os.unlink(path)
166
167 cwd = os.getcwd()
168 try:
169 # Work around path length limits in AF_UNIX
170 os.chdir(os.path.dirname(path))
171 self.server = self.loop.run_until_complete(
172 asyncio.start_unix_server(self.handle_client, os.path.basename(path), loop=self.loop)
173 )
174 finally:
175 os.chdir(cwd)
176
Andrew Geissler09036742021-06-25 14:25:14 -0500177 self.logger.debug('Listening on %r' % path)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500178
179 self._cleanup_socket = cleanup
180 self.address = "unix://%s" % os.path.abspath(path)
181
182 @abc.abstractmethod
183 def accept_client(self, reader, writer):
184 pass
185
186 async def handle_client(self, reader, writer):
187 # writer.transport.set_write_buffer_limits(0)
188 try:
189 client = self.accept_client(reader, writer)
190 await client.process_requests()
191 except Exception as e:
192 import traceback
193 self.logger.error('Error from client: %s' % str(e), exc_info=True)
194 traceback.print_exc()
195 writer.close()
Andrew Geissler09036742021-06-25 14:25:14 -0500196 self.logger.debug('Client disconnected')
Andrew Geisslerc926e172021-05-07 16:11:35 -0500197
198 def run_loop_forever(self):
199 try:
200 self.loop.run_forever()
201 except KeyboardInterrupt:
202 pass
203
204 def signal_handler(self):
Patrick Williams213cb262021-08-07 19:21:33 -0500205 self.logger.debug("Got exit signal")
Andrew Geisslerc926e172021-05-07 16:11:35 -0500206 self.loop.stop()
207
208 def serve_forever(self):
209 asyncio.set_event_loop(self.loop)
210 try:
211 self.loop.add_signal_handler(signal.SIGTERM, self.signal_handler)
Patrick Williams213cb262021-08-07 19:21:33 -0500212 signal.pthread_sigmask(signal.SIG_UNBLOCK, [signal.SIGTERM])
Andrew Geisslerc926e172021-05-07 16:11:35 -0500213
214 self.run_loop_forever()
215 self.server.close()
216
217 self.loop.run_until_complete(self.server.wait_closed())
Andrew Geissler09036742021-06-25 14:25:14 -0500218 self.logger.debug('Server shutting down')
Andrew Geisslerc926e172021-05-07 16:11:35 -0500219 finally:
220 if self.close_loop:
221 if sys.version_info >= (3, 6):
222 self.loop.run_until_complete(self.loop.shutdown_asyncgens())
223 self.loop.close()
224
225 if self._cleanup_socket is not None:
226 self._cleanup_socket()
Patrick Williams213cb262021-08-07 19:21:33 -0500227
228 def serve_as_process(self, *, prefunc=None, args=()):
229 def run():
230 if prefunc is not None:
231 prefunc(self, *args)
232 self.serve_forever()
233
234 # Temporarily block SIGTERM. The server process will inherit this
235 # block which will ensure it doesn't receive the SIGTERM until the
236 # handler is ready for it
237 mask = signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGTERM])
238 try:
239 self.process = multiprocessing.Process(target=run)
240 self.process.start()
241
242 return self.process
243 finally:
244 signal.pthread_sigmask(signal.SIG_SETMASK, mask)