88from utils .auto_update import Update
99from utils .dependencies import initialize_dependencies
1010from utils .plex_dbrepair import start_plex_dbrepair_worker
11- import subprocess , threading , time , tomllib , os , socket , errno , psutil
11+ from concurrent .futures import ThreadPoolExecutor , wait , FIRST_COMPLETED
12+ import subprocess , threading , time , tomllib , os , socket , errno , psutil , json
1213
1314
1415def log_ascii_art ():
@@ -265,6 +266,153 @@ def start_configured_process(config_obj, updater, key_name, exit_on_error=True):
265266 raise
266267
267268
269+ def _service_has_enabled_instance (config_obj : dict ) -> bool :
270+ if not isinstance (config_obj , dict ):
271+ return False
272+ if "instances" in config_obj and isinstance (config_obj ["instances" ], dict ):
273+ return any (
274+ isinstance (inst , dict ) and inst .get ("enabled" )
275+ for inst in config_obj ["instances" ].values ()
276+ )
277+ return bool (config_obj .get ("enabled" ))
278+
279+
280+ def _read_decypharr_mount_path (decypharr_cfg : dict ) -> str | None :
281+ if not decypharr_cfg .get ("use_embedded_rclone" ):
282+ return None
283+ config_file = decypharr_cfg .get ("config_file" )
284+ if config_file and os .path .exists (config_file ):
285+ try :
286+ with open (config_file , "r" ) as handle :
287+ data = json .load (handle )
288+ mount_path = (data .get ("rclone" ) or {}).get ("mount_path" )
289+ if isinstance (mount_path , str ) and mount_path .strip ():
290+ return mount_path
291+ except Exception as e :
292+ logger .debug ("Failed to read Decypharr mount path: %s" , e )
293+ return "/mnt/debrid/decypharr"
294+
295+
296+ def _collect_mount_paths (config_manager ) -> list [str ]:
297+ mount_paths = set ()
298+ rclone_instances = config_manager .get ("rclone" , {}).get ("instances" , {}) or {}
299+ for instance in rclone_instances .values ():
300+ if not isinstance (instance , dict ) or not instance .get ("enabled" ):
301+ continue
302+ mount_dir = instance .get ("mount_dir" )
303+ mount_name = instance .get ("mount_name" )
304+ if mount_dir and mount_name :
305+ mount_paths .add (os .path .join (mount_dir , mount_name ))
306+
307+ decypharr_cfg = config_manager .get ("decypharr" , {}) or {}
308+ if decypharr_cfg .get ("enabled" ) and decypharr_cfg .get ("use_embedded_rclone" ):
309+ mount_path = _read_decypharr_mount_path (decypharr_cfg )
310+ if mount_path :
311+ mount_paths .add (mount_path )
312+
313+ return sorted (mount_paths )
314+
315+
316+ def _merge_wait_for_mounts (config_obj : dict , mount_paths : list [str ]) -> None :
317+ existing = config_obj .get ("wait_for_mounts" ) or []
318+ merged = sorted (set (existing ) | set (mount_paths ))
319+ if merged :
320+ config_obj ["wait_for_mounts" ] = merged
321+
322+
323+ def _apply_mount_waits (config_manager , mount_paths : list [str ]) -> None :
324+ if not mount_paths :
325+ return
326+ mount_wait_keys = {
327+ "plex" ,
328+ "jellyfin" ,
329+ "emby" ,
330+ }
331+ for key in mount_wait_keys :
332+ cfg = config_manager .get (key , {})
333+ if not isinstance (cfg , dict ):
334+ continue
335+ if "instances" in cfg and isinstance (cfg ["instances" ], dict ):
336+ for inst in cfg ["instances" ].values ():
337+ if isinstance (inst , dict ) and inst .get ("enabled" ):
338+ _merge_wait_for_mounts (inst , mount_paths )
339+ elif cfg .get ("enabled" ):
340+ _merge_wait_for_mounts (cfg , mount_paths )
341+
342+
343+ def _build_dependency_map (config_manager ) -> dict [str , set [str ]]:
344+ deps = {
345+ "riven_backend" : {"postgres" },
346+ "riven_frontend" : {"riven_backend" },
347+ "zilean" : {"postgres" },
348+ "pgadmin" : {"postgres" },
349+ }
350+
351+ rclone_deps = set ()
352+ rclone_instances = config_manager .get ("rclone" , {}).get ("instances" , {}) or {}
353+ for instance in rclone_instances .values ():
354+ if not isinstance (instance , dict ) or not instance .get ("enabled" ):
355+ continue
356+ if instance .get ("zurg_enabled" ):
357+ rclone_deps .add ("zurg" )
358+ if instance .get ("decypharr_enabled" ):
359+ rclone_deps .add ("decypharr" )
360+ key_type = (instance .get ("key_type" ) or "" ).lower ()
361+ if key_type == "nzbdav" or instance .get ("core_service" ) == "nzbdav" :
362+ rclone_deps .add ("nzbdav" )
363+ if rclone_deps :
364+ deps ["rclone" ] = rclone_deps
365+
366+ return deps
367+
368+
369+ def _start_processes_with_dependencies (
370+ process_handler , updater , config_manager , keys : list [str ], dependency_map
371+ ) -> None :
372+ enabled = {
373+ key : _service_has_enabled_instance (config_manager .get (key , {})) for key in keys
374+ }
375+ pending = {key for key in keys if enabled .get (key )}
376+ deps = {
377+ key : {d for d in dependency_map .get (key , set ()) if enabled .get (d )}
378+ for key in pending
379+ }
380+
381+ def _start_key (key : str ) -> None :
382+ cfg = config_manager .get (key , {})
383+ start_configured_process (cfg , updater , key )
384+
385+ in_progress = {}
386+ completed = set ()
387+ with ThreadPoolExecutor () as executor :
388+ while pending or in_progress :
389+ if process_handler .shutting_down :
390+ for future in list (in_progress ):
391+ future .cancel ()
392+ return
393+ ready = [key for key in list (pending ) if deps .get (key , set ()) <= completed ]
394+ for key in ready :
395+ if process_handler .shutting_down :
396+ break
397+ pending .remove (key )
398+ in_progress [executor .submit (_start_key , key )] = key
399+
400+ if not in_progress :
401+ raise RuntimeError (
402+ f"Dependency resolution stalled. Remaining: { sorted (pending )} "
403+ )
404+
405+ done , _ = wait (in_progress .keys (), return_when = FIRST_COMPLETED )
406+ for future in done :
407+ key = in_progress .pop (future )
408+ try :
409+ future .result ()
410+ except Exception as e :
411+ logger .error ("Failed while starting %s: %s" , key , e )
412+ process_handler .shutdown (exit_code = 1 )
413+ completed .add (key )
414+
415+
268416def main ():
269417 log_ascii_art ()
270418
@@ -290,6 +438,8 @@ def main():
290438 process_handler .shutdown (exit_code = 1 )
291439
292440 _apply_global_port_reservations (config )
441+ mount_paths = _collect_mount_paths (config )
442+ _apply_mount_waits (config , mount_paths )
293443
294444 if config .get ("dumb" , {}).get ("api_service" , {}).get ("enabled" ):
295445 start_fastapi_process ()
@@ -351,8 +501,10 @@ def _get_metrics_cfg():
351501 time .sleep (interval )
352502
353503 try :
504+ if config .get ("traefik" , {}).get ("enabled" ):
505+ start_configured_process (config .get ("traefik" , {}), updater , "traefik" )
506+
354507 grouped_keys = [
355- "traefik" ,
356508 "zurg" ,
357509 "prowlarr" ,
358510 "radarr" ,
@@ -377,9 +529,10 @@ def _get_metrics_cfg():
377529 "tautulli" ,
378530 "seerr" ,
379531 ]
380- for key in grouped_keys :
381- cfg = config .get (key , {})
382- start_configured_process (cfg , updater , key )
532+ dependency_map = _build_dependency_map (config )
533+ _start_processes_with_dependencies (
534+ process_handler , updater , config , grouped_keys , dependency_map
535+ )
383536
384537 except Exception as e :
385538 logger .error (e )
0 commit comments