Files
Vito 2cf9f42e73 fix: diagnostics and monitor respect config, singleton lifecycle (#132) (#133)
* fix: diagnostics, backup, and restore now respect config.json db path (#132)

* test: add regression tests for config-aware diagnostics and backup (#132)

* fix(test): use fresh event loop to avoid aiohttp test pollution

* fix: don't kill singleton image monitor when node instance is garbage collected

The __del__ method was calling stop_monitoring() on the shared singleton
ImageMonitor, killing it for ALL nodes whenever any single node instance
was garbage collected between workflow runs. This caused the monitor to
restart on every execution and miss image save events due to race conditions.

* test: add regression tests for image monitor singleton lifecycle

Ensures cleanup/del never stops the shared monitor, singleton is
thread-safe, and observer survives across node instance lifecycles.

* style: address PR review — log suppressed exceptions, remove unused imports
2026-04-03 06:22:55 -07:00

478 lines
17 KiB
Python

"""Diagnostic utilities for troubleshooting the PromptManager gallery system.
This module provides comprehensive diagnostic tools to help identify and resolve
issues with the PromptManager system, particularly the gallery and image monitoring
functionality. It performs systematic checks of all system components and provides
clear feedback on potential problems.
The diagnostic system checks:
- Database connectivity and schema integrity
- Image table structure and data
- File system permissions and access
- ComfyUI output directory detection
- Required Python dependencies
- System integration points
Typical usage:
from utils.diagnostics import GalleryDiagnostics, run_diagnostics
# Run full diagnostic suite
results = run_diagnostics()
# Or create custom diagnostic instance
diagnostics = GalleryDiagnostics("custom_db.db")
database_status = diagnostics.check_database()
The diagnostics provide:
- Clear pass/fail status for each component
- Detailed error messages and remediation suggestions
- Statistics and system information
- Test utilities for verifying functionality
"""
import os
import sqlite3
from pathlib import Path
from typing import Dict, Any, List
from .logging_config import get_logger
class GalleryDiagnostics:
"""Diagnostics for the gallery system.
This class provides comprehensive diagnostic capabilities for the PromptManager
gallery system. It systematically checks all components and dependencies to
identify potential issues and provide actionable feedback.
The diagnostics cover:
- Database structure and connectivity
- Image tracking table integrity
- File system access and permissions
- ComfyUI integration points
- Python dependency availability
Each diagnostic method returns a standardized result dictionary with:
- status: 'ok', 'warning', or 'error'
- message: Descriptive message (for warnings/errors)
- Additional data specific to the diagnostic
"""
def __init__(self, db_path: str = None):
"""Initialize the diagnostics system.
Args:
db_path: Path to the SQLite database file to diagnose.
If None, resolves from config.json or defaults to
prompts.db in the extension root.
"""
if db_path is None:
db_path = self._resolve_db_path()
self.db_path = db_path
self.logger = get_logger("prompt_manager.diagnostics")
@staticmethod
def _resolve_db_path() -> str:
"""Resolve database path from config, matching PromptDatabase logic."""
extension_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
try:
from py.config import PromptManagerConfig
db_path = PromptManagerConfig.DEFAULT_DB_PATH
except Exception:
db_path = "prompts.db"
if not os.path.isabs(db_path):
db_path = os.path.join(extension_root, db_path)
return db_path
def run_full_diagnostic(self) -> Dict[str, Any]:
"""Run a complete diagnostic check.
Executes all diagnostic checks in sequence and provides a comprehensive
report of system status. Logs detailed information during the process.
Returns:
Dictionary mapping diagnostic categories to their results:
- database: Database connectivity and structure check
- images_table: Image tracking table specific check
- file_system: File system access and permissions check
- comfyui_output: ComfyUI output directory detection
- dependencies: Python dependency availability check
"""
self.logger.info("\n" + "=" * 60)
self.logger.info("[DIAG] PROMPTMANAGER GALLERY DIAGNOSTICS")
self.logger.info("=" * 60)
results = {
"database": self.check_database(),
"images_table": self.check_images_table(),
"file_system": self.check_file_system(),
"comfyui_output": self.check_comfyui_output(),
"dependencies": self.check_dependencies(),
}
self.logger.info("\n" + "=" * 60)
self.logger.info("[SUMMARY] DIAGNOSTIC SUMMARY")
self.logger.info("=" * 60)
for category, result in results.items():
status = "[PASS] PASS" if result["status"] == "ok" else "[FAIL] FAIL"
self.logger.info(f"{category.upper():<20} {status}")
if result["status"] != "ok":
self.logger.warning(f" Issue: {result['message']}")
self.logger.info("\n" + "=" * 60)
return results
def check_database(self) -> Dict[str, Any]:
"""Check database connection and structure.
Verifies that the database file exists, is accessible, and contains
the expected prompt table structure.
Returns:
Dictionary containing:
- status: 'ok' or 'error'
- message: Error description (if status is 'error')
- prompt_count: Number of prompts in database (if successful)
- has_images_table: Whether generated_images table exists (if successful)
"""
self.logger.info("\n[DB] Checking Database...")
try:
if not os.path.exists(self.db_path):
return {
"status": "error",
"message": f"Database file not found: {self.db_path}",
}
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
# Check prompts table
cursor = conn.execute("SELECT COUNT(*) as count FROM prompts")
prompt_count = cursor.fetchone()["count"]
self.logger.info(f" [NOTE] Prompts in database: {prompt_count}")
# Check if generated_images table exists
cursor = conn.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='generated_images'
""")
has_images_table = cursor.fetchone() is not None
self.logger.info(f" [IMG] Images table exists: {has_images_table}")
return {
"status": "ok",
"prompt_count": prompt_count,
"has_images_table": has_images_table,
}
except Exception as e:
return {"status": "error", "message": f"Database error: {str(e)}"}
def check_images_table(self) -> Dict[str, Any]:
"""Check the generated_images table specifically.
Examines the generated_images table structure and content to verify
the gallery system can function properly.
Returns:
Dictionary containing:
- status: 'ok' or 'error'
- message: Error description (if table missing or inaccessible)
- image_count: Number of images in table (if successful)
- recent_images: List of recent image records (if successful)
"""
self.logger.info("\n[IMG] Checking Images Table...")
try:
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
# Check if table exists
cursor = conn.execute("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='generated_images'
""")
if not cursor.fetchone():
return {
"status": "error",
"message": "generated_images table does not exist - run the updated code to create it",
}
# Check image records
cursor = conn.execute("SELECT COUNT(*) as count FROM generated_images")
image_count = cursor.fetchone()["count"]
self.logger.info(f" [STATS] Images in database: {image_count}")
# Get recent images
cursor = conn.execute("""
SELECT gi.*, p.text
FROM generated_images gi
LEFT JOIN prompts p ON gi.prompt_id = p.id
ORDER BY gi.generation_time DESC
LIMIT 5
""")
recent_images = [dict(row) for row in cursor.fetchall()]
self.logger.info(f" [TIME] Recent images: {len(recent_images)}")
for img in recent_images:
self.logger.info(
f" - {img['filename']} -> Prompt {img['prompt_id']}"
)
return {
"status": "ok",
"image_count": image_count,
"recent_images": recent_images,
}
except Exception as e:
return {"status": "error", "message": f"Images table error: {str(e)}"}
def check_file_system(self) -> Dict[str, Any]:
"""Check file system and permissions.
Verifies that the application has appropriate file system access
for reading images and writing database files.
Returns:
Dictionary containing:
- status: 'ok' or 'error'
- message: Error description (if issues detected)
- current_dir: Current working directory path
- can_write: Whether write access is available
"""
self.logger.info("\n[DIR] Checking File System...")
try:
# Check current directory
current_dir = os.getcwd()
self.logger.info(f" [FOLDER] Current directory: {current_dir}")
# Check if we can write to current directory
test_file = "test_write.tmp"
try:
with open(test_file, "w") as f:
f.write("test")
os.remove(test_file)
can_write = True
except (IOError, OSError):
can_write = False
self.logger.info(f" [EDIT] Can write to directory: {can_write}")
return {"status": "ok", "current_dir": current_dir, "can_write": can_write}
except Exception as e:
return {"status": "error", "message": f"File system error: {str(e)}"}
def check_comfyui_output(self) -> Dict[str, Any]:
"""Check ComfyUI output directories.
Attempts to locate ComfyUI output directories where generated images
would be stored. Checks both common locations and ComfyUI's configured paths.
Returns:
Dictionary containing:
- status: 'ok' if directories found, 'warning' if none found
- message: Warning message (if no directories found)
- output_dirs: List of detected output directory paths
"""
self.logger.info("\n[STYLE] Checking ComfyUI Output...")
output_dirs = []
def _add_dir(abs_path, source):
if abs_path not in output_dirs:
output_dirs.append(abs_path)
self.logger.info(f" [DIR] {source}: {abs_path}")
try:
image_files = []
for ext in [".png", ".jpg", ".jpeg", ".webp"]:
image_files.extend(Path(abs_path).rglob(f"*{ext}"))
self.logger.info(f" [IMG] Images found: {len(image_files)}")
except Exception as e:
self.logger.error(f" [FAIL] Error scanning: {e}")
# Check user-configured gallery directories from config.json
try:
from py.config import GalleryConfig
for cfg_dir in GalleryConfig.MONITORING_DIRECTORIES:
abs_path = os.path.abspath(cfg_dir)
if os.path.exists(abs_path):
_add_dir(abs_path, "Configured directory")
except Exception as e:
self.logger.debug(f" [NOTE] Could not load gallery config: {e}")
# Check ComfyUI's own output directory (respects --output-directory)
try:
import folder_paths
comfyui_output = folder_paths.get_output_directory()
if comfyui_output and os.path.exists(comfyui_output):
_add_dir(os.path.abspath(comfyui_output), "ComfyUI output dir")
except ImportError:
self.logger.warning(" [WARN] ComfyUI folder_paths not available")
# Fallback: check common relative paths
if not output_dirs:
for dir_path in [
"output",
"../output",
"../../output",
"ComfyUI/output",
"../ComfyUI/output",
"../../ComfyUI/output",
]:
abs_path = os.path.abspath(dir_path)
if os.path.exists(abs_path):
_add_dir(abs_path, "Found output dir")
return {
"status": "ok" if output_dirs else "warning",
"message": "No output directories found" if not output_dirs else None,
"output_dirs": output_dirs,
}
def check_dependencies(self) -> Dict[str, Any]:
"""Check required dependencies.
Verifies that all required Python packages are available for the
PromptManager system to function properly.
Returns:
Dictionary containing:
- status: 'ok' if all dependencies available, 'error' if any missing
- message: Error description (if dependencies missing)
- dependencies: Dictionary mapping package names to availability status
"""
self.logger.info("\n[PKG] Checking Dependencies...")
dependencies = {"watchdog": False, "PIL": False, "sqlite3": False}
# Check watchdog
try:
import watchdog
dependencies["watchdog"] = True
self.logger.info(f" [PASS] watchdog: {watchdog.__version__}")
except ImportError:
self.logger.error(" [FAIL] watchdog: NOT INSTALLED")
# Check PIL
try:
from PIL import Image
dependencies["PIL"] = True
self.logger.info(f" [PASS] PIL (Pillow): Available")
except ImportError:
self.logger.error(" [FAIL] PIL (Pillow): NOT AVAILABLE")
# Check sqlite3
try:
import sqlite3
dependencies["sqlite3"] = True
self.logger.info(f" [PASS] sqlite3: {sqlite3.sqlite_version}")
except ImportError:
self.logger.error(" [FAIL] sqlite3: NOT AVAILABLE")
all_deps_ok = all(dependencies.values())
return {
"status": "ok" if all_deps_ok else "error",
"message": "Missing dependencies" if not all_deps_ok else None,
"dependencies": dependencies,
}
def create_test_image_link(
self, prompt_id: int, test_image_path: str = None
) -> Dict[str, Any]:
"""Create a test image link to verify the system works.
Creates a test entry in the generated_images table to verify that
the image linking functionality is working correctly.
Args:
prompt_id: ID of an existing prompt to link the test image to
test_image_path: Optional path for the test image (uses fake path if None)
Returns:
Dictionary containing:
- status: 'ok' if test link created successfully, 'error' otherwise
- message: Success or error message
- image_id: ID of created test image record (if successful)
"""
self.logger.info(f"\n[TEST] Creating test image link for prompt {prompt_id}...")
try:
# Import database operations
import sys
import os
sys.path.insert(
0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
from database.operations import PromptDatabase
db = PromptDatabase()
# Create a fake image path if none provided
if not test_image_path:
test_image_path = "/fake/test/image.png"
# Create test metadata
test_metadata = {
"file_info": {
"size": 1024000,
"dimensions": [512, 512],
"format": "PNG",
},
"workflow": {"test": True},
"prompt": {"test_prompt": "This is a test"},
}
# Link the test image
image_id = db.link_image_to_prompt(
prompt_id=str(prompt_id),
image_path=test_image_path,
metadata=test_metadata,
)
self.logger.info(f" [PASS] Test image linked with ID: {image_id}")
return {
"status": "ok",
"image_id": image_id,
"message": f"Test image linked successfully with ID {image_id}",
}
except Exception as e:
return {
"status": "error",
"message": f"Failed to create test link: {str(e)}",
}
def run_diagnostics() -> Dict[str, Any]:
"""Run diagnostics from command line.
Convenience function to create a GalleryDiagnostics instance and run
the full diagnostic suite with default settings.
Returns:
Complete diagnostic results dictionary
"""
diagnostics = GalleryDiagnostics()
return diagnostics.run_full_diagnostic()
if __name__ == "__main__":
run_diagnostics()