From 806fedc09edd1edc512350b58fbeff0fc9ecc818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 18 Mar 2024 15:23:51 +0000 Subject: [PATCH 1/9] Add support to expose the Issues API --- rust/agama-lib/src/proxies.rs | 7 ++ rust/agama-server/src/web/common.rs | 125 +++++++++++++++++++++++++++- rust/agama-server/src/web/event.rs | 5 ++ 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index d460e88548..74fb587486 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -121,3 +121,10 @@ trait Questions1 { #[dbus_proxy(property)] fn set_interactive(&self, value: bool) -> zbus::Result<()>; } + +#[dbus_proxy(interface = "org.opensuse.Agama1.Issues", assume_defaults = true)] +trait Issues { + /// All property + #[dbus_proxy(property)] + fn all(&self) -> zbus::Result>; +} diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index 8739d6795b..2397018dff 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -5,7 +5,7 @@ use std::{pin::Pin, task::Poll}; use agama_lib::{ error::ServiceError, progress::Progress, - proxies::{ProgressProxy, ServiceStatusProxy}, + proxies::{IssuesProxy, ProgressProxy, ServiceStatusProxy}, }; use axum::{extract::State, routing::get, Json, Router}; use pin_project::pin_project; @@ -17,8 +17,8 @@ use crate::error::Error; use super::Event; -/// Builds a router to the `org.opensuse.Agama1.ServiceStatus` -/// interface of the given D-Bus object. +/// Builds a router to the `org.opensuse.Agama1.ServiceStatus` interface of the +/// given D-Bus object. /// /// ```no_run /// # use axum::{extract::State, routing::get, Json, Router}; @@ -231,3 +231,122 @@ async fn build_progress_proxy<'a>( .await?; Ok(proxy) } + +/// Builds a router to the `org.opensuse.Agama1.Issues` interface of a given +/// D-Bus object. +/// +/// ```no_run +/// # use axum::{extract::State, routing::get, Json, Router}; +/// # use agama_lib::connection; +/// # use agama_server::web::common::service_status_router; +/// # use tokio_test; +/// +/// # tokio_test::block_on(async { +/// async fn hello(state: State) {}; +/// +/// #[derive(Clone)] +/// struct HelloWorldState {}; +/// +/// let dbus = connection().await.unwrap(); +/// let issues_router = issues_router( +/// &dbus, "org.opensuse.HelloWorld", "/org/opensuse/hello" +/// ).await.unwrap(); +/// let router: Router = Router::new() +/// .route("/hello", get(hello)) +/// .merge(issues_router) +/// .with_state(HelloWorldState {}); +/// }); +/// ``` +/// +/// * `dbus`: D-Bus connection. +/// * `destination`: D-Bus service name. +/// * `path`: D-Bus object path. +pub async fn issues_router( + dbus: &zbus::Connection, + destination: &str, + path: &str, +) -> Result, ServiceError> { + let proxy = build_issues_proxy(dbus, destination, path).await?; + let state = IssuesState { proxy }; + Ok(Router::new() + .route("/issues", get(issues)) + .with_state(state)) +} + +async fn issues(State(state): State>) -> Result>, Error> { + let issues = state.proxy.all().await?; + let issues: Vec = issues.into_iter().map(Issue::from_tuple).collect(); + Ok(Json(issues)) +} + +#[derive(Clone)] +struct IssuesState<'a> { + proxy: IssuesProxy<'a>, +} + +#[derive(Clone, Debug, Serialize)] +pub struct Issue { + description: String, + details: Option, + source: u32, + severity: u32, +} + +impl Issue { + pub fn from_tuple( + (description, details, source, severity): (String, String, u32, u32), + ) -> Self { + let details = if details.is_empty() { + None + } else { + Some(details) + }; + + Self { + description, + details, + source, + severity, + } + } +} + +/// Builds a stream of the changes in the the `org.opensuse.Agama1.Issues` +/// interface of the given D-Bus object. +/// +/// * `dbus`: D-Bus connection. +/// * `destination`: D-Bus service name. +/// * `path`: D-Bus object path. +pub async fn issues_stream( + dbus: zbus::Connection, + destination: &'static str, + path: &'static str, +) -> Result + Send>>, Error> { + let proxy = build_issues_proxy(&dbus, destination, path).await?; + let stream = proxy + .receive_all_changed() + .await + .then(move |change| async move { + if let Ok(issues) = change.get().await { + let issues = issues.into_iter().map(Issue::from_tuple).collect(); + Some(Event::IssuesChanged { issues }) + } else { + None + } + }) + .filter_map(|e| e); + Ok(Box::pin(stream)) +} + +async fn build_issues_proxy<'a>( + dbus: &zbus::Connection, + destination: &str, + path: &str, +) -> Result, zbus::Error> { + let proxy = IssuesProxy::builder(&dbus) + .destination(destination.to_string())? + .path(path.to_string())? + .build() + .await?; + Ok(proxy) +} diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 045d4804ba..32244c26cd 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -4,6 +4,8 @@ use serde::Serialize; use std::collections::HashMap; use tokio::sync::broadcast::{Receiver, Sender}; +use super::common::Issue; + #[derive(Clone, Debug, Serialize)] #[serde(tag = "type")] pub enum Event { @@ -30,6 +32,9 @@ pub enum Event { service: String, status: u32, }, + IssuesChanged { + issues: Vec, + }, } pub type EventsSender = Sender; From 470ec1845ade3c0eeda12c13c83234f75093a7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 12:40:44 +0000 Subject: [PATCH 2/9] Support using several Issues interfaces on the same service --- rust/agama-server/src/web/common.rs | 10 ++++++---- rust/agama-server/src/web/event.rs | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index 2397018dff..5484111121 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -268,9 +268,7 @@ pub async fn issues_router( ) -> Result, ServiceError> { let proxy = build_issues_proxy(dbus, destination, path).await?; let state = IssuesState { proxy }; - Ok(Router::new() - .route("/issues", get(issues)) - .with_state(state)) + Ok(Router::new().route("/", get(issues)).with_state(state)) } async fn issues(State(state): State>) -> Result>, Error> { @@ -329,7 +327,11 @@ pub async fn issues_stream( .then(move |change| async move { if let Ok(issues) = change.get().await { let issues = issues.into_iter().map(Issue::from_tuple).collect(); - Some(Event::IssuesChanged { issues }) + Some(Event::IssuesChanged { + service: destination.to_string(), + path: path.to_string(), + issues, + }) } else { None } diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 32244c26cd..999e4d6619 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -33,6 +33,8 @@ pub enum Event { status: u32, }, IssuesChanged { + service: String, + path: String, issues: Vec, }, } From 4be905e4fac33f9859f18bc2ea2ce71a6372ba47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 13:50:18 +0000 Subject: [PATCH 3/9] Expose the issues in the Software service --- rust/agama-server/src/software/web.rs | 7 ++++++- rust/agama-server/src/web.rs | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index de7e907501..b1d73768e5 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -8,7 +8,7 @@ use crate::{ error::Error, web::{ - common::{progress_router, service_status_router}, + common::{issues_router, progress_router, service_status_router}, Event, }, }; @@ -117,6 +117,9 @@ pub async fn software_service(dbus: zbus::Connection) -> Result Result Res ) .await?, ); + stream.insert( + "software-issues", + issues_stream( + dbus.clone(), + "org.opensuse.Agama.Software1", + "/org/opensuse/Agama/Software1", + ) + .await?, + ); + stream.insert( + "software-product-issues", + issues_stream( + dbus.clone(), + "org.opensuse.Agama.Software1", + "/org/opensuse/Agama/Software1/Product", + ) + .await?, + ); tokio::pin!(stream); let e = events.clone(); From 9e2dec02c7dd6d35eeef55fb7239026d22be8bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 11:05:39 +0000 Subject: [PATCH 4/9] Adapt WithIssues to the HTTP/JSON API --- web/src/client/mixins.js | 118 +++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 67 deletions(-) diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 9523cdd40e..ddbf57db38 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -61,86 +61,70 @@ const VALIDATION_IFACE = "org.opensuse.Agama1.Validation"; */ /** -* @callback IssuesHandler -* @param {Issue[]} issues -* @return {void} -*/ - -/** - * Builds an issue from a D-Bus issue - * - * @param {DBusIssue} dbusIssue - * @return {Issue} + * @callback IssuesHandler + * @param {Issue[]} issues + * @return {void} */ -const buildIssue = (dbusIssue) => { - const source = (value) => { - switch (value) { - case 0: return "unknown"; - case 1: return "system"; - case 2: return "config"; - } - }; - const severity = (value) => { - return value === 0 ? "warn" : "error"; - }; +const ISSUES_SOURCES = [ + "unknown", + "system", + "config", +]; +const buildIssue = ({ description, details, source, severity }) => { return { - description: dbusIssue[0], - details: dbusIssue[1], - source: source(dbusIssue[2]), - severity: severity(dbusIssue[3]) + description, + details, + source: ISSUES_SOURCES[source], + severity: severity === 0 ? "warn" : "error", }; }; /** * Extends the given class with methods to get the issues over D-Bus - * @param {string} object_path - object_path + * + * @template {!WithHTTPClient} T * @param {T} superclass - superclass to extend - * @template {!WithDBusProxies} T + * @param {string} issues_path - validation resource path (e.g., "/manager/issues"). + * @param {string} dbus_path - service name (e.g., "/org/opensuse/Agama/Software1/product"). */ -const WithIssues = (superclass, object_path) => class extends superclass { - constructor(...args) { - super(...args); - this.proxies.issues = this.client.proxy(ISSUES_IFACE, object_path); - } - - /** - * Returns the issues - * - * @return {Promise} - */ - async getIssues() { - const proxy = await this.proxies.issues; - return proxy.All.map(buildIssue); - } +const WithIssues = (superclass, issues_path, dbus_path) => + class extends superclass { + /** + * Returns the issues + * + * @return {Promise} + */ + async getIssues() { + const response = await this.client.get(issues_path); + return response.issues.map(buildIssue); + } - /** - * Gets all issues with error severity - * - * @return {Promise} - */ - async getErrors() { - const issues = await this.getIssues(); - return issues.filter(i => i.severity === "error"); - } + /** + * Gets all issues with error severity + * + * @return {Promise} + */ + async getErrors() { + const issues = await this.getIssues(); + return issues.filter((i) => i.severity === "error"); + } - /** - * Registers a callback to run when the issues change - * - * @param {IssuesHandler} handler - callback function - * @return {import ("./dbus").RemoveFn} function to disable the callback - */ - onIssuesChange(handler) { - return this.client.onObjectChanged(object_path, ISSUES_IFACE, (changes) => { - if ("All" in changes) { - const dbusIssues = changes.All.v; - const issues = dbusIssues.map(buildIssue); - handler(issues); - } - }); - } -}; + /** + * Registers a callback to run when the issues change + * + * @param {IssuesHandler} handler - callback function + * @return {import ("./http").RemoveFn} function to disable the callback + */ + onIssuesChange(handler) { + return this.client.onEvent("IssuesChanged", ({ path, issues }) => { + if (path === dbus_path) { + handler(issues.map(buildIssue)); + } + }); + } + }; /** * Extends the given class with methods to get and track the service status From 2a89a6d38dac99b8a29d4d0bc7c9aafad68542fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 11:08:21 +0000 Subject: [PATCH 5/9] Small refactor of WithProgress#onProgressChange --- web/src/client/mixins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index ddbf57db38..144b104337 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -210,8 +210,8 @@ const WithProgress = (superclass, progress_path, service_name) => * @return {import ("./dbus").RemoveFn} function to disable the callback */ onProgressChange(handler) { - return this.client.onEvent("Progress", (progress) => { - if (progress?.service === service_name) { + return this.client.onEvent("Progress", ({ service, ...progress }) => { + if (service === service_name) { const { current_step, max_steps, current_title, finished } = progress; handler({ total: max_steps, From 0ec75110f23e37e4963f16a6a27fdd4d687d377a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 12:50:04 +0000 Subject: [PATCH 6/9] Apply the WithIssues mixin to the ProductClient --- web/src/client/software.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/client/software.js b/web/src/client/software.js index b8d8b1eb1f..0a9cbd0b8c 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -303,7 +303,7 @@ class SoftwareClient extends WithIssues( SOFTWARE_PATH, ) {} -class ProductClient { +class ProductBaseClient { /** * @param {import("./http").HTTPClient} client - HTTP client. */ @@ -355,4 +355,6 @@ class ProductClient { } } +class ProductClient extends WithIssues(ProductBaseClient, "/issues/product", PRODUCT_PATH) {} + export { ProductClient, SoftwareClient }; From 157e9302efd2b5ebebb27d7665b6334417fd0b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 13:46:33 +0000 Subject: [PATCH 7/9] Fix handling of status changes in WithStatus --- web/src/client/mixins.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 144b104337..c7a9b28b7d 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -97,8 +97,8 @@ const WithIssues = (superclass, issues_path, dbus_path) => * @return {Promise} */ async getIssues() { - const response = await this.client.get(issues_path); - return response.issues.map(buildIssue); + const issues = await this.client.get(issues_path); + return issues.map(buildIssue); } /** @@ -153,8 +153,8 @@ const WithStatus = (superclass, status_path, service_name) => * @return {function} function to disable the callback */ onStatusChange(handler) { - return this.client.onEvent("StatusChanged", ({ status, service }) => { - if (service === service_name && status) { + return this.client.onEvent("ServiceStatusChanged", ({ status, service }) => { + if (service === service_name) { handler(status); } }); From 29a1d8feed563f8c0bdcecc317b201b3b7057389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 19 Mar 2024 14:05:58 +0000 Subject: [PATCH 8/9] Use a constant when calling issues_router --- rust/agama-server/src/software/web.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index b1d73768e5..f5c7bd6cb5 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -114,12 +114,12 @@ fn reason_to_selected_by( pub async fn software_service(dbus: zbus::Connection) -> Result { const DBUS_SERVICE: &'static str = "org.opensuse.Agama.Software1"; const DBUS_PATH: &'static str = "/org/opensuse/Agama/Software1"; + const DBUS_PRODUCT_PATH: &'static str = "/org/opensuse/Agama/Software1/Product"; let status_router = service_status_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; let progress_router = progress_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; let software_issues = issues_router(&dbus, DBUS_SERVICE, DBUS_PATH).await?; - let product_issues = - issues_router(&dbus, DBUS_SERVICE, "/org/opensuse/Agama/Software1/Product").await?; + let product_issues = issues_router(&dbus, DBUS_SERVICE, DBUS_PRODUCT_PATH).await?; let product = ProductClient::new(dbus.clone()).await?; let software = SoftwareClient::new(dbus).await?; From 957e3f64c0f6675dd9910907322aa6d4692bb365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 20 Mar 2024 07:00:08 +0000 Subject: [PATCH 9/9] Fix WithIssues path for ProductClient --- web/src/client/software.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/client/software.js b/web/src/client/software.js index 0a9cbd0b8c..8ed8f2e8f7 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -355,6 +355,7 @@ class ProductBaseClient { } } -class ProductClient extends WithIssues(ProductBaseClient, "/issues/product", PRODUCT_PATH) {} +class ProductClient + extends WithIssues(ProductBaseClient, "software/issues/product", PRODUCT_PATH) {} export { ProductClient, SoftwareClient };