2222from pydantic import ValidationError
2323
2424
25- from appdaemon .dependency import DependencyResolutionFail , get_full_module_name
25+ from appdaemon .dependency import DependencyResolutionFail , find_all_dependents , get_full_module_name
2626from appdaemon .dependency_manager import DependencyManager
2727from appdaemon .models .config import AllAppConfig , AppConfig , GlobalModule
2828from appdaemon .models .config .app import SequenceConfig
@@ -338,7 +338,7 @@ async def initialize_app(self, app_name: str):
338338 else :
339339 await utils .run_in_executor (self , init_func )
340340
341- async def terminate_app (self , app_name : str , delete : bool = True ) -> bool :
341+ async def terminate_app (self , app_name : str , * , delete : bool = True ) -> bool :
342342 try :
343343 if (obj := self .objects .get (app_name )) and (terminate := getattr (obj .object , "terminate" , None )):
344344 self .logger .info ("Calling terminate() for '%s'" , app_name )
@@ -367,11 +367,9 @@ async def terminate_app(self, app_name: str, delete: bool = True) -> bool:
367367
368368 finally :
369369 self .logger .debug ("Cleaning up app '%s'" , app_name )
370- if obj := self .objects .get (app_name ):
371- if delete :
372- del self .objects [app_name ]
373- else :
374- obj .running = False
370+ obj = self .objects .pop (app_name , None ) if delete else self .objects .get (app_name )
371+ if obj is not None :
372+ obj .running = False
375373
376374 await self .increase_inactive_apps (app_name )
377375
@@ -434,13 +432,8 @@ async def start_app(self, app_name: str):
434432 case AppConfig ():
435433 # There is a valid app configuration for this dependency
436434 match self .objects .get (dep_name ):
437- case ManagedObject (type = "app" ) as obj :
438- # There is an app being managed that matches the dependency
439- if not obj .running :
440- # But it's not running, so raise an exception
441- raise ade .DependencyNotRunning (* exc_args )
442- case _:
443- # TODO: make this a different exception
435+ case ManagedObject (type = "app" , running = False ):
436+ # There is an app being managed that matches the dependency and isn't running
444437 raise ade .DependencyNotRunning (* exc_args )
445438 case GlobalModule () as dep_cfg :
446439 # The dependency is a legacy global module
@@ -472,7 +465,7 @@ async def start_app(self, app_name: str):
472465 }
473466 await self .AD .events .process_event ("admin" , event_data )
474467
475- async def stop_app (self , app_name : str , delete : bool = False ) -> bool :
468+ async def stop_app (self , app_name : str , * , delete : bool = False ) -> bool :
476469 """Stops the app
477470
478471 Returns:
@@ -481,7 +474,7 @@ async def stop_app(self, app_name: str, delete: bool = False) -> bool:
481474 try :
482475 if isinstance (self .app_config [app_name ], AppConfig ):
483476 self .logger .debug ("Stopping app '%s'" , app_name )
484- await self .terminate_app (app_name , delete )
477+ await self .terminate_app (app_name , delete = delete )
485478 except Exception :
486479 error_logger = logging .getLogger (f"Error.{ app_name } " )
487480 error_logger .warning ("-" * 60 )
@@ -1131,70 +1124,102 @@ async def _start_plugin_apps(self, plugin_ns: str | None, update_actions: Update
11311124 update_actions .apps .init |= deps
11321125
11331126 async def _stop_apps (self , update_actions : UpdateActions ):
1134- """Terminate apps. Returns the set of app names that failed to properly terminate .
1127+ """Terminate the apps from the update actions, including any dependent ones .
11351128
11361129 Part of self.check_app_updates sequence
11371130 """
11381131 stop_order = update_actions .apps .term_sort (self .dependency_manager )
1139- # stop_order = update_actions.apps.term_sort(self.app_config.depedency_graph())
1132+ indirect_stops = set ( stop_order ) - update_actions .apps .term_set
11401133 if stop_order :
1141- self .logger .info ("Stopping apps: %s" , update_actions .apps .term_set )
1142- self .logger .debug ("App stop order: %s" , stop_order )
1134+ self .logger .info ("Stopping apps: %s" , stop_order )
1135+ if indirect_stops :
1136+ self .logger .debug ("Dependent apps: %s" , indirect_stops )
11431137
11441138 failed_to_stop = set () # stores apps that had a problem terminating
11451139 for app_name in stop_order :
1146- if not await self .stop_app (app_name ):
1147- failed_to_stop .add (app_name )
1148- else :
1140+ successfully_stopped = await self .stop_app (app_name )
1141+ if successfully_stopped :
11491142 self .logger .info ("Stopped app '%s'" , app_name )
1143+ if app_name in indirect_stops :
1144+ update_actions .apps .init .add (app_name )
1145+ else :
1146+ failed_to_stop .add (app_name )
11501147
11511148 if failed_to_stop :
11521149 self .logger .debug ("Removing %s apps because they failed to stop cleanly" , len (failed_to_stop ))
11531150 update_actions .apps .init -= failed_to_stop
11541151 update_actions .apps .reload -= failed_to_stop
11551152
1153+ def _filter_running_apps (self , * trackers : Iterable [str ]) -> Iterable [Iterable [str ]]:
1154+ """App names that get added to the start order indirectly may already be running."""
1155+ for app_name in copy (trackers [0 ]):
1156+ match self .objects .get (app_name ):
1157+ case ManagedObject (running = True ):
1158+ self .logger .debug ("Dependent app '%s' is already running" , app_name )
1159+ for tracker in trackers :
1160+ match tracker :
1161+ case set ():
1162+ tracker .discard (app_name )
1163+ case list ():
1164+ tracker .remove (app_name )
1165+ case dict ():
1166+ tracker .pop (app_name , None )
1167+ return trackers
1168+
11561169 async def _create_and_start_apps (self , update_actions : UpdateActions ) -> None :
11571170 """Creates and starts apps that are in the init set of the update actions."""
11581171 if failed := update_actions .apps .failed :
11591172 self .logger .warning ("Failed to start apps: %s" , failed )
11601173
11611174 start_order = update_actions .apps .start_sort (self .dependency_manager , self .logger )
1162- if start_order :
1163- self .logger .info ("Starting apps: %s" , update_actions .apps .init_set )
1164- self .logger .debug ("App start order: %s" , start_order )
1165-
1166- for app_name in start_order :
1167- match self .app_config .root .get (app_name ):
1168- case AppConfig () as cfg if not cfg .disable :
1169- if await self .create_app_object (app_name ) is None :
1170- update_actions .apps .failed .add (app_name )
1171- case GlobalModule ():
1172- # Global modules are not started, they are just imported
1173- self .logger .debug (f"Skipping global module '{ app_name } '" )
1174- case None :
1175- self .logger .warning (f"App '{ app_name } ' not found in app config" )
1175+ indirect_starts = set (start_order ) - update_actions .apps .init_set
1176+ self ._filter_running_apps (indirect_starts , start_order )
11761177
1177- # Need to have already created the ManagedObjects for the threads to get assigned
1178- await self . AD . threading . calculate_pin_threads ()
1178+ if not start_order :
1179+ return
11791180
1180- # Need to recalculate start order to account for any failed object creations
1181- start_order = update_actions .apps .start_sort (self .dependency_manager , self .logger )
1182- if start_order :
1183- for app_name in start_order :
1184- match self .app_config .root .get (app_name ):
1185- case GlobalModule () as global_module :
1186- assert global_module .module_name in sys .modules , f"{ global_module .module_name } not in sys.modules"
1187- case AppConfig () as cfg :
1188- @ade .wrap_async (self .error , self .AD .app_dir , f"Failed to start '{ app_name } '" )
1189- async def safe_start (self : "AppManagement" ):
1190- try :
1191- await self .start_app (app_name )
1192- except Exception as exc :
1193- update_actions .apps .failed .add (app_name )
1194- raise ade .AppStartFailure (app_name ) from exc
1195-
1196- if await self .get_state (app_name ) != "compile_error" :
1197- await safe_start (self )
1181+ self .logger .info ("Starting apps: %s" , start_order )
1182+ if indirect_starts :
1183+ self .logger .debug ("Dependents: %s" , indirect_starts )
1184+
1185+ for app_name in start_order .copy ():
1186+ match self .app_config .root .get (app_name ):
1187+ case AppConfig (disable = False ):
1188+ if await self .create_app_object (app_name ) is None :
1189+ update_actions .apps .failed .add (app_name )
1190+ start_order .remove (app_name )
1191+ case GlobalModule ():
1192+ # Global modules are not started, they are just imported
1193+ self .logger .debug (f"Skipping global module '{ app_name } '" )
1194+ case None :
1195+ self .logger .warning (f"App '{ app_name } ' not found in app config" )
1196+
1197+ # Need to have already created the ManagedObjects for the threads to get assigned
1198+ await self .AD .threading .calculate_pin_threads ()
1199+
1200+ # Account for failures and apps that depend on them
1201+ failed = update_actions .apps .failed
1202+ failed_deps = find_all_dependents (update_actions .apps .failed , self .dependency_manager .app_deps .rev_graph )
1203+ prevented_apps = [a for a in start_order if a in failed_deps and a not in failed ]
1204+ if prevented_apps :
1205+ self .logger .warning ("Failures of other apps prevented these apps from starting: %s" , prevented_apps )
1206+ start_order = [a for a in start_order if a not in (failed | failed_deps )]
1207+
1208+ for app_name in start_order :
1209+ match self .app_config .root .get (app_name ):
1210+ case GlobalModule (module_name = str (mod_name )):
1211+ assert mod_name in sys .modules , f"{ mod_name } not in sys.modules"
1212+ case AppConfig ():
1213+ @ade .wrap_async (self .error , self .AD .app_dir , f"Failed to start '{ app_name } '" )
1214+ async def safe_start (self : "AppManagement" ):
1215+ try :
1216+ await self .start_app (app_name )
1217+ except Exception as exc :
1218+ update_actions .apps .failed .add (app_name )
1219+ raise ade .AppStartFailure (app_name ) from exc
1220+
1221+ if await self .get_state (app_name ) != "compile_error" :
1222+ await safe_start (self )
11981223
11991224 async def _import_modules (self , update_actions : UpdateActions ) -> set [str ]:
12001225 """Calls ``self.import_module`` for each module in the list
0 commit comments