Files
ruvnet--RuView/archive/v1/src/commands/stop.py
rUv 81cc241b9e chore(repo): move v1/ → archive/v1/ + add archive/README.md (#430)
The Rust port at v2/ has been the primary codebase since the rename
in #427. The Python implementation at v1/ is no longer the active
target; the only load-bearing path is the deterministic proof bundle
at v1/data/proof/ (per ADR-011 / ADR-028 witness verification).

Move the whole Python tree into archive/v1/ and document the policy
in archive/README.md: no new features, bug fixes only when they affect
a still-load-bearing path (currently just the proof), CI continues to
verify the proof on every push and PR.

Path references updated in 26 files via path-pattern sed (only
matches v1/<known-child> patterns, never bare v1 or API URLs like
/api/v1/). Two double-prefix typos (archive/archive/v1/) caught and
hand-fixed in verify-pipeline.yml and ADR-011.

Validated:
- Python proof verify.py imports cleanly at archive/v1/data/proof/
  (numpy/scipy still required; CI installs requirements-lock.txt
  from archive/v1/ now)
- cargo test --workspace --no-default-features → 1,539 passed,
  0 failed, 8 ignored (unaffected by Python tree relocation)
- ESP32-S3 on COM7 untouched (no firmware paths changed)

After-merge: contributors should re-run any local `python v1/...`
commands as `python archive/v1/...` (CLAUDE.md and CHANGELOG already
updated).
2026-04-25 23:07:52 -04:00

294 lines
8.4 KiB
Python

"""
Stop command implementation for WiFi-DensePose API
"""
import asyncio
import os
import signal
import time
from pathlib import Path
from typing import Optional
from src.config.settings import Settings
from src.logger import get_logger
logger = get_logger(__name__)
async def stop_command(
settings: Settings,
force: bool = False,
timeout: int = 30
) -> None:
"""Stop the WiFi-DensePose API server."""
logger.info("Stopping WiFi-DensePose API server...")
# Get server status
status = get_server_status(settings)
if not status["running"]:
if status["pid_file_exists"]:
logger.info("Server is not running, but PID file exists. Cleaning up...")
_cleanup_pid_file(settings)
else:
logger.info("Server is not running")
return
pid = status["pid"]
logger.info(f"Found running server with PID {pid}")
try:
if force:
await _force_stop_server(pid, settings)
else:
await _graceful_stop_server(pid, timeout, settings)
except Exception as e:
logger.error(f"Failed to stop server: {e}")
raise
async def _graceful_stop_server(pid: int, timeout: int, settings: Settings) -> None:
"""Stop server gracefully with timeout."""
logger.info(f"Attempting graceful shutdown (timeout: {timeout}s)...")
try:
# Send SIGTERM for graceful shutdown
os.kill(pid, signal.SIGTERM)
logger.info("Sent SIGTERM signal")
# Wait for process to terminate
start_time = time.time()
while time.time() - start_time < timeout:
try:
# Check if process is still running
os.kill(pid, 0)
await asyncio.sleep(1)
except OSError:
# Process has terminated
logger.info("Server stopped gracefully")
_cleanup_pid_file(settings)
return
# Timeout reached, force kill
logger.warning(f"Graceful shutdown timeout ({timeout}s) reached, forcing stop...")
await _force_stop_server(pid, settings)
except OSError as e:
if e.errno == 3: # No such process
logger.info("Process already terminated")
_cleanup_pid_file(settings)
else:
logger.error(f"Failed to send signal to process {pid}: {e}")
raise
async def _force_stop_server(pid: int, settings: Settings) -> None:
"""Force stop server immediately."""
logger.info("Force stopping server...")
try:
# Send SIGKILL for immediate termination
os.kill(pid, signal.SIGKILL)
logger.info("Sent SIGKILL signal")
# Wait a moment for process to die
await asyncio.sleep(2)
# Verify process is dead
try:
os.kill(pid, 0)
logger.error(f"Process {pid} still running after SIGKILL")
except OSError:
logger.info("Server force stopped")
except OSError as e:
if e.errno == 3: # No such process
logger.info("Process already terminated")
else:
logger.error(f"Failed to force kill process {pid}: {e}")
raise
finally:
_cleanup_pid_file(settings)
def _cleanup_pid_file(settings: Settings) -> None:
"""Clean up PID file."""
pid_file = Path(settings.log_directory) / "wifi-densepose-api.pid"
if pid_file.exists():
try:
pid_file.unlink()
logger.info("Cleaned up PID file")
except Exception as e:
logger.warning(f"Failed to remove PID file: {e}")
def get_server_status(settings: Settings) -> dict:
"""Get current server status."""
pid_file = Path(settings.log_directory) / "wifi-densepose-api.pid"
status = {
"running": False,
"pid": None,
"pid_file": str(pid_file),
"pid_file_exists": pid_file.exists(),
}
if pid_file.exists():
try:
with open(pid_file, 'r') as f:
pid = int(f.read().strip())
status["pid"] = pid
# Check if process is running
try:
os.kill(pid, 0) # Signal 0 just checks if process exists
status["running"] = True
except OSError:
# Process doesn't exist
status["running"] = False
except (ValueError, IOError):
# Invalid PID file
status["running"] = False
return status
async def stop_all_background_tasks(settings: Settings) -> None:
"""Stop all background tasks if they're running."""
logger.info("Stopping background tasks...")
try:
# This would typically involve connecting to a task queue or
# sending signals to background processes
# For now, we'll just log the action
logger.info("Background tasks stop signal sent")
except Exception as e:
logger.error(f"Failed to stop background tasks: {e}")
async def cleanup_resources(settings: Settings) -> None:
"""Clean up system resources."""
logger.info("Cleaning up resources...")
try:
# Close database connections
from src.database.connection import get_database_manager
db_manager = get_database_manager(settings)
await db_manager.close_all_connections()
logger.info("Database connections closed")
except Exception as e:
logger.warning(f"Failed to close database connections: {e}")
try:
# Clean up temporary files
temp_files = [
Path(settings.log_directory) / "temp",
Path(settings.backup_directory) / "temp",
]
for temp_path in temp_files:
if temp_path.exists() and temp_path.is_dir():
import shutil
shutil.rmtree(temp_path)
logger.info(f"Cleaned up temporary directory: {temp_path}")
except Exception as e:
logger.warning(f"Failed to clean up temporary files: {e}")
logger.info("Resource cleanup completed")
def is_server_running(settings: Settings) -> bool:
"""Check if server is currently running."""
status = get_server_status(settings)
return status["running"]
def get_server_pid(settings: Settings) -> Optional[int]:
"""Get server PID if running."""
status = get_server_status(settings)
return status["pid"] if status["running"] else None
async def wait_for_server_stop(settings: Settings, timeout: int = 30) -> bool:
"""Wait for server to stop with timeout."""
start_time = time.time()
while time.time() - start_time < timeout:
if not is_server_running(settings):
return True
await asyncio.sleep(1)
return False
def send_reload_signal(settings: Settings) -> bool:
"""Send reload signal to running server."""
status = get_server_status(settings)
if not status["running"]:
logger.error("Server is not running")
return False
try:
# Send SIGHUP for reload
os.kill(status["pid"], signal.SIGHUP)
logger.info("Sent reload signal to server")
return True
except OSError as e:
logger.error(f"Failed to send reload signal: {e}")
return False
async def restart_server(settings: Settings, timeout: int = 30) -> None:
"""Restart the server (stop then start)."""
logger.info("Restarting server...")
# Stop server if running
if is_server_running(settings):
await stop_command(settings, timeout=timeout)
# Wait for server to stop
if not await wait_for_server_stop(settings, timeout):
logger.error("Server did not stop within timeout, forcing restart")
await stop_command(settings, force=True)
# Start server
from src.commands.start import start_command
await start_command(settings)
def get_stop_status_summary(settings: Settings) -> dict:
"""Get a summary of stop operation status."""
status = get_server_status(settings)
return {
"server_running": status["running"],
"pid": status["pid"],
"pid_file_exists": status["pid_file_exists"],
"can_stop": status["running"],
"cleanup_needed": status["pid_file_exists"] and not status["running"],
}