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