#![doc(hidden)] #![expect( clippy::needless_pass_by_value, clippy::redundant_pub_crate, reason = "this private daemon-launch module keeps ownership explicit across listener bootstrap helpers" )] use std::{ net::{IpAddr, Ipv4Addr, TcpListener}, sync::{Arc, Mutex}, }; use aria2_rust_pro_core::RuntimeConfig; use aria2_rust_pro_rpc::{ InProcessRpcDispatcher, JsonRpcRequest, RpcMeta, RpcMethod, RpcServerConfig, RpcValue, serve_rpc_listener, }; use super::{ CliError, ParsedArguments, TransferInputEntry, derive_rpc_listen_host, derive_rpc_secret, derive_runtime_config, expand_transfer_entries, load_config_report, merged_profile, rpc_option_object, }; use crate::Invocation; #[derive(Debug)] struct RpcDaemonLaunch { runtime: RuntimeConfig, listen_ip: IpAddr, secret: Option, input_entries: Vec, } /// Runs the RPC daemon surface implied by a parsed CLI invocation. pub(super) fn run_rpc_daemon(parsed: ParsedArguments) -> Result<(), CliError> { let (config_path, inputs) = match &parsed.invocation { Invocation::Run { config_path, uris } => (config_path.clone(), uris.clone()), Invocation::Version | Invocation::Help { .. } => { return Err(CliError::Io( "rpc daemon launch requires a run invocation".to_owned(), )); } }; let launch = build_rpc_daemon_launch(&parsed, config_path, inputs)?; let listener = bind_rpc_listener(launch.listen_ip, launch.runtime.rpc_port)?; let mut dispatcher = InProcessRpcDispatcher::with_runtime(launch.runtime); seed_dispatcher_with_inputs(&mut dispatcher, launch.input_entries); let server_config = build_rpc_server_config(&listener, launch.secret)?; println!( "rpc daemon listening on {}:{}", launch.listen_ip, server_config.listen_addr.port() ); serve_rpc_listener(listener, server_config, Arc::new(Mutex::new(dispatcher))) .map_err(|error| CliError::Io(format!("rpc daemon serve failed: {error}"))) } fn build_rpc_daemon_launch( parsed: &ParsedArguments, config_path: Option, inputs: Vec, ) -> Result { let config_report = config_path .as_ref() .map(|path| load_config_report(path, true)) .transpose()?; let file_profile = config_report.as_ref().map(|report| &report.profile); let effective_profile = merged_profile(file_profile, parsed.cli_profile.as_ref()); let profile = effective_profile.as_ref(); let runtime = derive_runtime_config(profile, &parsed.profile); let listen_host = derive_rpc_listen_host(profile, &parsed.profile); let secret = derive_rpc_secret(profile, &parsed.profile); let listen_ip = listen_host .parse::() .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); let input_entries = expand_transfer_entries(&inputs, profile, Some(&parsed.cli_transfer_sources))?; Ok(RpcDaemonLaunch { runtime, listen_ip, secret, input_entries, }) } fn bind_rpc_listener(listen_ip: IpAddr, listen_port: u16) -> Result { TcpListener::bind((listen_ip, listen_port)) .map_err(|error| CliError::Io(format!("rpc daemon bind failed: {error}"))) } fn build_rpc_server_config( listener: &TcpListener, secret: Option, ) -> Result { Ok(RpcServerConfig { listen_addr: listener .local_addr() .map_err(|error| CliError::Io(format!("rpc daemon local addr failed: {error}")))?, secret_token: secret, ..RpcServerConfig::default() }) } fn seed_dispatcher_with_inputs( dispatcher: &mut InProcessRpcDispatcher, input_entries: Vec, ) { for entry in input_entries { let mut params = vec![RpcValue::Array( entry.uris.into_iter().map(RpcValue::String).collect(), )]; if let Some(options) = rpc_option_object(entry.profile.as_ref()) { params.push(options); } let _ = dispatcher.dispatch_json(JsonRpcRequest { jsonrpc: Some("2.0".to_owned()), id: None, method: RpcMethod::Aria2AddUri.as_str().to_owned(), params, meta: RpcMeta::default(), }); } }