Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/agama-software/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
208 changes: 208 additions & 0 deletions rust/agama-software/examples/write_bench.rs
Original file line number Diff line number Diff line change
@@ -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::<Event>(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<Vec<Issue>> =
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<Vec<Issue>> =
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<Vec<Issue>> =
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
}
17 changes: 9 additions & 8 deletions rust/agama-software/src/zypp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
-------------------------------------------------------------------
Wed Feb 25 08:13:55 UTC 2026 - Josef Reidinger <jreidinger@suse.com>

- 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 <jreidinger@suse.com>

Expand Down
Loading