Files
Huntarr-Sonarr/main.py

385 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Main entry point for Huntarr
Starts both the web server and the background processing tasks.
"""
import os
import threading
import sys
import signal
import logging # Use standard logging for initial setup
import atexit
import time
# Import path configuration early to set up environment
try:
from src.primary.utils import config_paths
print(f"Using config directory: {config_paths.CONFIG_DIR}")
except Exception as e:
print(f"Warning: Failed to initialize config paths: {str(e)}")
# Continue anyway - we'll handle this later
# Ensure the 'src' directory is in the Python path
# This allows importing modules from 'src.primary' etc.
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')))
# --- Early Logging Setup (Before importing app components) ---
# Basic logging to capture early errors during import or setup
log_level = logging.DEBUG if os.environ.get('DEBUG', 'false').lower() == 'true' else logging.INFO
# Create a custom formatter that uses local time
class LocalTimeFormatter(logging.Formatter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.converter = time.localtime # Use local time instead of UTC
# Disable basic logging to prevent duplicates - we use custom loggers
# logging.basicConfig(level=log_level, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
# Apply local time converter to all existing handlers
for handler in logging.root.handlers:
if hasattr(handler, 'formatter') and handler.formatter:
handler.formatter.converter = time.localtime
root_logger = logging.getLogger("HuntarrRoot") # Specific logger for this entry point
root_logger.info("--- Huntarr Main Process Starting ---")
root_logger.info(f"Python sys.path: {sys.path}")
# Check for Windows service commands
if sys.platform == 'win32' and len(sys.argv) > 1:
if sys.argv[1] == '--install-service':
try:
from src.primary.windows_service import install_service
success = install_service()
sys.exit(0 if success else 1)
except ImportError:
root_logger.error("Failed to import Windows service module. Make sure pywin32 is installed.")
sys.exit(1)
except Exception as e:
root_logger.exception(f"Error installing Windows service: {e}")
sys.exit(1)
elif sys.argv[1] == '--remove-service':
try:
from src.primary.windows_service import remove_service
success = remove_service()
sys.exit(0 if success else 1)
except ImportError:
root_logger.error("Failed to import Windows service module. Make sure pywin32 is installed.")
sys.exit(1)
except Exception as e:
root_logger.exception(f"Error removing Windows service: {e}")
sys.exit(1)
elif sys.argv[1] in ['--start', '--stop', '--restart', '--debug', '--update']:
try:
import win32serviceutil
service_name = "Huntarr"
if sys.argv[1] == '--start':
win32serviceutil.StartService(service_name)
print(f"Started {service_name} service")
elif sys.argv[1] == '--stop':
win32serviceutil.StopService(service_name)
print(f"Stopped {service_name} service")
elif sys.argv[1] == '--restart':
win32serviceutil.RestartService(service_name)
print(f"Restarted {service_name} service")
elif sys.argv[1] == '--debug':
# Run the service in debug mode directly
from src.primary.windows_service import HuntarrService
win32serviceutil.HandleCommandLine(HuntarrService)
elif sys.argv[1] == '--update':
# Update the service
win32serviceutil.StopService(service_name)
from src.primary.windows_service import install_service
install_service()
win32serviceutil.StartService(service_name)
print(f"Updated {service_name} service")
sys.exit(0)
except ImportError:
root_logger.error("Failed to import Windows service module. Make sure pywin32 is installed.")
sys.exit(1)
except Exception as e:
root_logger.exception(f"Error managing Windows service: {e}")
sys.exit(1)
try:
# Import the Flask app instance
from primary.web_server import app
# Import the background task starter function and shutdown helpers from the renamed file
from primary.background import start_huntarr, stop_event, shutdown_threads
# Configure logging first
import logging
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from primary.utils.logger import setup_main_logger, get_logger
from primary.utils.clean_logger import setup_clean_logging
# Initialize main logger
huntarr_logger = setup_main_logger()
# Initialize timezone from TZ environment variable
try:
from primary.settings_manager import initialize_timezone_from_env
initialize_timezone_from_env()
huntarr_logger.info("Timezone initialization completed.")
except Exception as e:
huntarr_logger.warning(f"Failed to initialize timezone from environment: {e}")
# Initialize clean logging for frontend consumption
setup_clean_logging()
huntarr_logger.info("Clean logging system initialized for frontend consumption.")
# Initialize database logging system
try:
from primary.utils.logs_database import get_logs_database, schedule_log_cleanup
logs_db = get_logs_database()
schedule_log_cleanup()
huntarr_logger.info("Database logging system initialized with scheduled cleanup.")
except Exception as e:
huntarr_logger.warning(f"Failed to initialize database logging: {e}")
huntarr_logger.info("Successfully imported application components.")
# Main function startup message removed to reduce log spam
except ImportError as e:
root_logger.critical(f"Fatal Error: Failed to import application components: {e}", exc_info=True)
root_logger.critical("Please ensure the application structure is correct, dependencies are installed (`pip install -r requirements.txt`), and the script is run from the project root.")
sys.exit(1)
except Exception as e:
root_logger.critical(f"Fatal Error: An unexpected error occurred during initial imports: {e}", exc_info=True)
sys.exit(1)
# Global variables for server management
waitress_server = None
shutdown_requested = threading.Event()
def run_background_tasks():
"""Runs the Huntarr background processing."""
bg_logger = get_logger("HuntarrBackground") # Use app's logger
try:
bg_logger.info("Starting Huntarr background tasks...")
start_huntarr() # This function contains the main loop and shutdown logic
except Exception as e:
bg_logger.exception(f"Critical error in Huntarr background tasks: {e}")
finally:
bg_logger.info("Huntarr background tasks stopped.")
def run_web_server():
"""Runs the Flask web server using Waitress in production."""
global waitress_server
web_logger = get_logger("WebServer") # Use app's logger
debug_mode = os.environ.get('DEBUG', 'false').lower() == 'true'
host = os.environ.get('FLASK_HOST', '0.0.0.0')
port = int(os.environ.get('PORT', 9705)) # Use PORT for consistency
web_logger.info(f"Starting web server on {host}:{port} (Debug: {debug_mode})...")
# Log the current authentication mode once at startup
try:
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from primary.settings_manager import load_settings
settings = load_settings("general")
local_access_bypass = settings.get("local_access_bypass", False)
proxy_auth_bypass = settings.get("proxy_auth_bypass", False)
if proxy_auth_bypass:
web_logger.info("🔓 Authentication Mode: NO LOGIN MODE (Proxy authentication bypass enabled)")
elif local_access_bypass:
web_logger.info("🏠 Authentication Mode: LOCAL ACCESS BYPASS (Local network authentication bypass enabled)")
else:
web_logger.info("🔐 Authentication Mode: STANDARD (Full authentication required)")
except Exception as e:
web_logger.warning(f"Could not determine authentication mode at startup: {e}")
if debug_mode:
# Use Flask's development server for debugging (less efficient, auto-reloads)
# Note: use_reloader=True can cause issues with threads starting twice.
web_logger.warning("Running in DEBUG mode with Flask development server.")
try:
app.run(host=host, port=port, debug=True, use_reloader=False)
except Exception as e:
web_logger.exception(f"Flask development server failed: {e}")
# Signal background thread to stop if server fails critically
if not stop_event.is_set():
stop_event.set()
else:
# Use Waitress for production with proper signal handling
try:
from waitress import serve
from waitress.server import create_server
import time
web_logger.info("Running with Waitress production server.")
# Create the server instance so we can shut it down gracefully
waitress_server = create_server(app, host=host, port=port, threads=8)
web_logger.info("Waitress server starting...")
# Start the server in a separate thread
server_thread = threading.Thread(target=waitress_server.run, daemon=True)
server_thread.start()
# Monitor for shutdown signal in the main thread
while not shutdown_requested.is_set() and not stop_event.is_set():
try:
# Check both shutdown events
if shutdown_requested.wait(timeout=0.5) or stop_event.wait(timeout=0.5):
break
except KeyboardInterrupt:
break
# Shutdown sequence
web_logger.info("Shutdown signal received. Stopping Waitress server...")
try:
waitress_server.close()
web_logger.info("Waitress server close() called.")
# Wait for server thread to finish
server_thread.join(timeout=3.0)
if server_thread.is_alive():
web_logger.warning("Server thread did not stop within timeout.")
else:
web_logger.info("Server thread stopped successfully.")
except Exception as e:
web_logger.exception(f"Error during Waitress server shutdown: {e}")
web_logger.info("Waitress server has stopped.")
except ImportError:
web_logger.error("Waitress not found. Falling back to Flask development server (NOT recommended for production).")
web_logger.error("Install waitress ('pip install waitress') for production use.")
try:
app.run(host=host, port=port, debug=False, use_reloader=False)
except Exception as e:
web_logger.exception(f"Flask development server (fallback) failed: {e}")
# Signal background thread to stop if server fails critically
if not stop_event.is_set():
stop_event.set()
except Exception as e:
web_logger.exception(f"Waitress server failed: {e}")
# Signal background thread to stop if server fails critically
if not stop_event.is_set():
stop_event.set()
def main_shutdown_handler(signum, frame):
"""Gracefully shut down the application."""
huntarr_logger.info(f"Received signal {signum}. Initiating graceful shutdown...")
# Set both shutdown events
if not stop_event.is_set():
stop_event.set()
if not shutdown_requested.is_set():
shutdown_requested.set()
# Also shutdown the Waitress server directly if it exists
global waitress_server
if waitress_server:
try:
huntarr_logger.info("Signaling Waitress server to shutdown...")
waitress_server.close()
except Exception as e:
huntarr_logger.warning(f"Error closing Waitress server: {e}")
def cleanup_handler():
"""Cleanup function called at exit"""
huntarr_logger.info("Exit cleanup handler called")
if not stop_event.is_set():
stop_event.set()
if not shutdown_requested.is_set():
shutdown_requested.set()
def main():
"""Main entry point function for Huntarr application.
This function is called by app_launcher.py in the packaged ARM application.
"""
# Register signal handlers for graceful shutdown in the main process
signal.signal(signal.SIGINT, main_shutdown_handler)
signal.signal(signal.SIGTERM, main_shutdown_handler)
# Register cleanup handler
atexit.register(cleanup_handler)
# Initialize databases with default configurations
try:
from primary.settings_manager import initialize_database
initialize_database()
huntarr_logger.info("Main database initialization completed successfully")
# Initialize manager database and migrate history if needed
from src.primary.utils.manager_database import get_manager_database
from src.primary.utils.database import get_database
manager_db = get_manager_database()
# Attempt to migrate history from huntarr.db if it exists
main_db = get_database()
if hasattr(main_db, 'db_path'):
try:
manager_db.migrate_from_huntarr_db(main_db.db_path)
huntarr_logger.info("Hunt Manager database initialized and migration completed")
except Exception as migration_error:
huntarr_logger.warning(f"History migration completed with warnings: {migration_error}")
except Exception as e:
huntarr_logger.error(f"Failed to initialize databases: {e}")
huntarr_logger.error("Application may not function correctly without database")
# Continue anyway - the app might still work with defaults
background_thread = None
try:
# Start background tasks in a daemon thread
# Daemon threads exit automatically if the main thread exits unexpectedly,
# but we'll try to join() them for a graceful shutdown.
background_thread = threading.Thread(target=run_background_tasks, name="HuntarrBackground", daemon=True)
background_thread.start()
# Start the web server in the main thread (blocking)
# This will run until the server is stopped (e.g., by Ctrl+C)
run_web_server()
except KeyboardInterrupt:
huntarr_logger.info("KeyboardInterrupt received in main thread. Shutting down...")
if not stop_event.is_set():
stop_event.set()
if not shutdown_requested.is_set():
shutdown_requested.set()
except Exception as e:
huntarr_logger.exception(f"An unexpected error occurred in the main execution block: {e}")
if not stop_event.is_set():
stop_event.set() # Ensure shutdown is triggered on unexpected errors
if not shutdown_requested.is_set():
shutdown_requested.set()
finally:
# --- Cleanup ---
huntarr_logger.info("Web server has stopped. Initiating final shutdown sequence...")
# Ensure the stop event is set (might already be set by signal handler or error)
if not stop_event.is_set():
huntarr_logger.warning("Stop event was not set before final cleanup. Setting now.")
stop_event.set()
# Wait for the background thread to finish cleanly
if background_thread and background_thread.is_alive():
huntarr_logger.info("Waiting for background tasks to complete...")
background_thread.join(timeout=5) # Reduced timeout for faster shutdown
if background_thread.is_alive():
huntarr_logger.warning("Background thread did not stop gracefully within the timeout.")
elif background_thread:
huntarr_logger.info("Background thread already stopped.")
else:
huntarr_logger.info("Background thread was not started.")
# Call the shutdown_threads function from primary.main (if it does more than just join)
# This might be redundant if start_huntarr handles its own cleanup via stop_event
# huntarr_logger.info("Calling shutdown_threads()...")
# shutdown_threads() # Uncomment if primary.main.shutdown_threads() does more cleanup
huntarr_logger.info("--- Huntarr Main Process Exiting ---")
return 0 # Success exit code
if __name__ == '__main__':
# Call the main function and exit with its return code
# This will use the return value from main() (0 for success) as the exit code
sys.exit(main())