Browse Source

Run backend from the main thread in dev mode (#1653)

Masen Furer 1 year ago
parent
commit
81fd9d1e9c
4 changed files with 71 additions and 30 deletions
  1. 28 0
      docs/DEBUGGING.md
  2. 6 4
      reflex/reflex.py
  3. 9 16
      reflex/utils/exec.py
  4. 28 10
      reflex/utils/processes.py

+ 28 - 0
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}/.."
+        }
+    ]
+}
+```

+ 6 - 4
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()

+ 9 - 16
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]],
     )
 
 

+ 28 - 10
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.")