rust_apt/
progress.rs

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