mirror of
https://github.com/smittix/intercept.git
synced 2026-07-10 10:38:12 -07:00
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:
+42
-2
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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.logging import get_logger
|
||||||
from utils.updater import (
|
from utils.updater import (
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
get_update_status,
|
|
||||||
dismiss_update,
|
dismiss_update,
|
||||||
|
get_update_status,
|
||||||
perform_update,
|
perform_update,
|
||||||
|
restart_application,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = get_logger('intercept.routes.updater')
|
logger = get_logger('intercept.routes.updater')
|
||||||
@@ -137,3 +138,42 @@ def dismiss_notification() -> Response:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}), 500
|
}), 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'
|
||||||
|
})
|
||||||
|
|||||||
+84
-2
@@ -9,11 +9,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.request import urlopen, Request
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.error import URLError, HTTPError
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from utils.database import get_setting, set_setting
|
from utils.database import get_setting, set_setting
|
||||||
@@ -509,6 +510,7 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
|
|||||||
'success': True,
|
'success': True,
|
||||||
'updated': True,
|
'updated': True,
|
||||||
'message': 'Update successful! Please restart the application.',
|
'message': 'Update successful! Please restart the application.',
|
||||||
|
'restart_required': True,
|
||||||
'requirements_changed': requirements_changed,
|
'requirements_changed': requirements_changed,
|
||||||
'stashed': stashed,
|
'stashed': stashed,
|
||||||
'stash_restored': stashed,
|
'stash_restored': stashed,
|
||||||
@@ -527,3 +529,83 @@ def perform_update(stash_changes: bool = False) -> dict[str, Any]:
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'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.'
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user