@@ -155,11 +155,22 @@ def check_environment() -> List[Tuple[str, str, bool]]:
155155def signal_handler (signum , frame ):
156156 print (f"\n { Fore .YELLOW } Received signal { signum } , shutting down server...{ Style .RESET_ALL } " )
157157
158+ # Check if we're already shutting down to avoid duplication
159+ if hasattr (signal_handler , 'shutting_down' ) and signal_handler .shutting_down :
160+ print ("Already shutting down, please wait..." )
161+ return
162+
163+ # Set flag to avoid duplicate shutdown
164+ signal_handler .shutting_down = True
165+
158166 set_server_status ("shutting_down" )
159167
160168 try :
161169 from .core .app import shutdown_event
162170
171+ # Mark that this is a real shutdown that needs a force exit
172+ shutdown_event .force_exit_required = True
173+
163174 loop = asyncio .get_event_loop ()
164175 if not loop .is_closed ():
165176 loop .create_task (shutdown_event ())
@@ -185,14 +196,9 @@ def delayed_exit():
185196 # Start a daemonic thread to force exit after a timeout
186197 force_exit_thread = threading .Thread (target = delayed_exit , daemon = True )
187198 force_exit_thread .start ()
188-
189- # Signal the main thread to exit immediately
190- # This is important to ensure that even if the server is stuck, the process will terminate
191- logger .info ("Sending SIGINT to the current process to ensure clean exit" )
192- time .sleep (1 ) # Give some time for the delayed_exit thread to start
193-
194- # If we're still running after 1 second, try to force kill the process again
195- os .kill (os .getpid (), signal .SIGTERM )
199+
200+ # Initialize the shutting down flag
201+ signal_handler .shutting_down = False
196202
197203
198204class NoopLifespan :
@@ -499,15 +505,18 @@ def __init__(self, config):
499505
500506 def install_signal_handlers (self ):
501507 def handle_exit (signum , frame ):
508+ if hasattr (handle_exit , 'called' ) and handle_exit .called :
509+ return
510+
511+ handle_exit .called = True
502512 self .should_exit = True
503513 logger .debug (f"Signal { signum } received in ServerWithCallback, setting should_exit=True" )
504514
505- # Try to propagate the signal to parent process
506- # This helps ensure the process exits properly
507- try :
508- os .kill (os .getpid (), signal .SIGTERM )
509- except Exception :
510- pass
515+ # Don't propagate signals back to avoid loops
516+ # The main signal_handler will handle process termination
517+
518+ # Initialize the flag
519+ handle_exit .called = False
511520
512521 signal .signal (signal .SIGINT , handle_exit )
513522 signal .signal (signal .SIGTERM , handle_exit )
@@ -694,6 +703,7 @@ def start_server(use_ngrok: bool = None, port: int = None, ngrok_auth_token: Opt
694703
695704 # Flag to track if startup has been completed
696705 startup_complete = [False ] # Using a list as a mutable reference
706+ should_force_exit = [False ] # To prevent premature shutdown
697707
698708 # Create a custom logging handler to detect when the application is ready
699709 class StartupDetectionHandler (logging .Handler ):
@@ -704,11 +714,16 @@ def __init__(self, server_ref):
704714 def emit (self , record ):
705715 if not startup_complete [0 ] and "Application startup complete" in record .getMessage ():
706716 logger .info ("Detected application startup complete message" )
707- if hasattr (self .server_ref , "handle_app_startup_complete" ):
708- self .server_ref .handle_app_startup_complete ()
709- else :
710- logger .warning ("Server reference doesn't have handle_app_startup_complete method" )
711- # Call on_startup directly as a fallback
717+ try :
718+ if hasattr (self .server_ref [0 ], "handle_app_startup_complete" ):
719+ self .server_ref [0 ].handle_app_startup_complete ()
720+ else :
721+ logger .warning ("Server reference doesn't have handle_app_startup_complete method" )
722+ # Call on_startup directly as a fallback
723+ on_startup ()
724+ except Exception as e :
725+ logger .error (f"Error in startup detection handler: { e } " )
726+ # Still try to display banners as a last resort
712727 on_startup ()
713728
714729 def on_startup ():
@@ -773,6 +788,18 @@ def on_startup():
773788 # Ensure server status is set to running even if display fails
774789 set_server_status ("running" )
775790
791+ # Define async callback that uvicorn can call
792+ async def on_startup_async ():
793+ # This is an async callback that uvicorn might call
794+ if not startup_complete [0 ]:
795+ on_startup ()
796+
797+ # Define this as a function to be called by uvicorn
798+ def callback_notify_function ():
799+ # If needed, create and return an awaitable
800+ loop = asyncio .get_event_loop ()
801+ return loop .create_task (on_startup_async ())
802+
776803 try :
777804 # Detect if we're in Google Colab
778805 in_colab = is_in_colab ()
@@ -795,19 +822,13 @@ def on_startup():
795822
796823 logger .info (f"Starting server on port { port } (Colab/ngrok mode)" )
797824
798- # Define the callback for Colab
799- async def on_startup_async ():
800- # This is an async callback that uvicorn might call
801- if not startup_complete [0 ]:
802- on_startup ()
803-
804825 config = uvicorn .Config (
805826 app ,
806827 host = "0.0.0.0" , # Bind to all interfaces in Colab
807828 port = port ,
808829 reload = False ,
809830 log_level = "info" ,
810- callback_notify = [ on_startup_async ] # Use a list for the callback
831+ callback_notify = callback_notify_function # Use a function, not a list
811832 )
812833
813834 server = ServerWithCallback (config )
@@ -861,7 +882,7 @@ async def on_startup_async():
861882 reload = False ,
862883 workers = 1 ,
863884 log_level = "info" ,
864- callback_notify = [ lambda : on_startup ()] # Use a lambda to prevent immediate execution
885+ callback_notify = callback_notify_function # Use a function, not a lambda or list
865886 )
866887
867888 server = ServerWithCallback (config )
@@ -910,6 +931,12 @@ async def on_startup_async():
910931 on_startup ()
911932
912933 except Exception as e :
934+ # Don't handle TypeError about 'list' object not being callable - that's exactly what we're fixing
935+ if "'list' object is not callable" in str (e ):
936+ logger .error ("Server error: callback_notify was passed a list instead of a callable function." )
937+ logger .error ("This is a known issue that will be fixed in the next version." )
938+ raise
939+
913940 logger .error (f"Server startup failed: { str (e )} " )
914941 logger .error (traceback .format_exc ())
915942 set_server_status ("error" )
@@ -923,7 +950,8 @@ async def on_startup_async():
923950 app = "locallab.core.minimal:app" , # Use a minimal app if available, or create one
924951 host = "127.0.0.1" ,
925952 port = port or 8000 ,
926- log_level = "info"
953+ log_level = "info" ,
954+ callback_notify = None # Don't use callbacks in the minimal server
927955 )
928956
929957 # Create a simple server
0 commit comments