From 0316ea325837258a269ef3dc17383e644b04186d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:14:04 +0000 Subject: [PATCH 1/3] Initial plan From 99e6e7e2b96caaed0887f9ccccdfdb4dfe881397 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:21:54 +0000 Subject: [PATCH 2/3] Replace os._exit(0) with ApiDependencies.shutdown() on KeyboardInterrupt Instead of immediately force-exiting the process on CTRL+C, call ApiDependencies.shutdown() to gracefully stop the download and install manager services, allowing active work to complete or cancel cleanly before the process exits. Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/run_app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/invokeai/app/run_app.py b/invokeai/app/run_app.py index e9be3032a44..f032ff9ab7a 100644 --- a/invokeai/app/run_app.py +++ b/invokeai/app/run_app.py @@ -9,8 +9,6 @@ def get_app(): def run_app() -> None: """The main entrypoint for the app.""" - import os - from invokeai.frontend.cli.arg_parser import InvokeAIArgs # Parse the CLI arguments before doing anything else, which ensures CLI args correctly override settings from other @@ -106,5 +104,8 @@ def run_app() -> None: loop.run_until_complete(server.serve()) except KeyboardInterrupt: logger.info("InvokeAI shutting down...") - # Force exit to avoid hanging on non-daemon background threads (e.g. model install service). - os._exit(0) + # Gracefully shut down services (e.g. model download and install managers) so that any + # active work is completed or cleanly cancelled before the process exits. + from invokeai.app.api.dependencies import ApiDependencies + + ApiDependencies.shutdown() From 4b70d392ceec7135c1d2ea4f9eb086092340e446 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:42:08 +0000 Subject: [PATCH 3/3] Make stop() idempotent in download and model install services When CTRL+C is pressed, uvicorn's graceful shutdown triggers the FastAPI lifespan which calls ApiDependencies.shutdown(), then a KeyboardInterrupt propagates from run_until_complete() hitting the except block which tries to call ApiDependencies.shutdown() a second time. Change both stop() methods to return silently (instead of raising) when the service is not running. This handles: - Double-shutdown: lifespan already stopped the services - Early interrupt: services were never fully started Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/services/download/download_default.py | 2 +- invokeai/app/services/model_install/model_install_default.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py index f65f00e2aee..7c7fe82124d 100644 --- a/invokeai/app/services/download/download_default.py +++ b/invokeai/app/services/download/download_default.py @@ -88,7 +88,7 @@ def stop(self, *args: Any, **kwargs: Any) -> None: """Stop the download worker threads.""" with self._lock: if not self._worker_pool: - raise Exception("Attempt to stop the download service before it was started") + return self._accept_download_requests = False # reject attempts to add new jobs to queue queued_jobs = [x for x in self.list_jobs() if x.status == DownloadJobStatus.WAITING] active_jobs = [x for x in self.list_jobs() if x.status == DownloadJobStatus.RUNNING] diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py index c1fb0c651f4..2726f3e5a91 100644 --- a/invokeai/app/services/model_install/model_install_default.py +++ b/invokeai/app/services/model_install/model_install_default.py @@ -313,7 +313,7 @@ def start(self, invoker: Optional[Invoker] = None) -> None: def stop(self, invoker: Optional[Invoker] = None) -> None: """Stop the installer thread; after this the object can be deleted and garbage collected.""" if not self._running: - raise Exception("Attempt to stop the install service before it was started") + return self._logger.debug("calling stop_event.set()") self._stop_event.set() self._clear_pending_jobs()