Add application restart endpoint for post-update restarts

Adds POST /updater/restart endpoint that gracefully restarts the
application using os.execv. Cleans up all decoder processes and
global state before replacing the process with a fresh instance.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-02 22:28:32 +00:00
parent f795180c7d
commit 9ac63bd75f
2 changed files with 126 additions and 4 deletions

View File

@@ -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'
})

View File

@@ -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.'
}