PageRenderTime 29ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/bb/asyncrpc/client.py

https://github.com/kergoth/bitbake
Python | 172 lines | 148 code | 21 blank | 3 comment | 17 complexity | 018155ac2148495ca873e3599e65111b MD5 | raw file
Possible License(s): GPL-2.0, MIT
  1. #
  2. # SPDX-License-Identifier: GPL-2.0-only
  3. #
  4. import abc
  5. import asyncio
  6. import json
  7. import os
  8. import socket
  9. import sys
  10. from . import chunkify, DEFAULT_MAX_CHUNK
  11. class AsyncClient(object):
  12. def __init__(self, proto_name, proto_version, logger, timeout=30):
  13. self.reader = None
  14. self.writer = None
  15. self.max_chunk = DEFAULT_MAX_CHUNK
  16. self.proto_name = proto_name
  17. self.proto_version = proto_version
  18. self.logger = logger
  19. self.timeout = timeout
  20. async def connect_tcp(self, address, port):
  21. async def connect_sock():
  22. return await asyncio.open_connection(address, port)
  23. self._connect_sock = connect_sock
  24. async def connect_unix(self, path):
  25. async def connect_sock():
  26. return await asyncio.open_unix_connection(path)
  27. self._connect_sock = connect_sock
  28. async def setup_connection(self):
  29. s = '%s %s\n\n' % (self.proto_name, self.proto_version)
  30. self.writer.write(s.encode("utf-8"))
  31. await self.writer.drain()
  32. async def connect(self):
  33. if self.reader is None or self.writer is None:
  34. (self.reader, self.writer) = await self._connect_sock()
  35. await self.setup_connection()
  36. async def close(self):
  37. self.reader = None
  38. if self.writer is not None:
  39. self.writer.close()
  40. self.writer = None
  41. async def _send_wrapper(self, proc):
  42. count = 0
  43. while True:
  44. try:
  45. await self.connect()
  46. return await proc()
  47. except (
  48. OSError,
  49. ConnectionError,
  50. json.JSONDecodeError,
  51. UnicodeDecodeError,
  52. ) as e:
  53. self.logger.warning("Error talking to server: %s" % e)
  54. if count >= 3:
  55. if not isinstance(e, ConnectionError):
  56. raise ConnectionError(str(e))
  57. raise e
  58. await self.close()
  59. count += 1
  60. async def send_message(self, msg):
  61. async def get_line():
  62. try:
  63. line = await asyncio.wait_for(self.reader.readline(), self.timeout)
  64. except asyncio.TimeoutError:
  65. raise ConnectionError("Timed out waiting for server")
  66. if not line:
  67. raise ConnectionError("Connection closed")
  68. line = line.decode("utf-8")
  69. if not line.endswith("\n"):
  70. raise ConnectionError("Bad message %r" % (line))
  71. return line
  72. async def proc():
  73. for c in chunkify(json.dumps(msg), self.max_chunk):
  74. self.writer.write(c.encode("utf-8"))
  75. await self.writer.drain()
  76. l = await get_line()
  77. m = json.loads(l)
  78. if m and "chunk-stream" in m:
  79. lines = []
  80. while True:
  81. l = (await get_line()).rstrip("\n")
  82. if not l:
  83. break
  84. lines.append(l)
  85. m = json.loads("".join(lines))
  86. return m
  87. return await self._send_wrapper(proc)
  88. async def ping(self):
  89. return await self.send_message(
  90. {'ping': {}}
  91. )
  92. class Client(object):
  93. def __init__(self):
  94. self.client = self._get_async_client()
  95. self.loop = asyncio.new_event_loop()
  96. # Override any pre-existing loop.
  97. # Without this, the PR server export selftest triggers a hang
  98. # when running with Python 3.7. The drawback is that there is
  99. # potential for issues if the PR and hash equiv (or some new)
  100. # clients need to both be instantiated in the same process.
  101. # This should be revisited if/when Python 3.9 becomes the
  102. # minimum required version for BitBake, as it seems not
  103. # required (but harmless) with it.
  104. asyncio.set_event_loop(self.loop)
  105. self._add_methods('connect_tcp', 'ping')
  106. @abc.abstractmethod
  107. def _get_async_client(self):
  108. pass
  109. def _get_downcall_wrapper(self, downcall):
  110. def wrapper(*args, **kwargs):
  111. return self.loop.run_until_complete(downcall(*args, **kwargs))
  112. return wrapper
  113. def _add_methods(self, *methods):
  114. for m in methods:
  115. downcall = getattr(self.client, m)
  116. setattr(self, m, self._get_downcall_wrapper(downcall))
  117. def connect_unix(self, path):
  118. # AF_UNIX has path length issues so chdir here to workaround
  119. cwd = os.getcwd()
  120. try:
  121. os.chdir(os.path.dirname(path))
  122. self.loop.run_until_complete(self.client.connect_unix(os.path.basename(path)))
  123. self.loop.run_until_complete(self.client.connect())
  124. finally:
  125. os.chdir(cwd)
  126. @property
  127. def max_chunk(self):
  128. return self.client.max_chunk
  129. @max_chunk.setter
  130. def max_chunk(self, value):
  131. self.client.max_chunk = value
  132. def close(self):
  133. self.loop.run_until_complete(self.client.close())
  134. if sys.version_info >= (3, 6):
  135. self.loop.run_until_complete(self.loop.shutdown_asyncgens())
  136. self.loop.close()