diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cac42e268f..0e85cf8a37 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ "agama-lib", "async-std", "clap", + "console", "convert_case", "indicatif", "serde", diff --git a/rust/agama-cli/Cargo.toml b/rust/agama-cli/Cargo.toml index 7c5430bc14..29164e354a 100644 --- a/rust/agama-cli/Cargo.toml +++ b/rust/agama-cli/Cargo.toml @@ -15,6 +15,7 @@ indicatif= "0.17.3" async-std = { version ="1.12.0", features = ["attributes"] } thiserror = "1.0.39" convert_case = "0.6.0" +console = "0.15.7" [[bin]] name = "agama" diff --git a/rust/agama-cli/src/main.rs b/rust/agama-cli/src/main.rs index c96c97c3f1..e066e04015 100644 --- a/rust/agama-cli/src/main.rs +++ b/rust/agama-cli/src/main.rs @@ -10,7 +10,7 @@ mod progress; use crate::error::CliError; use agama_lib::error::ServiceError; use agama_lib::manager::ManagerClient; -use agama_lib::progress::build_progress_monitor; +use agama_lib::progress::ProgressMonitor; use async_std::task::{self, block_on}; use commands::Commands; use config::run as run_config_cmd; @@ -53,9 +53,11 @@ async fn install(manager: &ManagerClient<'_>, max_attempts: u8) -> Result<(), Bo if !manager.can_install().await? { return Err(Box::new(CliError::ValidationError)); } + // Display the progress (if needed) and makes sure that the manager is ready manager.wait().await?; + let progress = task::spawn(async { show_progress().await }); // Try to start the installation up to max_attempts times. let mut attempts = 1; loop { @@ -75,7 +77,7 @@ async fn install(manager: &ManagerClient<'_>, max_attempts: u8) -> Result<(), Bo attempts += 1; sleep(Duration::from_secs(1)); } - println!("The installation process has started."); + let _ = progress.await; Ok(()) } @@ -83,7 +85,7 @@ async fn show_progress() -> Result<(), ServiceError> { // wait 1 second to give other task chance to start, so progress can display something task::sleep(Duration::from_secs(1)).await; let conn = agama_lib::connection().await?; - let mut monitor = build_progress_monitor(conn).await.unwrap(); + let mut monitor = ProgressMonitor::new(conn).await.unwrap(); let presenter = InstallerProgress::new(); monitor .run(presenter) @@ -96,7 +98,7 @@ async fn wait_for_services(manager: &ManagerClient<'_>) -> Result<(), Box, + bar: Option, } impl InstallerProgress { pub fn new() -> Self { - let progress = MultiProgress::new(); - Self { - progress, - bars: vec![], - } + Self { bar: None } + } + + fn update_bar(&mut self, progress: &Progress) { + let bar = self.bar.get_or_insert_with(|| { + let style = ProgressStyle::with_template("{spinner:.green} {msg}").unwrap(); + let bar = ProgressBar::new(0).with_style(style); + bar.enable_steady_tick(Duration::from_millis(120)); + bar + }); + bar.set_length(progress.max_steps.into()); + bar.set_position(progress.current_step.into()); + bar.set_message(progress.current_title.to_owned()); } } impl ProgressPresenter for InstallerProgress { - fn start(&mut self, progress: &[Progress]) { - let style = - ProgressStyle::with_template("{bar:40.green/white} {pos:>3}/{len:3} {msg}").unwrap(); - for info in progress.iter() { - let bar = self.progress.add(ProgressBar::new(info.max_steps.into())); - bar.set_style(style.clone()); - self.bars.push(bar); + fn start(&mut self, progress: &Progress) { + if !progress.finished { + self.update_main(&progress); } } - fn update(&mut self, progress: &[Progress]) { - for (i, info) in progress.iter().enumerate() { - let bar = &self.bars.get(i).unwrap(); - if info.finished { - bar.finish_with_message("Done"); - } else { - bar.set_length(info.max_steps.into()); - bar.set_position(info.current_step.into()); - bar.set_message(info.current_title.to_owned()); + fn update_main(&mut self, progress: &Progress) { + let counter = format!("[{}/{}]", &progress.current_step, &progress.max_steps); + + println!( + "{} {}", + style(&counter).bold().green(), + &progress.current_title + ); + } + + fn update_detail(&mut self, progress: &Progress) { + if progress.finished { + if let Some(bar) = self.bar.take() { + bar.finish_and_clear(); } + } else { + self.update_bar(&progress); + } + } + + fn finish(&mut self) { + if let Some(bar) = self.bar.take() { + bar.finish_and_clear(); } } } diff --git a/rust/agama-lib/src/progress.rs b/rust/agama-lib/src/progress.rs index 694359eb76..5fd4eed362 100644 --- a/rust/agama-lib/src/progress.rs +++ b/rust/agama-lib/src/progress.rs @@ -1,17 +1,61 @@ +//! This module offers a mechanism to report the installation progress in Agama's command-line +//! interface. +//! +//! The library does not prescribe any way to present that information to the user. As shown in the +//! example below, you can build your own presenter and implement the [ProgressPresenter] trait. +//! +//! ```no_run +//! # use agama_lib::progress::{Progress, ProgressMonitor, ProgressPresenter}; +//! # use async_std::task::block_on; +//! # use zbus; +//! +//! // Custom presenter +//! struct SimplePresenter {}; +//! +//! impl SimplePresenter { +//! fn report_progress(&self, progress: &Progress) { +//! println!("{}/{} {}", &progress.current_step, &progress.max_steps, &progress.title); +//! } +//! } +//! +//! impl ProgressPresenter for SimplePresenter { +//! fn start(&mut self, progress: &Progress) { +//! println!("Starting..."); +//! self.report_progress(progress); +//! } +//! +//! fn update(&mut self, progress: &Progress) { +//! self.report_progress(progress); +//! } +//! +//! fn finish(&mut self) { +//! println!("Done"); +//! } +//! } +//! +//! let connection = block_on(zbus::Connection::system()).unwrap(); +//! let mut monitor = block_on(ProgressMonitor::new(connection)).unwrap(); +//! monitor.run(SimplePresenter {}); +//! ``` + +use crate::error::ServiceError; use crate::proxies::ProgressProxy; -use futures::stream::StreamExt; -use futures::stream::{select_all, SelectAll}; -use futures_util::future::try_join3; +use futures::stream::{SelectAll, StreamExt}; +use futures_util::{future::try_join3, Stream}; use std::error::Error; -use zbus::{Connection, PropertyStream}; +use zbus::Connection; +/// Represents the progress for an Agama service. #[derive(Default, Debug)] pub struct Progress { + /// Current step pub current_step: u32, + /// Number of steps pub max_steps: u32, + /// Title of the current step pub current_title: String, + /// Whether the progress reporting is finished pub finished: bool, - pub object_path: String, } impl Progress { @@ -24,117 +68,114 @@ impl Progress { current_title, max_steps, finished, - object_path: proxy.path().to_string(), }) } } -pub async fn build_progress_monitor( - connection: Connection, -) -> Result, Box> { - let builder = ProgressMonitorBuilder::new(connection) - .add_proxy("org.opensuse.Agama1", "/org/opensuse/Agama1/Manager") - .add_proxy( - "org.opensuse.Agama.Software1", - "/org/opensuse/Agama/Software1", - ) - .add_proxy( - "org.opensuse.Agama.Storage1", - "/org/opensuse/Agama/Storage1", - ); - builder.build().await -} - -pub struct ProgressMonitorBuilder { - proxies: Vec<(String, String)>, - connection: Connection, -} - -impl<'a> ProgressMonitorBuilder { - pub fn new(connection: Connection) -> Self { - Self { - proxies: vec![], - connection, - } - } - - pub fn add_proxy(mut self, destination: &str, path: &str) -> Self { - self.proxies.push((destination.to_owned(), path.to_owned())); - self - } - - pub async fn build(self) -> Result, Box> { - let mut monitor = ProgressMonitor::default(); - - for (destination, path) in self.proxies { - let proxy = ProgressProxy::builder(&self.connection) - .path(path)? - .destination(destination)? - .build() - .await?; - monitor.add_proxy(proxy); - } - Ok(monitor) - } -} - -pub trait ProgressPresenter { - fn start(&mut self, progress: &[Progress]); - fn update(&mut self, progress: &[Progress]); -} - -#[derive(Default)] +/// Monitorizes and reports the progress of Agama's current operation. +/// +/// It implements a main/details reporter by listening to the manager and software services, +/// similar to Agama's web UI. How this information is displayed depends on the presenter (see +/// [ProgressMonitor.run]). pub struct ProgressMonitor<'a> { - pub proxies: Vec>, + manager_proxy: ProgressProxy<'a>, + software_proxy: ProgressProxy<'a>, } impl<'a> ProgressMonitor<'a> { - pub fn add_proxy(&mut self, proxy: ProgressProxy<'a>) { - self.proxies.push(proxy); + pub async fn new(connection: Connection) -> Result, ServiceError> { + let manager_proxy = ProgressProxy::builder(&connection) + .path("/org/opensuse/Agama1/Manager")? + .destination("org.opensuse.Agama1")? + .build() + .await?; + + let software_proxy = ProgressProxy::builder(&connection) + .path("/org/opensuse/Agama/Software1")? + .destination("org.opensuse.Agama.Software1")? + .build() + .await?; + + Ok(Self { + manager_proxy, + software_proxy, + }) } + /// Runs the monitor until the current operation finishes. pub async fn run( &mut self, mut presenter: impl ProgressPresenter, ) -> Result<(), Box> { + presenter.start(&self.main_progress().await); let mut changes = self.build_stream().await; - presenter.start(&self.collect_progress().await?); - while let Some(_change) = changes.next().await { - presenter.update(&self.collect_progress().await?); - if self.is_finished().await { - return Ok(()); - } + while let Some(stream) = changes.next().await { + match stream { + "/org/opensuse/Agama1/Manager" => { + let progress = self.main_progress().await; + if progress.finished { + presenter.finish(); + return Ok(()); + } else { + presenter.update_main(&progress); + } + } + "/org/opensuse/Agama/Software1" => { + presenter.update_detail(&self.detail_progress().await) + } + _ => eprintln!("Unknown"), + }; } Ok(()) } - async fn is_finished(&self) -> bool { - for proxy in &self.proxies { - if !proxy.finished().await.unwrap_or(false) { - return false; - } - } - true + /// Proxy that reports the progress. + async fn main_progress(&self) -> Progress { + Progress::from_proxy(&self.manager_proxy).await.unwrap() } - async fn collect_progress(&self) -> Result, Box> { - let mut progress = vec![]; - for proxy in &self.proxies { - let proxy_progress = Progress::from_proxy(proxy).await?; - progress.push(proxy_progress); - } - Ok(progress) + /// Proxy that reports the progress detail. + async fn detail_progress(&self) -> Progress { + Progress::from_proxy(&self.software_proxy).await.unwrap() } - async fn build_stream(&self) -> SelectAll> { - let mut streams = vec![]; - for proxy in &self.proxies { - let s = proxy.receive_current_step_changed().await; - streams.push(s); + /// Builds an stream of progress changes. + /// + /// It listens for changes in the `Current` property and generates a stream identifying the + /// proxy where the change comes from. + async fn build_stream(&self) -> SelectAll + '_> { + let mut streams = SelectAll::new(); + + let proxies = [&self.manager_proxy, &self.software_proxy]; + for proxy in proxies.iter() { + let stream = proxy.receive_current_step_changed().await; + let path = proxy.path().as_str(); + let tagged = stream.map(move |_| path); + streams.push(tagged); } - - select_all(streams) + streams } } + +/// Presents the progress to the user. +pub trait ProgressPresenter { + /// Starts the progress reporting. + /// + /// * `progress`: current main progress. + fn start(&mut self, progress: &Progress); + + /// Updates the progress. + /// + /// * `progress`: current progress. + fn update_main(&mut self, progress: &Progress); + + /// Updates the progress detail. + /// + /// * `progress`: current progress detail. + fn update_detail(&mut self, progress: &Progress); + + /// Finishes the progress reporting. + fn finish(&mut self); +} diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index fbf08d69a0..0d412218c2 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Jul 7 14:12:03 UTC 2023 - Imobach Gonzalez Sosa + +- Improve the progress reporting (gh#openSUSE/agama#653). + ------------------------------------------------------------------- Thu Jul 6 09:13:47 UTC 2023 - Imobach Gonzalez Sosa