Local mode runs rewritten components inside the local ComfyUI process and exists only for developing the extension, yet it was the default, so users who did not set the variable silently ran in it. settings now owns the default (remote) through configured_execution_mode(), and every host-side reader uses it. The Modal image environment pins local explicitly, as SSH and Vast workers already do, so container code keeps its in-process behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
4.8 KiB
Python
142 lines
4.8 KiB
Python
"""Startup bootstrap for the optional Modal Python SDK."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import importlib.util
|
|
import logging
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
try: # Package import inside ComfyUI; flat import inside workers and tests.
|
|
from .settings import configured_execution_mode
|
|
except ImportError: # pragma: no cover - exercised by flat imports.
|
|
from settings import configured_execution_mode
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MODAL_PACKAGE_SPEC = "modal==1.4.2"
|
|
_INSTALL_TIMEOUT_SECONDS = 300
|
|
|
|
|
|
def _remote_execution_requested(execution_mode: str | None = None) -> bool:
|
|
"""Return whether startup configuration requests Modal-backed execution."""
|
|
if execution_mode is None:
|
|
return configured_execution_mode() == "remote"
|
|
return execution_mode.strip().lower() == "remote"
|
|
|
|
|
|
def _build_install_command() -> list[str] | None:
|
|
"""Build an installer command targeting the current Python interpreter."""
|
|
uv_executable = shutil.which("uv")
|
|
if uv_executable is not None:
|
|
return [
|
|
uv_executable,
|
|
"pip",
|
|
"install",
|
|
"--python",
|
|
sys.executable,
|
|
MODAL_PACKAGE_SPEC,
|
|
]
|
|
if importlib.util.find_spec("pip") is not None:
|
|
return [sys.executable, "-m", "pip", "install", MODAL_PACKAGE_SPEC]
|
|
return None
|
|
|
|
|
|
def _log_installer_output(completed_process: subprocess.CompletedProcess[str]) -> None:
|
|
"""Log captured installer output at debug level for troubleshooting."""
|
|
if completed_process.stdout.strip():
|
|
logger.debug("Modal SDK installer stdout:\n%s", completed_process.stdout.strip())
|
|
if completed_process.stderr.strip():
|
|
logger.debug("Modal SDK installer stderr:\n%s", completed_process.stderr.strip())
|
|
|
|
|
|
def _install_modal_sdk() -> bool:
|
|
"""Install the pinned Modal SDK into the interpreter running ComfyUI."""
|
|
command = _build_install_command()
|
|
if command is None:
|
|
logger.error(
|
|
"Cannot install required Modal SDK %s automatically because neither uv nor pip "
|
|
"is available for Python interpreter %s.",
|
|
MODAL_PACKAGE_SPEC,
|
|
sys.executable,
|
|
)
|
|
return False
|
|
|
|
command_text = shlex.join(command)
|
|
logger.info("Running automatic Modal SDK installation: %s", command_text)
|
|
try:
|
|
completed_process = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
check=False,
|
|
text=True,
|
|
timeout=_INSTALL_TIMEOUT_SECONDS,
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
logger.error(
|
|
"Automatic Modal SDK installation timed out after %d seconds: %s",
|
|
_INSTALL_TIMEOUT_SECONDS,
|
|
command_text,
|
|
)
|
|
return False
|
|
except OSError as exc:
|
|
logger.error("Unable to start automatic Modal SDK installation %s: %s", command_text, exc)
|
|
return False
|
|
|
|
_log_installer_output(completed_process)
|
|
if completed_process.returncode != 0:
|
|
logger.error(
|
|
"Automatic Modal SDK installation failed with exit status %d. Retry manually: %s",
|
|
completed_process.returncode,
|
|
command_text,
|
|
)
|
|
return False
|
|
logger.info("Automatic Modal SDK installation completed successfully for %s.", MODAL_PACKAGE_SPEC)
|
|
return True
|
|
|
|
|
|
def _modal_sdk_is_importable() -> bool:
|
|
"""Return whether the Modal SDK imports without a missing-module error."""
|
|
try:
|
|
importlib.import_module("modal")
|
|
except ModuleNotFoundError as exc:
|
|
if exc.name != "modal":
|
|
logger.error(
|
|
"The Modal SDK is installed but cannot import because module %s is missing.",
|
|
exc.name,
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def ensure_modal_sdk_available(execution_mode: str | None = None) -> bool:
|
|
"""Install and import the pinned Modal SDK when remote execution needs it."""
|
|
if _modal_sdk_is_importable():
|
|
logger.debug("Modal SDK is already available to Python interpreter %s.", sys.executable)
|
|
return True
|
|
if not _remote_execution_requested(execution_mode):
|
|
logger.debug("Modal SDK is unavailable, but local execution mode does not require it.")
|
|
return False
|
|
|
|
logger.warning(
|
|
"Modal SDK is missing from Python interpreter %s; installing %s automatically.",
|
|
sys.executable,
|
|
MODAL_PACKAGE_SPEC,
|
|
)
|
|
if not _install_modal_sdk():
|
|
return False
|
|
|
|
importlib.invalidate_caches()
|
|
if not _modal_sdk_is_importable():
|
|
logger.error(
|
|
"Modal SDK %s was installed but is still not importable in Python interpreter %s.",
|
|
MODAL_PACKAGE_SPEC,
|
|
sys.executable,
|
|
)
|
|
return False
|
|
logger.info("Modal SDK %s is ready for remote execution.", MODAL_PACKAGE_SPEC)
|
|
return True
|