@@ -155,11 +155,22 @@ def check_environment() -> List[Tuple[str, str, bool]]:
155
155
def signal_handler (signum , frame ):
156
156
print (f"\n { Fore .YELLOW } Received signal { signum } , shutting down server...{ Style .RESET_ALL } " )
157
157
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
+
158
166
set_server_status ("shutting_down" )
159
167
160
168
try :
161
169
from .core .app import shutdown_event
162
170
171
+ # Mark that this is a real shutdown that needs a force exit
172
+ shutdown_event .force_exit_required = True
173
+
163
174
loop = asyncio .get_event_loop ()
164
175
if not loop .is_closed ():
165
176
loop .create_task (shutdown_event ())
@@ -185,14 +196,9 @@ def delayed_exit():
185
196
# Start a daemonic thread to force exit after a timeout
186
197
force_exit_thread = threading .Thread (target = delayed_exit , daemon = True )
187
198
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
196
202
197
203
198
204
class NoopLifespan :
@@ -499,15 +505,18 @@ def __init__(self, config):
499
505
500
506
def install_signal_handlers (self ):
501
507
def handle_exit (signum , frame ):
508
+ if hasattr (handle_exit , 'called' ) and handle_exit .called :
509
+ return
510
+
511
+ handle_exit .called = True
502
512
self .should_exit = True
503
513
logger .debug (f"Signal { signum } received in ServerWithCallback, setting should_exit=True" )
504
514
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
511
520
512
521
signal .signal (signal .SIGINT , handle_exit )
513
522
signal .signal (signal .SIGTERM , handle_exit )
@@ -694,6 +703,7 @@ def start_server(use_ngrok: bool = None, port: int = None, ngrok_auth_token: Opt
694
703
695
704
# Flag to track if startup has been completed
696
705
startup_complete = [False ] # Using a list as a mutable reference
706
+ should_force_exit = [False ] # To prevent premature shutdown
697
707
698
708
# Create a custom logging handler to detect when the application is ready
699
709
class StartupDetectionHandler (logging .Handler ):
@@ -704,11 +714,16 @@ def __init__(self, server_ref):
704
714
def emit (self , record ):
705
715
if not startup_complete [0 ] and "Application startup complete" in record .getMessage ():
706
716
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
712
727
on_startup ()
713
728
714
729
def on_startup ():
@@ -773,6 +788,18 @@ def on_startup():
773
788
# Ensure server status is set to running even if display fails
774
789
set_server_status ("running" )
775
790
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
+
776
803
try :
777
804
# Detect if we're in Google Colab
778
805
in_colab = is_in_colab ()
@@ -795,19 +822,13 @@ def on_startup():
795
822
796
823
logger .info (f"Starting server on port { port } (Colab/ngrok mode)" )
797
824
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
-
804
825
config = uvicorn .Config (
805
826
app ,
806
827
host = "0.0.0.0" , # Bind to all interfaces in Colab
807
828
port = port ,
808
829
reload = False ,
809
830
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
811
832
)
812
833
813
834
server = ServerWithCallback (config )
@@ -861,7 +882,7 @@ async def on_startup_async():
861
882
reload = False ,
862
883
workers = 1 ,
863
884
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
865
886
)
866
887
867
888
server = ServerWithCallback (config )
@@ -910,6 +931,12 @@ async def on_startup_async():
910
931
on_startup ()
911
932
912
933
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
+
913
940
logger .error (f"Server startup failed: { str (e )} " )
914
941
logger .error (traceback .format_exc ())
915
942
set_server_status ("error" )
@@ -923,7 +950,8 @@ async def on_startup_async():
923
950
app = "locallab.core.minimal:app" , # Use a minimal app if available, or create one
924
951
host = "127.0.0.1" ,
925
952
port = port or 8000 ,
926
- log_level = "info"
953
+ log_level = "info" ,
954
+ callback_notify = None # Don't use callbacks in the minimal server
927
955
)
928
956
929
957
# Create a simple server
0 commit comments