211 lines
6.5 KiB
Python
Executable File
211 lines
6.5 KiB
Python
Executable File
"""
|
|
aria2c RPC daemon management
|
|
|
|
Starts/stops the aria2c daemon with optimized settings for NAS downloads
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import time
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
import psutil
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Global daemon process
|
|
_aria2_process: Optional[subprocess.Popen] = None
|
|
|
|
|
|
def get_aria2_executable() -> str:
|
|
"""Get path to aria2c.exe in project folder"""
|
|
project_root = Path(__file__).parent.parent.parent.parent
|
|
aria2_exe = project_root / "aria2" / "aria2c.exe"
|
|
|
|
if not aria2_exe.exists():
|
|
raise FileNotFoundError(
|
|
f"aria2c.exe not found at {aria2_exe}. "
|
|
f"Please ensure aria2 folder exists in project root."
|
|
)
|
|
|
|
return str(aria2_exe)
|
|
|
|
|
|
def is_aria2_running(port: int = 6800) -> bool:
|
|
"""
|
|
Check if aria2c RPC server is already running on specified port
|
|
|
|
Args:
|
|
port: RPC port to check (default: 6800)
|
|
|
|
Returns:
|
|
True if aria2c is running on the port
|
|
"""
|
|
try:
|
|
for conn in psutil.net_connections():
|
|
# Check if laddr exists and has port attribute
|
|
if hasattr(conn, 'laddr') and conn.laddr and hasattr(conn.laddr, 'port'):
|
|
if conn.laddr.port == port and conn.status == 'LISTEN':
|
|
return True
|
|
return False
|
|
except Exception as e:
|
|
logger.warning(f"Could not check if aria2 is running: {e}")
|
|
return False
|
|
|
|
|
|
def start_aria2_daemon(
|
|
port: int = 6800,
|
|
secret: str = "dkidownload_secret_2025",
|
|
max_concurrent: int = 10,
|
|
max_connections: Optional[int] = None,
|
|
split: Optional[int] = None,
|
|
min_split_size: str = "1M",
|
|
download_dir: Optional[str] = None
|
|
) -> bool:
|
|
"""
|
|
Start aria2c RPC daemon with optimized settings
|
|
|
|
Args:
|
|
port: RPC port (default: 6800)
|
|
secret: RPC secret token for authentication
|
|
max_concurrent: Max concurrent downloads
|
|
max_connections: Max connections per server (default: from env ARIA2_MAX_CONNECTIONS_PER_FILE or 16)
|
|
split: Number of connections per file (default: same as max_connections)
|
|
min_split_size: Minimum size to split (e.g., "1M")
|
|
download_dir: Temporary download directory
|
|
|
|
Returns:
|
|
True if started successfully
|
|
"""
|
|
global _aria2_process
|
|
|
|
# Load max_connections from environment if not provided
|
|
if max_connections is None:
|
|
max_connections = int(
|
|
os.getenv('ARIA2_MAX_CONNECTIONS_PER_FILE', '16'))
|
|
|
|
# Default split to max_connections if not specified
|
|
if split is None:
|
|
split = max_connections
|
|
|
|
# Check if already running
|
|
if is_aria2_running(port):
|
|
logger.debug(f"aria2c already running on port {port}")
|
|
return True
|
|
|
|
try:
|
|
aria2_exe = get_aria2_executable()
|
|
|
|
# Default download dir: project_root/aria2/downloads
|
|
if download_dir is None:
|
|
project_root = Path(__file__).parent.parent.parent.parent
|
|
download_dir = str(project_root / "aria2" / "downloads")
|
|
|
|
# Create download dir if not exists
|
|
os.makedirs(download_dir, exist_ok=True)
|
|
|
|
# aria2c command with optimized settings
|
|
cmd = [
|
|
aria2_exe,
|
|
'--enable-rpc',
|
|
'--rpc-listen-all=false', # Only localhost for security
|
|
f'--rpc-listen-port={port}',
|
|
f'--rpc-secret={secret}',
|
|
f'--max-concurrent-downloads={max_concurrent}',
|
|
f'--max-connection-per-server={max_connections}',
|
|
f'--split={split}',
|
|
f'--min-split-size={min_split_size}',
|
|
'--continue=true', # Resume support
|
|
'--auto-file-renaming=false', # Don't auto-rename
|
|
f'--dir={download_dir}',
|
|
'--log=-', # Log to stdout
|
|
'--log-level=notice',
|
|
'--console-log-level=warn',
|
|
'--summary-interval=0', # Disable periodic summary
|
|
'--disable-ipv6=true', # Faster connection
|
|
'--check-certificate=false', # NAS uses self-signed cert
|
|
]
|
|
|
|
logger.debug(f"Starting aria2c daemon on port {port}...")
|
|
logger.debug(f"Download directory: {download_dir}")
|
|
|
|
# Start process (detached, no window on Windows)
|
|
_aria2_process = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0
|
|
)
|
|
|
|
# Wait a bit and check if started
|
|
time.sleep(1)
|
|
|
|
if _aria2_process.poll() is not None:
|
|
# Process died immediately
|
|
stdout, stderr = _aria2_process.communicate()
|
|
logger.error(f"aria2c failed to start: {stderr.decode()}")
|
|
return False
|
|
|
|
# Verify it's listening
|
|
if is_aria2_running(port):
|
|
logger.debug(
|
|
f"✅ aria2c daemon started successfully on port {port}")
|
|
return True
|
|
else:
|
|
logger.error("aria2c started but not listening on port")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to start aria2c: {e}", exc_info=True)
|
|
return False
|
|
|
|
|
|
def stop_aria2_daemon() -> bool:
|
|
"""
|
|
Stop the aria2c daemon gracefully
|
|
|
|
Returns:
|
|
True if stopped successfully
|
|
"""
|
|
global _aria2_process
|
|
|
|
if _aria2_process is None:
|
|
logger.debug("No aria2c process to stop")
|
|
return True
|
|
|
|
try:
|
|
logger.debug("Stopping aria2c daemon...")
|
|
_aria2_process.terminate()
|
|
|
|
# Wait up to 5 seconds for graceful shutdown
|
|
try:
|
|
_aria2_process.wait(timeout=5)
|
|
logger.debug("✅ aria2c daemon stopped")
|
|
except subprocess.TimeoutExpired:
|
|
logger.warning("aria2c didn't stop gracefully, forcing...")
|
|
_aria2_process.kill()
|
|
_aria2_process.wait()
|
|
logger.debug("✅ aria2c daemon force-killed")
|
|
|
|
_aria2_process = None
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error stopping aria2c: {e}")
|
|
return False
|
|
|
|
|
|
def restart_aria2_daemon(**kwargs) -> bool:
|
|
"""
|
|
Restart aria2c daemon with new settings
|
|
|
|
Args:
|
|
**kwargs: Arguments to pass to start_aria2_daemon()
|
|
|
|
Returns:
|
|
True if restarted successfully
|
|
"""
|
|
stop_aria2_daemon()
|
|
time.sleep(1)
|
|
return start_aria2_daemon(**kwargs)
|