You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
8.4 KiB

4 years ago
  1. import asyncio
  2. import signal
  3. import socket
  4. from abc import ABC, abstractmethod
  5. from yarl import URL
  6. from .web_app import Application
  7. __all__ = ('TCPSite', 'UnixSite', 'SockSite', 'BaseRunner',
  8. 'AppRunner', 'ServerRunner', 'GracefulExit')
  9. class GracefulExit(SystemExit):
  10. code = 1
  11. def _raise_graceful_exit():
  12. raise GracefulExit()
  13. class BaseSite(ABC):
  14. __slots__ = ('_runner', '_shutdown_timeout', '_ssl_context', '_backlog',
  15. '_server')
  16. def __init__(self, runner, *,
  17. shutdown_timeout=60.0, ssl_context=None,
  18. backlog=128):
  19. if runner.server is None:
  20. raise RuntimeError("Call runner.setup() before making a site")
  21. self._runner = runner
  22. self._shutdown_timeout = shutdown_timeout
  23. self._ssl_context = ssl_context
  24. self._backlog = backlog
  25. self._server = None
  26. @property
  27. @abstractmethod
  28. def name(self):
  29. pass # pragma: no cover
  30. @abstractmethod
  31. async def start(self):
  32. self._runner._reg_site(self)
  33. async def stop(self):
  34. self._runner._check_site(self)
  35. if self._server is None:
  36. self._runner._unreg_site(self)
  37. return # not started yet
  38. self._server.close()
  39. await self._server.wait_closed()
  40. await self._runner.shutdown()
  41. await self._runner.server.shutdown(self._shutdown_timeout)
  42. self._runner._unreg_site(self)
  43. class TCPSite(BaseSite):
  44. __slots__ = ('_host', '_port', '_reuse_address', '_reuse_port')
  45. def __init__(self, runner, host=None, port=None, *,
  46. shutdown_timeout=60.0, ssl_context=None,
  47. backlog=128, reuse_address=None,
  48. reuse_port=None):
  49. super().__init__(runner, shutdown_timeout=shutdown_timeout,
  50. ssl_context=ssl_context, backlog=backlog)
  51. if host is None:
  52. host = "0.0.0.0"
  53. self._host = host
  54. if port is None:
  55. port = 8443 if self._ssl_context else 8080
  56. self._port = port
  57. self._reuse_address = reuse_address
  58. self._reuse_port = reuse_port
  59. @property
  60. def name(self):
  61. scheme = 'https' if self._ssl_context else 'http'
  62. return str(URL.build(scheme=scheme, host=self._host, port=self._port))
  63. async def start(self):
  64. await super().start()
  65. loop = asyncio.get_event_loop()
  66. self._server = await loop.create_server(
  67. self._runner.server, self._host, self._port,
  68. ssl=self._ssl_context, backlog=self._backlog,
  69. reuse_address=self._reuse_address,
  70. reuse_port=self._reuse_port)
  71. class UnixSite(BaseSite):
  72. __slots__ = ('_path', )
  73. def __init__(self, runner, path, *,
  74. shutdown_timeout=60.0, ssl_context=None,
  75. backlog=128):
  76. super().__init__(runner, shutdown_timeout=shutdown_timeout,
  77. ssl_context=ssl_context, backlog=backlog)
  78. self._path = path
  79. @property
  80. def name(self):
  81. scheme = 'https' if self._ssl_context else 'http'
  82. return '{}://unix:{}:'.format(scheme, self._path)
  83. async def start(self):
  84. await super().start()
  85. loop = asyncio.get_event_loop()
  86. self._server = await loop.create_unix_server(
  87. self._runner.server, self._path,
  88. ssl=self._ssl_context, backlog=self._backlog)
  89. class SockSite(BaseSite):
  90. __slots__ = ('_sock', '_name')
  91. def __init__(self, runner, sock, *,
  92. shutdown_timeout=60.0, ssl_context=None,
  93. backlog=128):
  94. super().__init__(runner, shutdown_timeout=shutdown_timeout,
  95. ssl_context=ssl_context, backlog=backlog)
  96. self._sock = sock
  97. scheme = 'https' if self._ssl_context else 'http'
  98. if hasattr(socket, 'AF_UNIX') and sock.family == socket.AF_UNIX:
  99. name = '{}://unix:{}:'.format(scheme, sock.getsockname())
  100. else:
  101. host, port = sock.getsockname()[:2]
  102. name = str(URL.build(scheme=scheme, host=host, port=port))
  103. self._name = name
  104. @property
  105. def name(self):
  106. return self._name
  107. async def start(self):
  108. await super().start()
  109. loop = asyncio.get_event_loop()
  110. self._server = await loop.create_server(
  111. self._runner.server, sock=self._sock,
  112. ssl=self._ssl_context, backlog=self._backlog)
  113. class BaseRunner(ABC):
  114. __slots__ = ('_handle_signals', '_kwargs', '_server', '_sites')
  115. def __init__(self, *, handle_signals=False, **kwargs):
  116. self._handle_signals = handle_signals
  117. self._kwargs = kwargs
  118. self._server = None
  119. self._sites = []
  120. @property
  121. def server(self):
  122. return self._server
  123. @property
  124. def addresses(self):
  125. return [sock.getsockname()
  126. for site in self._sites
  127. for sock in site._server.sockets]
  128. @property
  129. def sites(self):
  130. return set(self._sites)
  131. async def setup(self):
  132. loop = asyncio.get_event_loop()
  133. if self._handle_signals:
  134. try:
  135. loop.add_signal_handler(signal.SIGINT, _raise_graceful_exit)
  136. loop.add_signal_handler(signal.SIGTERM, _raise_graceful_exit)
  137. except NotImplementedError: # pragma: no cover
  138. # add_signal_handler is not implemented on Windows
  139. pass
  140. self._server = await self._make_server()
  141. @abstractmethod
  142. async def shutdown(self):
  143. pass # pragma: no cover
  144. async def cleanup(self):
  145. loop = asyncio.get_event_loop()
  146. if self._server is None:
  147. # no started yet, do nothing
  148. return
  149. # The loop over sites is intentional, an exception on gather()
  150. # leaves self._sites in unpredictable state.
  151. # The loop guaranties that a site is either deleted on success or
  152. # still present on failure
  153. for site in list(self._sites):
  154. await site.stop()
  155. await self._cleanup_server()
  156. self._server = None
  157. if self._handle_signals:
  158. try:
  159. loop.remove_signal_handler(signal.SIGINT)
  160. loop.remove_signal_handler(signal.SIGTERM)
  161. except NotImplementedError: # pragma: no cover
  162. # remove_signal_handler is not implemented on Windows
  163. pass
  164. @abstractmethod
  165. async def _make_server(self):
  166. pass # pragma: no cover
  167. @abstractmethod
  168. async def _cleanup_server(self):
  169. pass # pragma: no cover
  170. def _reg_site(self, site):
  171. if site in self._sites:
  172. raise RuntimeError("Site {} is already registered in runner {}"
  173. .format(site, self))
  174. self._sites.append(site)
  175. def _check_site(self, site):
  176. if site not in self._sites:
  177. raise RuntimeError("Site {} is not registered in runner {}"
  178. .format(site, self))
  179. def _unreg_site(self, site):
  180. if site not in self._sites:
  181. raise RuntimeError("Site {} is not registered in runner {}"
  182. .format(site, self))
  183. self._sites.remove(site)
  184. class ServerRunner(BaseRunner):
  185. """Low-level web server runner"""
  186. __slots__ = ('_web_server',)
  187. def __init__(self, web_server, *, handle_signals=False, **kwargs):
  188. super().__init__(handle_signals=handle_signals, **kwargs)
  189. self._web_server = web_server
  190. async def shutdown(self):
  191. pass
  192. async def _make_server(self):
  193. return self._web_server
  194. async def _cleanup_server(self):
  195. pass
  196. class AppRunner(BaseRunner):
  197. """Web Application runner"""
  198. __slots__ = ('_app',)
  199. def __init__(self, app, *, handle_signals=False, **kwargs):
  200. super().__init__(handle_signals=handle_signals, **kwargs)
  201. if not isinstance(app, Application):
  202. raise TypeError("The first argument should be web.Application "
  203. "instance, got {!r}".format(app))
  204. self._app = app
  205. @property
  206. def app(self):
  207. return self._app
  208. async def shutdown(self):
  209. await self._app.shutdown()
  210. async def _make_server(self):
  211. loop = asyncio.get_event_loop()
  212. self._app._set_loop(loop)
  213. self._app.on_startup.freeze()
  214. await self._app.startup()
  215. self._app.freeze()
  216. return self._app._make_handler(loop=loop, **self._kwargs)
  217. async def _cleanup_server(self):
  218. await self._app.cleanup()