Skip to main content

aria2_core/engine/
download_command.rs

1use async_trait::async_trait;
2use futures::StreamExt;
3use futures::stream::FuturesUnordered;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6use tokio::sync::mpsc;
7use tracing::{debug, error, info, warn};
8
9use crate::constants;
10use crate::engine::active_output_registry::global_registry;
11use crate::engine::command::{Command, CommandStatus, ProgressUpdate};
12use crate::engine::concurrent_segment_manager::ConcurrentSegmentManager;
13use crate::engine::download_engine::DownloadEngine;
14use crate::engine::http_segment_downloader::HttpSegmentDownloader;
15use crate::engine::mirror_coordinator::{MirrorConfig, MirrorCoordinator};
16use crate::engine::retry_policy::RetryPolicy;
17use crate::error::{Aria2Error, RecoverableError, Result};
18use crate::filesystem::disk_writer::{
19    CachedDiskWriter, DefaultDiskWriter, DiskWriter, SeekableDiskWriter,
20};
21use crate::filesystem::file_allocation;
22use crate::filesystem::resume_helper::{ResumeHelper, ResumeState};
23use crate::http::client_pool;
24use crate::http::cookie::Cookie;
25use crate::http::cookie_storage::CookieStorage;
26use crate::http::socks_connector::{NoProxyMatcher, ProxyUrl};
27use crate::rate_limiter::{RateLimiter, RateLimiterConfig, ThrottledWriter};
28use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
29use crate::selector::adaptive_uri_selector::AdaptiveUriSelector;
30use crate::selector::server_stat_man::ServerStatMan;
31use crate::util::perf_monitor::{AtomicMetrics, Metrics, PerformanceMonitor};
32
33pub struct DownloadCommand {
34    group: Arc<tokio::sync::RwLock<RequestGroup>>,
35    client: Arc<reqwest::Client>,
36    output_path: std::path::PathBuf,
37    started: bool,
38    completed: bool,
39    completed_bytes: u64,
40    continue_enabled: bool,
41    file_allocation: String,
42    /// File size threshold (bytes) for mmap writes. Only used when
43    /// `file_allocation == "mmap"`. Files smaller than this use positioned I/O.
44    mmap_threshold: u64,
45    /// When `true`, zero-fill allocated blocks after `fallocate` on platforms
46    /// that don't zero-fill (macOS, Windows). See `file_allocation::fallocate`.
47    secure_falloc: bool,
48    cookie_storage: Arc<CookieStorage>,
49    cookie_file: Option<String>,
50    no_proxy_matcher: Option<NoProxyMatcher>,
51    /// Whether `HttpSegmentDownloader` may use the `HyperDirectClient` hot
52    /// path for plain-HTTP range GETs. Set to `true` only when no proxy is
53    /// configured (mirrors the client-selection condition in the
54    /// constructors). HTTPS and proxy URLs always fall back to reqwest.
55    use_hyper: bool,
56    /// Server statistics manager for intelligent mirror selection.
57    /// Shared across downloads to maintain historical performance data.
58    stat_man: Arc<ServerStatMan>,
59    /// Performance monitor for collecting metrics (optional, minimal overhead when enabled)
60    perf_monitor: Option<Arc<PerformanceMonitor>>,
61    /// Atomic metrics for low-overhead collection
62    atomic_metrics: Arc<AtomicMetrics>,
63    /// Parsed custom HTTP headers (Name, Value) applied to HEAD probes and range
64    /// GETs. Includes `User-Agent` / `Referer` overrides from options.
65    headers: Vec<(String, String)>,
66    /// Progress channel sender. Auto-created in the constructor so every
67    /// `DownloadCommand` uses lock-free progress reporting by default.
68    /// Intermediate batched progress updates are sent via this channel
69    /// instead of acquiring the `RequestGroup` write lock on every
70    /// checkpoint. The aggregator task (see
71    /// [`DownloadEngine::spawn_progress_aggregator`]) applies updates to the
72    /// `RequestGroup`.
73    progress_sender: Option<mpsc::UnboundedSender<ProgressUpdate>>,
74    /// Receiver half of the auto-created progress channel. Held until
75    /// `execute()` spawns the aggregator task (lazy spawn ensures we are in
76    /// a tokio runtime context). Taken and consumed by
77    /// [`spawn_progress_aggregator`](Self::spawn_progress_aggregator).
78    progress_receiver: Option<mpsc::UnboundedReceiver<ProgressUpdate>>,
79    /// Handle to the spawned progress aggregator task. Set when the
80    /// aggregator is spawned in `execute()`. Awaited in
81    /// [`drain_progress_aggregator`](Self::drain_progress_aggregator) to
82    /// ensure all queued updates are applied before the command completes.
83    progress_aggregator_handle: Option<tokio::task::JoinHandle<()>>,
84}
85
86/// Pinned, boxed, sendable future for a single segment fetch.
87/// Returns `(seg_idx, Result<Bytes, String>)` so the caller can correlate
88/// the result with the segment index and write offset.
89type SegmentFetchFuture = std::pin::Pin<
90    Box<dyn std::future::Future<Output = (u32, std::result::Result<bytes::Bytes, String>)> + Send>,
91>;
92
93impl DownloadCommand {
94    /// Create a DownloadCommand with a standalone RequestGroup (not registered
95    /// in RequestGroupMan). Use this for simple CLI/test scenarios where
96    /// external progress queries are not needed.
97    pub fn new(
98        gid: GroupId,
99        uri: &str,
100        options: &DownloadOptions,
101        output_dir: Option<&str>,
102        output_name: Option<&str>,
103    ) -> Result<Self> {
104        let group = Arc::new(tokio::sync::RwLock::new(RequestGroup::new(
105            gid,
106            vec![uri.to_string()],
107            options.clone(),
108        )));
109        Self::new_with_group(group, uri, options, output_dir, output_name)
110    }
111
112    /// Create a DownloadCommand with a shared RequestGroup Arc (registered in
113    /// RequestGroupMan). Use this for RPC and CLI paths where external progress
114    /// queries are needed.
115    pub fn new_with_group(
116        group: Arc<tokio::sync::RwLock<RequestGroup>>,
117        uri: &str,
118        options: &DownloadOptions,
119        output_dir: Option<&str>,
120        output_name: Option<&str>,
121    ) -> Result<Self> {
122        let dir = output_dir
123            .map(|d| d.to_string())
124            .or_else(|| options.dir.clone())
125            .unwrap_or_else(|| constants::DEFAULT_OUTPUT_DIR.to_string());
126
127        let filename = output_name
128            .map(|n| n.to_string())
129            .or_else(|| Self::extract_filename(uri))
130            .unwrap_or_else(|| constants::DEFAULT_FILENAME.to_string());
131
132        let path = std::path::PathBuf::from(&dir).join(&filename);
133
134        // Parse custom headers once (includes User-Agent / Referer overrides).
135        let headers = options.parsed_headers();
136
137        // Use global client if no proxy configuration AND no custom headers,
138        // otherwise create custom client (custom headers require per-request
139        // application but a custom client is still built when a proxy is set).
140        // `use_hyper` mirrors this condition: the `HyperDirectClient` hot path
141        // is only safe (and useful) when no proxy is in play.
142        let use_hyper = options.http_proxy.is_none() && options.all_proxy.is_none();
143        let client = if use_hyper {
144            // No proxy configuration - use shared global client for better performance.
145            // Custom headers are applied per-request in execute()/segment downloader.
146            client_pool::get_global_client()
147        } else {
148            // Proxy configuration present - create custom client
149            let mut builder = reqwest::Client::builder()
150                .connect_timeout(Duration::from_secs(
151                    constants::HTTP_DEFAULT_CONNECT_TIMEOUT_SECS,
152                ))
153                .timeout(Duration::from_secs(
154                    constants::HTTP_DEFAULT_OVERALL_TIMEOUT_SECS,
155                ))
156                .user_agent(constants::USER_AGENT)
157                .redirect(reqwest::redirect::Policy::limited(
158                    constants::HTTP_DEFAULT_MAX_REDIRECTS,
159                ))
160                .pool_max_idle_per_host(constants::HTTP_DEFAULT_POOL_MAX_IDLE_PER_HOST)
161                .pool_idle_timeout(Some(std::time::Duration::from_secs(
162                    constants::HTTP_DEFAULT_POOL_IDLE_TIMEOUT_SECS,
163                )))
164                .tcp_keepalive(Some(std::time::Duration::from_secs(
165                    constants::HTTP_DEFAULT_TCP_KEEPALIVE_SECS,
166                )));
167
168            if let Some(ref proxy) = options.http_proxy
169                && let Ok(proxy_url) = proxy.parse::<reqwest::Url>()
170                && let Ok(p) = reqwest::Proxy::all(proxy_url.to_string())
171            {
172                builder = builder.proxy(p);
173            }
174
175            // Apply all-proxy (global proxy for all protocols)
176            // Priority: protocol-specific proxy > all-proxy
177            if options.http_proxy.is_none()
178                && let Some(ref all_proxy) = options.all_proxy
179            {
180                match ProxyUrl::parse(all_proxy) {
181                    Ok(parsed) => {
182                        match parsed.protocol {
183                            crate::http::socks_connector::ProxyProtocol::Http
184                            | crate::http::socks_connector::ProxyProtocol::Https => {
185                                if let Ok(p) = reqwest::Proxy::all(all_proxy.to_string()) {
186                                    builder = builder.proxy(p);
187                                }
188                            }
189                            _ => {
190                                // SOCKS proxies require a custom connector.
191                                // For now, log a note that SOCKS integration is available
192                                // via the SocksConnector trait but requires manual TcpStream wrapping.
193                                tracing::info!(
194                                    "SOCKS proxy configured ({}) - use SocksConnector for direct TCP connections",
195                                    all_proxy
196                                );
197                            }
198                        }
199                    }
200                    Err(e) => {
201                        warn!("Failed to parse all-proxy URL '{}': {}", all_proxy, e);
202                    }
203                }
204            }
205
206            let client = builder.build().map_err(|e| {
207                Aria2Error::Fatal(crate::error::FatalError::Config(format!(
208                    "Failed to build HTTP client: {}",
209                    e
210                )))
211            })?;
212
213            Arc::new(client)
214        };
215
216        info!("DownloadCommand created: {} -> {}", uri, path.display());
217
218        let cookie_file = options.cookie_file.clone();
219        let cookie_storage = Arc::new(CookieStorage::new());
220
221        if let Some(ref cf) = cookie_file {
222            let p = std::path::Path::new(cf);
223            if p.exists() {
224                match cookie_storage.load_file(p) {
225                    Ok(n) => info!("Loaded {} cookies from file: {}", n, cf),
226                    Err(e) => warn!("Failed to load cookie file {}: {}", cf, e),
227                }
228            }
229        }
230
231        if let Some(ref cookies_str) = options.cookies {
232            let domain = Self::extract_host(uri);
233            for pair in cookies_str.split(';') {
234                let pair = pair.trim();
235                if pair.is_empty() {
236                    continue;
237                }
238                if let Some((name, value)) = pair.split_once('=') {
239                    let name = name.trim();
240                    let value = value.trim();
241                    if !name.is_empty() {
242                        cookie_storage.add(Cookie::new(name, value, &domain));
243                    }
244                }
245            }
246            if !cookie_storage.is_empty() {
247                info!("Manually set {} cookies", cookie_storage.count());
248            }
249        }
250
251        // Auto-create the progress channel so every DownloadCommand uses
252        // lock-free progress reporting by default. The aggregator is spawned
253        // lazily in execute() to guarantee a tokio runtime context.
254        let (progress_tx, progress_rx) = mpsc::unbounded_channel::<ProgressUpdate>();
255
256        Ok(Self {
257            group,
258            client,
259            output_path: path,
260            started: false,
261            completed: false,
262            completed_bytes: 0,
263            continue_enabled: true,
264            file_allocation: options
265                .file_allocation
266                .clone()
267                .unwrap_or_else(|| constants::DEFAULT_FILE_ALLOCATION.to_string()),
268            mmap_threshold: options.mmap_threshold.unwrap_or(256 * 1024 * 1024),
269            secure_falloc: options.secure_falloc,
270            cookie_storage,
271            cookie_file,
272            no_proxy_matcher: options
273                .no_proxy
274                .as_ref()
275                .map(|np| NoProxyMatcher::from_env_value(np)),
276            use_hyper,
277            stat_man: Arc::new(ServerStatMan::new()),
278            perf_monitor: None, // Disabled by default for zero overhead
279            atomic_metrics: Arc::new(AtomicMetrics::new()),
280            headers,
281            progress_sender: Some(progress_tx),
282            progress_receiver: Some(progress_rx),
283            progress_aggregator_handle: None,
284        })
285    }
286
287    /// Create a DownloadCommand with a custom HTTP client and standalone group.
288    pub fn new_with_client(
289        gid: GroupId,
290        uri: &str,
291        options: &DownloadOptions,
292        output_dir: Option<&str>,
293        output_name: Option<&str>,
294        client: Arc<reqwest::Client>,
295    ) -> Result<Self> {
296        let group = Arc::new(tokio::sync::RwLock::new(RequestGroup::new(
297            gid,
298            vec![uri.to_string()],
299            options.clone(),
300        )));
301        Self::new_with_group_and_client(group, uri, options, output_dir, output_name, client)
302    }
303
304    /// Create a DownloadCommand with a shared group Arc and custom HTTP client.
305    pub fn new_with_group_and_client(
306        group: Arc<tokio::sync::RwLock<RequestGroup>>,
307        uri: &str,
308        options: &DownloadOptions,
309        output_dir: Option<&str>,
310        output_name: Option<&str>,
311        client: Arc<reqwest::Client>,
312    ) -> Result<Self> {
313        let dir = output_dir
314            .map(|d| d.to_string())
315            .or_else(|| options.dir.clone())
316            .unwrap_or_else(|| constants::DEFAULT_OUTPUT_DIR.to_string());
317
318        let filename = output_name
319            .map(|n| n.to_string())
320            .or_else(|| Self::extract_filename(uri))
321            .unwrap_or_else(|| constants::DEFAULT_FILENAME.to_string());
322
323        let path = std::path::PathBuf::from(&dir).join(&filename);
324
325        let headers = options.parsed_headers();
326        info!(
327            "DownloadCommand created (shared client): {} -> {}",
328            uri,
329            path.display()
330        );
331
332        // Mirror the proxy-detection condition used by `new_with_group`: the
333        // `HyperDirectClient` hot path is only enabled when no proxy is
334        // configured. The caller is expected to pass a `client` consistent
335        // with `options` (i.e. a proxy-aware client when a proxy is set).
336        let use_hyper = options.http_proxy.is_none() && options.all_proxy.is_none();
337
338        let cookie_file = options.cookie_file.clone();
339        let cookie_storage = Arc::new(CookieStorage::new());
340
341        if let Some(ref cf) = cookie_file {
342            let p = std::path::Path::new(cf);
343            if p.exists() {
344                match cookie_storage.load_file(p) {
345                    Ok(n) => info!("Loaded {} cookies from file: {}", n, cf),
346                    Err(e) => warn!("Failed to load cookie file {}: {}", cf, e),
347                }
348            }
349        }
350
351        if let Some(ref cookies_str) = options.cookies {
352            let domain = Self::extract_host(uri);
353            for pair in cookies_str.split(';') {
354                let pair = pair.trim();
355                if pair.is_empty() {
356                    continue;
357                }
358                if let Some((name, value)) = pair.split_once('=') {
359                    let name = name.trim();
360                    let value = value.trim();
361                    if !name.is_empty() {
362                        cookie_storage.add(Cookie::new(name, value, &domain));
363                    }
364                }
365            }
366            if !cookie_storage.is_empty() {
367                info!("Manually set {} cookies", cookie_storage.count());
368            }
369        }
370
371        // Auto-create the progress channel so every DownloadCommand uses
372        // lock-free progress reporting by default. The aggregator is spawned
373        // lazily in execute() to guarantee a tokio runtime context.
374        let (progress_tx, progress_rx) = mpsc::unbounded_channel::<ProgressUpdate>();
375
376        Ok(Self {
377            group,
378            client,
379            output_path: path,
380            started: false,
381            completed: false,
382            completed_bytes: 0,
383            continue_enabled: true,
384            file_allocation: options
385                .file_allocation
386                .clone()
387                .unwrap_or_else(|| constants::DEFAULT_FILE_ALLOCATION.to_string()),
388            mmap_threshold: options.mmap_threshold.unwrap_or(256 * 1024 * 1024),
389            secure_falloc: options.secure_falloc,
390            cookie_storage,
391            cookie_file,
392            no_proxy_matcher: options
393                .no_proxy
394                .as_ref()
395                .map(|np| NoProxyMatcher::from_env_value(np)),
396            use_hyper,
397            stat_man: Arc::new(ServerStatMan::new()),
398            perf_monitor: None, // Disabled by default for zero overhead
399            atomic_metrics: Arc::new(AtomicMetrics::new()),
400            headers,
401            progress_sender: Some(progress_tx),
402            progress_receiver: Some(progress_rx),
403            progress_aggregator_handle: None,
404        })
405    }
406
407    /// Create a DownloadCommand with a shared ServerStatMan.
408    ///
409    /// This allows multiple downloads to share server performance statistics,
410    /// enabling intelligent mirror selection based on historical data.
411    pub fn new_with_stat_man(
412        gid: GroupId,
413        uri: &str,
414        options: &DownloadOptions,
415        output_dir: Option<&str>,
416        output_name: Option<&str>,
417        stat_man: Arc<ServerStatMan>,
418    ) -> Result<Self> {
419        let mut cmd = Self::new(gid, uri, options, output_dir, output_name)?;
420        cmd.stat_man = stat_man;
421        Ok(cmd)
422    }
423
424    /// Enable performance monitoring for this download.
425    ///
426    /// This adds minimal overhead (< 1%) to track throughput, latency, memory, and lock wait times.
427    pub fn enable_perf_monitor(&mut self) {
428        self.perf_monitor = Some(Arc::new(PerformanceMonitor::new()));
429    }
430
431    /// Attach a progress channel sender for lock-free progress reporting.
432    ///
433    /// When set, intermediate batched progress updates (every
434    /// `PROGRESS_UPDATE_BYTES`) are sent through this channel instead of
435    /// acquiring the `RequestGroup` write lock. A separate aggregator task
436    /// (see [`DownloadEngine::spawn_progress_aggregator`]) receives the
437    /// updates and applies them to the `RequestGroup`, fully decoupling the
438    /// download hot loop from the group lock.
439    ///
440    /// The final completion update (`g.complete()`) is NOT channel-ified and
441    /// still acquires the write lock directly, since it has side effects.
442    ///
443    /// NOTE: As of the internalize-default-optimizations refactor, every
444    /// `DownloadCommand` auto-creates a progress channel in its constructor,
445    /// so this method is rarely needed. It is kept `pub(crate)` for internal
446    /// test harnesses that need to override the auto-created sender.
447    #[allow(dead_code)]
448    pub(crate) fn with_progress_sender(
449        mut self,
450        sender: mpsc::UnboundedSender<ProgressUpdate>,
451    ) -> Self {
452        self.progress_sender = Some(sender);
453        // Clear the auto-created receiver so it does not linger; the caller
454        // is responsible for spawning the aggregator.
455        self.progress_receiver = None;
456        self
457    }
458
459    /// Spawn the progress aggregator task (lazy spawn).
460    ///
461    /// Called from `execute()` (which is async and guaranteed to have a
462    /// tokio runtime context). Takes the receiver half stored in the
463    /// constructor and spawns the aggregator via
464    /// [`DownloadEngine::spawn_progress_aggregator`]. The returned
465    /// `JoinHandle` is stored in `progress_aggregator_handle` and later
466    /// awaited by [`drain_progress_aggregator`](Self::drain_progress_aggregator).
467    ///
468    /// This is a no-op if the aggregator has already been spawned or if no
469    /// receiver is available (e.g., when an external caller used
470    /// [`with_progress_sender`](Self::with_progress_sender) to override the
471    /// auto-created channel).
472    pub(crate) fn spawn_progress_aggregator(&mut self) {
473        if self.progress_aggregator_handle.is_some() {
474            // Already spawned — avoid double-spawn.
475            return;
476        }
477        if let Some(rx) = self.progress_receiver.take() {
478            let handle = DownloadEngine::spawn_progress_aggregator(Arc::clone(&self.group), rx);
479            self.progress_aggregator_handle = Some(handle);
480        }
481        // If progress_receiver is None, either with_progress_sender was used
482        // (external ownership) or the aggregator was already spawned. In
483        // either case, the existing progress_sender (if any) drives the
484        // legacy direct-write path or an externally-managed aggregator.
485    }
486
487    /// Drain the progress aggregator: drop the sender to signal the
488    /// aggregator task to drain all queued updates and exit, then await
489    /// the handle to ensure all updates have been applied to the
490    /// `RequestGroup` before the command completes.
491    ///
492    /// This is a no-op if no aggregator was spawned (e.g., when the
493    /// constructor's receiver was never consumed).
494    pub(crate) async fn drain_progress_aggregator(&mut self) {
495        // Drop the sender first so the aggregator's recv() returns None
496        // after draining the queue, allowing the task to exit cleanly.
497        self.progress_sender = None;
498        if let Some(handle) = self.progress_aggregator_handle.take() {
499            // Await the aggregator task. Errors (panic in the task) are
500            // logged but do not fail the download — progress updates are
501            // best-effort and the final completion path writes directly.
502            if let Err(e) = handle.await {
503                warn!("Progress aggregator task ended unexpectedly: {}", e);
504            }
505        }
506    }
507
508    /// Get performance metrics snapshot (low-overhead, always available)
509    pub fn get_perf_metrics(&self) -> Metrics {
510        self.atomic_metrics.snapshot()
511    }
512
513    /// Get performance report if monitoring is enabled
514    pub fn get_perf_report(&self) -> Option<String> {
515        self.perf_monitor.as_ref().map(|m| m.export_text())
516    }
517
518    /// Get performance report in JSON format if monitoring is enabled
519    pub fn get_perf_report_json(&self) -> Option<String> {
520        self.perf_monitor.as_ref().map(|m| m.export_json())
521    }
522
523    fn extract_filename(uri: &str) -> Option<String> {
524        uri.rsplit('/')
525            .next()
526            .filter(|s| !s.is_empty() && *s != "/")
527            .map(|s| s.split('?').next().unwrap_or(s).to_string())
528    }
529
530    fn extract_host(uri: &str) -> String {
531        reqwest::Url::parse(uri)
532            .ok()
533            .and_then(|u| u.host_str().map(|h| h.to_string()))
534            .unwrap_or_else(|| constants::DEFAULT_HOST.to_string())
535    }
536
537    fn save_cookies_if_configured(&self) {
538        if let Some(ref cf) = self.cookie_file {
539            if let Err(e) = self.cookie_storage.save_file(std::path::Path::new(cf)) {
540                warn!("Failed to save cookie file {}: {}", cf, e);
541            } else {
542                info!("Cookies saved to {}", cf);
543            }
544        }
545    }
546
547    pub async fn group(&self) -> tokio::sync::RwLockReadGuard<'_, RequestGroup> {
548        self.group.read().await
549    }
550
551    pub async fn group_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, RequestGroup> {
552        self.group.write().await
553    }
554
555    /// Get the NoProxy matcher for checking if a host should bypass proxy
556    pub fn no_proxy_matcher(&self) -> Option<&NoProxyMatcher> {
557        self.no_proxy_matcher.as_ref()
558    }
559
560    fn should_use_concurrent(&self, total_length: u64, supports_range: bool) -> bool {
561        if !supports_range {
562            return false;
563        }
564        if total_length < constants::CONCURRENT_MIN_FILE_SIZE as u64 {
565            return false;
566        }
567
568        let split = { self.group.blocking_read().options().split.unwrap_or(1) };
569        split > 1
570    }
571
572    /// Construct the disk writer based on `file_allocation` and `mmap_threshold`.
573    ///
574    /// When `file_allocation == "mmap"` and `total_length >= mmap_threshold`,
575    /// returns a `CachedDiskWriter` backed by `MmapDiskWriter` (memory-mapped
576    /// I/O). Otherwise uses positioned I/O (`PositionedDiskWriter`).
577    fn create_writer(&self, total_length: u64) -> CachedDiskWriter {
578        let use_mmap = self.file_allocation == "mmap" && total_length >= self.mmap_threshold;
579        CachedDiskWriter::new_with_mmap(&self.output_path, Some(total_length), None, use_mmap)
580    }
581
582    async fn execute_sequential_download(
583        &mut self,
584        uri: &str,
585        resume_state: &crate::filesystem::resume_helper::ResumeState,
586        total_length: u64,
587    ) -> Result<()> {
588        // On non-Linux the `total_length` parameter is only consumed by the
589        // cfg-gated splice block below; bind it to suppress the unused warning.
590        #[cfg(not(target_os = "linux"))]
591        let _ = total_length;
592
593        // On Linux, attempt zero-copy splice download for plain HTTP with no
594        // cookies/headers and no resume. Falls back to the standard streaming
595        // path on any error (non-206, network failure, etc.).
596        #[cfg(target_os = "linux")]
597        {
598            if !resume_state.should_resume
599                && total_length > 0
600                && self.use_hyper
601                && !uri.starts_with("https://")
602                && self.headers.is_empty()
603                && self.cookie_storage.is_empty()
604            {
605                match self.try_splice_sequential(uri, total_length).await {
606                    Ok(()) => return Ok(()),
607                    Err(e) => {
608                        debug!(
609                            "Splice download failed for {}, falling back to streaming: {}",
610                            uri, e
611                        );
612                    }
613                }
614            }
615        }
616
617        let url_parsed = reqwest::Url::parse(uri).ok();
618        let mut request = if let Some(range_header) = ResumeHelper::build_range_header(resume_state)
619        {
620            debug!("Resume download: {}", range_header);
621            self.client.get(uri).header("Range", range_header)
622        } else {
623            self.client.get(uri)
624        };
625
626        if let Some(ref url) = url_parsed {
627            let host = url.host_str().unwrap_or("");
628            let path = url.path();
629            let secure = url.scheme() == "https";
630            let cookie_hdr = self.cookie_storage.to_header_string(host, path, secure);
631            if !cookie_hdr.is_empty() {
632                request = request.header("Cookie", &cookie_hdr);
633            }
634        }
635
636        // Apply custom HTTP headers (Referer, User-Agent, etc.)
637        for (name, value) in &self.headers {
638            request = request.header(name, value);
639        }
640
641        let response = request.send().await.map_err(|e| {
642            Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
643                message: format!("HTTP request failed: {}", e),
644            })
645        })?;
646
647        if let Some(ref url) = url_parsed {
648            let domain = url.host_str().unwrap_or("");
649            let path = url.path();
650            for sc_val in response.headers().get_all("set-cookie").iter() {
651                if let Ok(sc_str) = sc_val.to_str()
652                    && let Some(c) = Cookie::from_set_cookie_header(sc_str, domain, path)
653                {
654                    self.cookie_storage.add(c);
655                    debug!("Received Set-Cookie: {}", sc_str);
656                }
657            }
658        }
659
660        let status = response.status();
661        if !status.is_success() && status.as_u16() != 206 {
662            if status.as_u16() >= 500 {
663                return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
664                    code: status.as_u16(),
665                }));
666            }
667            return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
668                format!("HTTP error: {}", status),
669            )));
670        }
671
672        let resp_length = response.content_length().unwrap_or(0) as u64;
673        let actual_total = if resume_state.should_resume {
674            resume_state.start_offset + resp_length
675        } else {
676            resp_length
677        };
678        {
679            let mut g = self.group.write().await;
680            g.set_total_length(actual_total).await;
681            // Export to atomic field for session persistence
682            g.set_total_length_atomic(actual_total);
683        }
684
685        let start_offset = if resume_state.should_resume {
686            resume_state.start_offset
687        } else {
688            0
689        };
690        self.completed_bytes = start_offset;
691        let rate_limit = { self.group.read().await.options().max_download_limit };
692
693        let raw_writer = DefaultDiskWriter::new(&self.output_path);
694        let mut writer: Box<dyn DiskWriter> = match rate_limit {
695            Some(rate) if rate > 0 => {
696                let cfg = RateLimiterConfig::new(Some(rate), None);
697                let limiter = RateLimiter::new(&cfg);
698                debug!("Download speed limit enabled: {} bytes/s", rate);
699                // Register clone with RequestGroup for runtime rate updates
700                {
701                    let g = self.group.read().await;
702                    g.set_rate_limiter(limiter.clone()).await;
703                }
704                Box::new(ThrottledWriter::new(raw_writer, limiter))
705            }
706            _ => Box::new(raw_writer),
707        };
708
709        let mut stream = response.bytes_stream();
710        let mut last_speed_update = Instant::now();
711        let mut last_completed = 0u64;
712        let mut last_progress_update = 0u64; // Track last progress update for batch updates
713
714        // Size of each write piece. reqwest's `bytes_stream()` yields chunks
715        // whose sizes grow adaptively (8K → 16K → 32K → … → 256K+) on fast
716        // links. If we passed the entire chunk to `writer.write()` in one go,
717        // `completed_bytes` would only advance after the full chunk is
718        // throttled and written — at 80 KB/s a 417 KB chunk takes 5.2 s,
719        // during which no progress update is sent and the download appears
720        // stalled. By splitting into small pieces here, `completed_bytes`
721        // advances after each piece, and progress updates fire every 256 KB
722        // as intended. The `ThrottledWriter` still handles per-piece token
723        // acquisition, so rate limiting remains accurate.
724        let write_piece = constants::RATE_LIMITER_CHUNK_SIZE;
725
726        while let Some(chunk) = stream.next().await {
727            let data: bytes::Bytes = chunk.map_err(|e| {
728                Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
729                    message: e.to_string(),
730                })
731            })?;
732
733            let mut offset = 0usize;
734            while offset < data.len() {
735                let end = (offset + write_piece).min(data.len());
736                let piece = &data[offset..end];
737                writer.write(piece).await?;
738                self.completed_bytes += piece.len() as u64;
739                offset = end;
740
741                // Batch progress updates to reduce lock contention
742                // Only update progress every PROGRESS_UPDATE_BYTES (256KB)
743                if self.completed_bytes - last_progress_update
744                    >= constants::PROGRESS_UPDATE_BYTES as u64
745                {
746                    // Compute speed sample lock-free (refreshed every HTTP_SPEED_UPDATE_INTERVAL_MS).
747                    // `speed` is 0 when not enough time has elapsed since the last sample; in that
748                    // case neither the channel nor the fallback path should touch the speed fields.
749                    let elapsed = last_speed_update.elapsed();
750                    let speed = if elapsed.as_millis()
751                        >= constants::HTTP_SPEED_UPDATE_INTERVAL_MS as u128
752                    {
753                        let delta = self.completed_bytes - last_completed;
754                        let s = (delta as f64 / elapsed.as_secs_f64()) as u64;
755                        last_speed_update = Instant::now();
756                        last_completed = self.completed_bytes;
757                        s
758                    } else {
759                        0
760                    };
761
762                    if let Some(ref sender) = self.progress_sender {
763                        // Lock-free: send via channel, engine aggregator applies to RequestGroup.
764                        // A failed send (receiver dropped) is silently ignored; the final
765                        // completion path still writes progress directly.
766                        let _ = sender.send(ProgressUpdate {
767                            completed_bytes: self.completed_bytes,
768                            download_speed: speed,
769                            upload_speed: 0,
770                        });
771                    } else {
772                        // Fallback: direct write (backward compatible when no channel is set)
773                        let g = self.group.write().await;
774                        g.update_progress(self.completed_bytes).await;
775                        // Export to atomic fields for session persistence
776                        g.set_completed_length(self.completed_bytes);
777                        if speed > 0 {
778                            g.update_speed(speed, 0).await;
779                            // Update cached download speed for session persistence
780                            g.set_download_speed_cached(speed);
781                        }
782                    }
783
784                    // Record performance metrics (minimal overhead, lock-free).
785                    // Only recorded when a fresh speed sample was taken this tick.
786                    if speed > 0 {
787                        self.atomic_metrics.record_throughput(speed);
788                        if let Some(ref monitor) = self.perf_monitor {
789                            let metrics = Metrics::new(speed, elapsed.as_millis() as u64, 0, 0)
790                                .with_label("download_speed");
791                            monitor.record_metric("download_speed", metrics);
792                        }
793                    }
794                    last_progress_update = self.completed_bytes;
795                }
796            }
797        }
798
799        writer.finalize().await.ok();
800
801        // Calculate final speed using group's elapsed_time for consistency
802        let final_speed = {
803            let g = self.group.read().await;
804            let elapsed = g.elapsed_time().await;
805            match elapsed {
806                Some(d) if d.as_secs_f64() > 0.0 => {
807                    (self.completed_bytes as f64 / d.as_secs_f64()) as u64
808                }
809                _ => 0,
810            }
811        };
812        {
813            let mut g = self.group.write().await;
814            g.update_progress(self.completed_bytes).await;
815            g.update_speed(final_speed, 0).await;
816            // Export final progress to atomic fields for session persistence
817            g.set_completed_length(self.completed_bytes);
818            g.set_download_speed_cached(final_speed);
819            g.complete().await?;
820        }
821
822        info!(
823            "Sequential download complete: {} ({} bytes)",
824            self.output_path.display(),
825            self.completed_bytes
826        );
827        self.save_cookies_if_configured();
828        Ok(())
829    }
830
831    /// Attempt a zero-copy splice download for the sequential path (Linux only).
832    ///
833    /// Opens the output file, calls [`splice_http::try_splice_download`] to
834    /// transfer the response body via `splice(2)` directly from the kernel
835    /// socket buffer to the file, then updates the group state.
836    ///
837    /// Returns `Err` on any failure so the caller can fall back to the
838    /// standard streaming path. The file is truncated on open, so a failed
839    /// splice attempt leaves the file empty — the streaming path's
840    /// `DefaultDiskWriter` will truncate again and start fresh.
841    #[cfg(target_os = "linux")]
842    async fn try_splice_sequential(&mut self, uri: &str, total_length: u64) -> Result<()> {
843        // Open the output file for splice. Truncate since we're not resuming
844        // (resume is excluded by the caller's `can_splice` condition).
845        let file = std::fs::OpenOptions::new()
846            .write(true)
847            .create(true)
848            .truncate(true)
849            .open(&self.output_path)?;
850
851        let bytes = crate::http::splice_http::try_splice_download(uri, 0, total_length, &file, 0)
852            .await
853            .map_err(|e| Aria2Error::Io(format!("splice download failed: {e}")))?;
854
855        // Splice succeeded — update progress and complete the group.
856        self.completed_bytes = bytes;
857
858        let final_speed = {
859            let g = self.group.read().await;
860            let elapsed = g.elapsed_time().await;
861            match elapsed {
862                Some(d) if d.as_secs_f64() > 0.0 => (bytes as f64 / d.as_secs_f64()) as u64,
863                _ => 0,
864            }
865        };
866
867        {
868            let mut g = self.group.write().await;
869            g.set_total_length(bytes).await;
870            g.set_total_length_atomic(bytes);
871            g.update_progress(bytes).await;
872            g.set_completed_length(bytes);
873            g.update_speed(final_speed, 0).await;
874            g.set_download_speed_cached(final_speed);
875            g.complete().await?;
876        }
877
878        info!(
879            "Sequential download (splice) complete: {} ({} bytes)",
880            self.output_path.display(),
881            bytes
882        );
883        self.save_cookies_if_configured();
884        Ok(())
885    }
886
887    #[allow(dead_code)] // Reserved for future concurrent download execution strategy
888    async fn execute_concurrent_download(&mut self, uri: &str, total_length: u64) -> Result<()> {
889        let options = self.group.read().await.options().clone();
890        let split = options.split.unwrap_or(1) as usize;
891        let max_conn = options
892            .max_connection_per_server
893            .unwrap_or(constants::DEFAULT_MAX_CONNECTION_PER_SERVER as u16)
894            as usize;
895        let seg_size = total_length / split as u64;
896        let mut last_progress_update = 0u64; // Track last progress update for batch updates
897
898        info!(
899            "Concurrent download started: split={}, max_conn={}, segment_size={} bytes, total={}",
900            split, max_conn, seg_size, total_length
901        );
902
903        let mut manager =
904            ConcurrentSegmentManager::new(total_length, vec![uri.to_string()], Some(seg_size));
905        manager.set_max_connections_per_mirror(max_conn.min(split));
906
907        let url_parsed = reqwest::Url::parse(uri).ok();
908        let cookie_hdr = if let Some(ref url) = url_parsed {
909            self.cookie_storage.to_header_string(
910                url.host_str().unwrap_or(""),
911                url.path(),
912                url.scheme() == "https",
913            )
914        } else {
915            String::new()
916        };
917        let cookie_hdr_for_spawn: Option<String> = if cookie_hdr.is_empty() {
918            None
919        } else {
920            Some(cookie_hdr)
921        };
922
923        let mut writer = self.create_writer(total_length);
924
925        let limiter = options
926            .max_download_limit
927            .filter(|&r| r > 0)
928            .map(|r| RateLimiter::new(&RateLimiterConfig::new(Some(r), None)));
929        // Register clone with RequestGroup for runtime rate updates
930        if let Some(ref limiter) = limiter {
931            let g = self.group.read().await;
932            g.set_rate_limiter(limiter.clone()).await;
933        }
934
935        // FuturesUnordered drives all in-flight segment fetches concurrently.
936        // Wakeup is driven by FuturesUnordered::next() — no polling delay
937        // (replaces the previous sleep(5ms) + is_finished() busy-poll that
938        // wasted CPU and added up to 5ms latency per segment completion).
939        let mut active: FuturesUnordered<SegmentFetchFuture> = FuturesUnordered::new();
940        // Track the write offset for each in-flight future so we know where
941        // to write the downloaded data when the future completes.
942        let mut active_segs: std::collections::HashMap<u32, u64> = std::collections::HashMap::new();
943
944        loop {
945            // Fill slots up to max_conn concurrent segment fetches.
946            while active.len() < max_conn {
947                match manager.next_pending_segment_for_mirror(0) {
948                    Some((seg_idx, offset, length)) => {
949                        let url = uri.to_string();
950                        let dl = HttpSegmentDownloader::new(&self.client, self.use_hyper);
951                        let ch = cookie_hdr_for_spawn.clone();
952                        let headers = self.headers.clone();
953                        active_segs.insert(seg_idx, offset);
954                        let fut = Box::pin(async move {
955                            let result = dl
956                                .download_range(&url, offset, length, ch.as_deref(), &headers)
957                                .await
958                                .map_err(|e| e.to_string());
959                            (seg_idx, result)
960                        });
961                        active.push(fut);
962                        debug!(
963                            seg_idx = seg_idx,
964                            offset = offset,
965                            length = length,
966                            "Spawned segment fetch"
967                        );
968                    }
969                    None => break,
970                }
971            }
972
973            // If no active futures remain, we are either complete or stuck.
974            if active.is_empty() {
975                if manager.is_complete() {
976                    debug!("All segments complete");
977                    break;
978                }
979                if manager.has_failed_segments() && !manager.has_pending_segments() {
980                    return Err(Aria2Error::Recoverable(
981                        RecoverableError::TemporaryNetworkFailure {
982                            message: "Concurrent download: all segments failed".into(),
983                        },
984                    ));
985                }
986                // Defensive: no active, no pending, not complete, not all-failed.
987                // Break to avoid an infinite loop.
988                warn!("Concurrent download stuck: no active or pending segments but not complete");
989                break;
990            }
991
992            // Wait for the next segment to complete (no busy-poll!).
993            // Wakeup is driven by FuturesUnordered::next() — no polling delay.
994            if let Some((seg_idx, result)) = active.next().await {
995                let offset = active_segs.remove(&seg_idx).unwrap_or(0);
996                match result {
997                    Ok(data) => {
998                        let data_len = data.len();
999                        if let Some(ref lim) = limiter {
1000                            lim.acquire_download(data_len as u64).await;
1001                        }
1002                        // Zero-copy write: writer consumes the original Bytes;
1003                        // manager gets a cheap Arc-refcount-bumped clone.
1004                        let data_for_manager = data.clone();
1005                        writer.write_bytes_at(offset, data).await.map_err(|e| {
1006                            Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1007                                "Write failed: {}",
1008                                e
1009                            )))
1010                        })?;
1011                        manager.complete_segment(seg_idx, data_for_manager);
1012                        self.completed_bytes += data_len as u64;
1013
1014                        // Batch progress updates to reduce lock contention
1015                        if self.completed_bytes - last_progress_update
1016                            >= constants::PROGRESS_UPDATE_BYTES as u64
1017                        {
1018                            if let Some(ref sender) = self.progress_sender {
1019                                // Lock-free: send via channel, engine aggregator applies to RequestGroup.
1020                                let _ = sender.send(ProgressUpdate {
1021                                    completed_bytes: self.completed_bytes,
1022                                    download_speed: 0,
1023                                    upload_speed: 0,
1024                                });
1025                            } else {
1026                                // Fallback: direct write (backward compatible when no channel is set)
1027                                let g = self.group.write().await;
1028                                g.update_progress(self.completed_bytes).await;
1029                                // Export to atomic fields for session persistence
1030                                g.set_completed_length(self.completed_bytes);
1031                            }
1032                            last_progress_update = self.completed_bytes;
1033                        }
1034                    }
1035                    Err(e) => {
1036                        warn!(seg_idx = seg_idx, error = %e, "Segment download failed");
1037                        manager.fail_segment(seg_idx);
1038                    }
1039                }
1040            }
1041        }
1042
1043        writer.flush().await.map_err(|e| {
1044            Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1045                "Flush failed: {}",
1046                e
1047            )))
1048        })?;
1049
1050        // Calculate final speed using group's elapsed_time for consistency
1051        let final_speed = {
1052            let g = self.group.read().await;
1053            let elapsed = g.elapsed_time().await;
1054            match elapsed {
1055                Some(d) if d.as_secs_f64() > 0.0 => {
1056                    (self.completed_bytes as f64 / d.as_secs_f64()) as u64
1057                }
1058                _ => 0,
1059            }
1060        };
1061        {
1062            let mut g = self.group.write().await;
1063            g.update_progress(self.completed_bytes).await;
1064            g.update_speed(final_speed, 0).await;
1065            // Export final progress to atomic fields for session persistence
1066            g.set_completed_length(self.completed_bytes);
1067            g.set_download_speed_cached(final_speed);
1068            g.complete().await?;
1069        }
1070
1071        info!(
1072            "Concurrent download complete: {} ({} bytes)",
1073            self.output_path.display(),
1074            self.completed_bytes
1075        );
1076        self.save_cookies_if_configured();
1077        Ok(())
1078    }
1079
1080    async fn execute_sequential_download_with_retry(
1081        &mut self,
1082        uri: &str,
1083        resume_state: &ResumeState,
1084        total_length: u64,
1085        retry_policy: &RetryPolicy,
1086    ) -> Result<()> {
1087        let mut last_err = None;
1088
1089        for attempt in 0..=retry_policy.max_retries {
1090            if attempt > 0
1091                && let Some(wait) = retry_policy.compute_wait(attempt - 1)
1092            {
1093                info!(
1094                    "Sequential download retry #{} (waiting {:?})...",
1095                    attempt, wait
1096                );
1097                tokio::time::sleep(wait).await;
1098            }
1099
1100            match self
1101                .execute_sequential_download(uri, resume_state, total_length)
1102                .await
1103            {
1104                Ok(()) => return Ok(()),
1105                Err(e) => {
1106                    warn!("Sequential download attempt #{} failed: {}", attempt + 1, e);
1107                    last_err = Some(e);
1108                    if retry_policy.is_exhausted(attempt)
1109                        || !retry_policy
1110                            .should_retry_error(&format!("{:?}", last_err.as_ref().unwrap()))
1111                    {
1112                        break;
1113                    }
1114                }
1115            }
1116        }
1117
1118        Err(last_err.unwrap_or_else(|| {
1119            Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
1120                message: "All retries failed".into(),
1121            })
1122        }))
1123    }
1124
1125    async fn execute_concurrent_download_with_retry(
1126        &mut self,
1127        uri: &str,
1128        total_length: u64,
1129        resume_state: &ResumeState,
1130        max_retries_per_segment: u32,
1131    ) -> Result<()> {
1132        info!(
1133            "Using concurrent download mode (split={}, max_retries/segment={})",
1134            self.group.read().await.options().split.unwrap_or(1),
1135            max_retries_per_segment
1136        );
1137
1138        // Get all URIs from the request group for multi-mirror support
1139        let all_uris: Vec<String> = {
1140            let g = self.group.read().await;
1141            g.uris().to_vec()
1142        };
1143
1144        // Check if we have multiple mirrors
1145        let use_intelligent_selection = all_uris.len() > 1;
1146
1147        if use_intelligent_selection {
1148            info!(
1149                "Intelligent multi-mirror selection enabled: {} mirror sources",
1150                all_uris.len()
1151            );
1152            self.execute_concurrent_download_with_coordinator(
1153                &all_uris,
1154                total_length,
1155                resume_state,
1156                max_retries_per_segment,
1157            )
1158            .await
1159        } else {
1160            // Fallback to single-URI concurrent download
1161            self.execute_concurrent_download_single_uri(
1162                uri,
1163                total_length,
1164                resume_state,
1165                max_retries_per_segment,
1166            )
1167            .await
1168        }
1169    }
1170
1171    /// Execute concurrent download using MirrorCoordinator for intelligent mirror selection.
1172    ///
1173    /// This method uses the new MirrorCoordinator to intelligently select mirrors
1174    /// based on historical performance data and real-time feedback.
1175    async fn execute_concurrent_download_with_coordinator(
1176        &mut self,
1177        uris: &[String],
1178        total_length: u64,
1179        resume_state: &ResumeState,
1180        max_retries_per_segment: u32,
1181    ) -> Result<()> {
1182        let split = self.group.read().await.options().split.unwrap_or(1) as u64;
1183        let segment_size = total_length.div_ceil(split);
1184        let max_conn = self
1185            .group
1186            .read()
1187            .await
1188            .options()
1189            .max_connection_per_server
1190            .unwrap_or(constants::DEFAULT_MAX_CONNECTION_PER_SERVER as u16)
1191            as usize;
1192
1193        // Create mirror configuration
1194        let mirror_config = MirrorConfig {
1195            max_connections_per_mirror: max_conn.min(split as usize),
1196            max_total_connections: max_conn * uris.len(),
1197            speed_threshold: constants::MIRROR_SPEED_THRESHOLD,
1198            cooldown_secs: constants::MIRROR_COOLDOWN_SECS,
1199            max_retries: max_retries_per_segment,
1200        };
1201
1202        // Create URI selector with server stats
1203        let selector = Box::new(AdaptiveUriSelector::new_with_uris(
1204            Arc::clone(&self.stat_man),
1205            uris.to_vec(),
1206        ));
1207
1208        // Create segment manager with selector
1209        let segment_manager = ConcurrentSegmentManager::new_with_selector(
1210            total_length,
1211            uris.to_vec(),
1212            Some(segment_size),
1213            Arc::clone(&self.stat_man),
1214            selector,
1215        );
1216
1217        // Create mirror coordinator
1218        let mut coordinator = MirrorCoordinator::with_segment_manager(
1219            Arc::clone(&self.stat_man),
1220            Box::new(crate::selector::uri_selector::InorderUriSelector::new()),
1221            segment_manager,
1222            mirror_config,
1223            uris.to_vec(),
1224        );
1225
1226        if resume_state.should_resume {
1227            // Note: We'd need to expose mark_completed_up_to in MirrorCoordinator
1228            // For now, we'll handle this at the segment manager level
1229            debug!(
1230                "Resume: existing {} bytes, continuing from offset {}",
1231                resume_state.existing_length, resume_state.start_offset
1232            );
1233        }
1234
1235        let mut writer = self.create_writer(total_length);
1236        let mut last_speed_update = Instant::now();
1237        let mut last_completed = 0u64;
1238        let mut last_progress_update = 0u64; // Track last progress update for batch updates
1239
1240        while coordinator.has_pending_segments() || !coordinator.is_complete() {
1241            // Select mirror and segment
1242            while let Some((mirror_idx, mirror_url, (seg_idx, offset, length))) =
1243                coordinator.select_mirror_for_segment()
1244            {
1245                info!(
1246                    "Starting segment {} download: mirror={}, offset={}, size={}",
1247                    seg_idx, mirror_idx, offset, length
1248                );
1249
1250                let downloader = HttpSegmentDownloader::new(&self.client, self.use_hyper);
1251                let seg_start = Instant::now();
1252
1253                // Get cookie header for this mirror
1254                let url_parsed = reqwest::Url::parse(&mirror_url).ok();
1255                let cookie_hdr = if let Some(ref url) = url_parsed {
1256                    self.cookie_storage.to_header_string(
1257                        url.host_str().unwrap_or(""),
1258                        url.path(),
1259                        url.scheme() == "https",
1260                    )
1261                } else {
1262                    String::new()
1263                };
1264
1265                let result = downloader
1266                    .download_range(
1267                        &mirror_url,
1268                        offset,
1269                        length,
1270                        if cookie_hdr.is_empty() {
1271                            None
1272                        } else {
1273                            Some(&cookie_hdr)
1274                        },
1275                        &self.headers,
1276                    )
1277                    .await;
1278
1279                match result {
1280                    Ok(data) => {
1281                        let elapsed = seg_start.elapsed();
1282                        let speed = if elapsed.as_secs_f64() > 0.0 {
1283                            (data.len() as f64 / elapsed.as_secs_f64()) as u64
1284                        } else {
1285                            0
1286                        };
1287
1288                        debug!(
1289                            "Segment {} complete: {} bytes, speed={} B/s",
1290                            seg_idx,
1291                            data.len(),
1292                            speed
1293                        );
1294
1295                        // Zero-copy write: writer consumes the original Bytes;
1296                        // coordinator gets a cheap Arc-refcount-bumped clone.
1297                        let data_for_coordinator = data.clone();
1298                        writer.write_bytes_at(offset, data).await.map_err(|e| {
1299                            Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1300                                "Write failed: {}",
1301                                e
1302                            )))
1303                        })?;
1304
1305                        // Report success to coordinator for speed feedback
1306                        coordinator.on_segment_complete(
1307                            mirror_idx,
1308                            seg_idx,
1309                            data_for_coordinator,
1310                            speed,
1311                        );
1312                    }
1313                    Err(e) => {
1314                        warn!(
1315                            "Segment {} download failed (mirror={}): {}",
1316                            seg_idx, mirror_idx, e
1317                        );
1318
1319                        // Default error code for network errors
1320                        let error_code = constants::HTTP_DEFAULT_ERROR_CODE;
1321
1322                        // Report failure to coordinator
1323                        coordinator.on_segment_failed(mirror_idx, seg_idx, error_code);
1324                    }
1325                }
1326
1327                // Update progress
1328                self.completed_bytes = {
1329                    // Get completed bytes from coordinator's progress
1330                    let total = coordinator.num_segments() as u64;
1331                    let progress_pct = coordinator.progress();
1332                    if total > 0 {
1333                        (progress_pct / 100.0 * total as f64) as u64
1334                    } else {
1335                        0
1336                    }
1337                };
1338
1339                // Batch progress updates to reduce lock contention
1340                if self.completed_bytes - last_progress_update
1341                    >= constants::PROGRESS_UPDATE_BYTES as u64
1342                {
1343                    // Compute speed sample lock-free (refreshed every HTTP_SPEED_UPDATE_INTERVAL_MS).
1344                    let elapsed = last_speed_update.elapsed();
1345                    let speed = if elapsed.as_millis()
1346                        >= constants::HTTP_SPEED_UPDATE_INTERVAL_MS as u128
1347                    {
1348                        let delta = self.completed_bytes - last_completed;
1349                        let s = (delta as f64 / elapsed.as_secs_f64()) as u64;
1350                        last_speed_update = Instant::now();
1351                        last_completed = self.completed_bytes;
1352                        s
1353                    } else {
1354                        0
1355                    };
1356
1357                    if let Some(ref sender) = self.progress_sender {
1358                        // Lock-free: send via channel, engine aggregator applies to RequestGroup.
1359                        let _ = sender.send(ProgressUpdate {
1360                            completed_bytes: self.completed_bytes,
1361                            download_speed: speed,
1362                            upload_speed: 0,
1363                        });
1364                    } else {
1365                        // Fallback: direct write (backward compatible when no channel is set)
1366                        let g = self.group.write().await;
1367                        g.update_progress(self.completed_bytes).await;
1368                        g.set_completed_length(self.completed_bytes);
1369                        if speed > 0 {
1370                            g.update_speed(speed, 0).await;
1371                            g.set_download_speed_cached(speed);
1372                        }
1373                    }
1374                    last_progress_update = self.completed_bytes;
1375                }
1376            }
1377
1378            if coordinator.is_complete() {
1379                break;
1380            }
1381
1382            if coordinator.has_failed_segments() {
1383                error!("Permanently failed download segments exist");
1384                return Err(Aria2Error::Recoverable(
1385                    RecoverableError::TemporaryNetworkFailure {
1386                        message: "Some download segments permanently failed".into(),
1387                    },
1388                ));
1389            }
1390
1391            tokio::time::sleep(Duration::from_millis(100)).await;
1392        }
1393
1394        writer.flush().await.map_err(|e| {
1395            Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1396                "Flush failed: {}",
1397                e
1398            )))
1399        })?;
1400
1401        // Calculate final speed using group's elapsed_time for consistency
1402        let final_speed = {
1403            let g = self.group.read().await;
1404            let elapsed = g.elapsed_time().await;
1405            match elapsed {
1406                Some(d) if d.as_secs_f64() > 0.0 => {
1407                    (self.completed_bytes as f64 / d.as_secs_f64()) as u64
1408                }
1409                _ => 0,
1410            }
1411        };
1412
1413        {
1414            let mut g = self.group.write().await;
1415            g.set_total_length(self.completed_bytes).await;
1416            g.set_total_length_atomic(self.completed_bytes);
1417            g.set_completed_length(self.completed_bytes);
1418            g.update_speed(final_speed, 0).await;
1419            g.set_download_speed_cached(final_speed);
1420            g.complete().await?;
1421        }
1422
1423        // Save server stats for future use
1424        if let Err(e) = self.save_server_stats().await {
1425            warn!("Failed to save server statistics: {}", e);
1426        }
1427
1428        info!(
1429            "Multi-mirror concurrent download complete: {} ({} bytes, {} B/s)",
1430            self.output_path.display(),
1431            self.completed_bytes,
1432            final_speed
1433        );
1434        self.save_cookies_if_configured();
1435        Ok(())
1436    }
1437
1438    /// Execute concurrent download for a single URI (fallback method).
1439    async fn execute_concurrent_download_single_uri(
1440        &mut self,
1441        uri: &str,
1442        total_length: u64,
1443        resume_state: &ResumeState,
1444        max_retries_per_segment: u32,
1445    ) -> Result<()> {
1446        let split = self.group.read().await.options().split.unwrap_or(1) as u64;
1447        let segment_size = total_length.div_ceil(split);
1448        let mut manager =
1449            ConcurrentSegmentManager::new(total_length, vec![uri.to_string()], Some(segment_size));
1450        manager.set_max_retries(max_retries_per_segment);
1451
1452        if resume_state.should_resume {
1453            manager.mark_completed_up_to(resume_state.start_offset, resume_state.existing_length);
1454        }
1455
1456        let mut writer = self.create_writer(total_length);
1457
1458        while manager.has_pending_segments() || !manager.is_complete() {
1459            while let Some((seg_idx, offset, length)) = manager.next_pending_segment() {
1460                let seg_idx_u32 = seg_idx;
1461                info!(
1462                    "Starting segment {} download: offset={}, size={}",
1463                    seg_idx, offset, length
1464                );
1465                let downloader = HttpSegmentDownloader::new(&self.client, self.use_hyper);
1466                let seg_start = Instant::now();
1467                let result = downloader
1468                    .download_range(uri, offset, length, None, &self.headers)
1469                    .await;
1470
1471                match result {
1472                    Ok(data) => {
1473                        let elapsed = seg_start.elapsed();
1474                        let speed = if elapsed.as_secs_f64() > 0.0 {
1475                            (data.len() as f64 / elapsed.as_secs_f64()) as u64
1476                        } else {
1477                            0
1478                        };
1479                        debug!(
1480                            "Segment {} complete: {} bytes, speed={} B/s",
1481                            seg_idx,
1482                            data.len(),
1483                            speed
1484                        );
1485
1486                        // Zero-copy write: writer consumes the original Bytes;
1487                        // manager gets a cheap Arc-refcount-bumped clone.
1488                        let data_for_manager = data.clone();
1489                        writer.write_bytes_at(offset, data).await.map_err(|e| {
1490                            Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1491                                "Write failed: {}",
1492                                e
1493                            )))
1494                        })?;
1495
1496                        // Report completion with speed feedback
1497                        manager.report_segment_complete(
1498                            seg_idx_u32,
1499                            data_for_manager,
1500                            speed,
1501                            false,
1502                        );
1503                    }
1504                    Err(e) => {
1505                        warn!("Segment {} download failed: {}", seg_idx, e);
1506                        manager
1507                            .report_segment_failed(seg_idx_u32, constants::HTTP_DEFAULT_ERROR_CODE);
1508                    }
1509                }
1510
1511                self.completed_bytes = manager.completed_bytes();
1512                let g = self.group.write().await;
1513                g.update_progress(self.completed_bytes).await;
1514                // Export to atomic fields for session persistence
1515                g.set_completed_length(self.completed_bytes);
1516            }
1517
1518            if manager.is_complete() {
1519                break;
1520            }
1521
1522            if manager.has_permanently_failed_segments() {
1523                error!("Permanently failed download segments exist");
1524                return Err(Aria2Error::Recoverable(
1525                    RecoverableError::TemporaryNetworkFailure {
1526                        message: "Some download segments permanently failed".into(),
1527                    },
1528                ));
1529            }
1530
1531            tokio::time::sleep(Duration::from_millis(100)).await;
1532        }
1533
1534        writer.flush().await.map_err(|e| {
1535            Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1536                "Flush failed: {}",
1537                e
1538            )))
1539        })?;
1540        self.completed_bytes = manager.completed_bytes();
1541        let mut g = self.group.write().await;
1542        g.set_total_length(self.completed_bytes).await;
1543        // Export to atomic fields for session persistence
1544        g.set_total_length_atomic(self.completed_bytes);
1545        g.set_completed_length(self.completed_bytes);
1546        g.complete().await?;
1547        Ok(())
1548    }
1549
1550    /// Save server statistics to the default location.
1551    async fn save_server_stats(&self) -> std::result::Result<usize, String> {
1552        // In a real implementation, this would save to a configured path
1553        // For now, we just return success
1554        Ok(0)
1555    }
1556
1557    /// Get a reference to the server statistics manager.
1558    pub fn stat_man(&self) -> &Arc<ServerStatMan> {
1559        &self.stat_man
1560    }
1561}
1562
1563#[async_trait]
1564impl Command for DownloadCommand {
1565    async fn execute(&mut self) -> Result<()> {
1566        if !self.started {
1567            self.group.write().await.start().await?;
1568            self.started = true;
1569        }
1570
1571        let uri = {
1572            let g = self.group.read().await;
1573            g.uris().first().cloned().unwrap_or_default()
1574        };
1575
1576        if uri.is_empty() {
1577            return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
1578                "Download URI is empty".into(),
1579            )));
1580        }
1581
1582        debug!(
1583            "Starting download: {} -> {}",
1584            uri,
1585            self.output_path.display()
1586        );
1587
1588        if let Some(parent) = self.output_path.parent()
1589            && !parent.exists()
1590        {
1591            std::fs::create_dir_all(parent).map_err(|e| {
1592                Aria2Error::Fatal(crate::error::FatalError::Config(format!(
1593                    "Failed to create directory: {}",
1594                    e
1595                )))
1596            })?;
1597        }
1598
1599        // Resolve filename collision: if another active download is already
1600        // writing to self.output_path, replace it with a unique name such as
1601        // "file (1).ext" so that concurrent downloads do not silently overwrite.
1602        let original_path = self.output_path.clone();
1603        self.output_path = global_registry().resolve(&original_path).await;
1604        if self.output_path != original_path {
1605            info!(
1606                "Filename collision resolved: '{}' -> '{}'",
1607                original_path.display(),
1608                self.output_path.display()
1609            );
1610        }
1611
1612        // Ensure the resolved path is released when execute() returns (success or failure).
1613        let release_path = |path: &std::path::Path| {
1614            let p = path.to_path_buf();
1615            #[allow(clippy::let_underscore_future)]
1616            let _ = tokio::spawn(async move {
1617                global_registry().release(&p).await;
1618            });
1619        };
1620
1621        let url_for_head = reqwest::Url::parse(&uri).ok();
1622        let cookie_hdr_head = if let Some(ref url) = url_for_head {
1623            self.cookie_storage.to_header_string(
1624                url.host_str().unwrap_or(""),
1625                url.path(),
1626                url.scheme() == "https",
1627            )
1628        } else {
1629            String::new()
1630        };
1631        let mut head_req = self.client.head(&uri);
1632        if !cookie_hdr_head.is_empty() {
1633            head_req = head_req.header("Cookie", &cookie_hdr_head);
1634        }
1635        // Apply custom HTTP headers (Referer, User-Agent override, etc.) to the
1636        // HEAD probe so servers that 403 without them respond correctly.
1637        for (name, value) in &self.headers {
1638            head_req = head_req.header(name, value);
1639        }
1640        let head_resp = head_req.send().await.ok();
1641        let (total_length, supports_range) = if let Some(ref resp) = head_resp {
1642            let tl = resp.content_length().unwrap_or(0);
1643            let sr = resp
1644                .headers()
1645                .get("Accept-Ranges")
1646                .and_then(|v| v.to_str().ok())
1647                .is_some_and(|v| v.to_lowercase().contains("bytes"));
1648            (tl, sr)
1649        } else {
1650            (0, false)
1651        };
1652
1653        let resume_helper = ResumeHelper::new(&self.output_path, self.continue_enabled);
1654        let resume_state = resume_helper.detect(total_length).await?;
1655
1656        if resume_state.is_complete {
1657            info!(
1658                "File already exists completely, skipping download: {} ({} bytes)",
1659                self.output_path.display(),
1660                resume_state.existing_length
1661            );
1662            self.completed_bytes = resume_state.existing_length;
1663            let mut g = self.group.write().await;
1664            g.set_total_length(self.completed_bytes).await;
1665            g.update_progress(self.completed_bytes).await;
1666            // Export to atomic fields for session persistence
1667            g.set_total_length_atomic(self.completed_bytes);
1668            g.set_completed_length(self.completed_bytes);
1669            g.complete().await?;
1670            self.completed = true;
1671            release_path(&self.output_path);
1672            return Ok(());
1673        }
1674
1675        // Spawn the progress aggregator task now that we're in an async
1676        // context (guaranteed tokio runtime). The aggregator receives
1677        // lock-free progress updates from the download hot loop and applies
1678        // them to the RequestGroup. It is drained in the completion path
1679        // below to ensure all queued updates are applied before returning.
1680        self.spawn_progress_aggregator();
1681
1682        // Run the actual download in an async block so we can drain the
1683        // aggregator on ALL exit paths (success, download failure, and
1684        // preallocation failure) via a single synchronization point.
1685        let download_result: Result<()> = async {
1686            if total_length > 0 {
1687                file_allocation::preallocate_file(
1688                    &self.output_path,
1689                    total_length,
1690                    &self.file_allocation,
1691                    self.secure_falloc,
1692                )
1693                .await?;
1694            }
1695
1696            let options = self.group.read().await.options().clone();
1697
1698            if self.should_use_concurrent(total_length, supports_range) {
1699                if resume_state.should_resume {
1700                    info!(
1701                        "Concurrent mode + resume: existing {} bytes, continuing from offset {}",
1702                        resume_state.existing_length, resume_state.start_offset
1703                    );
1704                }
1705                let max_retries = options.max_retries;
1706                return self
1707                    .execute_concurrent_download_with_retry(
1708                        &uri,
1709                        total_length,
1710                        &resume_state,
1711                        max_retries,
1712                    )
1713                    .await;
1714            }
1715
1716            let retry_policy = RetryPolicy::new(options.max_retries, options.retry_wait * 1000);
1717            self.execute_sequential_download_with_retry(
1718                &uri,
1719                &resume_state,
1720                total_length,
1721                &retry_policy,
1722            )
1723            .await
1724        }
1725        .await;
1726
1727        // Drain the aggregator: drop the sender to signal the task to drain
1728        // all queued updates and exit, then await the handle. This guarantees
1729        // every progress update sent during the download has been applied to
1730        // the RequestGroup before the command completes. Runs on both success
1731        // and failure paths.
1732        self.drain_progress_aggregator().await;
1733
1734        if download_result.is_ok() {
1735            self.completed = true;
1736            // Re-sync final progress after aggregator drain.
1737            // Stale updates queued before the final completion may have
1738            // overwritten the completed_length; re-apply the correct value.
1739            let g = self.group.write().await;
1740            let total = g.total_length();
1741            g.update_progress(total).await;
1742            g.set_completed_length(total);
1743        }
1744        release_path(&self.output_path);
1745        download_result
1746    }
1747
1748    fn status(&self) -> CommandStatus {
1749        if self.completed {
1750            CommandStatus::Completed
1751        } else if self.completed_bytes > 0 {
1752            CommandStatus::Running
1753        } else {
1754            CommandStatus::Pending
1755        }
1756    }
1757
1758    fn timeout(&self) -> Option<Duration> {
1759        Some(Duration::from_secs(
1760            constants::HTTP_DEFAULT_COMMAND_TIMEOUT_SECS,
1761        ))
1762    }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767    use super::*;
1768    use crate::request::request_group::{DownloadOptions, GroupId, RequestGroup};
1769
1770    // ---- Test-only accessors for verifying auto-created channel state ----
1771
1772    impl DownloadCommand {
1773        /// True when the constructor auto-created a progress sender.
1774        fn has_progress_sender(&self) -> bool {
1775            self.progress_sender.is_some()
1776        }
1777
1778        /// True when the receiver is still pending (aggregator not yet spawned).
1779        fn has_progress_receiver(&self) -> bool {
1780            self.progress_receiver.is_some()
1781        }
1782
1783        /// True when the aggregator has been spawned and not yet drained.
1784        fn has_progress_aggregator_handle(&self) -> bool {
1785            self.progress_aggregator_handle.is_some()
1786        }
1787
1788        /// Send a progress update via the internal channel (test helper).
1789        fn send_progress_update(&self, update: ProgressUpdate) {
1790            if let Some(ref sender) = self.progress_sender {
1791                let _ = sender.send(update);
1792            } else {
1793                panic!("test called send_progress_update but no sender is set");
1794            }
1795        }
1796    }
1797
1798    /// SubTask 2.7: Verify that `DownloadCommand::new` auto-creates a
1799    /// progress channel (sender + receiver) without requiring the caller to
1800    /// call `with_progress_sender()`. The aggregator handle should be `None`
1801    /// until `execute()` spawns it lazily.
1802    #[test]
1803    fn test_progress_channel_auto_created() {
1804        let cmd = DownloadCommand::new(
1805            GroupId::new(1),
1806            "http://example.com/file.bin",
1807            &DownloadOptions::default(),
1808            None,
1809            None,
1810        )
1811        .expect("DownloadCommand::new should succeed with a valid HTTP URI");
1812
1813        // The constructor must auto-create both halves of the channel.
1814        assert!(
1815            cmd.has_progress_sender(),
1816            "progress_sender should be Some after construction (auto-created)"
1817        );
1818        assert!(
1819            cmd.has_progress_receiver(),
1820            "progress_receiver should be Some after construction (held for lazy spawn)"
1821        );
1822        // The aggregator is NOT spawned in the constructor (lazy spawn in
1823        // execute() to guarantee a tokio runtime context).
1824        assert!(
1825            !cmd.has_progress_aggregator_handle(),
1826            "progress_aggregator_handle should be None until execute() spawns it"
1827        );
1828    }
1829
1830    /// SubTask 2.8: Verify that progress updates sent through the
1831    /// auto-created channel are applied to the `RequestGroup` by the
1832    /// aggregator task. This exercises the full pipeline: constructor →
1833    /// spawn aggregator → send update → drain aggregator → verify group.
1834    #[tokio::test]
1835    async fn test_progress_updates_flow_through_channel() {
1836        // Build a shared RequestGroup and keep a clone for verification.
1837        let group = Arc::new(tokio::sync::RwLock::new(RequestGroup::new(
1838            GroupId::new(2),
1839            vec!["http://example.com/file.bin".to_string()],
1840            DownloadOptions::default(),
1841        )));
1842        let group_clone = Arc::clone(&group);
1843
1844        let mut cmd = DownloadCommand::new_with_group(
1845            group,
1846            "http://example.com/file.bin",
1847            &DownloadOptions::default(),
1848            None,
1849            None,
1850        )
1851        .expect("DownloadCommand::new_with_group should succeed");
1852
1853        // The constructor auto-created the channel.
1854        assert!(cmd.has_progress_sender());
1855        assert!(cmd.has_progress_receiver());
1856
1857        // Lazily spawn the aggregator (simulates what execute() does).
1858        cmd.spawn_progress_aggregator();
1859        assert!(cmd.has_progress_aggregator_handle());
1860        // Receiver is consumed by the spawn.
1861        assert!(!cmd.has_progress_receiver());
1862
1863        // Send a progress update through the auto-created channel.
1864        cmd.send_progress_update(ProgressUpdate {
1865            completed_bytes: 4096,
1866            download_speed: 0,
1867            upload_speed: 0,
1868        });
1869
1870        // Drain: drop sender → aggregator drains queue → aggregator exits.
1871        cmd.drain_progress_aggregator().await;
1872        assert!(!cmd.has_progress_sender());
1873        assert!(!cmd.has_progress_aggregator_handle());
1874
1875        // The aggregator should have applied the update to the group's
1876        // atomic completed_length mirror.
1877        let completed = { group_clone.read().await.get_completed_length() };
1878        assert_eq!(
1879            completed, 4096,
1880            "aggregator should have applied the progress update to RequestGroup"
1881        );
1882    }
1883}