Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 1 | # Copyright (C) 2019 Garmin Ltd. |
| 2 | # |
| 3 | # SPDX-License-Identifier: GPL-2.0-only |
| 4 | # |
| 5 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 6 | import asyncio |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 7 | import json |
| 8 | import logging |
| 9 | import socket |
Brad Bishop | 00e122a | 2019-10-05 11:10:57 -0400 | [diff] [blame] | 10 | import os |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 11 | from . import chunkify, DEFAULT_MAX_CHUNK, create_async_client |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 12 | |
| 13 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 14 | logger = logging.getLogger("hashserv.client") |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 15 | |
| 16 | |
| 17 | class HashConnectionError(Exception): |
| 18 | pass |
| 19 | |
| 20 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 21 | class AsyncClient(object): |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 22 | MODE_NORMAL = 0 |
| 23 | MODE_GET_STREAM = 1 |
| 24 | |
| 25 | def __init__(self): |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 26 | self.reader = None |
| 27 | self.writer = None |
| 28 | self.mode = self.MODE_NORMAL |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 29 | self.max_chunk = DEFAULT_MAX_CHUNK |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 30 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 31 | async def connect_tcp(self, address, port): |
| 32 | async def connect_sock(): |
| 33 | return await asyncio.open_connection(address, port) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 34 | |
| 35 | self._connect_sock = connect_sock |
| 36 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 37 | async def connect_unix(self, path): |
| 38 | async def connect_sock(): |
| 39 | return await asyncio.open_unix_connection(path) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 40 | |
| 41 | self._connect_sock = connect_sock |
| 42 | |
Andrew Geissler | 09209ee | 2020-12-13 08:44:15 -0600 | [diff] [blame] | 43 | async def connect(self): |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 44 | if self.reader is None or self.writer is None: |
| 45 | (self.reader, self.writer) = await self._connect_sock() |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 46 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 47 | self.writer.write("OEHASHEQUIV 1.1\n\n".encode("utf-8")) |
| 48 | await self.writer.drain() |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 49 | |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 50 | cur_mode = self.mode |
| 51 | self.mode = self.MODE_NORMAL |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 52 | await self._set_mode(cur_mode) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 53 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 54 | async def close(self): |
| 55 | self.reader = None |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 56 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 57 | if self.writer is not None: |
| 58 | self.writer.close() |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 59 | self.writer = None |
| 60 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 61 | async def _send_wrapper(self, proc): |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 62 | count = 0 |
| 63 | while True: |
| 64 | try: |
Andrew Geissler | 09209ee | 2020-12-13 08:44:15 -0600 | [diff] [blame] | 65 | await self.connect() |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 66 | return await proc() |
| 67 | except ( |
| 68 | OSError, |
| 69 | HashConnectionError, |
| 70 | json.JSONDecodeError, |
| 71 | UnicodeDecodeError, |
| 72 | ) as e: |
| 73 | logger.warning("Error talking to server: %s" % e) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 74 | if count >= 3: |
| 75 | if not isinstance(e, HashConnectionError): |
| 76 | raise HashConnectionError(str(e)) |
| 77 | raise e |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 78 | await self.close() |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 79 | count += 1 |
| 80 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 81 | async def send_message(self, msg): |
| 82 | async def get_line(): |
| 83 | line = await self.reader.readline() |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 84 | if not line: |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 85 | raise HashConnectionError("Connection closed") |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 86 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 87 | line = line.decode("utf-8") |
| 88 | |
| 89 | if not line.endswith("\n"): |
| 90 | raise HashConnectionError("Bad message %r" % message) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 91 | |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 92 | return line |
| 93 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 94 | async def proc(): |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 95 | for c in chunkify(json.dumps(msg), self.max_chunk): |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 96 | self.writer.write(c.encode("utf-8")) |
| 97 | await self.writer.drain() |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 98 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 99 | l = await get_line() |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 100 | |
| 101 | m = json.loads(l) |
Andrew Geissler | d1e8949 | 2021-02-12 15:35:20 -0600 | [diff] [blame] | 102 | if m and "chunk-stream" in m: |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 103 | lines = [] |
| 104 | while True: |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 105 | l = (await get_line()).rstrip("\n") |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 106 | if not l: |
| 107 | break |
| 108 | lines.append(l) |
| 109 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 110 | m = json.loads("".join(lines)) |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 111 | |
| 112 | return m |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 113 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 114 | return await self._send_wrapper(proc) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 115 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 116 | async def send_stream(self, msg): |
| 117 | async def proc(): |
| 118 | self.writer.write(("%s\n" % msg).encode("utf-8")) |
| 119 | await self.writer.drain() |
| 120 | l = await self.reader.readline() |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 121 | if not l: |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 122 | raise HashConnectionError("Connection closed") |
| 123 | return l.decode("utf-8").rstrip() |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 124 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 125 | return await self._send_wrapper(proc) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 126 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 127 | async def _set_mode(self, new_mode): |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 128 | if new_mode == self.MODE_NORMAL and self.mode == self.MODE_GET_STREAM: |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 129 | r = await self.send_stream("END") |
| 130 | if r != "ok": |
| 131 | raise HashConnectionError("Bad response from server %r" % r) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 132 | elif new_mode == self.MODE_GET_STREAM and self.mode == self.MODE_NORMAL: |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 133 | r = await self.send_message({"get-stream": None}) |
| 134 | if r != "ok": |
| 135 | raise HashConnectionError("Bad response from server %r" % r) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 136 | elif new_mode != self.mode: |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 137 | raise Exception( |
| 138 | "Undefined mode transition %r -> %r" % (self.mode, new_mode) |
| 139 | ) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 140 | |
| 141 | self.mode = new_mode |
| 142 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 143 | async def get_unihash(self, method, taskhash): |
| 144 | await self._set_mode(self.MODE_GET_STREAM) |
| 145 | r = await self.send_stream("%s %s" % (method, taskhash)) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 146 | if not r: |
| 147 | return None |
| 148 | return r |
| 149 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 150 | async def report_unihash(self, taskhash, method, outhash, unihash, extra={}): |
| 151 | await self._set_mode(self.MODE_NORMAL) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 152 | m = extra.copy() |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 153 | m["taskhash"] = taskhash |
| 154 | m["method"] = method |
| 155 | m["outhash"] = outhash |
| 156 | m["unihash"] = unihash |
| 157 | return await self.send_message({"report": m}) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 158 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 159 | async def report_unihash_equiv(self, taskhash, method, unihash, extra={}): |
| 160 | await self._set_mode(self.MODE_NORMAL) |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 161 | m = extra.copy() |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 162 | m["taskhash"] = taskhash |
| 163 | m["method"] = method |
| 164 | m["unihash"] = unihash |
| 165 | return await self.send_message({"report-equiv": m}) |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 166 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 167 | async def get_taskhash(self, method, taskhash, all_properties=False): |
| 168 | await self._set_mode(self.MODE_NORMAL) |
| 169 | return await self.send_message( |
| 170 | {"get": {"taskhash": taskhash, "method": method, "all": all_properties}} |
| 171 | ) |
Andrew Geissler | 475cb72 | 2020-07-10 16:00:51 -0500 | [diff] [blame] | 172 | |
Andrew Geissler | d1e8949 | 2021-02-12 15:35:20 -0600 | [diff] [blame] | 173 | async def get_outhash(self, method, outhash, taskhash): |
| 174 | await self._set_mode(self.MODE_NORMAL) |
| 175 | return await self.send_message( |
| 176 | {"get-outhash": {"outhash": outhash, "taskhash": taskhash, "method": method}} |
| 177 | ) |
| 178 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 179 | async def get_stats(self): |
| 180 | await self._set_mode(self.MODE_NORMAL) |
| 181 | return await self.send_message({"get-stats": None}) |
Brad Bishop | a34c030 | 2019-09-23 22:34:48 -0400 | [diff] [blame] | 182 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 183 | async def reset_stats(self): |
| 184 | await self._set_mode(self.MODE_NORMAL) |
| 185 | return await self.send_message({"reset-stats": None}) |
| 186 | |
| 187 | async def backfill_wait(self): |
| 188 | await self._set_mode(self.MODE_NORMAL) |
| 189 | return (await self.send_message({"backfill-wait": None}))["tasks"] |
| 190 | |
| 191 | |
| 192 | class Client(object): |
| 193 | def __init__(self): |
| 194 | self.client = AsyncClient() |
| 195 | self.loop = asyncio.new_event_loop() |
| 196 | |
| 197 | for call in ( |
| 198 | "connect_tcp", |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 199 | "close", |
| 200 | "get_unihash", |
| 201 | "report_unihash", |
| 202 | "report_unihash_equiv", |
| 203 | "get_taskhash", |
| 204 | "get_stats", |
| 205 | "reset_stats", |
| 206 | "backfill_wait", |
| 207 | ): |
| 208 | downcall = getattr(self.client, call) |
| 209 | setattr(self, call, self._get_downcall_wrapper(downcall)) |
| 210 | |
| 211 | def _get_downcall_wrapper(self, downcall): |
| 212 | def wrapper(*args, **kwargs): |
| 213 | return self.loop.run_until_complete(downcall(*args, **kwargs)) |
| 214 | |
| 215 | return wrapper |
| 216 | |
Andrew Geissler | 09209ee | 2020-12-13 08:44:15 -0600 | [diff] [blame] | 217 | def connect_unix(self, path): |
| 218 | # AF_UNIX has path length issues so chdir here to workaround |
| 219 | cwd = os.getcwd() |
| 220 | try: |
| 221 | os.chdir(os.path.dirname(path)) |
| 222 | self.loop.run_until_complete(self.client.connect_unix(os.path.basename(path))) |
| 223 | self.loop.run_until_complete(self.client.connect()) |
| 224 | finally: |
| 225 | os.chdir(cwd) |
| 226 | |
Andrew Geissler | 6ce62a2 | 2020-11-30 19:58:47 -0600 | [diff] [blame] | 227 | @property |
| 228 | def max_chunk(self): |
| 229 | return self.client.max_chunk |
| 230 | |
| 231 | @max_chunk.setter |
| 232 | def max_chunk(self, value): |
| 233 | self.client.max_chunk = value |