Skip to main content

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