basereload.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. from __future__ import annotations
  2. import logging
  3. import os
  4. import signal
  5. import sys
  6. import threading
  7. from collections.abc import Callable, Iterator
  8. from pathlib import Path
  9. from socket import socket
  10. from types import FrameType
  11. from uvicorn._ansi import style
  12. from uvicorn._subprocess import get_subprocess
  13. from uvicorn.config import Config
  14. HANDLED_SIGNALS = (
  15. signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
  16. signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
  17. )
  18. logger = logging.getLogger("uvicorn.error")
  19. class BaseReload:
  20. def __init__(
  21. self,
  22. config: Config,
  23. target: Callable[[list[socket] | None], None],
  24. sockets: list[socket],
  25. ) -> None:
  26. self.config = config
  27. self.target = target
  28. self.sockets = sockets
  29. self.should_exit = threading.Event()
  30. self.pid = os.getpid()
  31. self.is_restarting = False
  32. self.reloader_name: str | None = None
  33. def signal_handler(self, sig: int, frame: FrameType | None) -> None: # pragma: full coverage
  34. """
  35. A signal handler that is registered with the parent process.
  36. """
  37. if sys.platform == "win32" and self.is_restarting:
  38. self.is_restarting = False
  39. else:
  40. self.should_exit.set()
  41. def run(self) -> None:
  42. self.startup()
  43. for changes in self:
  44. if changes:
  45. logger.warning(
  46. "%s detected changes in %s. Reloading...",
  47. self.reloader_name,
  48. ", ".join(map(_display_path, changes)),
  49. )
  50. self.restart()
  51. self.shutdown()
  52. def pause(self) -> None:
  53. if self.should_exit.wait(self.config.reload_delay):
  54. raise StopIteration()
  55. def __iter__(self) -> Iterator[list[Path] | None]:
  56. return self
  57. def __next__(self) -> list[Path] | None:
  58. return self.should_restart()
  59. def startup(self) -> None:
  60. message = f"Started reloader process [{self.pid}] using {self.reloader_name}"
  61. color_message = "Started reloader process [{}] using {}".format(
  62. style(str(self.pid), fg="cyan", bold=True),
  63. style(str(self.reloader_name), fg="cyan", bold=True),
  64. )
  65. logger.info(message, extra={"color_message": color_message})
  66. for sig in HANDLED_SIGNALS:
  67. signal.signal(sig, self.signal_handler)
  68. self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets)
  69. self.process.start()
  70. def restart(self) -> None:
  71. if sys.platform == "win32": # pragma: py-not-win32
  72. self.is_restarting = True
  73. assert self.process.pid is not None
  74. os.kill(self.process.pid, signal.CTRL_C_EVENT)
  75. # This is a workaround to ensure the Ctrl+C event is processed
  76. sys.stdout.write(" ") # This has to be a non-empty string
  77. sys.stdout.flush()
  78. else: # pragma: py-win32
  79. self.process.terminate()
  80. self.process.join()
  81. self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets)
  82. self.process.start()
  83. def shutdown(self) -> None:
  84. if sys.platform == "win32":
  85. self.should_exit.set() # pragma: py-not-win32
  86. else:
  87. self.process.terminate() # pragma: py-win32
  88. self.process.join()
  89. for sock in self.sockets:
  90. sock.close()
  91. message = f"Stopping reloader process [{str(self.pid)}]"
  92. color_message = "Stopping reloader process [{}]".format(style(str(self.pid), fg="cyan", bold=True))
  93. logger.info(message, extra={"color_message": color_message})
  94. def should_restart(self) -> list[Path] | None:
  95. raise NotImplementedError("Reload strategies should override should_restart()")
  96. def _display_path(path: Path) -> str:
  97. try:
  98. return f"'{path.relative_to(Path.cwd())}'"
  99. except ValueError:
  100. return f"'{path}'"