Files
rays eb86f889b3 fix(logging): unify Doctor startup output
Standardize backend log identities and severity highlighting while keeping persisted logs ANSI-free. Restore the encoding-aware startup banner and emit its art as one tightly packed block.
2026-08-09 05:38:01 +08:00

230 lines
7.1 KiB
Python

"""
ComfyUI-Doctor Prestartup Script
This runs BEFORE any custom nodes are imported.
Installs the SmartLogger as early as possible to capture all import errors.
"""
import datetime
import importlib.util
import os
import sys
import tempfile
# Get the directory of this script (ComfyUI-Doctor folder)
DOCTOR_DIR = os.path.dirname(os.path.abspath(__file__))
_FALLBACK_LOG_DIR = os.path.join(tempfile.gettempdir(), "ComfyUI-Doctor", "logs")
def _load_terminal_output():
"""Load the shared formatter without assuming a package import root."""
terminal_output_file = os.path.join(DOCTOR_DIR, "terminal_output.py")
if not os.path.exists(terminal_output_file):
return None
try:
spec = importlib.util.spec_from_file_location(
"comfyui_doctor_prestartup_terminal_output",
terminal_output_file,
)
if spec is None or spec.loader is None:
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
except Exception:
return None
def _load_doctor_data_dir():
"""Resolve doctor_paths without assuming the extension root is on sys.path."""
doctor_paths_file = os.path.join(DOCTOR_DIR, "services", "doctor_paths.py")
if not os.path.exists(doctor_paths_file):
return None
try:
spec = importlib.util.spec_from_file_location(
"comfyui_doctor_prestartup_doctor_paths",
doctor_paths_file,
)
if spec is None or spec.loader is None:
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return getattr(module, "get_doctor_data_dir", None)
except Exception:
return None
# R26: keep prestartup bootstrap file-path-based. ComfyUI may execute this
# before the extension root is available as a top-level import root.
_get_doctor_data_dir = _load_doctor_data_dir()
_terminal_output = _load_terminal_output()
_emit_doctor_log = getattr(_terminal_output, "emit_doctor_log", None)
_strip_ansi = getattr(_terminal_output, "strip_ansi", lambda value: str(value))
def _resolve_log_dir() -> str:
"""
Resolve a writable log directory for the earliest capture phase.
IMPORTANT:
- Never assume the extension folder is writable (ComfyUI Desktop resources may be read-only).
- Never raise here; prestartup must not break ComfyUI startup.
"""
if _get_doctor_data_dir:
try:
return os.path.join(_get_doctor_data_dir(), "logs")
except Exception:
pass
return _FALLBACK_LOG_DIR
LOG_DIR = _resolve_log_dir()
# Ensure logs directory exists
try:
os.makedirs(LOG_DIR, exist_ok=True)
except Exception:
LOG_DIR = _FALLBACK_LOG_DIR
try:
os.makedirs(LOG_DIR, exist_ok=True)
except Exception:
LOG_DIR = ""
# Generate log filename
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
LOG_PATH = os.path.join(LOG_DIR, f"comfyui_debug_{timestamp}.log") if LOG_DIR else ""
# ============================================================
# Minimal Logger (Prestartup Phase)
# A lightweight version that just captures output to file.
# Full analysis capabilities will be loaded in __init__.py later.
# ============================================================
class PrestartupLogger:
"""
Minimal stdout/stderr interceptor for the prestartup phase.
Captures output to log file before full logger is available.
"""
_original_stdout = None
_original_stderr = None
_log_file = None
_installed = False
@classmethod
def install(cls, log_path):
if cls._installed:
return
cls._original_stdout = sys.stdout
cls._original_stderr = sys.stderr
cls._log_file = None
if log_path:
try:
cls._log_file = open(log_path, "a", encoding="utf-8")
except Exception:
cls._log_file = None
sys.stdout = cls(cls._original_stdout)
sys.stderr = cls(cls._original_stderr)
cls._installed = True
@classmethod
def get_log_path(cls):
return LOG_PATH
@classmethod
def is_installed(cls):
return cls._installed
@classmethod
def uninstall(cls):
if not cls._installed:
return
# Restore original streams if we still have them
try:
if cls._original_stdout:
sys.stdout = cls._original_stdout
if cls._original_stderr:
sys.stderr = cls._original_stderr
except Exception:
pass
# Close log file handle
try:
if cls._log_file:
cls._log_file.flush()
cls._log_file.close()
except Exception:
pass
cls._log_file = None
cls._original_stdout = None
cls._original_stderr = None
cls._installed = False
def __init__(self, stream):
self.stream = stream
def write(self, message):
try:
self.stream.write(message)
self.stream.flush()
except (OSError, AttributeError, UnicodeError, ValueError):
pass # Stream may be unavailable during early startup
try:
if PrestartupLogger._log_file:
# CRITICAL: terminal highlighting must never enter persisted logs.
PrestartupLogger._log_file.write(_strip_ansi(message))
PrestartupLogger._log_file.flush()
except (OSError, AttributeError, UnicodeError, ValueError):
pass # Log file may be unavailable
def flush(self):
try:
self.stream.flush()
if PrestartupLogger._log_file:
PrestartupLogger._log_file.flush()
except (OSError, AttributeError, UnicodeError, ValueError):
pass # Stream or file may be unavailable
@property
def encoding(self):
return getattr(self.stream, 'encoding', 'utf-8')
def isatty(self):
return getattr(self.stream, 'isatty', lambda: False)()
def fileno(self):
try:
return self.stream.fileno()
except Exception:
return -1
# Install minimal logger IMMEDIATELY
PrestartupLogger.install(LOG_PATH)
def _emit_startup_line(message: str, level: str = "INFO") -> None:
if _emit_doctor_log is not None:
_emit_doctor_log(message, level, stream=sys.stdout)
return
safe_message = str(message).encode("ascii", "backslashreplace").decode("ascii")
safe_level = str(level or "INFO").upper()
if safe_level not in {"DEBUG", "DETAIL", "INFO", "WARNING", "ERROR", "CRITICAL"}:
safe_level = "INFO"
try:
print(f"[ComfyUI-Doctor {safe_level}] {safe_message}")
except (OSError, AttributeError, UnicodeError, ValueError):
pass
_emit_startup_line("Prestartup hook activated (early capture)")
if LOG_PATH:
_emit_startup_line(f"Log file: {LOG_PATH}")
else:
_emit_startup_line("Log file unavailable (fallback disabled)", "WARNING")
# Store log path for __init__.py to retrieve
if LOG_PATH:
os.environ["COMFYUI_DOCTOR_LOG_PATH"] = LOG_PATH