From e75dcf828439fb265128943bb18eb0cd605f850b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 29 Feb 2024 06:19:28 +0000 Subject: [PATCH 01/19] rust: add a software service * By now it only implements a list of products. --- rust/agama-lib/src/product.rs | 2 +- rust/agama-server/src/agama-web-server.rs | 2 +- rust/agama-server/src/lib.rs | 1 + rust/agama-server/src/software.rs | 2 + rust/agama-server/src/software/web.rs | 73 +++++++++++++++++++++++ rust/agama-server/src/web.rs | 6 +- 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 rust/agama-server/src/software.rs create mode 100644 rust/agama-server/src/software/web.rs diff --git a/rust/agama-lib/src/product.rs b/rust/agama-lib/src/product.rs index e85173f6c9..8b352f602d 100644 --- a/rust/agama-lib/src/product.rs +++ b/rust/agama-lib/src/product.rs @@ -5,6 +5,6 @@ mod proxies; mod settings; mod store; -pub use client::ProductClient; +pub use client::{Product, ProductClient}; pub use settings::ProductSettings; pub use store::ProductStore; diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index d8a16fc565..56343bfdf5 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -45,7 +45,7 @@ async fn serve_command(address: &str) -> anyhow::Result<()> { run_monitor(tx.clone()).await?; let config = web::ServiceConfig::load().unwrap(); - let service = web::service(config, tx); + let service = web::service(config, tx).await; axum::serve(listener, service) .await .expect("could not mount app on listener"); diff --git a/rust/agama-server/src/lib.rs b/rust/agama-server/src/lib.rs index 421b9e3efd..1a5c30c1a2 100644 --- a/rust/agama-server/src/lib.rs +++ b/rust/agama-server/src/lib.rs @@ -2,5 +2,6 @@ pub mod error; pub mod l10n; pub mod network; pub mod questions; +pub mod software; pub mod web; pub use web::service; diff --git a/rust/agama-server/src/software.rs b/rust/agama-server/src/software.rs new file mode 100644 index 0000000000..d74ed1900c --- /dev/null +++ b/rust/agama-server/src/software.rs @@ -0,0 +1,2 @@ +pub mod web; +pub use web::software_service; diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs new file mode 100644 index 0000000000..29e6c285d7 --- /dev/null +++ b/rust/agama-server/src/software/web.rs @@ -0,0 +1,73 @@ +//! This module implements the web API for the software module. +//! +//! It is a wrapper around the YaST D-Bus API. + +use crate::web::EventsSender; +use agama_lib::{connection, product::Product, software::proxies::SoftwareProductProxy}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Json, Router, +}; +use serde_json::json; +use thiserror::Error; + +#[derive(Clone)] +struct SoftwareState<'a> { + software: SoftwareProductProxy<'a>, +} + +#[derive(Error, Debug)] +pub enum SoftwareError { + #[error("Service error: {0}")] + Error(#[from] zbus::Error), +} + +impl IntoResponse for SoftwareError { + fn into_response(self) -> Response { + let body = json!({ + "error": self.to_string() + }); + (StatusCode::BAD_REQUEST, Json(body)).into_response() + } +} + +/// Sets up and returns the axum service for the software module. +/// +/// * `events`: channel to send the events to the main service. +pub async fn software_service(_events: EventsSender) -> Router { + let connection = connection().await.unwrap(); + let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); + let state = SoftwareState { software: proxy }; + Router::new() + .route("/products", get(products)) + .with_state(state) +} + +/// Returns the list of available products. +/// +/// * `state`: service state. +async fn products<'a>( + State(state): State>, +) -> Result>, SoftwareError> { + let products = state.software.available_products().await?; + let products = products + .into_iter() + .map(|(id, name, data)| { + let description = data + .get("description") + .and_then(|d| d.downcast_ref::()) + .unwrap_or(""); + + Product { + id, + name, + description: description.to_string(), + } + }) + .collect(); + + Ok(Json(products)) +} diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 7cbb1ec6b8..82b0c0c02a 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -21,6 +21,7 @@ pub use docs::ApiDoc; pub use event::{Event, EventsReceiver, EventsSender}; use crate::l10n::web::l10n_service; +use crate::software::web::software_service; use axum::Router; pub use service::MainServiceBuilder; @@ -30,9 +31,10 @@ use self::progress::EventsProgressPresenter; /// /// * `config`: service configuration. /// * `events`: D-Bus connection. -pub fn service(config: ServiceConfig, events: EventsSender) -> Router { +pub async fn service(config: ServiceConfig, events: EventsSender) -> Router { MainServiceBuilder::new(events.clone()) - .add_service("/l10n", l10n_service(events)) + .add_service("/l10n", l10n_service(events.clone())) + .add_service("/software", software_service(events).await) .with_config(config) .build() } From d424b689bbccde8bccb1ce666ef7a6e1fdd68ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 29 Feb 2024 09:47:35 +0000 Subject: [PATCH 02/19] rust: add support to select a product via HTTP --- rust/agama-lib/src/product/client.rs | 5 ++++- rust/agama-server/src/software/web.rs | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 106166b621..e7fcc49e6f 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -8,7 +8,7 @@ use zbus::Connection; use super::proxies::RegistrationProxy; /// Represents a software product -#[derive(Debug, Serialize)] +#[derive(Default, Debug, Serialize)] pub struct Product { /// Product ID (eg., "ALP", "Tumbleweed", etc.) pub id: String, @@ -16,6 +16,8 @@ pub struct Product { pub name: String, /// Product description pub description: String, + /// Whether the product is selected + pub selected: bool, } /// D-Bus client for the software service @@ -48,6 +50,7 @@ impl<'a> ProductClient<'a> { id, name, description: description.to_string(), + ..Default::default() } }) .collect(); diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 29e6c285d7..1e46eb3791 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -5,10 +5,10 @@ use crate::web::EventsSender; use agama_lib::{connection, product::Product, software::proxies::SoftwareProductProxy}; use axum::{ - extract::State, + extract::{Path, State}, http::StatusCode, response::{IntoResponse, Response}, - routing::get, + routing::{get, put}, Json, Router, }; use serde_json::json; @@ -43,6 +43,7 @@ pub async fn software_service(_events: EventsSender) -> Router { let state = SoftwareState { software: proxy }; Router::new() .route("/products", get(products)) + .route("/products/:id/select", put(select_product)) .with_state(state) } @@ -53,6 +54,7 @@ async fn products<'a>( State(state): State>, ) -> Result>, SoftwareError> { let products = state.software.available_products().await?; + let selected_product = state.software.selected_product().await?; let products = products .into_iter() .map(|(id, name, data)| { @@ -60,14 +62,28 @@ async fn products<'a>( .get("description") .and_then(|d| d.downcast_ref::()) .unwrap_or(""); + let selected = selected_product == id; Product { id, name, description: description.to_string(), + selected, } }) .collect(); Ok(Json(products)) } + +/// Selects a product. +/// +/// * `state`: service state. +/// * `id`: product ID. +async fn select_product<'a>( + State(state): State>, + Path(id): Path, +) -> Result<(), SoftwareError> { + state.software.select_product(&id).await?; + Ok(()) +} From 57cac7f97ff9638c2697c34daeae249cf5cb4bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 29 Feb 2024 12:00:30 +0000 Subject: [PATCH 03/19] rust: emit product changes --- rust/agama-server/src/software.rs | 2 +- rust/agama-server/src/software/web.rs | 14 +++++++++++++- rust/agama-server/src/web.rs | 14 +++++++------- rust/agama-server/src/web/event.rs | 1 + 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/rust/agama-server/src/software.rs b/rust/agama-server/src/software.rs index d74ed1900c..9f234e6cff 100644 --- a/rust/agama-server/src/software.rs +++ b/rust/agama-server/src/software.rs @@ -1,2 +1,2 @@ pub mod web; -pub use web::software_service; +pub use web::{software_monitor, software_service}; diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 1e46eb3791..62a069f3e0 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -2,7 +2,7 @@ //! //! It is a wrapper around the YaST D-Bus API. -use crate::web::EventsSender; +use crate::web::{Event, EventsSender}; use agama_lib::{connection, product::Product, software::proxies::SoftwareProductProxy}; use axum::{ extract::{Path, State}, @@ -13,6 +13,7 @@ use axum::{ }; use serde_json::json; use thiserror::Error; +use tokio_stream::StreamExt; #[derive(Clone)] struct SoftwareState<'a> { @@ -34,6 +35,17 @@ impl IntoResponse for SoftwareError { } } +pub async fn software_monitor(events: EventsSender) { + let connection = connection().await.unwrap(); + let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); + let mut stream = proxy.receive_selected_product_changed().await; + while let Some(change) = stream.next().await { + if let Ok(id) = change.get().await { + _ = events.send(Event::ProductChanged { id }); + } + } +} + /// Sets up and returns the axum service for the software module. /// /// * `events`: channel to send the events to the main service. diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 82b0c0c02a..5940a2336a 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -4,6 +4,11 @@ //! * Emit relevant events via websocket. //! * Serve the code for the web user interface (not implemented yet). +use self::progress::EventsProgressPresenter; +use crate::l10n::web::l10n_service; +use crate::software::web::{software_monitor, software_service}; +use axum::Router; + mod auth; mod config; mod docs; @@ -19,14 +24,8 @@ pub use auth::generate_token; pub use config::ServiceConfig; pub use docs::ApiDoc; pub use event::{Event, EventsReceiver, EventsSender}; - -use crate::l10n::web::l10n_service; -use crate::software::web::software_service; -use axum::Router; pub use service::MainServiceBuilder; -use self::progress::EventsProgressPresenter; - /// Returns a service that implements the web-based Agama API. /// /// * `config`: service configuration. @@ -45,7 +44,7 @@ pub async fn service(config: ServiceConfig, events: EventsSender) -> Router { /// /// * `events`: channel to send the events to. pub async fn run_monitor(events: EventsSender) -> Result<(), ServiceError> { - let presenter = EventsProgressPresenter::new(events); + let presenter = EventsProgressPresenter::new(events.clone()); let connection = connection().await?; let mut monitor = ProgressMonitor::new(connection).await?; tokio::spawn(async move { @@ -53,5 +52,6 @@ pub async fn run_monitor(events: EventsSender) -> Result<(), ServiceError> { eprintln!("Could not monitor the D-Bus server: {}", error); } }); + tokio::spawn(async move { software_monitor(events.clone()).await }); Ok(()) } diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 76db326bc4..409adec0d0 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -7,6 +7,7 @@ use tokio::sync::broadcast::{Receiver, Sender}; pub enum Event { LocaleChanged { locale: String }, Progress(Progress), + ProductChanged { id: String }, } pub type EventsSender = Sender; From fc3e12aaf93bbb8a96b7c17a0bdb72bea531709f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 1 Mar 2024 11:44:02 +0000 Subject: [PATCH 04/19] rust: move software config to a separate resource --- rust/agama-lib/src/product/client.rs | 3 -- rust/agama-server/src/software/web.rs | 53 +++++++++++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index e7fcc49e6f..b7b848ac32 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -16,8 +16,6 @@ pub struct Product { pub name: String, /// Product description pub description: String, - /// Whether the product is selected - pub selected: bool, } /// D-Bus client for the software service @@ -50,7 +48,6 @@ impl<'a> ProductClient<'a> { id, name, description: description.to_string(), - ..Default::default() } }) .collect(); diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 62a069f3e0..ec792fd752 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -5,12 +5,13 @@ use crate::web::{Event, EventsSender}; use agama_lib::{connection, product::Product, software::proxies::SoftwareProductProxy}; use axum::{ - extract::{Path, State}, + extract::State, http::StatusCode, response::{IntoResponse, Response}, routing::{get, put}, Json, Router, }; +use serde::{Deserialize, Serialize}; use serde_json::json; use thiserror::Error; use tokio_stream::StreamExt; @@ -18,6 +19,24 @@ use tokio_stream::StreamExt; #[derive(Clone)] struct SoftwareState<'a> { software: SoftwareProductProxy<'a>, + connection: zbus::Connection, +} + +#[derive(Clone, Serialize, Deserialize)] +struct SoftwareConfig { + patterns: Option>, + product: Option, +} + +impl SoftwareConfig { + pub async fn from_dbus(connection: &zbus::Connection) -> Result { + let software = SoftwareProductProxy::new(&connection).await?; + let product = software.selected_product().await?; + Ok(SoftwareConfig { + patterns: Some(vec![]), + product: Some(product), + }) + } } #[derive(Error, Debug)] @@ -52,10 +71,13 @@ pub async fn software_monitor(events: EventsSender) { pub async fn software_service(_events: EventsSender) -> Router { let connection = connection().await.unwrap(); let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); - let state = SoftwareState { software: proxy }; + let state = SoftwareState { + connection, + software: proxy, + }; Router::new() .route("/products", get(products)) - .route("/products/:id/select", put(select_product)) + .route("/config", put(set_config).get(get_config)) .with_state(state) } @@ -66,7 +88,6 @@ async fn products<'a>( State(state): State>, ) -> Result>, SoftwareError> { let products = state.software.available_products().await?; - let selected_product = state.software.selected_product().await?; let products = products .into_iter() .map(|(id, name, data)| { @@ -74,13 +95,11 @@ async fn products<'a>( .get("description") .and_then(|d| d.downcast_ref::()) .unwrap_or(""); - let selected = selected_product == id; Product { id, name, description: description.to_string(), - selected, } }) .collect(); @@ -88,14 +107,26 @@ async fn products<'a>( Ok(Json(products)) } -/// Selects a product. +/// Sets the software configuration. /// /// * `state`: service state. -/// * `id`: product ID. -async fn select_product<'a>( +/// * `config`: software configuration. +async fn set_config<'a>( State(state): State>, - Path(id): Path, + Json(config): Json, ) -> Result<(), SoftwareError> { - state.software.select_product(&id).await?; + if let Some(product) = config.product { + state.software.select_product(&product).await?; + } Ok(()) } + +/// Returns the software configuration +/// +/// * `state` : service state. +async fn get_config<'a>( + State(state): State>, +) -> Result, SoftwareError> { + let config = SoftwareConfig::from_dbus(&state.connection).await?; + Ok(Json(config)) +} From 2b5be300b7296b5902ed760196db62a635a622e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 1 Mar 2024 12:02:19 +0000 Subject: [PATCH 05/19] rust: share the SoftwareProductProxy --- rust/agama-server/src/software/web.rs | 29 +++++++++------------------ 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index ec792fd752..9ef7c35ec8 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -18,8 +18,7 @@ use tokio_stream::StreamExt; #[derive(Clone)] struct SoftwareState<'a> { - software: SoftwareProductProxy<'a>, - connection: zbus::Connection, + product: SoftwareProductProxy<'a>, } #[derive(Clone, Serialize, Deserialize)] @@ -28,17 +27,6 @@ struct SoftwareConfig { product: Option, } -impl SoftwareConfig { - pub async fn from_dbus(connection: &zbus::Connection) -> Result { - let software = SoftwareProductProxy::new(&connection).await?; - let product = software.selected_product().await?; - Ok(SoftwareConfig { - patterns: Some(vec![]), - product: Some(product), - }) - } -} - #[derive(Error, Debug)] pub enum SoftwareError { #[error("Service error: {0}")] @@ -71,10 +59,7 @@ pub async fn software_monitor(events: EventsSender) { pub async fn software_service(_events: EventsSender) -> Router { let connection = connection().await.unwrap(); let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); - let state = SoftwareState { - connection, - software: proxy, - }; + let state = SoftwareState { product: proxy }; Router::new() .route("/products", get(products)) .route("/config", put(set_config).get(get_config)) @@ -87,7 +72,7 @@ pub async fn software_service(_events: EventsSender) -> Router { async fn products<'a>( State(state): State>, ) -> Result>, SoftwareError> { - let products = state.software.available_products().await?; + let products = state.product.available_products().await?; let products = products .into_iter() .map(|(id, name, data)| { @@ -116,7 +101,7 @@ async fn set_config<'a>( Json(config): Json, ) -> Result<(), SoftwareError> { if let Some(product) = config.product { - state.software.select_product(&product).await?; + state.product.select_product(&product).await?; } Ok(()) } @@ -127,6 +112,10 @@ async fn set_config<'a>( async fn get_config<'a>( State(state): State>, ) -> Result, SoftwareError> { - let config = SoftwareConfig::from_dbus(&state.connection).await?; + let product = state.product.selected_product().await?; + let config = SoftwareConfig { + patterns: Some(vec![]), + product: Some(product), + }; Ok(Json(config)) } From 5edea12a7a3c3b0e2d4ff2273aa6cafbefe8e7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 1 Mar 2024 12:21:53 +0000 Subject: [PATCH 06/19] rust: use the software client in the web API --- rust/agama-lib/src/product/client.rs | 1 + rust/agama-server/src/software/web.rs | 37 ++++++++++----------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index b7b848ac32..ba7934419e 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -19,6 +19,7 @@ pub struct Product { } /// D-Bus client for the software service +#[derive(Clone)] pub struct ProductClient<'a> { product_proxy: SoftwareProductProxy<'a>, registration_proxy: RegistrationProxy<'a>, diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 9ef7c35ec8..f98522113b 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -3,7 +3,12 @@ //! It is a wrapper around the YaST D-Bus API. use crate::web::{Event, EventsSender}; -use agama_lib::{connection, product::Product, software::proxies::SoftwareProductProxy}; +use agama_lib::{ + connection, + error::ServiceError, + product::{Product, ProductClient}, + software::proxies::SoftwareProductProxy, +}; use axum::{ extract::State, http::StatusCode, @@ -18,7 +23,7 @@ use tokio_stream::StreamExt; #[derive(Clone)] struct SoftwareState<'a> { - product: SoftwareProductProxy<'a>, + client: ProductClient<'a>, } #[derive(Clone, Serialize, Deserialize)] @@ -30,7 +35,7 @@ struct SoftwareConfig { #[derive(Error, Debug)] pub enum SoftwareError { #[error("Service error: {0}")] - Error(#[from] zbus::Error), + Error(#[from] ServiceError), } impl IntoResponse for SoftwareError { @@ -58,8 +63,8 @@ pub async fn software_monitor(events: EventsSender) { /// * `events`: channel to send the events to the main service. pub async fn software_service(_events: EventsSender) -> Router { let connection = connection().await.unwrap(); - let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); - let state = SoftwareState { product: proxy }; + let client = ProductClient::new(connection).await.unwrap(); + let state = SoftwareState { client }; Router::new() .route("/products", get(products)) .route("/config", put(set_config).get(get_config)) @@ -72,23 +77,7 @@ pub async fn software_service(_events: EventsSender) -> Router { async fn products<'a>( State(state): State>, ) -> Result>, SoftwareError> { - let products = state.product.available_products().await?; - let products = products - .into_iter() - .map(|(id, name, data)| { - let description = data - .get("description") - .and_then(|d| d.downcast_ref::()) - .unwrap_or(""); - - Product { - id, - name, - description: description.to_string(), - } - }) - .collect(); - + let products = state.client.products().await?; Ok(Json(products)) } @@ -101,7 +90,7 @@ async fn set_config<'a>( Json(config): Json, ) -> Result<(), SoftwareError> { if let Some(product) = config.product { - state.product.select_product(&product).await?; + state.client.select_product(&product).await?; } Ok(()) } @@ -112,7 +101,7 @@ async fn set_config<'a>( async fn get_config<'a>( State(state): State>, ) -> Result, SoftwareError> { - let product = state.product.selected_product().await?; + let product = state.client.product().await?; let config = SoftwareConfig { patterns: Some(vec![]), product: Some(product), From 9c65f8566b97b1a9178ead2bb09adfbc9cccb76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 1 Mar 2024 14:46:22 +0000 Subject: [PATCH 07/19] rust: add patterns support to the HTTP API --- rust/agama-lib/src/software.rs | 2 +- rust/agama-lib/src/software/client.rs | 35 ++++++++++++- rust/agama-server/src/software/web.rs | 71 ++++++++++++++++++++++++--- 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/rust/agama-lib/src/software.rs b/rust/agama-lib/src/software.rs index 53f2d6bf51..ccdc1fce18 100644 --- a/rust/agama-lib/src/software.rs +++ b/rust/agama-lib/src/software.rs @@ -5,6 +5,6 @@ pub mod proxies; mod settings; mod store; -pub use client::SoftwareClient; +pub use client::{Pattern, SelectionReason, SoftwareClient}; pub use settings::SoftwareSettings; pub use store::SoftwareStore; diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index 0670467b02..d3a49d4196 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -1,6 +1,7 @@ use super::proxies::Software1Proxy; use crate::error::ServiceError; use serde::Serialize; +use std::collections::HashMap; use zbus::Connection; /// Represents a software product @@ -20,7 +21,26 @@ pub struct Pattern { pub order: String, } +/// Represents the reason why a pattern is selected. +#[derive(Clone, Copy)] +pub enum SelectionReason { + /// The pattern was selected by the user. + User = 0, + /// The pattern was selected automatically. + Auto = 1, +} + +impl From for SelectionReason { + fn from(value: u8) -> Self { + match value { + 0 => Self::User, + _ => Self::Auto, + } + } +} + /// D-Bus client for the software service +#[derive(Clone)] pub struct SoftwareClient<'a> { software_proxy: Software1Proxy<'a>, } @@ -55,18 +75,29 @@ impl<'a> SoftwareClient<'a> { /// Returns the ids of patterns selected by user pub async fn user_selected_patterns(&self) -> Result, ServiceError> { - const USER_SELECTED: u8 = 0; let patterns: Vec = self .software_proxy .selected_patterns() .await? .into_iter() - .filter(|(_id, reason)| *reason == USER_SELECTED) + .filter(|(_id, reason)| *reason == SelectionReason::User as u8) .map(|(id, _reason)| id) .collect(); Ok(patterns) } + /// Returns the selected pattern and the reason each one selected. + pub async fn selected_patterns( + &self, + ) -> Result, ServiceError> { + let patterns = self.software_proxy.selected_patterns().await?; + let patterns = patterns + .into_iter() + .map(|(id, reason)| (id, reason.into())) + .collect(); + Ok(patterns) + } + /// Selects patterns by user pub async fn select_patterns(&self, patterns: &[String]) -> Result<(), ServiceError> { let patterns: Vec<&str> = patterns.iter().map(AsRef::as_ref).collect(); diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index f98522113b..66a14a2055 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -7,7 +7,7 @@ use agama_lib::{ connection, error::ServiceError, product::{Product, ProductClient}, - software::proxies::SoftwareProductProxy, + software::{proxies::SoftwareProductProxy, Pattern, SelectionReason, SoftwareClient}, }; use axum::{ extract::State, @@ -23,7 +23,8 @@ use tokio_stream::StreamExt; #[derive(Clone)] struct SoftwareState<'a> { - client: ProductClient<'a>, + product: ProductClient<'a>, + software: SoftwareClient<'a>, } #[derive(Clone, Serialize, Deserialize)] @@ -63,10 +64,12 @@ pub async fn software_monitor(events: EventsSender) { /// * `events`: channel to send the events to the main service. pub async fn software_service(_events: EventsSender) -> Router { let connection = connection().await.unwrap(); - let client = ProductClient::new(connection).await.unwrap(); - let state = SoftwareState { client }; + let product = ProductClient::new(connection.clone()).await.unwrap(); + let software = SoftwareClient::new(connection).await.unwrap(); + let state = SoftwareState { product, software }; Router::new() .route("/products", get(products)) + .route("/patterns", get(patterns)) .route("/config", put(set_config).get(get_config)) .with_state(state) } @@ -77,10 +80,56 @@ pub async fn software_service(_events: EventsSender) -> Router { async fn products<'a>( State(state): State>, ) -> Result>, SoftwareError> { - let products = state.client.products().await?; + let products = state.product.products().await?; Ok(Json(products)) } +/// Represents a pattern. +/// +/// It augments the information coming from the D-Bus client. +#[derive(Serialize)] +pub struct PatternItem { + #[serde(flatten)] + pattern: Pattern, + status: PatternStatus, +} + +/// Pattern status. +#[derive(Serialize, Clone, Copy)] +enum PatternStatus { + Available, + UserSelected, + AutoSelected, +} + +impl From for PatternStatus { + fn from(value: SelectionReason) -> Self { + match value { + SelectionReason::User => Self::UserSelected, + SelectionReason::Auto => Self::AutoSelected, + } + } +} + +async fn patterns<'a>( + State(state): State>, +) -> Result>, SoftwareError> { + let patterns = state.software.patterns(true).await?; + let selected = state.software.selected_patterns().await?; + let items = patterns + .into_iter() + .map(|pattern| { + let status: PatternStatus = selected + .get(&pattern.id) + .map(|r| (*r).into()) + .unwrap_or(PatternStatus::Available); + PatternItem { pattern, status } + }) + .collect(); + + Ok(Json(items)) +} + /// Sets the software configuration. /// /// * `state`: service state. @@ -90,8 +139,13 @@ async fn set_config<'a>( Json(config): Json, ) -> Result<(), SoftwareError> { if let Some(product) = config.product { - state.client.select_product(&product).await?; + state.product.select_product(&product).await?; + } + + if let Some(patterns) = config.patterns { + state.software.select_patterns(&patterns).await?; } + Ok(()) } @@ -101,9 +155,10 @@ async fn set_config<'a>( async fn get_config<'a>( State(state): State>, ) -> Result, SoftwareError> { - let product = state.client.product().await?; + let product = state.product.product().await?; + let patterns = state.software.user_selected_patterns().await?; let config = SoftwareConfig { - patterns: Some(vec![]), + patterns: Some(patterns), product: Some(product), }; Ok(Json(config)) From 0cbb4367558d697c95e411617b72712b8da80b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 1 Mar 2024 17:01:46 +0000 Subject: [PATCH 08/19] rust: emit patterns selection changes --- rust/agama-server/src/software/web.rs | 23 ++++++++++++++++++++--- rust/agama-server/src/web.rs | 4 ++-- rust/agama-server/src/web/event.rs | 1 + 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 66a14a2055..3c680a5492 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -7,7 +7,10 @@ use agama_lib::{ connection, error::ServiceError, product::{Product, ProductClient}, - software::{proxies::SoftwareProductProxy, Pattern, SelectionReason, SoftwareClient}, + software::{ + proxies::{Software1Proxy, SoftwareProductProxy}, + Pattern, SelectionReason, SoftwareClient, + }, }; use axum::{ extract::State, @@ -48,8 +51,12 @@ impl IntoResponse for SoftwareError { } } -pub async fn software_monitor(events: EventsSender) { - let connection = connection().await.unwrap(); +pub async fn software_monitor(connection: zbus::Connection, events: EventsSender) { + tokio::spawn(monitor_product_changed(connection.clone(), events.clone())); + tokio::spawn(monitor_patterns_changed(connection.clone(), events.clone())); +} + +async fn monitor_product_changed(connection: zbus::Connection, events: EventsSender) { let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); let mut stream = proxy.receive_selected_product_changed().await; while let Some(change) = stream.next().await { @@ -59,6 +66,16 @@ pub async fn software_monitor(events: EventsSender) { } } +async fn monitor_patterns_changed(connection: zbus::Connection, events: EventsSender) { + let proxy = Software1Proxy::new(&connection).await.unwrap(); + let mut stream = proxy.receive_selected_patterns_changed().await; + while let Some(change) = stream.next().await { + if let Ok(patterns) = change.get().await { + _ = events.send(Event::PatternsChanged); + } + } +} + /// Sets up and returns the axum service for the software module. /// /// * `events`: channel to send the events to the main service. diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 5940a2336a..4f52e2a3aa 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -46,12 +46,12 @@ pub async fn service(config: ServiceConfig, events: EventsSender) -> Router { pub async fn run_monitor(events: EventsSender) -> Result<(), ServiceError> { let presenter = EventsProgressPresenter::new(events.clone()); let connection = connection().await?; - let mut monitor = ProgressMonitor::new(connection).await?; + let mut monitor = ProgressMonitor::new(connection.clone()).await?; tokio::spawn(async move { if let Err(error) = monitor.run(presenter).await { eprintln!("Could not monitor the D-Bus server: {}", error); } }); - tokio::spawn(async move { software_monitor(events.clone()).await }); + software_monitor(connection, events.clone()).await; Ok(()) } diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 409adec0d0..d810007c82 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -8,6 +8,7 @@ pub enum Event { LocaleChanged { locale: String }, Progress(Progress), ProductChanged { id: String }, + PatternsChanged, } pub type EventsSender = Sender; From 35ffb0e6cbaed9124f5f59a438a01f5873a51d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Sat, 2 Mar 2024 10:09:33 +0000 Subject: [PATCH 09/19] rust: emit stream events in a single place --- rust/agama-server/src/software.rs | 2 +- rust/agama-server/src/software/web.rs | 46 ++++++++++++++++----------- rust/agama-server/src/web.rs | 19 +++++++++-- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/rust/agama-server/src/software.rs b/rust/agama-server/src/software.rs index 9f234e6cff..b882542d90 100644 --- a/rust/agama-server/src/software.rs +++ b/rust/agama-server/src/software.rs @@ -1,2 +1,2 @@ pub mod web; -pub use web::{software_monitor, software_service}; +pub use web::{software_service, software_stream}; diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 3c680a5492..3168a5927c 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -22,7 +22,7 @@ use axum::{ use serde::{Deserialize, Serialize}; use serde_json::json; use thiserror::Error; -use tokio_stream::StreamExt; +use tokio_stream::{Stream, StreamExt}; #[derive(Clone)] struct SoftwareState<'a> { @@ -51,29 +51,39 @@ impl IntoResponse for SoftwareError { } } -pub async fn software_monitor(connection: zbus::Connection, events: EventsSender) { - tokio::spawn(monitor_product_changed(connection.clone(), events.clone())); - tokio::spawn(monitor_patterns_changed(connection.clone(), events.clone())); +pub async fn software_stream(connection: zbus::Connection) -> impl Stream { + StreamExt::merge( + product_changed_stream(connection.clone()).await, + patterns_changed_stream(connection.clone()).await, + ) } -async fn monitor_product_changed(connection: zbus::Connection, events: EventsSender) { +async fn product_changed_stream(connection: zbus::Connection) -> impl Stream { let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); - let mut stream = proxy.receive_selected_product_changed().await; - while let Some(change) = stream.next().await { - if let Ok(id) = change.get().await { - _ = events.send(Event::ProductChanged { id }); - } - } + proxy + .receive_selected_product_changed() + .await + .then(|change| async move { + if let Ok(id) = change.get().await { + return Some(Event::ProductChanged { id }); + } + None + }) + .filter_map(|e| e) } -async fn monitor_patterns_changed(connection: zbus::Connection, events: EventsSender) { +async fn patterns_changed_stream(connection: zbus::Connection) -> impl Stream { let proxy = Software1Proxy::new(&connection).await.unwrap(); - let mut stream = proxy.receive_selected_patterns_changed().await; - while let Some(change) = stream.next().await { - if let Ok(patterns) = change.get().await { - _ = events.send(Event::PatternsChanged); - } - } + proxy + .receive_selected_patterns_changed() + .await + .then(|change| async move { + if let Ok(_pattens) = change.get().await { + return Some(Event::PatternsChanged); + } + None + }) + .filter_map(|e| e) } /// Sets up and returns the axum service for the software module. diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 4f52e2a3aa..1b2da2e713 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -6,7 +6,7 @@ use self::progress::EventsProgressPresenter; use crate::l10n::web::l10n_service; -use crate::software::web::{software_monitor, software_service}; +use crate::software::web::{software_service, software_stream}; use axum::Router; mod auth; @@ -25,6 +25,7 @@ pub use config::ServiceConfig; pub use docs::ApiDoc; pub use event::{Event, EventsReceiver, EventsSender}; pub use service::MainServiceBuilder; +use tokio_stream::StreamExt; /// Returns a service that implements the web-based Agama API. /// @@ -52,6 +53,20 @@ pub async fn run_monitor(events: EventsSender) -> Result<(), ServiceError> { eprintln!("Could not monitor the D-Bus server: {}", error); } }); - software_monitor(connection, events.clone()).await; + tokio::spawn(run_events_monitor(connection, events.clone())); + Ok(()) } + +/// Emits the events from the system streams through the events channel. +/// +/// * `connection`: D-Bus connection. +/// * `events`: channel to send the events to. +pub async fn run_events_monitor(connection: zbus::Connection, events: EventsSender) { + let stream = software_stream(connection).await; + tokio::pin!(stream); + let e = events.clone(); + while let Some(event) = stream.next().await { + _ = e.send(event); + } +} From 6340803a7b6be2d8d02dbfb2e0067cfdf4f7e3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 13:39:53 +0000 Subject: [PATCH 10/19] doc: update the OpenAPI documentation --- rust/Cargo.lock | 1 + rust/agama-lib/Cargo.toml | 1 + rust/agama-lib/src/product/client.rs | 2 +- rust/agama-lib/src/software/client.rs | 2 +- rust/agama-server/src/software/web.rs | 41 ++++++++++++++++++++------- rust/agama-server/src/web/docs.rs | 18 ++++++++++-- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8bef766f82..cc576c6eaf 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -112,6 +112,7 @@ dependencies = [ "tokio", "tokio-stream", "url", + "utoipa", "zbus", ] diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 5ef8166609..40702e2fde 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -21,4 +21,5 @@ thiserror = "1.0.39" tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } tokio-stream = "0.1.14" url = "2.5.0" +utoipa = "4.2.0" zbus = { version = "3", default-features = false, features = ["tokio"] } diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index ba7934419e..2f4e18f432 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -8,7 +8,7 @@ use zbus::Connection; use super::proxies::RegistrationProxy; /// Represents a software product -#[derive(Default, Debug, Serialize)] +#[derive(Default, Debug, Serialize, utoipa::ToSchema)] pub struct Product { /// Product ID (eg., "ALP", "Tumbleweed", etc.) pub id: String, diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index d3a49d4196..401a9b7480 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use zbus::Connection; /// Represents a software product -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, utoipa::ToSchema)] pub struct Pattern { /// Pattern ID (eg., "aaa_base", "gnome") pub id: String, diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 3168a5927c..1d2f208288 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -1,6 +1,9 @@ //! This module implements the web API for the software module. //! -//! It is a wrapper around the YaST D-Bus API. +//! The module offers two public functions: +//! +//! * `software_service` which returns the Axum service. +//! * `software_stream` which offers an stream that emits the software events coming from D-Bus. use crate::web::{Event, EventsSender}; use agama_lib::{ @@ -51,6 +54,9 @@ impl IntoResponse for SoftwareError { } } +/// Returns an stream that emits software related events coming from D-Bus. +/// +/// * `connection`: D-Bus connection to listen for events. pub async fn software_stream(connection: zbus::Connection) -> impl Stream { StreamExt::merge( product_changed_stream(connection.clone()).await, @@ -87,8 +93,6 @@ async fn patterns_changed_stream(connection: zbus::Connection) -> impl Stream Router { let connection = connection().await.unwrap(); let product = ProductClient::new(connection.clone()).await.unwrap(); @@ -104,6 +108,9 @@ pub async fn software_service(_events: EventsSender) -> Router { /// Returns the list of available products. /// /// * `state`: service state. +#[utoipa::path(get, path = "/software/products", responses( + (status = 200, description = "List of known products") +))] async fn products<'a>( State(state): State>, ) -> Result>, SoftwareError> { @@ -114,8 +121,8 @@ async fn products<'a>( /// Represents a pattern. /// /// It augments the information coming from the D-Bus client. -#[derive(Serialize)] -pub struct PatternItem { +#[derive(Serialize, utoipa::ToSchema)] +pub struct PatternEntry { #[serde(flatten)] pattern: Pattern, status: PatternStatus, @@ -138,9 +145,15 @@ impl From for PatternStatus { } } +/// Returns the list of software patterns. +/// +/// * `state`: service state. +#[utoipa::path(get, path = "/software/patterns", responses( + (status = 200, description = "List of known software patterns") +))] async fn patterns<'a>( State(state): State>, -) -> Result>, SoftwareError> { +) -> Result>, SoftwareError> { let patterns = state.software.patterns(true).await?; let selected = state.software.selected_patterns().await?; let items = patterns @@ -150,7 +163,7 @@ async fn patterns<'a>( .get(&pattern.id) .map(|r| (*r).into()) .unwrap_or(PatternStatus::Available); - PatternItem { pattern, status } + PatternEntry { pattern, status } }) .collect(); @@ -161,8 +174,11 @@ async fn patterns<'a>( /// /// * `state`: service state. /// * `config`: software configuration. -async fn set_config<'a>( - State(state): State>, +#[utoipa::path(put, path = "/software/config", responses( + (status = 200, description = "Set the software configuration") +))] +async fn set_config( + State(state): State>, Json(config): Json, ) -> Result<(), SoftwareError> { if let Some(product) = config.product { @@ -179,8 +195,11 @@ async fn set_config<'a>( /// Returns the software configuration /// /// * `state` : service state. -async fn get_config<'a>( - State(state): State>, +#[utoipa::path(get, path = "/software/config", responses( + (status = 200, description = "Software configuration") +))] +async fn get_config( + State(state): State>, ) -> Result, SoftwareError> { let product = state.product.product().await?; let patterns = state.software.user_selected_patterns().await?; diff --git a/rust/agama-server/src/web/docs.rs b/rust/agama-server/src/web/docs.rs index 58a1de6aed..d7b46459ae 100644 --- a/rust/agama-server/src/web/docs.rs +++ b/rust/agama-server/src/web/docs.rs @@ -3,13 +3,27 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( info(description = "Agama web API description"), - paths(super::http::ping, crate::l10n::web::locales), + paths( + super::http::ping, + crate::software::web::patterns, + crate::l10n::web::get_config, + crate::l10n::web::keymaps, + crate::l10n::web::locales, + crate::l10n::web::set_config, + crate::l10n::web::timezones, + crate::software::web::get_config, + crate::software::web::set_config, + crate::software::web::patterns, + ), components( schemas(super::http::PingResponse), schemas(crate::l10n::LocaleEntry), schemas(crate::l10n::web::LocaleConfig), schemas(crate::l10n::Keymap), - schemas(crate::l10n::TimezoneEntry) + schemas(crate::l10n::TimezoneEntry), + schemas(agama_lib::software::Pattern), + schemas(agama_lib::product::Product), + schemas(crate::software::web::PatternEntry) ) )] pub struct ApiDoc; From 0e0b0e3e8c8fdbef0a44743b8a501fd3edc532d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 14:39:37 +0000 Subject: [PATCH 11/19] rust: refactor the CLI arguments handling --- rust/agama-server/src/agama-web-server.rs | 37 +++++++++++++++-------- rust/agama-server/src/software/web.rs | 24 +++++++-------- rust/agama-server/src/web.rs | 8 +++-- 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index 56343bfdf5..a0c29ce5d0 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -4,7 +4,8 @@ use agama_dbus_server::{ l10n::helpers, web::{self, run_monitor}, }; -use clap::{Parser, Subcommand}; +use agama_lib::connection_to; +use clap::{Args, Parser, Subcommand}; use tokio::sync::broadcast::channel; use tracing_subscriber::prelude::*; use utoipa::OpenApi; @@ -12,16 +13,22 @@ use utoipa::OpenApi; #[derive(Subcommand, Debug)] enum Commands { /// Start the API server. - Serve { - // Address to listen on (":::3000" listens for both IPv6 and IPv4 - // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) - #[arg(long, default_value = ":::3000")] - address: String, - }, + Serve(ServeArgs), /// Display the API documentation in OpenAPI format. Openapi, } +#[derive(Debug, Args)] +pub struct ServeArgs { + // Address to listen on (":::3000" listens for both IPv6 and IPv4 + // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only) + #[arg(long, default_value = ":::3000")] + address: String, + // Agama D-Bus address + #[arg(long, default_value = "unix:path=/run/agama/bus")] + dbus_address: String, +} + #[derive(Parser, Debug)] #[command( version, @@ -33,22 +40,26 @@ struct Cli { } /// Start serving the API. -async fn serve_command(address: &str) -> anyhow::Result<()> { +/// +/// `args`: command-line arguments. +async fn serve_command(args: ServeArgs) -> anyhow::Result<()> { let journald = tracing_journald::layer().expect("could not connect to journald"); tracing_subscriber::registry().with(journald).init(); - let listener = tokio::net::TcpListener::bind(address) + let listener = tokio::net::TcpListener::bind(&args.address) .await - .unwrap_or_else(|_| panic!("could not listen on {}", address)); + .unwrap_or_else(|_| panic!("could not listen on {}", &args.address)); let (tx, _) = channel(16); run_monitor(tx.clone()).await?; - let config = web::ServiceConfig::load().unwrap(); - let service = web::service(config, tx).await; + let config = web::ServiceConfig::load()?; + let dbus = connection_to(&args.dbus_address).await?; + let service = web::service(config, tx, dbus).await; axum::serve(listener, service) .await .expect("could not mount app on listener"); + Ok(()) } @@ -60,7 +71,7 @@ fn openapi_command() -> anyhow::Result<()> { async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { - Commands::Serve { address } => serve_command(&address).await, + Commands::Serve(args) => serve_command(args).await, Commands::Openapi => openapi_command(), } } diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 1d2f208288..becfd5bc2a 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -5,9 +5,8 @@ //! * `software_service` which returns the Axum service. //! * `software_stream` which offers an stream that emits the software events coming from D-Bus. -use crate::web::{Event, EventsSender}; +use crate::web::Event; use agama_lib::{ - connection, error::ServiceError, product::{Product, ProductClient}, software::{ @@ -57,15 +56,15 @@ impl IntoResponse for SoftwareError { /// Returns an stream that emits software related events coming from D-Bus. /// /// * `connection`: D-Bus connection to listen for events. -pub async fn software_stream(connection: zbus::Connection) -> impl Stream { +pub async fn software_stream(dbus: zbus::Connection) -> impl Stream { StreamExt::merge( - product_changed_stream(connection.clone()).await, - patterns_changed_stream(connection.clone()).await, + product_changed_stream(dbus.clone()).await, + patterns_changed_stream(dbus.clone()).await, ) } -async fn product_changed_stream(connection: zbus::Connection) -> impl Stream { - let proxy = SoftwareProductProxy::new(&connection).await.unwrap(); +async fn product_changed_stream(dbus: zbus::Connection) -> impl Stream { + let proxy = SoftwareProductProxy::new(&dbus).await.unwrap(); proxy .receive_selected_product_changed() .await @@ -78,8 +77,8 @@ async fn product_changed_stream(connection: zbus::Connection) -> impl Stream impl Stream { - let proxy = Software1Proxy::new(&connection).await.unwrap(); +async fn patterns_changed_stream(dbus: zbus::Connection) -> impl Stream { + let proxy = Software1Proxy::new(&dbus).await.unwrap(); proxy .receive_selected_patterns_changed() .await @@ -93,10 +92,9 @@ async fn patterns_changed_stream(connection: zbus::Connection) -> impl Stream Router { - let connection = connection().await.unwrap(); - let product = ProductClient::new(connection.clone()).await.unwrap(); - let software = SoftwareClient::new(connection).await.unwrap(); +pub async fn software_service(dbus: zbus::Connection) -> Router { + let product = ProductClient::new(dbus.clone()).await.unwrap(); + let software = SoftwareClient::new(dbus).await.unwrap(); let state = SoftwareState { product, software }; Router::new() .route("/products", get(products)) diff --git a/rust/agama-server/src/web.rs b/rust/agama-server/src/web.rs index 1b2da2e713..d789bb0034 100644 --- a/rust/agama-server/src/web.rs +++ b/rust/agama-server/src/web.rs @@ -31,10 +31,14 @@ use tokio_stream::StreamExt; /// /// * `config`: service configuration. /// * `events`: D-Bus connection. -pub async fn service(config: ServiceConfig, events: EventsSender) -> Router { +pub async fn service( + config: ServiceConfig, + events: EventsSender, + dbus: zbus::Connection, +) -> Router { MainServiceBuilder::new(events.clone()) .add_service("/l10n", l10n_service(events.clone())) - .add_service("/software", software_service(events).await) + .add_service("/software", software_service(dbus).await) .with_config(config) .build() } From 6e9d476e9a162a725114b9d4f2ce7b720d466488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 14:39:47 +0000 Subject: [PATCH 12/19] rust: fix service tests --- rust/agama-server/tests/service.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rust/agama-server/tests/service.rs b/rust/agama-server/tests/service.rs index 6179484be8..59157d3bce 100644 --- a/rust/agama-server/tests/service.rs +++ b/rust/agama-server/tests/service.rs @@ -11,19 +11,20 @@ use axum::{ routing::get, Router, }; -use common::body_to_string; +use common::{body_to_string, DBusServer}; use std::error::Error; use tokio::{sync::broadcast::channel, test}; use tower::ServiceExt; -fn build_service() -> Router { +async fn build_service() -> Router { let (tx, _) = channel(16); - service(ServiceConfig::default(), tx) + let server = DBusServer::new().start().await.unwrap(); + service(ServiceConfig::default(), tx, server.connection()).await } #[test] async fn test_ping() -> Result<(), Box> { - let web_service = build_service(); + let web_service = build_service().await; let request = Request::builder().uri("/ping").body(Body::empty()).unwrap(); let response = web_service.oneshot(request).await.unwrap(); From 602a6f8a82bc6bedc2b6335c88df33c5aa2ff33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 14:46:55 +0000 Subject: [PATCH 13/19] rust: remove unneeded lifetime annotations --- rust/agama-server/src/software/web.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index becfd5bc2a..4fb8222fec 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -109,8 +109,8 @@ pub async fn software_service(dbus: zbus::Connection) -> Router { #[utoipa::path(get, path = "/software/products", responses( (status = 200, description = "List of known products") ))] -async fn products<'a>( - State(state): State>, +async fn products( + State(state): State>, ) -> Result>, SoftwareError> { let products = state.product.products().await?; Ok(Json(products)) @@ -149,8 +149,8 @@ impl From for PatternStatus { #[utoipa::path(get, path = "/software/patterns", responses( (status = 200, description = "List of known software patterns") ))] -async fn patterns<'a>( - State(state): State>, +async fn patterns( + State(state): State>, ) -> Result>, SoftwareError> { let patterns = state.software.patterns(true).await?; let selected = state.software.selected_patterns().await?; From 4417e43032fd1e395453e65b426bd1bfef20a48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 15:30:49 +0000 Subject: [PATCH 14/19] software web functions return an result --- rust/agama-server/src/software/web.rs | 34 ++++++++++++++++----------- rust/agama-server/src/web.rs | 12 ++++++---- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 4fb8222fec..c863184d41 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -5,7 +5,7 @@ //! * `software_service` which returns the Axum service. //! * `software_stream` which offers an stream that emits the software events coming from D-Bus. -use crate::web::Event; +use crate::{error::Error, web::Event}; use agama_lib::{ error::ServiceError, product::{Product, ProductClient}, @@ -56,16 +56,18 @@ impl IntoResponse for SoftwareError { /// Returns an stream that emits software related events coming from D-Bus. /// /// * `connection`: D-Bus connection to listen for events. -pub async fn software_stream(dbus: zbus::Connection) -> impl Stream { - StreamExt::merge( - product_changed_stream(dbus.clone()).await, - patterns_changed_stream(dbus.clone()).await, - ) +pub async fn software_stream(dbus: zbus::Connection) -> Result, Error> { + Ok(StreamExt::merge( + product_changed_stream(dbus.clone()).await?, + patterns_changed_stream(dbus.clone()).await?, + )) } -async fn product_changed_stream(dbus: zbus::Connection) -> impl Stream { - let proxy = SoftwareProductProxy::new(&dbus).await.unwrap(); - proxy +async fn product_changed_stream( + dbus: zbus::Connection, +) -> Result, Error> { + let proxy = SoftwareProductProxy::new(&dbus).await?; + let stream = proxy .receive_selected_product_changed() .await .then(|change| async move { @@ -74,12 +76,15 @@ async fn product_changed_stream(dbus: zbus::Connection) -> impl Stream impl Stream { - let proxy = Software1Proxy::new(&dbus).await.unwrap(); - proxy +async fn patterns_changed_stream( + dbus: zbus::Connection, +) -> Result, Error> { + let proxy = Software1Proxy::new(&dbus).await?; + let stream = proxy .receive_selected_patterns_changed() .await .then(|change| async move { @@ -88,7 +93,8 @@ async fn patterns_changed_stream(dbus: zbus::Connection) -> impl Stream Result<(), ServiceError> { /// /// * `connection`: D-Bus connection. /// * `events`: channel to send the events to. -pub async fn run_events_monitor(connection: zbus::Connection, events: EventsSender) { - let stream = software_stream(connection).await; +pub async fn run_events_monitor(dbus: zbus::Connection, events: EventsSender) -> Result<(), Error> { + let stream = software_stream(dbus).await?; tokio::pin!(stream); let e = events.clone(); while let Some(event) = stream.next().await { _ = e.send(event); } + Ok(()) } From 46795899728a4b36bc230bb58f9148c20cddce84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 15:50:09 +0000 Subject: [PATCH 15/19] rust: add an endpoint /software/proposal --- rust/agama-lib/src/software/client.rs | 4 ++++ rust/agama-server/src/software/web.rs | 28 +++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index 401a9b7480..c866f8be34 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -111,4 +111,8 @@ impl<'a> SoftwareClient<'a> { Ok(()) } } + + pub async fn used_disk_space(&self) -> Result { + Ok(self.software_proxy.used_disk_space().await?) + } } diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index c863184d41..3d6f2cb25b 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -103,8 +103,9 @@ pub async fn software_service(dbus: zbus::Connection) -> Router { let software = SoftwareClient::new(dbus).await.unwrap(); let state = SoftwareState { product, software }; Router::new() - .route("/products", get(products)) .route("/patterns", get(patterns)) + .route("/products", get(products)) + .route("/proposal", get(proposal)) .route("/config", put(set_config).get(get_config)) .with_state(state) } @@ -196,7 +197,7 @@ async fn set_config( Ok(()) } -/// Returns the software configuration +/// Returns the software configuration. /// /// * `state` : service state. #[utoipa::path(get, path = "/software/config", responses( @@ -213,3 +214,26 @@ async fn get_config( }; Ok(Json(config)) } + +#[derive(Serialize, utoipa::ToSchema)] +/// Software proposal information. +struct SoftwareProposal { + /// Space required for installation. It is returned as a formatted string which includes + /// a number and a unit (e.g., "GiB"). + size: String, +} + +/// Returns the proposal information. +/// +/// At this point, only the required space is reported. +#[utoipa::path( + get, path = "/software/proposal", responses( + (status = 200, description = "Software proposal") +))] +async fn proposal( + State(state): State>, +) -> Result, SoftwareError> { + let size = state.software.used_disk_space().await?; + let proposal = SoftwareProposal { size }; + Ok(Json(proposal)) +} From 4896fb2ea44a5186f1b2a106191d32b3afb8b3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 21:40:27 +0000 Subject: [PATCH 16/19] rust: add an endpoint to start the software probing --- rust/agama-lib/src/software/client.rs | 5 +++++ rust/agama-server/src/software/web.rs | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index c866f8be34..63a4cd1214 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -115,4 +115,9 @@ impl<'a> SoftwareClient<'a> { pub async fn used_disk_space(&self) -> Result { Ok(self.software_proxy.used_disk_space().await?) } + + /// Starts the process to read the repositories data. + pub async fn probe(&self) -> Result<(), ServiceError> { + Ok(self.software_proxy.probe().await?) + } } diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 3d6f2cb25b..d9e4b335c8 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -18,7 +18,7 @@ use axum::{ extract::State, http::StatusCode, response::{IntoResponse, Response}, - routing::{get, put}, + routing::{get, post, put}, Json, Router, }; use serde::{Deserialize, Serialize}; @@ -107,6 +107,7 @@ pub async fn software_service(dbus: zbus::Connection) -> Router { .route("/products", get(products)) .route("/proposal", get(proposal)) .route("/config", put(set_config).get(get_config)) + .route("/probe", post(probe)) .with_state(state) } @@ -237,3 +238,15 @@ async fn proposal( let proposal = SoftwareProposal { size }; Ok(Json(proposal)) } + +/// Returns the proposal information. +/// +/// At this point, only the required space is reported. +#[utoipa::path( + post, path = "/software/probe", responses( + (status = 200, description = "Software proposal") +))] +async fn probe(State(state): State>) -> Result, SoftwareError> { + state.software.probe().await?; + Ok(Json(())) +} From 6d6ee2d96bbfd3091494f0957168fb04628cc6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 22:17:08 +0000 Subject: [PATCH 17/19] rust: PatternsChanged includes the list of patterns --- rust/agama-server/src/software/web.rs | 23 ++++++++++++++++++----- rust/agama-server/src/web/event.rs | 4 +++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index d9e4b335c8..5a9233cdcb 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -23,6 +23,7 @@ use axum::{ }; use serde::{Deserialize, Serialize}; use serde_json::json; +use std::collections::HashMap; use thiserror::Error; use tokio_stream::{Stream, StreamExt}; @@ -88,8 +89,10 @@ async fn patterns_changed_stream( .receive_selected_patterns_changed() .await .then(|change| async move { - if let Ok(_pattens) = change.get().await { - return Some(Event::PatternsChanged); + if let Ok(patterns) = change.get().await { + let patterns: HashMap = + patterns.into_iter().map(|(k, v)| (k, v.into())).collect(); + return Some(Event::PatternsChanged(patterns)); } None }) @@ -136,10 +139,10 @@ pub struct PatternEntry { /// Pattern status. #[derive(Serialize, Clone, Copy)] -enum PatternStatus { +pub enum PatternStatus { + UserSelected = 0, + AutoSelected = 1, Available, - UserSelected, - AutoSelected, } impl From for PatternStatus { @@ -151,6 +154,16 @@ impl From for PatternStatus { } } +impl From for PatternStatus { + fn from(value: u8) -> Self { + match value { + 0 => PatternStatus::UserSelected, + 1 => PatternStatus::AutoSelected, + _ => PatternStatus::Available, + } + } +} + /// Returns the list of software patterns. /// /// * `state`: service state. diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index d810007c82..8f6b3bb015 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,5 +1,7 @@ +use crate::software::web::PatternStatus; use agama_lib::progress::Progress; use serde::Serialize; +use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; #[derive(Clone, Serialize)] @@ -8,7 +10,7 @@ pub enum Event { LocaleChanged { locale: String }, Progress(Progress), ProductChanged { id: String }, - PatternsChanged, + PatternsChanged(HashMap), } pub type EventsSender = Sender; From ab55a05342340b3a71fe4300796eda86e3e8683c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 4 Mar 2024 22:24:02 +0000 Subject: [PATCH 18/19] rust: document used_disk_space --- rust/agama-lib/src/software/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index 63a4cd1214..b7f0a3a681 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -112,6 +112,9 @@ impl<'a> SoftwareClient<'a> { } } + /// Returns the required space for installing the selected patterns. + /// + /// It returns a formatted string including the size and the unit. pub async fn used_disk_space(&self) -> Result { Ok(self.software_proxy.used_disk_space().await?) } From c1b2853e0376326fef763de8ab57530ccdab49d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 5 Mar 2024 06:46:41 +0000 Subject: [PATCH 19/19] rust: merge PatternStatus and SelectedBy --- rust/agama-lib/src/software.rs | 2 +- rust/agama-lib/src/software/client.rs | 17 ++++++----- rust/agama-server/src/software/web.rs | 44 ++++++--------------------- rust/agama-server/src/web/event.rs | 5 ++- 4 files changed, 22 insertions(+), 46 deletions(-) diff --git a/rust/agama-lib/src/software.rs b/rust/agama-lib/src/software.rs index ccdc1fce18..43e2c1fd62 100644 --- a/rust/agama-lib/src/software.rs +++ b/rust/agama-lib/src/software.rs @@ -5,6 +5,6 @@ pub mod proxies; mod settings; mod store; -pub use client::{Pattern, SelectionReason, SoftwareClient}; +pub use client::{Pattern, SelectedBy, SoftwareClient}; pub use settings::SoftwareSettings; pub use store::SoftwareStore; diff --git a/rust/agama-lib/src/software/client.rs b/rust/agama-lib/src/software/client.rs index b7f0a3a681..239c1a16a4 100644 --- a/rust/agama-lib/src/software/client.rs +++ b/rust/agama-lib/src/software/client.rs @@ -22,19 +22,22 @@ pub struct Pattern { } /// Represents the reason why a pattern is selected. -#[derive(Clone, Copy)] -pub enum SelectionReason { +#[derive(Clone, Copy, Debug, Serialize)] +pub enum SelectedBy { /// The pattern was selected by the user. User = 0, /// The pattern was selected automatically. Auto = 1, + /// The pattern has not be selected. + None = 2, } -impl From for SelectionReason { +impl From for SelectedBy { fn from(value: u8) -> Self { match value { 0 => Self::User, - _ => Self::Auto, + 1 => Self::Auto, + _ => Self::None, } } } @@ -80,16 +83,14 @@ impl<'a> SoftwareClient<'a> { .selected_patterns() .await? .into_iter() - .filter(|(_id, reason)| *reason == SelectionReason::User as u8) + .filter(|(_id, reason)| *reason == SelectedBy::User as u8) .map(|(id, _reason)| id) .collect(); Ok(patterns) } /// Returns the selected pattern and the reason each one selected. - pub async fn selected_patterns( - &self, - ) -> Result, ServiceError> { + pub async fn selected_patterns(&self) -> Result, ServiceError> { let patterns = self.software_proxy.selected_patterns().await?; let patterns = patterns .into_iter() diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 5a9233cdcb..74bb2b2874 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -11,7 +11,7 @@ use agama_lib::{ product::{Product, ProductClient}, software::{ proxies::{Software1Proxy, SoftwareProductProxy}, - Pattern, SelectionReason, SoftwareClient, + Pattern, SelectedBy, SoftwareClient, }, }; use axum::{ @@ -90,7 +90,7 @@ async fn patterns_changed_stream( .await .then(|change| async move { if let Ok(patterns) = change.get().await { - let patterns: HashMap = + let patterns: HashMap = patterns.into_iter().map(|(k, v)| (k, v.into())).collect(); return Some(Event::PatternsChanged(patterns)); } @@ -134,34 +134,7 @@ async fn products( pub struct PatternEntry { #[serde(flatten)] pattern: Pattern, - status: PatternStatus, -} - -/// Pattern status. -#[derive(Serialize, Clone, Copy)] -pub enum PatternStatus { - UserSelected = 0, - AutoSelected = 1, - Available, -} - -impl From for PatternStatus { - fn from(value: SelectionReason) -> Self { - match value { - SelectionReason::User => Self::UserSelected, - SelectionReason::Auto => Self::AutoSelected, - } - } -} - -impl From for PatternStatus { - fn from(value: u8) -> Self { - match value { - 0 => PatternStatus::UserSelected, - 1 => PatternStatus::AutoSelected, - _ => PatternStatus::Available, - } - } + selected_by: SelectedBy, } /// Returns the list of software patterns. @@ -178,11 +151,14 @@ async fn patterns( let items = patterns .into_iter() .map(|pattern| { - let status: PatternStatus = selected + let selected_by: SelectedBy = selected .get(&pattern.id) - .map(|r| (*r).into()) - .unwrap_or(PatternStatus::Available); - PatternEntry { pattern, status } + .copied() + .unwrap_or(SelectedBy::None); + PatternEntry { + pattern, + selected_by, + } }) .collect(); diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 8f6b3bb015..a84956c2f3 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -1,5 +1,4 @@ -use crate::software::web::PatternStatus; -use agama_lib::progress::Progress; +use agama_lib::{progress::Progress, software::SelectedBy}; use serde::Serialize; use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; @@ -10,7 +9,7 @@ pub enum Event { LocaleChanged { locale: String }, Progress(Progress), ProductChanged { id: String }, - PatternsChanged(HashMap), + PatternsChanged(HashMap), } pub type EventsSender = Sender;