diff --git a/docs/DEBUGGING.md b/docs/DEBUGGING.md new file mode 100644 index 000000000..8eef6c64c --- /dev/null +++ b/docs/DEBUGGING.md @@ -0,0 +1,28 @@ +# Debugging + +It is possible to run Reflex apps in dev mode under a debugger. + +1. Run Reflex as a module: `python -m reflex run --env dev` +2. Set current working directory to the dir containing `rxconfig.py` + +## VSCode + +The following launch configuration can be used to interactively debug a Reflex +app with breakpoints. + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Reflex App", + "type": "python", + "request": "launch", + "module": "reflex", + "args": "run --env dev", + "justMyCode": true, + "cwd": "${fileDirname}/.." + } + ] +} +``` \ No newline at end of file diff --git a/reflex/reflex.py b/reflex/reflex.py index db29e5c2b..2824a1e26 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -1,7 +1,7 @@ """Reflex CLI to create, run, and deploy apps.""" +import atexit import os -import signal from pathlib import Path import httpx @@ -164,16 +164,18 @@ def run( telemetry.send(f"run-{env.value}", config.telemetry_enabled) # Display custom message when there is a keyboard interrupt. - signal.signal(signal.SIGINT, processes.catch_keyboard_interrupt) + atexit.register(processes.atexit_handler) # Run the frontend and backend together. commands = [] if frontend: setup_frontend(Path.cwd()) commands.append((frontend_cmd, Path.cwd(), frontend_port)) - if backend: + if backend and env == constants.Env.PROD: commands.append((backend_cmd, app.__name__, backend_host, backend_port)) - processes.run_concurrently(*commands) + with processes.run_concurrently_context(*commands): + if env == constants.Env.DEV: + backend_cmd(app.__name__, backend_host, int(backend_port)) @cli.command() diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index d675c47e0..d2cb42445 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -7,6 +7,8 @@ import platform import sys from pathlib import Path +import uvicorn + from reflex import constants from reflex.config import get_config from reflex.utils import console, path_ops, prerequisites, processes @@ -95,22 +97,13 @@ def run_backend( port: The app port loglevel: The log level. """ - processes.new_process( - [ - "uvicorn", - f"{app_name}:{constants.APP_VAR}.{constants.API_VAR}", - "--host", - host, - "--port", - str(port), - "--log-level", - loglevel.value, - "--reload", - "--reload-dir", - app_name.split(".")[0], - ], - run=True, - show_logs=True, + uvicorn.run( + app=f"{app_name}:{constants.APP_VAR}.{constants.API_VAR}", + host=host, + port=port, + log_level=loglevel.value, + reload=True, + reload_dirs=[app_name.split(".")[0]], ) diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index eb1e9c2d7..efcd44991 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -8,7 +8,7 @@ import os import signal import subprocess from concurrent import futures -from typing import Callable, List, Optional, Tuple, Union +from typing import Callable, Generator, List, Optional, Tuple, Union import psutil import typer @@ -145,13 +145,23 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs): return fn(args, **kwargs) -def run_concurrently(*fns: Union[Callable, Tuple]): +@contextlib.contextmanager +def run_concurrently_context( + *fns: Union[Callable, Tuple] +) -> Generator[list[futures.Future], None, None]: """Run functions concurrently in a thread pool. - Args: *fns: The functions to run. + + Yields: + The futures for the functions. """ + # If no functions are provided, yield an empty list and return. + if not fns: + yield [] + return + # Convert the functions to tuples. fns = [fn if isinstance(fn, tuple) else (fn,) for fn in fns] # type: ignore @@ -160,11 +170,24 @@ def run_concurrently(*fns: Union[Callable, Tuple]): # Submit the tasks. tasks = [executor.submit(*fn) for fn in fns] # type: ignore + # Yield control back to the main thread while tasks are running. + yield tasks + # Get the results in the order completed to check any exceptions. for task in futures.as_completed(tasks): task.result() +def run_concurrently(*fns: Union[Callable, Tuple]) -> None: + """Run functions concurrently in a thread pool. + + Args: + *fns: The functions to run. + """ + with run_concurrently_context(*fns): + pass + + def stream_logs( message: str, process: subprocess.Popen, @@ -247,11 +270,6 @@ def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str break -def catch_keyboard_interrupt(signal, frame): - """Display a custom message with the current time when exiting an app. - - Args: - signal: The keyboard interrupt signal. - frame: The current stack frame. - """ +def atexit_handler(): + """Display a custom message with the current time when exiting an app.""" console.log("Reflex app stopped.")