7878
7979@asynccontextmanager
8080async def lifespan (app : FastAPI ):
81- # Suppress WinError 10054 on Windows asyncio loop
82- if sys .platform == "win32" :
81+ global state
82+ logging .info ("Lifespan starting..." )
83+
84+ # Ensure loop is set
85+ try :
8386 loop = asyncio .get_running_loop ()
87+ if state :
88+ state .loop = loop
89+ # Set exception handler
90+ if sys .platform == "win32" :
8491
85- def custom_exception_handler (loop , context ):
86- if "WinError 10054" in str (context .get ("exception" , "" )):
87- return
88- loop .default_exception_handler (context )
92+ def custom_exception_handler (loop , context ):
93+ if "WinError 10054" in str (context .get ("exception" , "" )):
94+ return
95+ loop .default_exception_handler (context )
96+
97+ loop .set_exception_handler (custom_exception_handler )
8998
90- loop .set_exception_handler (custom_exception_handler )
99+ state .start_background_tasks ()
100+ logging .info ("Background tasks started in lifespan" )
101+ except Exception as e :
102+ logging .error (f"Error in lifespan startup: { e } " )
91103
92104 yield
105+ logging .info ("Lifespan shutting down..." )
106+
107+
108+ app = FastAPI (lifespan = lifespan )
93109
94110
95111app = FastAPI (lifespan = lifespan )
@@ -122,22 +138,48 @@ def __init__(self):
122138
123139 # Config Path
124140 self .config_path = os .path .join (self .app_data_dir , "gui_config.json" )
125- default_config = os .path .join (self .script_dir , "gui_config.json" )
126141
127- # Check if config exists in AppData, if not copy from default (if exists) or create empty
142+ # MIGRATION: Check for legacy config in local folder (next to exe or script)
143+ # If running from EXE, sys.executable is the EXE. If script, __file__ is the script.
144+ current_dir = os .path .dirname (
145+ os .path .abspath (
146+ sys .executable if getattr (sys , "frozen" , False ) else __file__
147+ )
148+ )
149+ legacy_config = os .path .join (current_dir , "gui_config.json" )
150+
151+ # Alternative legacy location: one level up from backend (Resources folder)
152+ resources_dir = os .path .dirname (current_dir )
153+ legacy_config_alt = os .path .join (resources_dir , "gui_config.json" )
154+
155+ # Check if config exists in AppData, if not try to migrate or create empty
128156 if not os .path .exists (self .config_path ):
129- if os .path .exists (default_config ):
157+ found_legacy = None
158+ if os .path .exists (legacy_config ):
159+ found_legacy = legacy_config
160+ elif os .path .exists (legacy_config_alt ):
161+ found_legacy = legacy_config_alt
162+
163+ if found_legacy :
130164 try :
131- with open ( default_config , "r" ) as f :
132- data = f . read ()
133- with open ( self . config_path , "w" ) as f :
134- f . write ( data )
165+ logging . info (
166+ f"Migrating legacy config from { found_legacy } to { self . config_path } "
167+ )
168+ shutil . copy2 ( found_legacy , self . config_path )
135169 except Exception as e :
136- print (f"Failed to copy default config: { e } " )
170+ logging . error (f"Failed to migrate legacy config: { e } " )
137171 else :
138- # Create empty default config
139- with open (self .config_path , "w" ) as f :
140- f .write (json .dumps ({"servers" : []}))
172+ default_config = os .path .join (self .script_dir , "gui_config.json" )
173+ if os .path .exists (default_config ):
174+ try :
175+ shutil .copy2 (default_config , self .config_path )
176+ except Exception as e :
177+ logging .error (f"Failed to copy default config: { e } " )
178+ else :
179+ # Create empty default config
180+ with open (self .config_path , "w" ) as f :
181+ json .dump ({"servers" : []}, f )
182+ logging .info ("Created new empty config in AppData" )
141183
142184 self .config_manager = ConfigManager (self .config_path )
143185
@@ -147,22 +189,22 @@ def __init__(self):
147189 self .mods_manager = ModsManager ()
148190
149191 self .active_websockets : List [WebSocket ] = []
150- self .loop = asyncio .get_running_loop ()
151- self .selected_server_id = None
192+ self .loop : Optional [ asyncio .AbstractEventLoop ] = None
193+ self .selected_server_id : Optional [ str ] = None
152194
153195 # Log broadcasting control (prevents WS flood from starving the API)
154- self ._log_queue : asyncio .Queue = asyncio . Queue ( maxsize = 2000 )
196+ self ._log_queue : Optional [ asyncio .Queue ] = None
155197 self ._log_broadcaster_task : Optional [asyncio .Task ] = None
156198
157199 # Tunnel management
158- self .tunnel_process = None
159- self .tunnel_address = None
200+ self .tunnel_process : Optional [ subprocess . Popen ] = None
201+ self .tunnel_address : Optional [ str ] = None
160202
161203 # Install Progress Tracking
162- self .install_progress = 0
163- self .install_status_msg = ""
164- self .install_error = None
165- self .installed_server_id = None
204+ self .install_progress : int = 0
205+ self .install_status_msg : str = ""
206+ self .install_error : Optional [ str ] = None
207+ self .installed_server_id : Optional [ str ] = None
166208
167209 self .world_size_cache = {}
168210 self .world_size_inflight = set ()
@@ -175,11 +217,15 @@ def __init__(self):
175217 self .app_log_history = collections .deque (maxlen = 500 )
176218
177219 def start_background_tasks (self ):
220+ if self ._log_queue is None :
221+ self ._log_queue = asyncio .Queue (maxsize = 2000 )
178222 if self ._log_broadcaster_task is None :
179223 self ._log_broadcaster_task = asyncio .create_task (self ._log_broadcaster ())
180224
181225 def _enqueue_log_from_loop (self , msg_obj : dict ):
182226 """Must be called from the asyncio loop thread."""
227+ if self ._log_queue is None :
228+ return
183229 try :
184230 if self ._log_queue .full () and msg_obj .get ("level" ) in ("normal" , "info" ):
185231 return
@@ -195,10 +241,8 @@ def _enqueue_log_from_loop(self, msg_obj: dict):
195241 return
196242
197243 async def _log_broadcaster (self ):
198- """Batches log messages and streams them to WebSocket clients.
199-
200- This avoids scheduling thousands of per-line tasks during server startup.
201- """
244+ if self ._log_queue is None :
245+ return
202246 while True :
203247 msg = await self ._log_queue .get ()
204248
@@ -349,10 +393,13 @@ def broadcast_log_sync(self, message, level="normal", server_id=None):
349393 # deque auto-evicts oldest when full — no manual pop needed
350394
351395 # Thread-safe enqueue into the asyncio queue (only non-verbose logs)
352- try :
353- self .loop .call_soon_threadsafe (self ._enqueue_log_from_loop , msg_obj )
354- except Exception :
355- return
396+ if self .loop :
397+ try :
398+ self .loop .call_soon_threadsafe (
399+ self ._enqueue_log_from_loop , msg_obj
400+ )
401+ except Exception :
402+ return
356403 except Exception as e :
357404 print (f"Error logging: { e } " )
358405
@@ -371,14 +418,8 @@ async def broadcast_log(self, message: dict):
371418 pass
372419
373420
374- state : Optional [AppState ] = None
375-
376-
377- @app .on_event ("startup" )
378- async def startup_event ():
379- global state
380- state = AppState ()
381- state .start_background_tasks ()
421+ state : AppState = AppState ()
422+ logging .info (f"Global state initialized. Config path: { state .config_path } " )
382423
383424
384425# --- Models ---
@@ -757,6 +798,7 @@ def install_server(req: InstallRequest):
757798 install_path = os .path .join (req .parent_path , req .folder_name )
758799
759800 def run_install ():
801+ logging .info ("Installation thread started" )
760802 try :
761803 # Helper to send structured progress
762804 def send_progress (pct , msg , ** kwargs ):
@@ -911,6 +953,7 @@ def forge_progress(p):
911953 )
912954
913955 except Exception as e :
956+ logging .exception (f"Installation failed: { e } " )
914957 state .install_error = str (e )
915958 state .broadcast_log_sync (f"Installation failed: { e } " , "error" )
916959 state .broadcast_log_sync ({"type" : "progress" , "value" : 0 , "error" : str (e )})
0 commit comments