diff --git a/routes/updater.py b/routes/updater.py index f80d8c4..2583905 100644 --- a/routes/updater.py +++ b/routes/updater.py @@ -2,14 +2,15 @@ from __future__ import annotations -from flask import Blueprint, jsonify, request, Response +from flask import Blueprint, Response, jsonify, request from utils.logging import get_logger from utils.updater import ( check_for_updates, - get_update_status, dismiss_update, + get_update_status, perform_update, + restart_application, ) logger = get_logger('intercept.routes.updater') @@ -137,3 +138,42 @@ def dismiss_notification() -> Response: 'success': False, 'error': str(e) }), 500 + + +@updater_bp.route('/restart', methods=['POST']) +def restart_app() -> Response: + """ + Restart the application. + + This endpoint triggers a graceful restart of the application: + 1. Stops all running decoder processes + 2. Cleans up global state + 3. Replaces the current process with a fresh instance + + The response may not be received by the client since the process + is replaced immediately. Clients should poll /health until the + server responds again. + + Returns: + JSON with restart status (may not be delivered) + """ + import threading + + logger.info("Restart requested via API") + + # Send response before restarting + # Use a short delay to allow the response to be sent + def delayed_restart(): + import time + time.sleep(0.5) # Allow response to be sent + restart_application() + + # Start restart in a background thread so we can return a response + restart_thread = threading.Thread(target=delayed_restart, daemon=False) + restart_thread.start() + + return jsonify({ + 'success': True, + 'message': 'Application is restarting. Please wait...', + 'action': 'restart' + }) diff --git a/utils/updater.py b/utils/updater.py index 0b9b902..d0ac50c 100644 --- a/utils/updater.py +++ b/utils/updater.py @@ -9,11 +9,12 @@ import logging import os import re import subprocess +import sys import time from datetime import datetime from typing import Any -from urllib.request import urlopen, Request -from urllib.error import URLError, HTTPError +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen import config from utils.database import get_setting, set_setting @@ -509,6 +510,7 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]: 'success': True, 'updated': True, 'message': 'Update successful! Please restart the application.', + 'restart_required': True, 'requirements_changed': requirements_changed, 'stashed': stashed, 'stash_restored': stashed, @@ -527,3 +529,83 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]: 'success': False, 'error': str(e) } + + +def restart_application() -> dict[str, Any]: + """ + Restart the application using os.execv to replace the current process. + + This function: + 1. Cleans up all running decoder processes + 2. Stops the cleanup manager + 3. Replaces the current process with a fresh Python interpreter + + Returns: + Dict with status (though this is typically not reached due to execv) + """ + import app as app_module + from utils.cleanup import cleanup_manager + from utils.process import cleanup_all_processes + + logger.info("Application restart requested") + + try: + # Step 1: Kill all decoder processes + logger.info("Stopping all decoder processes...") + cleanup_all_processes() + + # Step 2: Clear global process state + with app_module.process_lock: + app_module.current_process = None + with app_module.sensor_lock: + app_module.sensor_process = None + with app_module.wifi_lock: + app_module.wifi_process = None + with app_module.adsb_lock: + app_module.adsb_process = None + with app_module.ais_lock: + app_module.ais_process = None + with app_module.acars_lock: + app_module.acars_process = None + with app_module.aprs_lock: + app_module.aprs_process = None + app_module.aprs_rtl_process = None + with app_module.dsc_lock: + app_module.dsc_process = None + app_module.dsc_rtl_process = None + + # Step 3: Clear SDR device registry + with app_module.sdr_device_registry_lock: + app_module.sdr_device_registry.clear() + + # Step 4: Stop cleanup manager + logger.info("Stopping cleanup manager...") + cleanup_manager.stop() + + # Step 5: Prepare for restart using os.execv + # Get the Python executable and script path + python_executable = sys.executable + script_path = os.path.abspath(sys.argv[0]) + + # Build argument list (preserve original command-line args) + args = [python_executable, script_path] + sys.argv[1:] + + logger.info(f"Restarting with: {' '.join(args)}") + + # Flush any pending log output + logging.shutdown() + + # Use os.execv to replace the current process + # This will not return - the process is replaced entirely + os.execv(python_executable, args) + + # This code is never reached + return {'success': True, 'message': 'Restarting...'} + + except Exception as e: + logger.error(f"Restart failed: {e}") + return { + 'success': False, + 'error': str(e), + 'message': 'Failed to restart application. Please restart manually.' + }