5151# The timeout (minutes) to check for the port.
5252DEFAULT_TIMEOUT = 15
5353POLL_INTERVAL = 0.25
54+ FRONTEND_STARTUP_TIMEOUT = 60
5455FRONTEND_POPEN_ARGS = {}
5556T = TypeVar ("T" )
5657TimeoutType = int | float | None
@@ -341,7 +342,9 @@ def _run_backend(context: contextvars.Context) -> None:
341342 "Creating backend in a new thread..."
342343 ) # for pytest diagnosis
343344 self .backend_thread = threading .Thread (
344- target = _run_backend , args = (contextvars .copy_context (),)
345+ target = _run_backend ,
346+ args = (contextvars .copy_context (),),
347+ name = f"reflex-backend-{ self .app_name } " ,
345348 )
346349 self .backend_thread .start ()
347350 print ("Backend started." ) # for pytest diagnosis #noqa: T201
@@ -375,20 +378,7 @@ def _wait_frontend(self):
375378 if self .frontend_process is None or self .frontend_process .stdout is None :
376379 msg = "Frontend process has no stdout."
377380 raise RuntimeError (msg )
378- while self .frontend_url is None :
379- line = self .frontend_process .stdout .readline ()
380- if not line :
381- break
382- print (line ) # for pytest diagnosis #noqa: T201
383- m = re .search (reflex .constants .ReactRouter .FRONTEND_LISTENING_REGEX , line )
384- if m is not None :
385- self .frontend_url = m .group (1 )
386- config = get_config ()
387- config .deploy_url = self .frontend_url
388- break
389- if self .frontend_url is None :
390- msg = "Frontend did not start"
391- raise RuntimeError (msg )
381+ frontend_ready = threading .Event ()
392382
393383 def consume_frontend_output ():
394384 while True :
@@ -399,23 +389,54 @@ def consume_frontend_output():
399389 # catch I/O operation on closed file.
400390 except ValueError as e :
401391 console .error (str (e ))
392+ frontend_ready .set ()
402393 break
403394 if not line :
395+ frontend_ready .set ()
404396 break
405-
406- self .frontend_output_thread = threading .Thread (target = consume_frontend_output )
397+ print (line ) # for pytest diagnosis #noqa: T201
398+ m = re .search (
399+ reflex .constants .ReactRouter .FRONTEND_LISTENING_REGEX ,
400+ line ,
401+ )
402+ if m is not None and self .frontend_url is None :
403+ self .frontend_url = m .group (1 )
404+ config = get_config ()
405+ config .deploy_url = self .frontend_url
406+ frontend_ready .set ()
407+
408+ self .frontend_output_thread = threading .Thread (
409+ target = consume_frontend_output ,
410+ name = f"reflex-frontend-{ self .app_name } " ,
411+ )
407412 self .frontend_output_thread .start ()
408413
414+ if not frontend_ready .wait (timeout = FRONTEND_STARTUP_TIMEOUT ):
415+ msg = f"Frontend did not start within { FRONTEND_STARTUP_TIMEOUT } seconds."
416+ raise RuntimeError (msg )
417+ if self .frontend_url is None :
418+ return_code = self .frontend_process .poll ()
419+ if return_code is not None :
420+ msg = f"Frontend did not start (exit code: { return_code } )."
421+ else :
422+ msg = "Frontend did not start."
423+ raise RuntimeError (msg )
424+
409425 def start (self ) -> Self :
410426 """Start the backend in a new thread and dev frontend as a separate process.
411427
412428 Returns:
413429 self
414430 """
415431 self ._initialize_app ()
416- self ._start_backend ()
417- self ._start_frontend ()
418- self ._wait_frontend ()
432+ try :
433+ self ._start_backend ()
434+ self ._start_frontend ()
435+ self ._wait_frontend ()
436+ except Exception :
437+ with contextlib .suppress (Exception ):
438+ self .stop ()
439+ raise
419440 return self
420441
421442 @staticmethod
@@ -474,11 +495,23 @@ def stop(self) -> None:
474495 with contextlib .suppress (psutil .NoSuchProcess ):
475496 child .kill ()
476497 # wait for main process to exit
477- self .frontend_process .communicate ()
498+ try :
499+ self .frontend_process .communicate (timeout = 10 )
500+ except subprocess .TimeoutExpired :
501+ self .frontend_process .kill ()
502+ self .frontend_process .communicate ()
478503 if self .backend_thread is not None :
479- self .backend_thread .join ()
504+ self .backend_thread .join (timeout = 30 )
505+ if self .backend_thread .is_alive ():
506+ console .warn (
507+ f"Backend thread { self .backend_thread .name !r} did not stop cleanly."
508+ )
480509 if self .frontend_output_thread is not None :
481- self .frontend_output_thread .join ()
510+ self .frontend_output_thread .join (timeout = 10 )
511+ if self .frontend_output_thread .is_alive ():
512+ console .warn (
513+ f"Frontend output thread { self .frontend_output_thread .name !r} did not stop cleanly."
514+ )
482515
483516 def __exit__ (self , * excinfo ) -> None :
484517 """Contextmanager protocol for `stop()`.
@@ -782,7 +815,10 @@ def _start_frontend(self):
782815
783816 print ("Frontend starting..." ) # for pytest diagnosis #noqa: T201
784817
785- self .frontend_thread = threading .Thread (target = self ._run_frontend )
818+ self .frontend_thread = threading .Thread (
819+ target = self ._run_frontend ,
820+ name = f"reflex-frontend-{ self .app_name } " ,
821+ )
786822 self .frontend_thread .start ()
787823
788824 def _wait_frontend (self ):
@@ -814,7 +850,9 @@ def _run_backend(context: contextvars.Context) -> None:
814850 "Creating backend in a new thread..."
815851 )
816852 self .backend_thread = threading .Thread (
817- target = _run_backend , args = (contextvars .copy_context (),)
853+ target = _run_backend ,
854+ args = (contextvars .copy_context (),),
855+ name = f"reflex-backend-{ self .app_name } " ,
818856 )
819857 self .backend_thread .start ()
820858 print ("Backend started." ) # for pytest diagnosis #noqa: T201
@@ -831,4 +869,8 @@ def stop(self):
831869 if self .frontend_server is not None :
832870 self .frontend_server .shutdown ()
833871 if self .frontend_thread is not None :
834- self .frontend_thread .join ()
872+ self .frontend_thread .join (timeout = 15 )
873+ if self .frontend_thread .is_alive ():
874+ console .warn (
875+ f"Frontend thread { self .frontend_thread .name !r} did not stop cleanly."
876+ )
0 commit comments