""" 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)