http_server_head.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import asyncio
  2. import errno
  3. import os
  4. import sys
  5. import logging
  6. import ipaddress
  7. from distutils.version import LooseVersion
  8. import ray.dashboard.utils as dashboard_utils
  9. import ray.dashboard.optional_utils as dashboard_optional_utils
  10. # All third-party dependencies that are not included in the minimal Ray
  11. # installation must be included in this file. This allows us to determine if
  12. # the agent has the necessary dependencies to be started.
  13. from ray.dashboard.optional_deps import aiohttp, hdrs
  14. # Logger for this module. It should be configured at the entry point
  15. # into the program using Ray. Ray provides a default configuration at
  16. # entry/init points.
  17. logger = logging.getLogger(__name__)
  18. routes = dashboard_optional_utils.ClassMethodRouteTable
  19. def setup_static_dir():
  20. build_dir = os.path.join(
  21. os.path.dirname(os.path.abspath(__file__)), "client", "build"
  22. )
  23. module_name = os.path.basename(os.path.dirname(__file__))
  24. if not os.path.isdir(build_dir):
  25. raise dashboard_utils.FrontendNotFoundError(
  26. errno.ENOENT,
  27. "Dashboard build directory not found. If installing "
  28. "from source, please follow the additional steps "
  29. "required to build the dashboard"
  30. f"(cd python/ray/{module_name}/client "
  31. "&& npm install "
  32. "&& npm ci "
  33. "&& npm run build)",
  34. build_dir,
  35. )
  36. static_dir = os.path.join(build_dir, "static")
  37. routes.static("/static", static_dir, follow_symlinks=True)
  38. return build_dir
  39. class HttpServerDashboardHead:
  40. def __init__(self, ip, http_host, http_port, http_port_retries):
  41. self.ip = ip
  42. self.http_host = http_host
  43. self.http_port = http_port
  44. self.http_port_retries = http_port_retries
  45. # Below attirubtes are filled after `run` API is invoked.
  46. self.runner = None
  47. # Setup Dashboard Routes
  48. try:
  49. build_dir = setup_static_dir()
  50. logger.info("Setup static dir for dashboard: %s", build_dir)
  51. except dashboard_utils.FrontendNotFoundError as ex:
  52. # Not to raise FrontendNotFoundError due to NPM incompatibilities
  53. # with Windows.
  54. # Please refer to ci.sh::build_dashboard_front_end()
  55. if sys.platform in ["win32", "cygwin"]:
  56. logger.warning(ex)
  57. else:
  58. raise ex
  59. dashboard_optional_utils.ClassMethodRouteTable.bind(self)
  60. # Create a http session for all modules.
  61. # aiohttp<4.0.0 uses a 'loop' variable, aiohttp>=4.0.0 doesn't anymore
  62. if LooseVersion(aiohttp.__version__) < LooseVersion("4.0.0"):
  63. self.http_session = aiohttp.ClientSession(loop=asyncio.get_event_loop())
  64. else:
  65. self.http_session = aiohttp.ClientSession()
  66. @routes.get("/")
  67. async def get_index(self, req) -> aiohttp.web.FileResponse:
  68. return aiohttp.web.FileResponse(
  69. os.path.join(
  70. os.path.dirname(os.path.abspath(__file__)), "client/build/index.html"
  71. )
  72. )
  73. @routes.get("/favicon.ico")
  74. async def get_favicon(self, req) -> aiohttp.web.FileResponse:
  75. return aiohttp.web.FileResponse(
  76. os.path.join(
  77. os.path.dirname(os.path.abspath(__file__)), "client/build/favicon.ico"
  78. )
  79. )
  80. def get_address(self):
  81. assert self.http_host and self.http_port
  82. return self.http_host, self.http_port
  83. async def run(self, modules):
  84. # Bind http routes of each module.
  85. for c in modules:
  86. dashboard_optional_utils.ClassMethodRouteTable.bind(c)
  87. # Http server should be initialized after all modules loaded.
  88. # working_dir uploads for job submission can be up to 100MiB.
  89. app = aiohttp.web.Application(client_max_size=100 * 1024 ** 2)
  90. app.add_routes(routes=routes.bound_routes())
  91. self.runner = aiohttp.web.AppRunner(app)
  92. await self.runner.setup()
  93. last_ex = None
  94. for i in range(1 + self.http_port_retries):
  95. try:
  96. site = aiohttp.web.TCPSite(self.runner, self.http_host, self.http_port)
  97. await site.start()
  98. break
  99. except OSError as e:
  100. last_ex = e
  101. self.http_port += 1
  102. logger.warning("Try to use port %s: %s", self.http_port, e)
  103. else:
  104. raise Exception(
  105. f"Failed to find a valid port for dashboard after "
  106. f"{self.http_port_retries} retries: {last_ex}"
  107. )
  108. self.http_host, self.http_port, *_ = site._server.sockets[0].getsockname()
  109. self.http_host = (
  110. self.ip
  111. if ipaddress.ip_address(self.http_host).is_unspecified
  112. else self.http_host
  113. )
  114. logger.info(
  115. "Dashboard head http address: %s:%s", self.http_host, self.http_port
  116. )
  117. # Dump registered http routes.
  118. dump_routes = [r for r in app.router.routes() if r.method != hdrs.METH_HEAD]
  119. for r in dump_routes:
  120. logger.info(r)
  121. logger.info("Registered %s routes.", len(dump_routes))
  122. async def cleanup(self):
  123. # Wait for finish signal.
  124. await self.runner.cleanup()