processes.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. """Process operations."""
  2. from __future__ import annotations
  3. import contextlib
  4. import os
  5. import signal
  6. import subprocess
  7. import sys
  8. from typing import List, Optional
  9. from urllib.parse import urlparse
  10. import psutil
  11. from reflex import constants
  12. from reflex.config import get_config
  13. from reflex.utils import console, prerequisites
  14. def kill(pid):
  15. """Kill a process.
  16. Args:
  17. pid: The process ID.
  18. """
  19. os.kill(pid, signal.SIGTERM)
  20. def get_num_workers() -> int:
  21. """Get the number of backend worker processes.
  22. Returns:
  23. The number of backend worker processes.
  24. """
  25. return 1 if prerequisites.get_redis() is None else (os.cpu_count() or 1) * 2 + 1
  26. def get_api_port() -> int:
  27. """Get the API port.
  28. Returns:
  29. The API port.
  30. """
  31. port = urlparse(get_config().api_url).port
  32. if port is None:
  33. port = urlparse(constants.API_URL).port
  34. assert port is not None
  35. return port
  36. def get_process_on_port(port) -> Optional[psutil.Process]:
  37. """Get the process on the given port.
  38. Args:
  39. port: The port.
  40. Returns:
  41. The process on the given port.
  42. """
  43. for proc in psutil.process_iter(["pid", "name", "cmdline"]):
  44. try:
  45. for conns in proc.connections(kind="inet"):
  46. if conns.laddr.port == int(port):
  47. return proc
  48. except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
  49. pass
  50. return None
  51. def is_process_on_port(port) -> bool:
  52. """Check if a process is running on the given port.
  53. Args:
  54. port: The port.
  55. Returns:
  56. Whether a process is running on the given port.
  57. """
  58. return get_process_on_port(port) is not None
  59. def kill_process_on_port(port):
  60. """Kill the process on the given port.
  61. Args:
  62. port: The port.
  63. """
  64. if get_process_on_port(port) is not None:
  65. with contextlib.suppress(psutil.AccessDenied):
  66. get_process_on_port(port).kill() # type: ignore
  67. def change_or_terminate_port(port, _type) -> str:
  68. """Terminate or change the port.
  69. Args:
  70. port: The port.
  71. _type: The type of the port.
  72. Returns:
  73. The new port or the current one.
  74. """
  75. console.info(
  76. f"Something is already running on port [bold underline]{port}[/bold underline]. This is the port the {_type} runs on."
  77. )
  78. frontend_action = console.ask("Kill or change it?", choices=["k", "c", "n"])
  79. if frontend_action == "k":
  80. kill_process_on_port(port)
  81. return port
  82. elif frontend_action == "c":
  83. new_port = console.ask("Specify the new port")
  84. # Check if also the new port is used
  85. if is_process_on_port(new_port):
  86. return change_or_terminate_port(new_port, _type)
  87. else:
  88. console.info(
  89. f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
  90. )
  91. return new_port
  92. else:
  93. console.log("Exiting...")
  94. sys.exit()
  95. def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
  96. """Wrapper over subprocess.Popen to unify the launch of child processes.
  97. Args:
  98. args: A string, or a sequence of program arguments.
  99. run: Whether to run the process to completion.
  100. show_logs: Whether to show the logs of the process.
  101. **kwargs: Kwargs to override default wrap values to pass to subprocess.Popen as arguments.
  102. Returns:
  103. Execute a child program in a new process.
  104. """
  105. # Add the node bin path to the PATH environment variable.
  106. env = {
  107. **os.environ,
  108. "PATH": os.pathsep.join([constants.NODE_BIN_PATH, os.environ["PATH"]]),
  109. }
  110. kwargs = {
  111. "env": env,
  112. "stderr": None if show_logs else subprocess.STDOUT,
  113. "stdout": None if show_logs else subprocess.PIPE,
  114. "universal_newlines": True,
  115. "encoding": "UTF-8",
  116. **kwargs,
  117. }
  118. console.debug(f"Running command: {args}")
  119. fn = subprocess.run if run else subprocess.Popen
  120. return fn(args, **kwargs)
  121. def stream_logs(
  122. message: str,
  123. process: subprocess.Popen,
  124. ):
  125. """Stream the logs for a process.
  126. Args:
  127. message: The message to display.
  128. process: The process.
  129. Yields:
  130. The lines of the process output.
  131. """
  132. with process:
  133. console.debug(message)
  134. if process.stdout is None:
  135. return
  136. for line in process.stdout:
  137. console.debug(line, end="")
  138. yield line
  139. if process.returncode != 0:
  140. console.error(f"Error during {message}")
  141. console.error(
  142. "Run in with [bold]--loglevel debug[/bold] to see the full error."
  143. )
  144. os._exit(1)
  145. def show_logs(
  146. message: str,
  147. process: subprocess.Popen,
  148. ):
  149. """Show the logs for a process.
  150. Args:
  151. message: The message to display.
  152. process: The process.
  153. """
  154. for _ in stream_logs(message, process):
  155. pass
  156. def show_status(message: str, process: subprocess.Popen):
  157. """Show the status of a process.
  158. Args:
  159. message: The initial message to display.
  160. process: The process.
  161. """
  162. with console.status(message) as status:
  163. for line in stream_logs(message, process):
  164. status.update(f"{message} {line}")
  165. def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str]):
  166. """Show a progress bar for a process.
  167. Args:
  168. message: The message to display.
  169. process: The process.
  170. checkpoints: The checkpoints to advance the progress bar.
  171. """
  172. # Iterate over the process output.
  173. with console.progress() as progress:
  174. task = progress.add_task(f"{message}: ", total=len(checkpoints))
  175. for line in stream_logs(message, process):
  176. # Check for special strings and update the progress bar.
  177. for special_string in checkpoints:
  178. if special_string in line:
  179. progress.update(task, advance=1)
  180. if special_string == checkpoints[-1]:
  181. progress.update(task, completed=len(checkpoints))
  182. break
  183. def catch_keyboard_interrupt(signal, frame):
  184. """Display a custom message with the current time when exiting an app.
  185. Args:
  186. signal: The keyboard interrupt signal.
  187. frame: The current stack frame.
  188. """
  189. console.log("Reflex app stopped.")