oma_apt/
progress.rs

1//! Contains Progress struct for updating the package list.
2use std::fmt::Write as _;
3use std::io::{Write, stdout};
4use std::os::fd::RawFd;
5use std::pin::Pin;
6
7use cxx::{ExternType, UniquePtr};
8
9use crate::config::Config;
10use crate::error::raw::pending_error;
11use crate::raw::{AcqTextStatus, ItemDesc, ItemState, PkgAcquire, acquire_status};
12use crate::util::{
13    NumSys, get_apt_progress_string, terminal_height, terminal_width, time_str, unit_str,
14};
15
16/// Customize the output shown during file downloads.
17pub trait DynAcquireProgress {
18    /// Called on c++ to set the pulse interval.
19    fn pulse_interval(&self) -> usize;
20
21    /// Called when an item is confirmed to be up-to-date.
22    fn hit(&mut self, item: &ItemDesc);
23
24    /// Called when an Item has started to download
25    fn fetch(&mut self, item: &ItemDesc);
26
27    /// Called when an Item fails to download
28    fn fail(&mut self, item: &ItemDesc);
29
30    /// Called periodically to provide the overall progress information
31    fn pulse(&mut self, status: &AcqTextStatus, owner: &PkgAcquire);
32
33    /// Called when an item is successfully and completely fetched.
34    fn done(&mut self, item: &ItemDesc);
35
36    /// Called when progress has started
37    fn start(&mut self);
38
39    /// Called when progress has finished
40    fn stop(&mut self, status: &AcqTextStatus);
41}
42
43/// Customize the output of operation progress on things like opening the cache.
44pub trait DynOperationProgress {
45    fn update(&mut self, operation: String, percent: f32);
46    fn done(&mut self);
47}
48
49/// Customize the output of installation progress.
50pub trait DynInstallProgress {
51    fn status_changed(
52        &mut self,
53        pkgname: String,
54        steps_done: u64,
55        total_steps: u64,
56        action: String,
57    );
58    fn error(&mut self, pkgname: String, steps_done: u64, total_steps: u64, error: String);
59}
60
61/// A struct aligning with `apt`'s AcquireStatus.
62///
63/// This struct takes a struct with impl AcquireProgress
64/// It sets itself as the callback from C++ AcqTextStatus
65/// which will then call the functions on this struct.
66/// This struct will then forward those calls to your struct via
67/// trait methods.
68pub struct AcquireProgress<'a> {
69    status: UniquePtr<AcqTextStatus>,
70    inner: Box<dyn DynAcquireProgress + 'a>,
71}
72
73impl<'a> AcquireProgress<'a> {
74    /// Create a new AcquireProgress Struct from a struct that implements
75    /// AcquireProgress trait.
76    pub fn new(inner: impl DynAcquireProgress + 'a) -> Self {
77        Self {
78            status: unsafe { acquire_status() },
79            inner: Box::new(inner),
80        }
81    }
82
83    /// Create a new AcquireProgress Struct with the default `apt`
84    /// implementation.
85    pub fn apt() -> Self {
86        Self::new(AptAcquireProgress::new())
87    }
88
89    /// Create a new AcquireProgress Struct that outputs nothing.
90    pub fn quiet() -> Self {
91        Self::new(AptAcquireProgress::disable())
92    }
93
94    /// Sets AcquireProgress as the AcqTextStatus callback and
95    /// returns a Pinned mutable reference to AcqTextStatus.
96    pub fn mut_status(&mut self) -> Pin<&mut AcqTextStatus> {
97        unsafe {
98            // Create raw mutable pointer to ourself
99            let raw_ptr = &mut *(self as *mut AcquireProgress);
100            // Pin AcqTextStatus in place so it is not moved in memory
101            // Segfault can occur if it is moved
102            let mut status = self.status.pin_mut();
103
104            // Set our raw pointer we created as the callback for C++ AcqTextStatus.
105            // AcqTextStatus will then be fed into libapt who will call its methods
106            // providing information. AcqTextStatus then uses this pointer to send that
107            // information back to rust on this struct. This struct will then send it
108            // through the trait methods on the `inner` object.
109            status.as_mut().set_callback(raw_ptr);
110            status
111        }
112    }
113
114    /// Called on c++ to set the pulse interval.
115    pub(crate) fn pulse_interval(&mut self) -> usize {
116        self.inner.pulse_interval()
117    }
118
119    /// Called when an item is confirmed to be up-to-date.
120    pub(crate) fn hit(&mut self, item: &ItemDesc) {
121        self.inner.hit(item)
122    }
123
124    /// Called when an Item has started to download
125    pub(crate) fn fetch(&mut self, item: &ItemDesc) {
126        self.inner.fetch(item)
127    }
128
129    /// Called when an Item fails to download
130    pub(crate) fn fail(&mut self, item: &ItemDesc) {
131        self.inner.fail(item)
132    }
133
134    /// Called periodically to provide the overall progress information
135    pub(crate) fn pulse(&mut self, owner: &PkgAcquire) {
136        self.inner.pulse(&self.status, owner)
137    }
138
139    /// Called when progress has started
140    pub(crate) fn start(&mut self) {
141        self.inner.start()
142    }
143
144    /// Called when an item is successfully and completely fetched.
145    pub(crate) fn done(&mut self, item: &ItemDesc) {
146        self.inner.done(item)
147    }
148
149    /// Called when progress has finished
150    pub(crate) fn stop(&mut self) {
151        self.inner.stop(&self.status)
152    }
153}
154
155impl Default for AcquireProgress<'_> {
156    fn default() -> Self {
157        Self::apt()
158    }
159}
160
161/// Impl for sending AcquireProgress across the barrier.
162unsafe impl ExternType for AcquireProgress<'_> {
163    type Id = cxx::type_id!("AcquireProgress");
164    type Kind = cxx::kind::Trivial;
165}
166
167/// Allows lengthy operations to communicate their progress.
168///
169/// The [`Default`] and only implementation of this is
170/// [`self::OperationProgress::quiet`].
171pub struct OperationProgress<'a> {
172    inner: Box<dyn DynOperationProgress + 'a>,
173}
174
175impl<'a> OperationProgress<'a> {
176    /// Create a new OpProgress Struct from a struct that implements
177    /// AcquireProgress trait.
178    pub fn new(inner: impl DynOperationProgress + 'static) -> Self {
179        Self {
180            inner: Box::new(inner),
181        }
182    }
183
184    /// Returns a OperationProgress that outputs no data
185    ///
186    /// Generally I have not found much use for displaying OpProgress
187    pub fn quiet() -> Self {
188        Self::new(NoOpProgress {})
189    }
190
191    /// Called when an operation has been updated.
192    fn update(&mut self, operation: String, percent: f32) {
193        self.inner.update(operation, percent)
194    }
195
196    /// Called when an operation has finished.
197    fn done(&mut self) {
198        self.inner.done()
199    }
200
201    pub fn pin(&mut self) -> Pin<&mut OperationProgress<'a>> {
202        Pin::new(self)
203    }
204}
205
206impl Default for OperationProgress<'_> {
207    fn default() -> Self {
208        Self::quiet()
209    }
210}
211
212/// Impl for sending AcquireProgress across the barrier.
213unsafe impl ExternType for OperationProgress<'_> {
214    type Id = cxx::type_id!("OperationProgress");
215    type Kind = cxx::kind::Trivial;
216}
217
218/// Enum for displaying Progress of Package Installation.
219///
220/// The [`Default`] implementation mirrors apt's.
221pub enum InstallProgress<'a> {
222    Fancy(InstallProgressFancy<'a>),
223    Fd(RawFd),
224}
225
226impl InstallProgress<'_> {
227    /// Create a new OpProgress Struct from a struct that implements
228    /// AcquireProgress trait.
229    pub fn new(inner: impl DynInstallProgress + 'static) -> Self {
230        Self::Fancy(InstallProgressFancy::new(inner))
231    }
232
233    /// Send dpkg status messages to an File Descriptor.
234    /// This required more work to implement but is the most flexible.
235    pub fn fd(fd: RawFd) -> Self {
236        Self::Fd(fd)
237    }
238
239    /// Returns InstallProgress that mimics apt's fancy progress
240    pub fn apt() -> Self {
241        Self::new(AptInstallProgress::new())
242    }
243}
244
245impl Default for InstallProgress<'_> {
246    fn default() -> Self {
247        Self::apt()
248    }
249}
250
251/// Struct for displaying Progress of Package Installation.
252///
253/// The [`Default`] implementation mirrors apt's.
254pub struct InstallProgressFancy<'a> {
255    inner: Box<dyn DynInstallProgress + 'a>,
256}
257
258impl<'a> InstallProgressFancy<'a> {
259    /// Create a new OpProgress Struct from a struct that implements
260    /// AcquireProgress trait.
261    pub fn new(inner: impl DynInstallProgress + 'static) -> Self {
262        Self {
263            inner: Box::new(inner),
264        }
265    }
266
267    /// Returns InstallProgress that mimics apt's fancy progress
268    pub fn apt() -> Self {
269        Self::new(AptInstallProgress::new())
270    }
271
272    fn status_changed(
273        &mut self,
274        pkgname: String,
275        steps_done: u64,
276        total_steps: u64,
277        action: String,
278    ) {
279        self.inner
280            .status_changed(pkgname, steps_done, total_steps, action)
281    }
282
283    fn error(&mut self, pkgname: String, steps_done: u64, total_steps: u64, error: String) {
284        self.inner.error(pkgname, steps_done, total_steps, error)
285    }
286
287    pub fn pin(&mut self) -> Pin<&mut InstallProgressFancy<'a>> {
288        Pin::new(self)
289    }
290}
291
292impl Default for InstallProgressFancy<'_> {
293    fn default() -> Self {
294        Self::apt()
295    }
296}
297
298/// Impl for sending InstallProgressFancy across the barrier.
299unsafe impl ExternType for InstallProgressFancy<'_> {
300    type Id = cxx::type_id!("InstallProgressFancy");
301    type Kind = cxx::kind::Trivial;
302}
303
304/// Internal struct to pass into [`crate::Cache::resolve`]. The C++ library for
305/// this wants a progress parameter for this, but it doesn't appear to be doing
306/// anything. Furthermore, [the Python-APT implementation doesn't accept a
307/// parameter for their dependency resolution functionality](https://apt-team.pages.debian.net/python-apt/library/apt_pkg.html#apt_pkg.ProblemResolver.resolve),
308/// so we should be safe to remove it here.
309struct NoOpProgress {}
310
311impl DynOperationProgress for NoOpProgress {
312    fn update(&mut self, _operation: String, _percent: f32) {}
313
314    fn done(&mut self) {}
315}
316
317/// AptAcquireProgress is the default struct for the update method on the cache.
318///
319/// This struct mimics the output of `apt update`.
320#[derive(Default, Debug)]
321pub struct AptAcquireProgress {
322    lastline: usize,
323    pulse_interval: usize,
324    disable: bool,
325    config: Config,
326}
327
328impl AptAcquireProgress {
329    /// Returns a new default progress instance.
330    pub fn new() -> Self {
331        Self::default()
332    }
333
334    /// Returns a disabled progress instance. No output will be shown.
335    pub fn disable() -> Self {
336        AptAcquireProgress {
337            disable: true,
338            ..Default::default()
339        }
340    }
341
342    /// Helper function to clear the last line.
343    fn clear_last_line(&mut self, term_width: usize) {
344        if self.disable {
345            return;
346        }
347
348        if self.lastline == 0 {
349            return;
350        }
351
352        if self.lastline > term_width {
353            self.lastline = term_width
354        }
355
356        print!("\r{}", " ".repeat(self.lastline));
357        print!("\r");
358        stdout().flush().unwrap();
359    }
360}
361
362impl DynAcquireProgress for AptAcquireProgress {
363    /// Used to send the pulse interval to the apt progress class.
364    ///
365    /// Pulse Interval is in microseconds.
366    ///
367    /// Example: 1 second = 1000000 microseconds.
368    ///
369    /// Apt default is 500000 microseconds or 0.5 seconds.
370    ///
371    /// The higher the number, the less frequent pulse updates will be.
372    ///
373    /// Pulse Interval set to 0 assumes the apt defaults.
374    fn pulse_interval(&self) -> usize {
375        self.pulse_interval
376    }
377
378    /// Called when an item is confirmed to be up-to-date.
379    ///
380    /// Prints out the short description and the expected size.
381    fn hit(&mut self, item: &ItemDesc) {
382        if self.disable {
383            return;
384        }
385
386        self.clear_last_line(terminal_width() - 1);
387
388        println!("\rHit:{} {}", item.owner().id(), item.description());
389    }
390
391    /// Called when an Item has started to download
392    ///
393    /// Prints out the short description and the expected size.
394    fn fetch(&mut self, item: &ItemDesc) {
395        if self.disable {
396            return;
397        }
398
399        self.clear_last_line(terminal_width() - 1);
400
401        let mut string = format!("\rGet:{} {}", item.owner().id(), item.description());
402
403        let file_size = item.owner().file_size();
404        if file_size != 0 {
405            string.push_str(&format!(" [{}]", unit_str(file_size, NumSys::Decimal)));
406        }
407
408        println!("{string}");
409    }
410
411    /// Called when an item is successfully and completely fetched.
412    ///
413    /// We don't print anything here to remain consistent with apt.
414    fn done(&mut self, _item: &ItemDesc) {
415        // self.clear_last_line(terminal_width() - 1);
416
417        // println!("This is done!");
418    }
419
420    /// Called when progress has started.
421    ///
422    /// Start does not pass information into the method.
423    ///
424    /// We do not print anything here to remain consistent with apt.
425    /// lastline length is set to 0 to ensure consistency when progress begins.
426    fn start(&mut self) {
427        self.lastline = 0;
428    }
429
430    /// Called when progress has finished.
431    ///
432    /// Stop does not pass information into the method.
433    ///
434    /// prints out the bytes downloaded and the overall average line speed.
435    fn stop(&mut self, owner: &AcqTextStatus) {
436        if self.disable {
437            return;
438        }
439
440        self.clear_last_line(terminal_width() - 1);
441
442        if pending_error() {
443            return;
444        }
445
446        if owner.fetched_bytes() != 0 {
447            println!(
448                "Fetched {} in {} ({}/s)",
449                unit_str(owner.fetched_bytes(), NumSys::Decimal),
450                time_str(owner.elapsed_time()),
451                unit_str(owner.current_cps(), NumSys::Decimal)
452            );
453        } else {
454            println!("Nothing to fetch.");
455        }
456    }
457
458    /// Called when an Item fails to download.
459    ///
460    /// Print out the ErrorText for the Item.
461    fn fail(&mut self, item: &ItemDesc) {
462        if self.disable {
463            return;
464        }
465
466        self.clear_last_line(terminal_width() - 1);
467
468        let mut show_error = true;
469        let error_text = item.owner().error_text();
470        let desc = format!("{} {}", item.owner().id(), item.description());
471
472        match item.owner().status() {
473            ItemState::StatIdle | ItemState::StatDone => {
474                println!("\rIgn: {desc}");
475                let key = "Acquire::Progress::Ignore::ShowErrorText";
476                if error_text.is_empty() || self.config.bool(key, false) {
477                    show_error = false;
478                }
479            }
480            _ => {
481                println!("\rErr: {desc}");
482            }
483        }
484
485        if show_error {
486            println!("\r{error_text}");
487        }
488    }
489
490    /// Called periodically to provide the overall progress information
491    ///
492    /// Draws the current progress.
493    /// Each line has an overall percent meter and a per active item status
494    /// meter along with an overall bandwidth and ETA indicator.
495    fn pulse(&mut self, status: &AcqTextStatus, owner: &PkgAcquire) {
496        if self.disable {
497            return;
498        }
499
500        // Minus 1 for the cursor
501        let term_width = terminal_width() - 1;
502
503        let mut string = String::new();
504        let mut percent_str = format!("\r{:.0}%", status.percent());
505        let mut eta_str = String::new();
506
507        // Set the ETA string if there is a rate of download
508        let current_cps = status.current_cps();
509        if current_cps != 0 {
510            let _ = write!(
511                eta_str,
512                " {} {}",
513                // Current rate of download
514                unit_str(current_cps, NumSys::Decimal),
515                // ETA String
516                time_str((status.total_bytes() - status.current_bytes()) / current_cps)
517            );
518        }
519
520        for worker in owner.workers().iter() {
521            let mut work_string = String::new();
522            work_string.push_str(" [");
523
524            let Ok(item) = worker.item() else {
525                if !worker.status().is_empty() {
526                    work_string.push_str(&worker.status());
527                    work_string.push(']');
528                }
529                continue;
530            };
531
532            let id = item.owner().id();
533            if id != 0 {
534                let _ = write!(work_string, " {id} ");
535            }
536            work_string.push_str(&item.short_desc());
537
538            let sub = item.owner().active_subprocess();
539            if !sub.is_empty() {
540                work_string.push(' ');
541                work_string.push_str(&sub);
542            }
543
544            work_string.push(' ');
545            work_string.push_str(&unit_str(worker.current_size(), NumSys::Decimal));
546
547            if worker.total_size() > 0 && !item.owner().complete() {
548                let _ = write!(
549                    work_string,
550                    "/{} {}%",
551                    unit_str(worker.total_size(), NumSys::Decimal),
552                    (worker.current_size() * 100) / worker.total_size()
553                );
554            }
555
556            work_string.push(']');
557
558            if (string.len() + work_string.len() + percent_str.len() + eta_str.len()) > term_width {
559                break;
560            }
561
562            string.push_str(&work_string);
563        }
564
565        // Display at least something if there is no worker strings
566        if string.is_empty() {
567            string = " [Working]".to_string()
568        }
569
570        // Push the worker strings on the percent string
571        percent_str.push_str(&string);
572
573        // Fill the remaining space in the terminal if eta exists
574        if !eta_str.is_empty() {
575            let fill_size = percent_str.len() + eta_str.len();
576            if fill_size < term_width {
577                percent_str.push_str(&" ".repeat(term_width - fill_size))
578            }
579        }
580
581        // Push the final eta to the end of the filled string
582        percent_str.push_str(&eta_str);
583
584        // Print and flush stdout
585        print!("{percent_str}");
586        stdout().flush().unwrap();
587
588        if self.lastline > percent_str.len() {
589            self.clear_last_line(term_width);
590        }
591
592        self.lastline = percent_str.len();
593    }
594}
595
596/// Default struct to handle the output of a transaction.
597pub struct AptInstallProgress {
598    config: Config,
599}
600
601impl AptInstallProgress {
602    pub fn new() -> Self {
603        Self {
604            config: Config::new(),
605        }
606    }
607}
608
609impl Default for AptInstallProgress {
610    fn default() -> Self {
611        Self::new()
612    }
613}
614
615impl DynInstallProgress for AptInstallProgress {
616    fn status_changed(
617        &mut self,
618        _pkgname: String,
619        steps_done: u64,
620        total_steps: u64,
621        _action: String,
622    ) {
623        // Get the terminal's width and height.
624        let term_height = terminal_height();
625        let term_width = terminal_width();
626
627        // Save the current cursor position.
628        print!("\x1b7");
629
630        // Go to the progress reporting line.
631        print!("\x1b[{term_height};0f");
632        std::io::stdout().flush().unwrap();
633
634        // Convert the float to a percentage string.
635        let percent = steps_done as f32 / total_steps as f32;
636        let mut percent_str = (percent * 100.0).round().to_string();
637
638        let percent_padding = match percent_str.len() {
639            1 => "  ",
640            2 => " ",
641            3 => "",
642            _ => unreachable!(),
643        };
644
645        percent_str = percent_padding.to_owned() + &percent_str;
646
647        // Get colors for progress reporting.
648        // NOTE: The APT implementation confusingly has 'Progress-fg' for 'bg_color',
649        // and the same the other way around.
650        let bg_color = self
651            .config
652            .find("Dpkg::Progress-Fancy::Progress-fg", "\x1b[42m");
653        let fg_color = self
654            .config
655            .find("Dpkg::Progress-Fancy::Progress-bg", "\x1b[30m");
656        const BG_COLOR_RESET: &str = "\x1b[49m";
657        const FG_COLOR_RESET: &str = "\x1b[39m";
658
659        print!("{bg_color}{fg_color}Progress: [{percent_str}%]{BG_COLOR_RESET}{FG_COLOR_RESET} ");
660
661        // The length of "Progress: [100%] ".
662        const PROGRESS_STR_LEN: usize = 17;
663
664        // Print the progress bar.
665        // We should safely be able to convert the `usize`.try_into() into the `u32`
666        // needed by `get_apt_progress_string`, as usize ints only take up 8 bytes on a
667        // 64-bit processor.
668        print!(
669            "{}",
670            get_apt_progress_string(percent, (term_width - PROGRESS_STR_LEN).try_into().unwrap())
671        );
672        std::io::stdout().flush().unwrap();
673
674        // If this is the last change, remove the progress reporting bar.
675        // if steps_done == total_steps {
676        // print!("{}", " ".repeat(term_width));
677        // print!("\x1b[0;{}r", term_height);
678        // }
679        // Finally, go back to the previous cursor position.
680        print!("\x1b8");
681        std::io::stdout().flush().unwrap();
682    }
683
684    // TODO: Need to figure out when to use this.
685    fn error(&mut self, _pkgname: String, _steps_done: u64, _total_steps: u64, _error: String) {}
686}
687
688#[allow(clippy::needless_lifetimes)]
689#[cxx::bridge]
690pub(crate) mod raw {
691    extern "Rust" {
692        type AcquireProgress<'a>;
693        type OperationProgress<'a>;
694        type InstallProgressFancy<'a>;
695
696        /// Called when an operation has been updated.
697        fn update(self: &mut OperationProgress, operation: String, percent: f32);
698
699        /// Called when an operation has finished.
700        fn done(self: &mut OperationProgress);
701
702        /// Called when the install status has changed.
703        fn status_changed(
704            self: &mut InstallProgressFancy,
705            pkgname: String,
706            steps_done: u64,
707            total_steps: u64,
708            action: String,
709        );
710
711        // TODO: What kind of errors can be returned here?
712        // Research and update higher level structs as well
713        // TODO: Create custom errors when we have better information
714        fn error(
715            self: &mut InstallProgressFancy,
716            pkgname: String,
717            steps_done: u64,
718            total_steps: u64,
719            error: String,
720        );
721
722        /// Called on c++ to set the pulse interval.
723        fn pulse_interval(self: &mut AcquireProgress) -> usize;
724
725        /// Called when an item is confirmed to be up-to-date.
726        fn hit(self: &mut AcquireProgress, item: &ItemDesc);
727
728        /// Called when an Item has started to download
729        fn fetch(self: &mut AcquireProgress, item: &ItemDesc);
730
731        /// Called when an Item fails to download
732        fn fail(self: &mut AcquireProgress, item: &ItemDesc);
733
734        /// Called periodically to provide the overall progress information
735        fn pulse(self: &mut AcquireProgress, owner: &PkgAcquire);
736
737        /// Called when an item is successfully and completely fetched.
738        fn done(self: &mut AcquireProgress, item: &ItemDesc);
739
740        /// Called when progress has started
741        fn start(self: &mut AcquireProgress);
742
743        /// Called when progress has finished
744        fn stop(self: &mut AcquireProgress);
745    }
746
747    extern "C++" {
748        type ItemDesc = crate::acquire::raw::ItemDesc;
749        type PkgAcquire = crate::acquire::raw::PkgAcquire;
750        include!("oma-apt/apt-pkg-c/types.h");
751    }
752}