diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0798a9c22c..00799263c2 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -149,6 +149,7 @@ dependencies = [ "agama-storage", "agama-utils", "async-trait", + "gettext-rs", "merge-struct", "serde", "serde_json", @@ -1893,9 +1894,9 @@ dependencies = [ [[package]] name = "gettext-rs" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44e92f7dc08430aca7ed55de161253a22276dfd69c5526e5c5e95d1f7cf338a" +checksum = "5d5857dc1b7f0fee86961de833f434e29494d72af102ce5355738c0664222bdf" dependencies = [ "gettext-sys", "locale_config", diff --git a/rust/agama-files/src/runner.rs b/rust/agama-files/src/runner.rs index 1657af4a25..d7ff1bbb02 100644 --- a/rust/agama-files/src/runner.rs +++ b/rust/agama-files/src/runner.rs @@ -188,12 +188,11 @@ impl ScriptsRunner { /// Ancillary function to start the progress. fn start_progress(&self, scripts: &[&Script]) { - let messages: Vec<_> = scripts + let steps: Vec<_> = scripts .iter() .map(|s| format!("Running user script '{}'", s.name())) .collect(); - let steps: Vec<_> = messages.iter().map(|s| s.as_ref()).collect(); - let progress_action = progress::message::StartWithSteps::new(Scope::Files, &steps); + let progress_action = progress::message::StartWithSteps::new(Scope::Files, steps); _ = self.progress.cast(progress_action); } diff --git a/rust/agama-manager/Cargo.toml b/rust/agama-manager/Cargo.toml index 8c4d82daae..cdb190f426 100644 --- a/rust/agama-manager/Cargo.toml +++ b/rust/agama-manager/Cargo.toml @@ -20,6 +20,7 @@ serde_json = "1.0.140" tracing = "0.1.41" serde = { version = "1.0.228", features = ["derive"] } serde_with = "3.16.1" +gettext-rs = { version = "0.7.7", features = ["gettext-system"] } [dev-dependencies] test-context = "0.4.1" diff --git a/rust/agama-manager/src/message.rs b/rust/agama-manager/src/message.rs index 6c839e5663..7d84979008 100644 --- a/rust/agama-manager/src/message.rs +++ b/rust/agama-manager/src/message.rs @@ -27,13 +27,6 @@ use agama_utils::{ }; use serde_json::Value; -/// Gets the installation status. -pub struct GetStatus; - -impl Message for GetStatus { - type Reply = Status; -} - /// Gets the information of the underlying system. #[derive(Debug)] pub struct GetSystem; diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 522b12e7f0..1277968faa 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -25,7 +25,7 @@ use agama_utils::{ self, event, files::scripts::ScriptsGroup, manager::{self, LicenseContent}, - status::State, + status::Stage, Action, Config, Event, Issue, IssueMap, Proposal, Scope, Status, SystemInfo, }, issue, licenses, @@ -33,6 +33,7 @@ use agama_utils::{ progress, question, }; use async_trait::async_trait; +use gettextrs::gettext; use merge_struct::merge; use network::NetworkSystemClient; use serde_json::Value; @@ -75,6 +76,8 @@ pub enum Error { NetworkSystem(#[from] network::NetworkSystemError), #[error(transparent)] Hardware(#[from] hardware::Error), + #[error("Cannot dispatch this action in {current} stage (expected {expected}).")] + UnexpectedStage { current: Stage, expected: Stage }, } pub struct Starter { @@ -221,7 +224,6 @@ impl Starter { }; let mut service = Service { - events: self.events, questions: self.questions, progress, issues, @@ -233,8 +235,6 @@ impl Starter { products: products::Registry::default(), licenses: licenses::Registry::default(), hardware, - // FIXME: state is already used for service state. - state: State::Configuring, config: Config::default(), system: manager::SystemInfo::default(), product: None, @@ -258,10 +258,8 @@ pub struct Service { licenses: licenses::Registry, hardware: hardware::Registry, product: Option>>, - state: State, config: Config, system: manager::SystemInfo, - events: event::Sender, } impl Service { @@ -424,25 +422,6 @@ impl Service { Ok(()) } - async fn install(&mut self) -> Result<(), Error> { - self.state = State::Installing; - self.events.send(Event::StateChanged)?; - // TODO: translate progress steps. - self.progress - .call(progress::message::StartWithSteps::new( - Scope::Manager, - &["Installing l10n"], - )) - .await?; - self.l10n.call(l10n::message::Install).await?; - self.progress - .call(progress::message::Finish::new(Scope::Manager)) - .await?; - self.state = State::Finished; - self.events.send(Event::StateChanged)?; - Ok(()) - } - fn set_product(&mut self, config: &Config) -> Result<(), Error> { self.product = None; self.update_product(config) @@ -480,6 +459,14 @@ impl Service { } Ok(()) } + + async fn check_stage(&self, expected: Stage) -> Result<(), Error> { + let current = self.progress.call(progress::message::GetStage).await?; + if current != expected { + return Err(Error::UnexpectedStage { expected, current }); + } + Ok(()) + } } impl Actor for Service { @@ -487,14 +474,11 @@ impl Actor for Service { } #[async_trait] -impl MessageHandler for Service { +impl MessageHandler for Service { /// It returns the status of the installation. - async fn handle(&mut self, _message: message::GetStatus) -> Result { - let progresses = self.progress.call(progress::message::Get).await?; - Ok(Status { - state: self.state.clone(), - progresses, - }) + async fn handle(&mut self, message: progress::message::GetStatus) -> Result { + let status = self.progress.call(message).await?; + Ok(status) } } @@ -554,6 +538,7 @@ impl MessageHandler for Service { impl MessageHandler for Service { /// Sets the user configuration with the given values. async fn handle(&mut self, message: message::SetConfig) -> Result<(), Error> { + self.check_stage(Stage::Configuring).await?; self.set_config(message.config).await } } @@ -577,6 +562,7 @@ impl MessageHandler for Service { /// It merges the current config with the given one. If some scope is missing in the given /// config, then it keeps the values from the current config. async fn handle(&mut self, message: message::UpdateConfig) -> Result<(), Error> { + self.check_stage(Stage::Configuring).await?; let config = merge(&self.config, &message.config).map_err(|_| Error::MergeConfig)?; let config = merge_network(config, message.config); self.update_config(config).await @@ -623,6 +609,8 @@ impl MessageHandler for Service { impl MessageHandler for Service { /// It runs the given action. async fn handle(&mut self, message: message::RunAction) -> Result<(), Error> { + self.check_stage(Stage::Configuring).await?; + match message.action { Action::ConfigureL10n(config) => { self.configure_l10n(config).await?; @@ -634,7 +622,15 @@ impl MessageHandler for Service { self.probe_storage().await?; } Action::Install => { - self.install().await?; + let action = InstallAction { + l10n: self.l10n.clone(), + network: self.network.clone(), + software: self.software.clone(), + storage: self.storage.clone(), + files: self.files.clone(), + progress: self.progress.clone(), + }; + action.run(); } } Ok(()) @@ -653,6 +649,7 @@ impl MessageHandler for Service { impl MessageHandler for Service { /// It sets the storage model. async fn handle(&mut self, message: message::SetStorageModel) -> Result<(), Error> { + self.check_stage(Stage::Configuring).await?; Ok(self .storage .call(storage::message::SetConfigModel::new(message.model)) @@ -667,6 +664,7 @@ impl MessageHandler for Service { &mut self, message: message::SolveStorageModel, ) -> Result, Error> { + self.check_stage(Stage::Configuring).await?; Ok(self .storage .call(storage::message::SolveConfigModel::new(message.model)) @@ -679,7 +677,100 @@ impl MessageHandler for Service { impl MessageHandler for Service { /// It sets the software resolvables. async fn handle(&mut self, message: software::message::SetResolvables) -> Result<(), Error> { + self.check_stage(Stage::Configuring).await?; self.software.call(message).await?; Ok(()) } } + +/// Implements the installation process. +/// +/// This action runs on a separate Tokio task to prevent the manager from blocking. +struct InstallAction { + l10n: Handler, + network: NetworkSystemClient, + software: Handler, + storage: Handler, + files: Handler, + progress: Handler, +} + +impl InstallAction { + /// Runs the installation process on a separate Tokio task. + pub fn run(mut self) { + tokio::spawn(async move { + if let Err(error) = self.install().await { + tracing::error!("Installation failed: {error}"); + if let Err(error) = self + .progress + .call(progress::message::SetStage::new(Stage::Failed)) + .await + { + tracing::error!( + "It was not possible to set the stage to {}: {error}", + Stage::Failed + ); + } + } + }); + } + + async fn install(&mut self) -> Result<(), Error> { + // NOTE: consider a NextState message? + self.progress + .call(progress::message::SetStage::new(Stage::Installing)) + .await?; + + // + // Preparation + // + self.progress + .call(progress::message::StartWithSteps::new( + Scope::Manager, + vec![ + gettext("Prepare the system"), + gettext("Install software"), + gettext("Configure the system"), + ], + )) + .await?; + + self.storage.call(storage::message::Install).await?; + self.files + .call(files::message::RunScripts::new( + ScriptsGroup::PostPartitioning, + )) + .await?; + + // + // Installation + // + self.progress + .call(progress::message::Next::new(Scope::Manager)) + .await?; + self.software.call(software::message::Install).await?; + + // + // Configuration + // + self.progress + .call(progress::message::Next::new(Scope::Manager)) + .await?; + self.l10n.call(l10n::message::Install).await?; + self.software.call(software::message::Finish).await?; + self.files.call(files::message::WriteFiles).await?; + self.storage.call(storage::message::Finish).await?; + + // + // Finish progress and changes + // + self.progress + .call(progress::message::Finish::new(Scope::Manager)) + .await?; + + self.progress + .call(progress::message::SetStage::new(Stage::Finished)) + .await?; + Ok(()) + } +} diff --git a/rust/agama-server/src/server/web.rs b/rust/agama-server/src/server/web.rs index c9201b70cd..a9ff8c6d48 100644 --- a/rust/agama-server/src/server/web.rs +++ b/rust/agama-server/src/server/web.rs @@ -33,7 +33,7 @@ use agama_utils::{ question::{Question, QuestionSpec, UpdateQuestion}, Action, Config, IssueWithScope, Patch, Status, SystemInfo, }, - question, + progress, question, }; use axum::{ extract::{Path, Query, State}, @@ -136,7 +136,7 @@ pub fn server_with_state(state: ServerState) -> Result { ) )] async fn get_status(State(state): State) -> ServerResult> { - let status = state.manager.call(message::GetStatus).await?; + let status = state.manager.call(progress::message::GetStatus).await?; Ok(Json(status)) } diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index 4b4342ea05..ab11952ca3 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -174,7 +174,7 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() - .schema_from::() + .schema_from::() .schema_from::() .build() } diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index 4833efc941..de5478f8e2 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -28,6 +28,7 @@ use agama_utils::{ products::ProductSpec, progress, question, }; +use gettextrs::gettext; use std::{collections::HashMap, path::Path}; use tokio::sync::{ mpsc::{self, UnboundedSender}, @@ -246,10 +247,10 @@ impl ZyppServer { _ = progress.cast(progress::message::StartWithSteps::new( Scope::Software, - &[ - "Updating the list of repositories", - "Refreshing metadata from the repositories", - "Calculating the software proposal", + vec![ + gettext("Updating the list of repositories"), + gettext("Refreshing metadata from the repositories"), + gettext("Calculating the software proposal"), ], )); let old_state = self.read(zypp)?; diff --git a/rust/agama-storage/src/monitor.rs b/rust/agama-storage/src/monitor.rs index 5a5b623e14..1b293bbece 100644 --- a/rust/agama-storage/src/monitor.rs +++ b/rust/agama-storage/src/monitor.rs @@ -183,7 +183,7 @@ impl Monitor { return Err(Error::ProgressChangedData); }; self.progress - .cast(progress::message::Set::new(progress_data.into()))?; + .cast(progress::message::SetProgress::new(progress_data.into()))?; Ok(()) } diff --git a/rust/agama-utils/src/api/event.rs b/rust/agama-utils/src/api/event.rs index 3506038445..576bdfc206 100644 --- a/rust/agama-utils/src/api/event.rs +++ b/rust/agama-utils/src/api/event.rs @@ -26,8 +26,8 @@ use tokio::sync::broadcast; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Event { - // The state of the installation changed. - StateChanged, + // The stage of the installation changed. + StageChanged, /// Progress changed. ProgressChanged { progress: Progress, diff --git a/rust/agama-utils/src/api/status.rs b/rust/agama-utils/src/api/status.rs index 156560c942..185295d641 100644 --- a/rust/agama-utils/src/api/status.rs +++ b/rust/agama-utils/src/api/status.rs @@ -22,23 +22,26 @@ use crate::api::progress::Progress; use serde::Serialize; // Information about the status of the installation. -#[derive(Serialize, utoipa::ToSchema)] +#[derive(Clone, Default, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Status { - /// State of the installation - pub state: State, + /// Stage of the installation + pub stage: Stage, /// Active progresses pub progresses: Vec, } /// Represents the current state of the installation process. -#[derive(Clone, Serialize, utoipa::ToSchema)] +#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq, strum::Display, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] -pub enum State { +pub enum Stage { + #[default] /// Configuring the installation Configuring, /// Installing the system Installing, /// Installation finished Finished, + /// Installation failed + Failed, } diff --git a/rust/agama-utils/src/progress.rs b/rust/agama-utils/src/progress.rs index f958b10286..4dd7e3dba6 100644 --- a/rust/agama-utils/src/progress.rs +++ b/rust/agama-utils/src/progress.rs @@ -31,30 +31,38 @@ mod tests { event::{self, Event}, progress::{self, Progress}, scope::Scope, + status::Stage, }, progress::{ message, service::{self, Service}, }, }; + use test_context::{test_context, AsyncTestContext}; use tokio::sync::broadcast; - fn start_testing_service() -> (event::Receiver, Handler) { - let (events, receiver) = broadcast::channel::(16); - let handler = Service::starter(events).start(); - (receiver, handler) + struct Context { + events_rx: event::Receiver, + handler: Handler, } - #[tokio::test] - async fn test_progress() -> Result<(), Box> { - let (mut receiver, handler) = start_testing_service(); + impl AsyncTestContext for Context { + async fn setup() -> Self { + let (events_tx, events_rx) = broadcast::channel::(16); + let handler = Service::starter(events_tx).start(); + Self { events_rx, handler } + } + } + #[test_context(Context)] + #[tokio::test] + async fn test_progress(ctx: &mut Context) -> Result<(), Box> { // Start a progress (first step) - handler + ctx.handler .call(message::Start::new(Scope::L10n, 3, "first step")) .await?; - let event = receiver.recv().await.unwrap(); + let event = ctx.events_rx.recv().await.unwrap(); let Event::ProgressChanged { progress: event_progress, } = event @@ -68,21 +76,21 @@ mod tests { assert_eq!(event_progress.step, "first step"); assert_eq!(event_progress.index, 1); - let progresses = handler.call(message::Get).await?; + let progresses = ctx.handler.call(message::GetProgress).await?; assert_eq!(progresses.len(), 1); let progress = progresses.first().unwrap(); assert_eq!(*progress, event_progress); // Second step - handler + ctx.handler .call(message::NextWithStep::new(Scope::L10n, "second step")) .await?; - let event = receiver.recv().await.unwrap(); + let event = ctx.events_rx.recv().await.unwrap(); assert!(matches!(event, Event::ProgressChanged { progress: _ })); - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); let progress = progresses.first().unwrap(); assert_eq!(progress.scope, Scope::L10n); assert_eq!(progress.size, 3); @@ -91,12 +99,12 @@ mod tests { assert_eq!(progress.index, 2); // Last step (without step text) - handler.call(message::Next::new(Scope::L10n)).await?; + ctx.handler.call(message::Next::new(Scope::L10n)).await?; - let event = receiver.recv().await.unwrap(); + let event = ctx.events_rx.recv().await.unwrap(); assert!(matches!(event, Event::ProgressChanged { progress: _ })); - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); let progress = progresses.first().unwrap(); assert_eq!(progress.scope, Scope::L10n); assert_eq!(progress.size, 3); @@ -105,29 +113,30 @@ mod tests { assert_eq!(progress.index, 3); // Finish the progress - handler.call(message::Finish::new(Scope::L10n)).await?; + ctx.handler.call(message::Finish::new(Scope::L10n)).await?; - let event = receiver.recv().await.unwrap(); + let event = ctx.events_rx.recv().await.unwrap(); assert!(matches!( event, Event::ProgressFinished { scope: Scope::L10n } )); - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); assert!(progresses.is_empty()); Ok(()) } + #[test_context(Context)] #[tokio::test] - async fn test_set_progress() -> Result<(), Box> { - let (mut receiver, handler) = start_testing_service(); - + async fn test_set_progress(ctx: &mut Context) -> Result<(), Box> { // Set first progress. let progress = Progress::new(Scope::Storage, 3, "first step".to_string()); - handler.call(message::Set::new(progress)).await?; + ctx.handler + .call(message::SetProgress::new(progress)) + .await?; - let event = receiver.recv().await.unwrap(); + let event = ctx.events_rx.recv().await.unwrap(); let Event::ProgressChanged { progress: event_progress, } = event @@ -141,7 +150,7 @@ mod tests { assert_eq!(event_progress.step, "first step"); assert_eq!(event_progress.index, 1); - let progresses = handler.call(message::Get).await?; + let progresses = ctx.handler.call(message::GetProgress).await?; assert_eq!(progresses.len(), 1); let progress = progresses.first().unwrap(); @@ -149,9 +158,11 @@ mod tests { // Set second progress let progress = Progress::new(Scope::Storage, 3, "second step".to_string()); - handler.call(message::Set::new(progress)).await?; + ctx.handler + .call(message::SetProgress::new(progress)) + .await?; - let event = receiver.recv().await.unwrap(); + let event = ctx.events_rx.recv().await.unwrap(); let Event::ProgressChanged { progress: event_progress, } = event @@ -165,7 +176,7 @@ mod tests { assert_eq!(event_progress.step, "second step"); assert_eq!(event_progress.index, 1); - let progresses = handler.call(message::Get).await?; + let progresses = ctx.handler.call(message::GetProgress).await?; assert_eq!(progresses.len(), 1); let progress = progresses.first().unwrap(); @@ -174,19 +185,22 @@ mod tests { Ok(()) } + #[test_context(Context)] #[tokio::test] - async fn test_progress_with_steps() -> Result<(), Box> { - let (_receiver, handler) = start_testing_service(); - + async fn test_progress_with_steps(ctx: &mut Context) -> Result<(), Box> { // Start a progress (first step) - handler + ctx.handler .call(message::StartWithSteps::new( Scope::L10n, - &["first step", "second step", "third step"], + vec![ + "first step".to_string(), + "second step".to_string(), + "third step".to_string(), + ], )) .await?; - let progresses = handler.call(message::Get).await?; + let progresses = ctx.handler.call(message::GetProgress).await?; let progress = progresses.first().unwrap(); assert_eq!(progress.scope, Scope::L10n); assert_eq!(progress.size, 3); @@ -198,42 +212,41 @@ mod tests { assert_eq!(progress.index, 1); // Second step - handler.call(message::Next::new(Scope::L10n)).await?; + ctx.handler.call(message::Next::new(Scope::L10n)).await?; - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); let progress = progresses.first().unwrap(); assert_eq!(progress.step, "second step"); assert_eq!(progress.index, 2); // Third step - handler.call(message::Next::new(Scope::L10n)).await?; + ctx.handler.call(message::Next::new(Scope::L10n)).await?; - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); let progress = progresses.first().unwrap(); assert_eq!(progress.step, "third step"); assert_eq!(progress.index, 3); // Finish the progress - handler.call(message::Finish::new(Scope::L10n)).await?; + ctx.handler.call(message::Finish::new(Scope::L10n)).await?; - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); assert!(progresses.is_empty()); Ok(()) } + #[test_context(Context)] #[tokio::test] - async fn test_several_progresses() -> Result<(), Box> { - let (_receiver, handler) = start_testing_service(); - - handler + async fn test_several_progresses(ctx: &mut Context) -> Result<(), Box> { + ctx.handler .call(message::Start::new(Scope::Manager, 2, "")) .await?; - handler + ctx.handler .call(message::Start::new(Scope::L10n, 2, "")) .await?; - let progresses = handler.call(message::Get).await.unwrap(); + let progresses = ctx.handler.call(message::GetProgress).await.unwrap(); assert_eq!(progresses.len(), 2); assert_eq!(progresses[0].scope, Scope::Manager); assert_eq!(progresses[1].scope, Scope::L10n); @@ -241,14 +254,15 @@ mod tests { Ok(()) } + #[test_context(Context)] #[tokio::test] - async fn test_progress_missing_step() -> Result<(), Box> { - let (_receiver, handler) = start_testing_service(); - - handler + async fn test_progress_missing_step( + ctx: &mut Context, + ) -> Result<(), Box> { + ctx.handler .call(message::Start::new(Scope::L10n, 1, "")) .await?; - let error = handler.call(message::Next::new(Scope::L10n)).await; + let error = ctx.handler.call(message::Next::new(Scope::L10n)).await; assert!(matches!( error, Err(service::Error::Progress(progress::Error::MissingStep( @@ -259,14 +273,13 @@ mod tests { Ok(()) } + #[test_context(Context)] #[tokio::test] - async fn test_missing_progress() -> Result<(), Box> { - let (_receiver, handler) = start_testing_service(); - - handler + async fn test_missing_progress(ctx: &mut Context) -> Result<(), Box> { + ctx.handler .call(message::Start::new(Scope::Manager, 2, "")) .await?; - let error = handler.call(message::Next::new(Scope::L10n)).await; + let error = ctx.handler.call(message::Next::new(Scope::L10n)).await; assert!(matches!( error, Err(service::Error::MissingProgress(Scope::L10n)) @@ -275,22 +288,28 @@ mod tests { Ok(()) } + #[test_context(Context)] #[tokio::test] - async fn test_duplicated_progress() -> Result<(), Box> { - let (_receiver, handler) = start_testing_service(); - - handler + async fn test_duplicated_progress(ctx: &mut Context) -> Result<(), Box> { + ctx.handler .call(message::Start::new(Scope::L10n, 2, "")) .await?; - let error = handler.call(message::Start::new(Scope::L10n, 1, "")).await; + let error = ctx + .handler + .call(message::Start::new(Scope::L10n, 1, "")) + .await; assert!(matches!( error, Err(service::Error::DuplicatedProgress(Scope::L10n)) )); - let error = handler - .call(message::StartWithSteps::new(Scope::L10n, &["step"])) + let error = ctx + .handler + .call(message::StartWithSteps::new( + Scope::L10n, + vec!["step".to_string()], + )) .await; assert!(matches!( error, @@ -299,4 +318,19 @@ mod tests { Ok(()) } + + #[test_context(Context)] + #[tokio::test] + async fn test_set_and_get_stage(ctx: &mut Context) -> Result<(), Box> { + let stage = ctx.handler.call(message::GetStage).await?; + assert_eq!(stage, Stage::Configuring); + + ctx.handler + .call(message::SetStage::new(Stage::Installing)) + .await?; + + let stage = ctx.handler.call(message::GetStage).await?; + assert_eq!(stage, Stage::Installing); + Ok(()) + } } diff --git a/rust/agama-utils/src/progress/message.rs b/rust/agama-utils/src/progress/message.rs index 2ca5b8e5db..3ac6ffdf8f 100644 --- a/rust/agama-utils/src/progress/message.rs +++ b/rust/agama-utils/src/progress/message.rs @@ -21,24 +21,32 @@ use crate::actor::Message; use crate::api::progress::Progress; use crate::api::scope::Scope; +use crate::api::status::Stage; +use crate::api::Status; -pub struct Get; +pub struct GetStatus; -impl Message for Get { +impl Message for GetStatus { + type Reply = Status; +} + +pub struct GetProgress; + +impl Message for GetProgress { type Reply = Vec; } -pub struct Set { +pub struct SetProgress { pub progress: Progress, } -impl Set { +impl SetProgress { pub fn new(progress: Progress) -> Self { Self { progress } } } -impl Message for Set { +impl Message for SetProgress { type Reply = (); } @@ -68,11 +76,8 @@ pub struct StartWithSteps { } impl StartWithSteps { - pub fn new(scope: Scope, steps: &[&str]) -> Self { - Self { - scope, - steps: steps.into_iter().map(ToString::to_string).collect(), - } + pub fn new(scope: Scope, steps: Vec) -> Self { + Self { scope, steps } } } @@ -125,3 +130,23 @@ impl Finish { impl Message for Finish { type Reply = (); } + +pub struct SetStage { + pub stage: Stage, +} + +impl SetStage { + pub fn new(stage: Stage) -> Self { + Self { stage } + } +} + +impl Message for SetStage { + type Reply = (); +} + +pub struct GetStage; + +impl Message for GetStage { + type Reply = Stage; +} diff --git a/rust/agama-utils/src/progress/service.rs b/rust/agama-utils/src/progress/service.rs index 791300987c..b591c8b8ad 100644 --- a/rust/agama-utils/src/progress/service.rs +++ b/rust/agama-utils/src/progress/service.rs @@ -19,6 +19,8 @@ // find current contact information at www.suse.com. use crate::actor::{self, Actor, Handler, MessageHandler}; +use crate::api::status::Stage; +use crate::api::Status; use crate::{ api::event::{self, Event}, api::progress::{self, Progress}, @@ -57,7 +59,7 @@ impl Starter { pub fn start(self) -> Handler { let service = Service { events: self.event, - progresses: vec![], + status: Status::default(), }; let handler = actor::spawn(service); @@ -67,7 +69,7 @@ impl Starter { pub struct Service { events: event::Sender, - progresses: Vec, + status: Status, } impl Service { @@ -75,16 +77,41 @@ impl Service { Starter::new(events) } + fn get_status(&self) -> &Status { + &self.status + } + + fn get_stage(&self) -> Stage { + self.status.stage + } + + // NOTE: this method might be implemented by Status. + fn get_progresses(&mut self) -> &Vec { + &self.status.progresses + } + + fn add_progress(&mut self, progress: Progress) { + self.status.progresses.push(progress); + } + + fn update_progress(&mut self, index: usize, progress: Progress) { + self.status.progresses[index] = progress; + } + fn get_progress(&self, scope: Scope) -> Option<&Progress> { - self.progresses.iter().find(|p| p.scope == scope) + self.status.progresses.iter().find(|p| p.scope == scope) } fn get_mut_progress(&mut self, scope: Scope) -> Option<&mut Progress> { - self.progresses.iter_mut().find(|p| p.scope == scope) + self.status.progresses.iter_mut().find(|p| p.scope == scope) } fn get_progress_index(&self, scope: Scope) -> Option { - self.progresses.iter().position(|p| p.scope == scope) + self.status.progresses.iter().position(|p| p.scope == scope) + } + + fn remove_progress(&mut self, index: usize) { + self.status.progresses.remove(index); } fn send_progress_changed(&self, progress: Progress) -> Result<(), Error> { @@ -98,20 +125,43 @@ impl Actor for Service { } #[async_trait] -impl MessageHandler for Service { - async fn handle(&mut self, _message: message::Get) -> Result, Error> { - Ok(self.progresses.clone()) +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::GetStatus) -> Result { + Ok(self.get_status().clone()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::GetStage) -> Result { + Ok(self.get_stage()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, message: message::SetStage) -> Result<(), Error> { + self.status.stage = message.stage; + self.events.send(Event::StageChanged)?; + Ok(()) + } +} + +#[async_trait] +impl MessageHandler for Service { + async fn handle(&mut self, _message: message::GetProgress) -> Result, Error> { + Ok(self.get_progresses().clone()) } } #[async_trait] -impl MessageHandler for Service { - async fn handle(&mut self, message: message::Set) -> Result<(), Error> { +impl MessageHandler for Service { + async fn handle(&mut self, message: message::SetProgress) -> Result<(), Error> { let progress = message.progress; if let Some(index) = self.get_progress_index(progress.scope) { - self.progresses[index] = progress.clone(); + self.update_progress(index, progress.clone()); } else { - self.progresses.push(progress.clone()); + self.add_progress(progress.clone()); } self.send_progress_changed(progress)?; Ok(()) @@ -125,7 +175,7 @@ impl MessageHandler for Service { return Err(Error::DuplicatedProgress(message.scope)); } let progress = Progress::new(message.scope, message.size, message.step); - self.progresses.push(progress.clone()); + self.add_progress(progress.clone()); self.send_progress_changed(progress)?; Ok(()) } @@ -138,7 +188,7 @@ impl MessageHandler for Service { return Err(Error::DuplicatedProgress(message.scope)); } let progress = Progress::new_with_steps(message.scope, message.steps); - self.progresses.push(progress.clone()); + self.add_progress(progress.clone()); self.send_progress_changed(progress)?; Ok(()) } @@ -176,7 +226,7 @@ impl MessageHandler for Service { let index = self .get_progress_index(message.scope) .ok_or(Error::MissingProgress(message.scope))?; - self.progresses.remove(index); + self.remove_progress(index); self.events.send(Event::ProgressFinished { scope: message.scope, })?; diff --git a/web/src/App.tsx b/web/src/App.tsx index e5c60f5b10..d41a455684 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -41,22 +41,22 @@ import { isEmpty } from "radashi"; const Content = () => { const location = useLocation(); const product = useProduct(); - const { progresses, state } = useStatus(); + const { progresses, stage } = useStatus(); const isBusy = !isEmpty(progresses); console.log("App Content component", { progresses, - state, + stage, product, location: location.pathname, }); - if (state === "installing") { + if (stage === "installing") { console.log("Navigating to the installation progress page"); return ; } - if (state === "finished") { + if (stage === "finished") { console.log("Navigating to the finished page"); return ; } diff --git a/web/src/model/status.ts b/web/src/model/status.ts index f6c3ec4be2..3bac8dfcd2 100644 --- a/web/src/model/status.ts +++ b/web/src/model/status.ts @@ -36,7 +36,7 @@ const fetchInstallerStatus = async (): Promise => { // TODO: remove export { fetchInstallerStatus }; -type State = "installing" | "configuring" | "finished"; +type Stage = "installing" | "configuring" | "finished"; type Scope = "manager" | "l10n" | "product" | "software" | "storage" | "iscsci" | "users"; type Progress = { index: number; @@ -47,8 +47,8 @@ type Progress = { }; type Status = { - state: State; + stage: Stage; progresses: Progress[]; }; -export type { Status, State, Scope, Progress }; +export type { Status, Stage, Scope, Progress };