diff --git a/rust/agama-software/Cargo.toml b/rust/agama-software/Cargo.toml index 22603fa14d..6f94c89597 100644 --- a/rust/agama-software/Cargo.toml +++ b/rust/agama-software/Cargo.toml @@ -33,3 +33,4 @@ openssl = "0.10.75" serde_yaml = "0.9.34" tempfile = "3.23.0" tracing-subscriber = "0.3.18" +glob = "0.3.1" diff --git a/rust/agama-software/examples/write_bench.rs b/rust/agama-software/examples/write_bench.rs new file mode 100644 index 0000000000..cc15413ed3 --- /dev/null +++ b/rust/agama-software/examples/write_bench.rs @@ -0,0 +1,208 @@ +// Copyright (c) [2026] SUSE LLC +// +// All Rights Reserved. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, contact SUSE LLC. +// +// To contact SUSE LLC about this file by physical or electronic mail, you may +// find current contact information at www.suse.com. + +use agama_security as security; +use agama_software::state::SoftwareStateBuilder; +use agama_software::zypp_server::{SoftwareAction, ZyppServer, ZyppServerResult}; +use agama_utils::api::question::{Answer, AnswerRule, Config}; +use agama_utils::api::software::SoftwareConfig; +use agama_utils::products::Registry; +use agama_utils::{ + actor, + api::{event::Event, Issue}, + progress, question, +}; +use camino::{Utf8Path, Utf8PathBuf}; +use glob::glob; +use std::fs; +use std::path::Path; +use std::result::Result; +use std::time::SystemTime; +use tokio::sync::{broadcast, oneshot}; + +fn cleanup_past_leftovers(root_dir: &Path) { + remove_repos(root_dir); + remove_rpmdb(root_dir); +} + +fn remove_repos(root_dir: &Path) { + let repo_dir = root_dir.join("etc/zypp/repos.d/"); + + for path in glob(&format!("{}/*.repo", repo_dir.display())) + // unwrap OK: literal pattern syntax is correct + .unwrap() + .filter_map(Result::ok) + { + fs::remove_file(path).expect("failed to remove repo file"); + } +} + +fn remove_rpmdb(root_dir: &Path) { + let rpmdb_dir = root_dir.join("usr/lib/sysimage/rpm/"); + if fs::exists(&rpmdb_dir).unwrap_or(false) { + fs::remove_dir_all(rpmdb_dir).expect("removing RPM data failed"); + } +} + +#[tokio::main] +async fn main() { + let now = SystemTime::now(); + println!("now: {:?}", now); + + let root_dir = + Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../zypp-agama/fixtures/zypp_repos_root"); + cleanup_past_leftovers(root_dir.as_std_path()); + let zypp_root = + Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../zypp-agama/fixtures/zypp_root_tmp"); + + let install_dir = Utf8PathBuf::from("/mnt"); + let client = ZyppServer::start(&zypp_root, &install_dir).expect("starting zypp server failed"); + + // Setup event broadcast channel + let (event_tx, _event_rx) = broadcast::channel::(100); // Buffer size 100 + + // Spawn progress service + let progress_service_starter = progress::service::Starter::new(event_tx.clone()); + let progress_handler = progress_service_starter.start(); + + // Spawn question service + let question_service = question::service::Service::new(event_tx.clone()); + let question_handler = actor::spawn(question_service); + + // Pre-configure the answer to the GPG key question + let answer = Answer { + action: "Trust".to_string(), + value: None, + }; + let rule = AnswerRule { + class: Some("software.import_gpg".to_string()), + text: None, + data: None, + answer, + }; + let config = Config { + policy: None, + answers: Some(vec![rule]), + }; + question_handler + .call(question::message::SetConfig::new(Some(config))) + .await + .unwrap(); + + // Spawn the security service + let security_service_starter = security::service::Starter::new(question_handler.clone()); + let security_handler = security_service_starter.start().unwrap(); + + let (tx, rx) = oneshot::channel(); + + let product_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../products.d"); + let mut registry = Registry::new(product_dir); + registry.read().unwrap(); + let product = registry.find("Tumbleweed", None).unwrap(); + let software_state = SoftwareStateBuilder::for_product(&product).build(); + + println!( + "before first write: {:?}ms", + now.elapsed().unwrap().as_millis() + ); + let now = SystemTime::now(); + client + .send(SoftwareAction::Write { + state: software_state, + progress: progress_handler.clone(), + question: question_handler.clone(), + security: security_handler.clone(), + tx, + }) + .expect("Failed to send SoftwareAction::Write"); + + let result: ZyppServerResult> = + rx.await.expect("Failed to receive response from server"); + if let Err(err) = result { + panic!("SoftwareAction::Write failed: {:?}", err); + } + println!( + "after first write: {:?}ms", + now.elapsed().unwrap().as_millis() + ); + + let config = agama_utils::api::software::Config { + software: Some(SoftwareConfig { + patterns: Some(agama_utils::api::software::PatternsConfig::PatternsMap( + agama_utils::api::software::PatternsMap { + add: Some(vec!["gnome".to_string()]), + remove: None, + }, + )), + ..Default::default() + }), + ..Default::default() + }; + let software_state = SoftwareStateBuilder::for_product(&product) + .with_config(&config) + .build(); + let (tx, rx) = oneshot::channel(); + + let now = SystemTime::now(); + client + .send(SoftwareAction::Write { + state: software_state, + progress: progress_handler.clone(), + question: question_handler.clone(), + security: security_handler.clone(), + tx, + }) + .expect("Failed to send SoftwareAction::Write"); + + let result: ZyppServerResult> = + rx.await.expect("Failed to receive response from server"); + if let Err(err) = result { + panic!("SoftwareAction::Write failed: {:?}", err); + } + println!( + "after second write: {:?}ms", + now.elapsed().unwrap().as_millis() + ); + + let software_state = SoftwareStateBuilder::for_product(&product).build(); + let (tx, rx) = oneshot::channel(); + + let now = SystemTime::now(); + client + .send(SoftwareAction::Write { + state: software_state, + progress: progress_handler.clone(), + question: question_handler.clone(), + security: security_handler.clone(), + tx, + }) + .expect("Failed to send SoftwareAction::Write"); + + let result: ZyppServerResult> = + rx.await.expect("Failed to receive response from server"); + if let Err(err) = result { + panic!("SoftwareAction::Write failed: {:?}", err); + } + println!( + "after third write: {:?}ms", + now.elapsed().unwrap().as_millis() + ); + // NOTE: here we drop sender channel, which result in exit of zypp thread due to closed channel +} diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index a700c1fc4a..d69a1a7f59 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -329,7 +329,6 @@ impl ZyppServer { // TODO: add information about the current registration state let old_state = self.read(zypp)?; - if let Some(registration_config) = &state.registration { self.update_registration(registration_config, zypp, &security_srv, &mut issues); } @@ -358,7 +357,6 @@ impl ZyppServer { .iter() .filter(|r| !aliases.contains(&r.alias)) .collect(); - for repo in &to_add { let result = zypp.add_repository(&repo.alias, &repo.url, |percent, alias| { tracing::info!("Adding repository {} ({}%)", alias, percent); @@ -389,7 +387,7 @@ impl ZyppServer { } progress.cast(progress::message::Next::new(Scope::Software))?; - if to_add.is_empty() || to_remove.is_empty() { + if !to_add.is_empty() || !to_remove.is_empty() { let result = zypp.load_source( |percent, alias| { tracing::info!("Refreshing repositories: {} ({}%)", alias, percent); @@ -456,7 +454,10 @@ impl ZyppServer { self.only_required = state.options.only_required; tracing::info!("Install only required packages: {}", self.only_required); // run the solver to select the dependencies, ignore the errors, the solver runs again later - let _ = zypp.run_solver(self.only_required); + if let Ok(false) = zypp.run_solver(self.only_required) { + let message = gettext("There are conflicts in software selection"); + issues.push(Issue::new("software.conflict", &message)); + } // unselect packages including the autoselected dependencies for (name, r#type, selection) in &state.resolvables.to_vec() { @@ -466,10 +467,10 @@ impl ZyppServer { } _ = progress.cast(progress::message::Finish::new(Scope::Software)); - match zypp.run_solver(self.only_required) { - Ok(result) => println!("Solver result: {result}"), - Err(error) => println!("Solver failed: {error}"), - }; + if let Ok(false) = zypp.run_solver(self.only_required) { + let message = gettext("There are software conflict in software selection"); + issues.push(Issue::new("software.conflict", &message)); + } if let Err(e) = tx.send(Ok(issues)) { tracing::error!("failed to send list of issues after write: {:?}", e); diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 9e332d28f1..eb1e732c17 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Wed Feb 25 08:13:55 UTC 2026 - Josef Reidinger + +- Optimize writing software state to reduce time spend when + selecting pattern (bsc#1257794) +- report properly when software selection contain conflict + ------------------------------------------------------------------- Mon Feb 23 21:15:48 UTC 2026 - Josef Reidinger