158 lines
5.1 KiB
Python
158 lines
5.1 KiB
Python
"""Modal app-existence checks used before constructing the cloud entrypoint."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from typing import Any, Callable, Mapping, Sequence
|
|
|
|
try:
|
|
from .settings import modal_deployment_app_name
|
|
except ImportError: # pragma: no cover - flat Modal-container import.
|
|
from settings import modal_deployment_app_name
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _modal_exception_types(
|
|
modal_module: Any,
|
|
names: Sequence[str],
|
|
) -> tuple[type[BaseException], ...]:
|
|
"""Return available Modal exception types from a Modal SDK-like module."""
|
|
exception_namespace = getattr(modal_module, "exception", None)
|
|
if exception_namespace is None:
|
|
return tuple()
|
|
|
|
error_types: list[type[BaseException]] = []
|
|
for error_name in names:
|
|
error_type = getattr(exception_namespace, error_name, None)
|
|
if isinstance(error_type, type) and issubclass(error_type, BaseException):
|
|
error_types.append(error_type)
|
|
return tuple(error_types)
|
|
|
|
|
|
def _modal_lookup_create_flag_name(app_lookup: Callable[..., Any]) -> str | None:
|
|
"""Return the App.lookup keyword that disables creation when available."""
|
|
try:
|
|
lookup_signature = inspect.signature(app_lookup)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
for flag_name in ("create_if_missing", "create"):
|
|
if flag_name in lookup_signature.parameters:
|
|
return flag_name
|
|
return None
|
|
|
|
|
|
def _modal_app_exists_via_sdk(modal_module: Any, app_name: str) -> bool | None:
|
|
"""Check for a Modal app through a non-creating SDK lookup when possible."""
|
|
app_namespace = getattr(modal_module, "App", None)
|
|
app_lookup = getattr(app_namespace, "lookup", None)
|
|
if not callable(app_lookup):
|
|
return None
|
|
|
|
create_flag_name = _modal_lookup_create_flag_name(app_lookup)
|
|
if create_flag_name is None:
|
|
return None
|
|
|
|
not_found_errors = _modal_exception_types(modal_module, ("NotFoundError",))
|
|
try:
|
|
app_lookup(app_name, **{create_flag_name: False})
|
|
except not_found_errors:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _app_name_from_modal_app_list_entry(entry: Mapping[str, Any]) -> str | None:
|
|
"""Extract an app name from one ``modal app list --json`` entry."""
|
|
for key in ("name", "app_name", "Name"):
|
|
value = entry.get(key)
|
|
if isinstance(value, str) and value.strip():
|
|
return value.strip()
|
|
return None
|
|
|
|
|
|
def _modal_app_exists_via_cli(app_name: str) -> bool | None:
|
|
"""Check for a Modal app by parsing the CLI's JSON output when available."""
|
|
modal_cli = shutil.which("modal")
|
|
if modal_cli is None:
|
|
return None
|
|
|
|
command = [modal_cli, "app", "list", "--json"]
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
logger.warning("Unable to list Modal apps through the Modal CLI: %s", exc)
|
|
return None
|
|
if completed.returncode != 0:
|
|
logger.warning(
|
|
"Modal CLI app list failed with exit code %s: %s",
|
|
completed.returncode,
|
|
completed.stderr.strip(),
|
|
)
|
|
return None
|
|
|
|
try:
|
|
listed_apps = json.loads(completed.stdout)
|
|
except json.JSONDecodeError as exc:
|
|
logger.warning("Modal CLI app list returned invalid JSON: %s", exc)
|
|
return None
|
|
if not isinstance(listed_apps, list):
|
|
return None
|
|
|
|
return any(
|
|
_app_name_from_modal_app_list_entry(entry) == app_name
|
|
for entry in listed_apps
|
|
if isinstance(entry, Mapping)
|
|
)
|
|
|
|
|
|
def _running_inside_modal_container(modal_module: Any) -> bool:
|
|
"""Return whether this import is happening inside a Modal worker container."""
|
|
if os.getenv("MODAL_TASK_ID"):
|
|
return True
|
|
is_local = getattr(modal_module, "is_local", None)
|
|
if callable(is_local):
|
|
try:
|
|
return not bool(is_local())
|
|
except RuntimeError:
|
|
return False
|
|
return False
|
|
|
|
|
|
def _existing_modal_app_error_message(app_name: str) -> str:
|
|
"""Return the user-facing error for refusing to overwrite a Modal app."""
|
|
return (
|
|
f"Modal app {app_name!r} already exists. Delete the existing app before launching "
|
|
"ComfyUI-Modal with this configuration, especially after changing the workflow GPU "
|
|
"target or the COMFY_MODAL_GPU compatibility fallback, "
|
|
f"for example: modal app stop {app_name!r}."
|
|
)
|
|
|
|
|
|
def guard_against_existing_modal_app(
|
|
settings: Any,
|
|
modal_module: Any,
|
|
*,
|
|
error_type: type[RuntimeError],
|
|
) -> None:
|
|
"""Fail local app construction when the configured Modal app already exists."""
|
|
if _running_inside_modal_container(modal_module):
|
|
return
|
|
|
|
app_name = modal_deployment_app_name(settings)
|
|
app_exists = _modal_app_exists_via_sdk(modal_module, app_name)
|
|
if app_exists is None:
|
|
app_exists = _modal_app_exists_via_cli(app_name)
|
|
if app_exists:
|
|
raise error_type(_existing_modal_app_error_message(app_name))
|