diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a9d6ef75f1..afd017c6e7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -235,6 +235,7 @@ dependencies = [ "agama-locale-data", "agama-utils", "async-trait", + "gettext-rs", "glob", "regex", "serde", @@ -5729,6 +5730,7 @@ dependencies = [ name = "zypp-agama" version = "0.1.0" dependencies = [ + "tracing", "url", "zypp-agama-sys", ] @@ -5738,4 +5740,5 @@ name = "zypp-agama-sys" version = "0.1.0" dependencies = [ "bindgen 0.72.1", + "tracing", ] diff --git a/rust/agama-manager/src/start.rs b/rust/agama-manager/src/start.rs index 22723f504e..631159939c 100644 --- a/rust/agama-manager/src/start.rs +++ b/rust/agama-manager/src/start.rs @@ -57,7 +57,7 @@ pub async fn start( let progress = progress::start(events.clone()).await?; let l10n = l10n::start(issues.clone(), events.clone()).await?; let network = network::start().await?; - let software = software::start(issues.clone(), progress.clone(), events.clone()).await?; + let software = software::start(issues.clone(), &progress, &questions, events.clone()).await?; let storage = storage::start(progress.clone(), issues.clone(), events.clone(), dbus).await?; let mut service = Service::new( diff --git a/rust/agama-software/Cargo.toml b/rust/agama-software/Cargo.toml index b10801571e..aedecf13e5 100644 --- a/rust/agama-software/Cargo.toml +++ b/rust/agama-software/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true agama-locale-data = { path = "../agama-locale-data" } agama-utils = { path = "../agama-utils" } async-trait = "0.1.89" +gettext-rs = { version = "0.7.1", features = ["gettext-system"] } glob = "0.3.1" regex = "1.11.0" serde = { version = "1.0.210", features = ["derive"] } diff --git a/rust/agama-software/src/callbacks.rs b/rust/agama-software/src/callbacks.rs new file mode 100644 index 0000000000..0d7e40dfb1 --- /dev/null +++ b/rust/agama-software/src/callbacks.rs @@ -0,0 +1 @@ +pub mod commit_download; diff --git a/rust/agama-software/src/callbacks/commit_download.rs b/rust/agama-software/src/callbacks/commit_download.rs new file mode 100644 index 0000000000..d2140231b2 --- /dev/null +++ b/rust/agama-software/src/callbacks/commit_download.rs @@ -0,0 +1,99 @@ +use agama_utils::{actor::Handler, api::question::QuestionSpec, progress, question}; +use gettextrs::gettext; +use tokio::runtime::Handle; +use zypp_agama::callbacks::pkg_download::{Callback, DownloadError}; + +#[derive(Clone)] +pub struct CommitDownload { + progress: Handler, + questions: Handler, +} + +impl CommitDownload { + pub fn new( + progress: Handler, + questions: Handler, + ) -> Self { + Self { + progress, + questions, + } + } +} + +impl Callback for CommitDownload { + fn start_preload(&self) { + // TODO: report progress that we start preloading packages + tracing::info!("Start preload"); + } + + fn problem( + &self, + name: &str, + error: DownloadError, + description: &str, + ) -> zypp_agama::callbacks::ProblemResponse { + // TODO: make it generic for any problemResponse questions + let labels = [gettext("Retry"), gettext("Ignore")]; + let actions = [ + ("Retry", labels[0].as_str()), + ("Ignore", labels[1].as_str()), + ]; + let error_str = error.to_string(); + let data = [("package", name), ("error_code", error_str.as_str())]; + let question = QuestionSpec::new(description, "software.package_error.provide_error") + .with_actions(&actions) + .with_data(&data); + let result = Handle::current().block_on(async move { + self.questions + .call(question::message::Ask::new(question)) + .await + }); + let Ok(answer) = result else { + tracing::warn!("Failed to ask question {:?}", result); + return zypp_agama::callbacks::ProblemResponse::ABORT; + }; + + let Some(answer_str) = answer.answer else { + tracing::warn!("No answer provided"); + return zypp_agama::callbacks::ProblemResponse::ABORT; + }; + + answer_str + .action + .as_str() + .parse::() + .unwrap_or(zypp_agama::callbacks::ProblemResponse::ABORT) + } + + fn gpg_check( + &self, + resolvable_name: &str, + _repo_url: &str, + check_result: zypp_agama::callbacks::pkg_download::GPGCheckResult, + ) -> Option { + if check_result == zypp_agama::callbacks::pkg_download::GPGCheckResult::Ok { + // GPG is happy, so we are also happy and lets just continue + return None; + } + + // do not log URL here as it can contain sensitive info and it is visible from other logs + tracing::warn!( + "GPG check failed for {:?} with {:?}", + resolvable_name, + check_result + ); + + // TODO: implement the DUD case: + // DUD (Driver Update Disk) + // ignore the error when the package comes from the DUD repository and + // the DUD package GPG checks are disabled via a boot option + // + // if repo_url == Agama::Software::Manager.dud_repository_url && ignore_dud_packages_gpg_errors? { + // logger.info "Ignoring the GPG check failure for a DUD package" + // return Ok(ProblemResponse::IGNORE); + // } + + None + } +} diff --git a/rust/agama-software/src/lib.rs b/rust/agama-software/src/lib.rs index 808d465c07..b38d4f5669 100644 --- a/rust/agama-software/src/lib.rs +++ b/rust/agama-software/src/lib.rs @@ -44,5 +44,6 @@ pub use service::Service; mod model; pub use model::{Model, ModelAdapter, Resolvable, ResolvableType}; +mod callbacks; pub mod message; mod zypp_server; diff --git a/rust/agama-software/src/model.rs b/rust/agama-software/src/model.rs index 386a55bc04..4f47bcc348 100644 --- a/rust/agama-software/src/model.rs +++ b/rust/agama-software/src/model.rs @@ -25,7 +25,7 @@ use agama_utils::{ Issue, }, products::{ProductSpec, UserPattern}, - progress, + progress, question, }; use async_trait::async_trait; use tokio::sync::{mpsc, oneshot}; @@ -79,14 +79,22 @@ pub struct Model { zypp_sender: mpsc::UnboundedSender, // FIXME: what about having a SoftwareServiceState to keep business logic state? selected_product: Option, + progress: Handler, + question: Handler, } impl Model { /// Initializes the struct with the information from the underlying system. - pub fn new(zypp_sender: mpsc::UnboundedSender) -> Result { + pub fn new( + zypp_sender: mpsc::UnboundedSender, + progress: Handler, + question: Handler, + ) -> Result { Ok(Self { zypp_sender, selected_product: None, + progress, + question, }) } } @@ -144,7 +152,11 @@ impl ModelAdapter for Model { async fn install(&self) -> Result { let (tx, rx) = oneshot::channel(); - self.zypp_sender.send(SoftwareAction::Install(tx))?; + self.zypp_sender.send(SoftwareAction::Install( + tx, + self.progress.clone(), + self.question.clone(), + ))?; Ok(rx.await??) } diff --git a/rust/agama-software/src/start.rs b/rust/agama-software/src/start.rs index 4e802835dd..96622b295b 100644 --- a/rust/agama-software/src/start.rs +++ b/rust/agama-software/src/start.rs @@ -26,7 +26,7 @@ use crate::{ use agama_utils::{ actor::{self, Handler}, api::event, - issue, progress, + issue, progress, question, }; #[derive(thiserror::Error, Debug)] @@ -49,12 +49,13 @@ pub enum Error { /// * `issues`: handler to the issues service. pub async fn start( issues: Handler, - progress: Handler, + progress: &Handler, + question: &Handler, events: event::Sender, ) -> Result, Error> { let zypp_sender = ZyppServer::start()?; - let model = Model::new(zypp_sender)?; - let mut service = Service::new(model, issues, progress, events); + let model = Model::new(zypp_sender, progress.clone(), question.clone())?; + let mut service = Service::new(model, issues, progress.clone(), events); // FIXME: this should happen after spawning the task. service.setup().await?; let handler = actor::spawn(service); diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index 63f7030828..0f56dc06ed 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -25,7 +25,7 @@ use agama_utils::{ Issue, IssueSeverity, Scope, }, products::ProductSpec, - progress, + progress, question, }; use std::path::Path; use tokio::sync::{ @@ -34,7 +34,11 @@ use tokio::sync::{ }; use zypp_agama::ZyppError; -use crate::model::state::{self, SoftwareState}; +use crate::{ + callbacks::commit_download, + model::state::{self, SoftwareState}, +}; + const TARGET_DIR: &str = "/run/agama/software_ng_zypp"; const GPG_KEYS: &str = "/usr/lib/rpm/gnupg/keys/gpg-*"; @@ -86,7 +90,11 @@ pub enum ZyppServerError { pub type ZyppServerResult = Result; pub enum SoftwareAction { - Install(oneshot::Sender>), + Install( + oneshot::Sender>, + Handler, + Handler, + ), Finish(oneshot::Sender>), GetPatternsMetadata(Vec, oneshot::Sender>>), ComputeProposal( @@ -172,8 +180,9 @@ impl ZyppServer { SoftwareAction::GetPatternsMetadata(names, tx) => { self.get_patterns(names, tx, zypp).await?; } - SoftwareAction::Install(tx) => { - tx.send(self.install(zypp)) + SoftwareAction::Install(tx, progress, question) => { + let callback = commit_download::CommitDownload::new(progress, question); + tx.send(self.install(zypp, &callback)) .map_err(|_| ZyppDispatchError::ResponseChannelClosed)?; } SoftwareAction::Finish(tx) => { @@ -187,10 +196,15 @@ impl ZyppServer { } // Install rpms - fn install(&self, zypp: &zypp_agama::Zypp) -> ZyppServerResult { + fn install( + &self, + zypp: &zypp_agama::Zypp, + download_callback: &commit_download::CommitDownload, + ) -> ZyppServerResult { let target = "/mnt"; zypp.switch_target(target)?; - let result = zypp.commit()?; + // TODO: write real install callbacks beside download ones + let result = zypp.commit(download_callback)?; tracing::info!("libzypp commit ends with {}", result); Ok(result) } diff --git a/rust/zypp-agama/Cargo.toml b/rust/zypp-agama/Cargo.toml index a3df587f59..3a0d9d51ec 100644 --- a/rust/zypp-agama/Cargo.toml +++ b/rust/zypp-agama/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] zypp-agama-sys = { path="./zypp-agama-sys" } url = "2.5.7" +tracing = "0.1.41" diff --git a/rust/zypp-agama/README.md b/rust/zypp-agama/README.md new file mode 100644 index 0000000000..5c20edaab9 --- /dev/null +++ b/rust/zypp-agama/README.md @@ -0,0 +1,22 @@ +## Zypp Agama + +crate which purpose is to have thin layer to libzypp for agama purpose. + +### How to Add New Libzypp Call + +- at first create its C API in `zypp-agama-sys/c-layer/include` directory and write its implementation to cxx file. +- generate new FFI bindings (in low level, unsafe Rust), in `rust/zypp-agama-sys` by running cargo build +- write a (regular, safe) Rust wrapper, in `src` + +### Libzypp Notes + +- libzypp is not thread safe +- for seeing how it works see yast2-pkg-bindings and zypper as some parameters in calls are ignored +- goal is to have thin layer close to libzypp and build logic on top of it in more advanced language + +### Interesting Resources + +- https://doc.rust-lang.org/nomicon/ffi.html +- https://adventures.michaelfbryan.com/posts/rust-closures-in-ffi/ +- https://www.khoury.northeastern.edu/home/lth/larceny/notes/note7-ffi.html +- https://cliffle.com/blog/not-thread-safe/ ( interesting part how to ensure in rust that some data is not thread safe ) \ No newline at end of file diff --git a/rust/zypp-agama/src/callbacks.rs b/rust/zypp-agama/src/callbacks.rs index 9c611ce525..a773425dc3 100644 --- a/rust/zypp-agama/src/callbacks.rs +++ b/rust/zypp-agama/src/callbacks.rs @@ -1,13 +1,7 @@ -use std::os::raw::{c_char, c_int, c_void}; +use std::{fmt, str::FromStr}; -use zypp_agama_sys::{ - DownloadProgressCallbacks, ZyppDownloadFinishCallback, ZyppDownloadProblemCallback, - ZyppDownloadProgressCallback, ZyppDownloadStartCallback, PROBLEM_RESPONSE, - PROBLEM_RESPONSE_PROBLEM_ABORT, PROBLEM_RESPONSE_PROBLEM_IGNORE, - PROBLEM_RESPONSE_PROBLEM_RETRY, -}; - -use crate::helpers::string_from_ptr; +pub mod download_progress; +pub mod pkg_download; // empty progress callback pub fn empty_progress(_value: i64, _text: String) -> bool { @@ -20,141 +14,36 @@ pub enum ProblemResponse { IGNORE, } -impl From for PROBLEM_RESPONSE { +impl From for zypp_agama_sys::PROBLEM_RESPONSE { fn from(response: ProblemResponse) -> Self { match response { - ProblemResponse::ABORT => PROBLEM_RESPONSE_PROBLEM_ABORT, - ProblemResponse::IGNORE => PROBLEM_RESPONSE_PROBLEM_IGNORE, - ProblemResponse::RETRY => PROBLEM_RESPONSE_PROBLEM_RETRY, + ProblemResponse::ABORT => zypp_agama_sys::PROBLEM_RESPONSE_PROBLEM_ABORT, + ProblemResponse::IGNORE => zypp_agama_sys::PROBLEM_RESPONSE_PROBLEM_IGNORE, + ProblemResponse::RETRY => zypp_agama_sys::PROBLEM_RESPONSE_PROBLEM_RETRY, } } } -// generic trait to -pub trait DownloadProgress { - // callback when download start - fn start(&self, _url: &str, _localfile: &str) {} - // callback when download is in progress - fn progress(&self, _value: i32, _url: &str, _bps_avg: f64, _bps_current: f64) -> bool { - true - } - // callback when problem occurs - fn problem(&self, _url: &str, _error_id: i32, _description: &str) -> ProblemResponse { - ProblemResponse::ABORT - } - // callback when download finishes either successfully or with error - fn finish(&self, _url: &str, _error_id: i32, _reason: &str) {} -} - -// Default progress that do nothing -pub struct EmptyDownloadProgress; -impl DownloadProgress for EmptyDownloadProgress {} - -unsafe extern "C" fn download_progress_start( - url: *const c_char, - localfile: *const c_char, - user_data: *mut c_void, -) where - F: FnMut(String, String), -{ - let user_data = &mut *(user_data as *mut F); - user_data(string_from_ptr(url), string_from_ptr(localfile)); -} - -fn get_download_progress_start(_closure: &F) -> ZyppDownloadStartCallback -where - F: FnMut(String, String), -{ - Some(download_progress_start::) -} - -unsafe extern "C" fn download_progress_progress( - value: c_int, - url: *const c_char, - bps_avg: f64, - bps_current: f64, - user_data: *mut c_void, -) -> bool -where - F: FnMut(i32, String, f64, f64) -> bool, -{ - let user_data = &mut *(user_data as *mut F); - user_data(value, string_from_ptr(url), bps_avg, bps_current) -} +impl FromStr for ProblemResponse { + type Err = String; -fn get_download_progress_progress(_closure: &F) -> ZyppDownloadProgressCallback -where - F: FnMut(i32, String, f64, f64) -> bool, -{ - Some(download_progress_progress::) -} - -unsafe extern "C" fn download_progress_problem( - url: *const c_char, - error: c_int, - description: *const c_char, - user_data: *mut c_void, -) -> PROBLEM_RESPONSE -where - F: FnMut(String, c_int, String) -> ProblemResponse, -{ - let user_data = &mut *(user_data as *mut F); - let res = user_data(string_from_ptr(url), error, string_from_ptr(description)); - res.into() -} - -fn get_download_progress_problem(_closure: &F) -> ZyppDownloadProblemCallback -where - F: FnMut(String, c_int, String) -> ProblemResponse, -{ - Some(download_progress_problem::) -} - -unsafe extern "C" fn download_progress_finish( - url: *const c_char, - error: c_int, - reason: *const c_char, - user_data: *mut c_void, -) where - F: FnMut(String, c_int, String), -{ - let user_data = &mut *(user_data as *mut F); - user_data(string_from_ptr(url), error, string_from_ptr(reason)); -} - -fn get_download_progress_finish(_closure: &F) -> ZyppDownloadFinishCallback -where - F: FnMut(String, c_int, String), -{ - Some(download_progress_finish::) + fn from_str(s: &str) -> Result { + match s { + "Retry" => Ok(ProblemResponse::RETRY), + "Ignore" => Ok(ProblemResponse::IGNORE), + "Abort" => Ok(ProblemResponse::ABORT), + _ => Err(format!("Unknown action {:?}", s)), + } + } } -pub(crate) fn with_c_download_callbacks(callbacks: &impl DownloadProgress, block: &mut F) -> R -where - F: FnMut(DownloadProgressCallbacks) -> R, -{ - let mut start_call = |url: String, localfile: String| callbacks.start(&url, &localfile); - let cb_start = get_download_progress_start(&start_call); - let mut progress_call = |value, url: String, bps_avg, bps_current| { - callbacks.progress(value, &url, bps_avg, bps_current) - }; - let cb_progress = get_download_progress_progress(&progress_call); - let mut problem_call = - |url: String, error, description: String| callbacks.problem(&url, error, &description); - let cb_problem = get_download_progress_problem(&problem_call); - let mut finish_call = - |url: String, error, description: String| callbacks.finish(&url, error, &description); - let cb_finish = get_download_progress_finish(&finish_call); - - let callbacks = DownloadProgressCallbacks { - start: cb_start, - start_data: &mut start_call as *mut _ as *mut c_void, - progress: cb_progress, - progress_data: &mut progress_call as *mut _ as *mut c_void, - problem: cb_problem, - problem_data: &mut problem_call as *mut _ as *mut c_void, - finish: cb_finish, - finish_data: &mut finish_call as *mut _ as *mut c_void, - }; - block(callbacks) +impl fmt::Display for ProblemResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + ProblemResponse::ABORT => "Abort", + ProblemResponse::IGNORE => "Ignore", + ProblemResponse::RETRY => "Retry", + }; + write!(f, "{}", s) + } } diff --git a/rust/zypp-agama/src/callbacks/download_progress.rs b/rust/zypp-agama/src/callbacks/download_progress.rs new file mode 100644 index 0000000000..103eec5128 --- /dev/null +++ b/rust/zypp-agama/src/callbacks/download_progress.rs @@ -0,0 +1,214 @@ +use std::{ + fmt::Display, + os::raw::{c_char, c_int, c_void}, +}; + +use crate::{ + callbacks::ProblemResponse, + helpers::{as_c_void, string_from_ptr}, +}; + +pub enum DownloadError { + NoError, + NotFound, // the requested Url was not found + IO, // IO error + AccessDenied, // user authent. failed while accessing restricted file + Error, // other error +} + +impl From for DownloadError { + fn from(error: zypp_agama_sys::DownloadProgressError) -> Self { + match error { + zypp_agama_sys::DownloadProgressError_DPE_NO_ERROR => DownloadError::NoError, + zypp_agama_sys::DownloadProgressError_DPE_NOT_FOUND => DownloadError::NotFound, + zypp_agama_sys::DownloadProgressError_DPE_ACCESS_DENIED => DownloadError::AccessDenied, + zypp_agama_sys::DownloadProgressError_DPE_IO => DownloadError::IO, + zypp_agama_sys::DownloadProgressError_DPE_ERROR => DownloadError::Error, + _ => { + tracing::error!("Unknown error code {:?}", error); + DownloadError::Error + } + } + } +} + +impl Display for DownloadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + DownloadError::NoError => "NoError", + DownloadError::NotFound => "NotFound", + DownloadError::IO => "IO", + DownloadError::AccessDenied => "AccessDenied", + DownloadError::Error => "Error", + }; + write!(f, "{}", str) + } +} + +/// A trait for handling download progress callbacks from `libzypp`. +/// +/// Implementors of this trait can be used to monitor download of repository +/// metadata in refresh_metadata method (usage can be extended in future). +pub trait Callback { + /// Called when a download starts. + /// + /// # Parameters + /// + /// * `_url`: The URL of the file being downloaded. + /// * `_localfile`: The local path where the file will be stored. + fn start(&self, _url: &str, _localfile: &str) {} + + /// Called periodically to report download progress. + /// + /// # Parameters + /// + /// * `_value`: The progress of the download, typically a percentage from 0 to 100. + /// * `_url`: The URL of the file being downloaded. + /// * `_bps_avg`: The average download speed in bytes per second. + /// * `_bps_current`: The current download speed in bytes per second. + /// + /// # Returns + /// + /// `true` to continue the download, `false` to abort it. + fn progress(&self, _value: i32, _url: &str, _bps_avg: f64, _bps_current: f64) -> bool { + true + } + + /// Called when a problem occurs during the download. + /// + /// # Parameters + /// + /// * `_url`: The URL of the file being downloaded. + /// * `_error_id`: The type of error that occurred. [DownloadError::NoError] should not happen. + /// * `_description`: A human-readable description of the error. + /// + /// # Returns + /// + /// A [ProblemResponse] indicating how to proceed (e.g., abort, retry, ignore). + fn problem(&self, _url: &str, _error_id: DownloadError, _description: &str) -> ProblemResponse { + ProblemResponse::ABORT + } + + /// Called when the download finishes, either successfully or with an error. + /// + /// # Parameters + /// + /// * `_url`: The URL of the downloaded file. + /// * `_error_id`: [DownloadError::NoError] on success, or the specific error on failure. + /// * `_reason`: A string providing more details about the finish status. + fn finish(&self, _url: &str, _error_id: DownloadError, _reason: &str) {} +} + +// Default progress that do nothing +pub struct EmptyCallback; +impl Callback for EmptyCallback {} + +unsafe extern "C" fn start(url: *const c_char, localfile: *const c_char, user_data: *mut c_void) +where + F: FnMut(String, String), +{ + let user_data = &mut *(user_data as *mut F); + user_data(string_from_ptr(url), string_from_ptr(localfile)); +} + +fn get_start(_closure: &F) -> zypp_agama_sys::ZyppDownloadStartCallback +where + F: FnMut(String, String), +{ + Some(start::) +} + +unsafe extern "C" fn progress( + value: c_int, + url: *const c_char, + bps_avg: f64, + bps_current: f64, + user_data: *mut c_void, +) -> bool +where + F: FnMut(i32, String, f64, f64) -> bool, +{ + let user_data = &mut *(user_data as *mut F); + user_data(value, string_from_ptr(url), bps_avg, bps_current) +} + +fn get_progress(_closure: &F) -> zypp_agama_sys::ZyppDownloadProgressCallback +where + F: FnMut(i32, String, f64, f64) -> bool, +{ + Some(progress::) +} + +unsafe extern "C" fn problem( + url: *const c_char, + error: zypp_agama_sys::DownloadProgressError, + description: *const c_char, + user_data: *mut c_void, +) -> zypp_agama_sys::PROBLEM_RESPONSE +where + F: FnMut(String, DownloadError, String) -> ProblemResponse, +{ + let user_data = &mut *(user_data as *mut F); + let res = user_data( + string_from_ptr(url), + error.into(), + string_from_ptr(description), + ); + res.into() +} + +fn get_problem(_closure: &F) -> zypp_agama_sys::ZyppDownloadProblemCallback +where + F: FnMut(String, DownloadError, String) -> ProblemResponse, +{ + Some(problem::) +} + +unsafe extern "C" fn finish( + url: *const c_char, + error: zypp_agama_sys::DownloadProgressError, + reason: *const c_char, + user_data: *mut c_void, +) where + F: FnMut(String, DownloadError, String), +{ + let user_data = &mut *(user_data as *mut F); + user_data(string_from_ptr(url), error.into(), string_from_ptr(reason)); +} + +fn get_finish(_closure: &F) -> zypp_agama_sys::ZyppDownloadFinishCallback +where + F: FnMut(String, DownloadError, String), +{ + Some(finish::) +} + +pub(crate) fn with_callback(callbacks: &impl Callback, block: &mut F) -> R +where + F: FnMut(zypp_agama_sys::DownloadProgressCallbacks) -> R, +{ + let mut start_call = |url: String, localfile: String| callbacks.start(&url, &localfile); + let cb_start = get_start(&start_call); + let mut progress_call = |value, url: String, bps_avg, bps_current| { + callbacks.progress(value, &url, bps_avg, bps_current) + }; + let cb_progress = get_progress(&progress_call); + let mut problem_call = + |url: String, error, description: String| callbacks.problem(&url, error, &description); + let cb_problem = get_problem(&problem_call); + let mut finish_call = + |url: String, error, description: String| callbacks.finish(&url, error, &description); + let cb_finish = get_finish(&finish_call); + + let callbacks = zypp_agama_sys::DownloadProgressCallbacks { + start: cb_start, + start_data: as_c_void(&mut start_call), + progress: cb_progress, + progress_data: as_c_void(&mut progress_call), + problem: cb_problem, + problem_data: as_c_void(&mut problem_call), + finish: cb_finish, + finish_data: as_c_void(&mut finish_call), + }; + block(callbacks) +} diff --git a/rust/zypp-agama/src/callbacks/pkg_download.rs b/rust/zypp-agama/src/callbacks/pkg_download.rs new file mode 100644 index 0000000000..fd5d241031 --- /dev/null +++ b/rust/zypp-agama/src/callbacks/pkg_download.rs @@ -0,0 +1,288 @@ +use std::{ + fmt::Display, + os::raw::{c_char, c_void}, +}; + +use crate::{ + callbacks::ProblemResponse, + helpers::{as_c_void, string_from_ptr}, +}; + +pub enum DownloadError { + NoError, + NotFound, // the requested Url was not found + IO, // IO error + Invalid, // the downloaded file is invalid +} + +impl From for DownloadError { + fn from(error: zypp_agama_sys::DownloadResolvableError) -> Self { + match error { + zypp_agama_sys::DownloadResolvableError_DRE_NO_ERROR => DownloadError::NoError, + zypp_agama_sys::DownloadResolvableError_DRE_NOT_FOUND => DownloadError::NotFound, + zypp_agama_sys::DownloadResolvableError_DRE_IO => DownloadError::IO, + zypp_agama_sys::DownloadResolvableError_DRE_INVALID => DownloadError::Invalid, + _ => { + tracing::error!("Unknown error code {:?}", error); + DownloadError::Invalid + } + } + } +} + +impl Display for DownloadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + DownloadError::NoError => "NoError", + DownloadError::NotFound => "NotFound", + DownloadError::IO => "IO", + DownloadError::Invalid => "Invalid", + }; + write!(f, "{}", str) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum GPGCheckResult { + Ok, // Signature is OK. + NotFound, // Signature is unknown type. + Fail, // Signature does not verify. + NotTrusted, // Signature is OK, but key is not trusted. + NoKey, // Public key is unavailable. + Error, // File does not exist or can't be opened. + NoSig, // File has no gpg signature (only digests). +} + +impl From for GPGCheckResult { + fn from(value: zypp_agama_sys::GPGCheckPackageResult) -> Self { + match value { + zypp_agama_sys::GPGCheckPackageResult_CHK_OK => GPGCheckResult::Ok, + zypp_agama_sys::GPGCheckPackageResult_CHK_NOTFOUND => GPGCheckResult::NotFound, + zypp_agama_sys::GPGCheckPackageResult_CHK_FAIL => GPGCheckResult::Fail, + zypp_agama_sys::GPGCheckPackageResult_CHK_NOTTRUSTED => GPGCheckResult::NotTrusted, + zypp_agama_sys::GPGCheckPackageResult_CHK_NOKEY => GPGCheckResult::NoKey, + zypp_agama_sys::GPGCheckPackageResult_CHK_ERROR => GPGCheckResult::Error, + zypp_agama_sys::GPGCheckPackageResult_CHK_NOSIG => GPGCheckResult::NoSig, + _ => { + tracing::error!("Unknown error code {:?}", value); + GPGCheckResult::Error + } + } + } +} + +pub enum PreloadError { + NoError, + NotFound, // the requested Url was not found + IO, // IO error + AccessDenied, // user authent. failed while accessing restricted file + Error, // other error +} + +impl From for PreloadError { + fn from(error: zypp_agama_sys::DownloadResolvableFileError) -> Self { + match error { + zypp_agama_sys::DownloadResolvableFileError_DRFE_NO_ERROR => PreloadError::NoError, + zypp_agama_sys::DownloadResolvableFileError_DRFE_NOT_FOUND => PreloadError::NotFound, + zypp_agama_sys::DownloadResolvableFileError_DRFE_IO => PreloadError::IO, + zypp_agama_sys::DownloadResolvableFileError_DRFE_ACCESS_DENIED => { + PreloadError::AccessDenied + } + zypp_agama_sys::DownloadResolvableFileError_DRFE_ERROR => PreloadError::Error, + _ => { + tracing::error!("Unknown error code {:?}", error); + PreloadError::Error + } + } + } +} + +/// Callbacks for the download phase of a zypp commit. +/// +/// This trait provides hooks into the package download process, which consists of two main phases: +/// +/// 1. A parallel preload phase that attempts to download all required packages. +/// 2. A verification phase that checks the downloaded content, including GPG signatures. +/// In this phase it should also allow to retry of download of specific package ( TODO: verify it ) +/// +/// These callbacks are a combination of libzypp's `DownloadResolvableReport` and +/// `CommitPreloadReport`. If more callbacks are needed, it can be extended as need arise. +pub trait Callback { + /// callback when start preloading packages during commit phase + /// + /// Corresponding libzypp callback name: DownloadResolvableReport::pkgGpgCheck + fn start_preload(&self) {} + /// callback when problem occurs during download of resolvable + /// + /// Corresponding libzypp callback name: DownloadResolvableReport::problem + fn problem(&self, _name: &str, _error: DownloadError, _description: &str) -> ProblemResponse { + ProblemResponse::ABORT + } + /// Callback after a GPG check is performed on a package. + /// + /// This method is called for every package after its GPG signature has been checked, + /// including when the check is successful (`GPGCheckResult::Ok`). The result of the + /// check is passed in the `check_result` parameter. + /// + /// The implementation can return an `Option` to decide how to proceed. + /// If `None` is returned, any potential issue (like a failed GPG check) might be + /// propagated and handled by other callbacks or mechanisms within libzypp. + /// + /// Corresponding libzypp callback name: DownloadResolvableReport::pkgGpgCheck + fn gpg_check( + &self, + _resolvable_name: &str, + _repo_url: &str, + _check_result: GPGCheckResult, + ) -> Option { + None + } + + /// Callback executed when the preload of a single file finishes. + /// + /// This method is called for each file after the preload attempt is complete, + /// regardless of whether it succeeded or failed. + /// + /// Since this callback does not return a value, it cannot be used to request a + /// retry if the download fails. It is libzypp feature to move forward with failure. + /// TODO: For failures it is probably need to check if we require a different callback + /// or `problem` here is enough. + /// + /// Corresponding libzypp callback name: CommitPreloadReport::fileDone + fn finish_preload( + &self, + _url: &str, + _local_path: &str, + _error: PreloadError, + _error_details: &str, + ) { + } +} + +// Default progress that do nothing +pub struct EmptyCallback; +impl Callback for EmptyCallback {} + +unsafe extern "C" fn start_preload(user_data: *mut c_void) +where + F: FnMut(), +{ + let user_data = &mut *(user_data as *mut F); + user_data(); +} + +fn get_start_preload(_closure: &F) -> zypp_agama_sys::ZyppDownloadResolvableStartCallback +where + F: FnMut(), +{ + Some(start_preload::) +} + +unsafe extern "C" fn problem( + resolvable_name: *const c_char, + error: zypp_agama_sys::DownloadResolvableError, + description: *const c_char, + user_data: *mut c_void, +) -> zypp_agama_sys::PROBLEM_RESPONSE +where + F: FnMut(String, DownloadError, String) -> ProblemResponse, +{ + let user_data = &mut *(user_data as *mut F); + let res = user_data( + string_from_ptr(resolvable_name), + error.into(), + string_from_ptr(description), + ); + res.into() +} + +fn get_problem(_closure: &F) -> zypp_agama_sys::ZyppDownloadResolvableProblemCallback +where + F: FnMut(String, DownloadError, String) -> ProblemResponse, +{ + Some(problem::) +} + +unsafe extern "C" fn gpg_check( + resolvable_name: *const c_char, + repo_url: *const c_char, + check_result: zypp_agama_sys::GPGCheckPackageResult, + user_data: *mut c_void, +) -> zypp_agama_sys::OPTIONAL_PROBLEM_RESPONSE +where + F: FnMut(String, String, GPGCheckResult) -> Option, +{ + let user_data = &mut *(user_data as *mut F); + let res = user_data( + string_from_ptr(resolvable_name), + string_from_ptr(repo_url), + check_result.into(), + ); + match res { + Some(response) => response.into(), + None => zypp_agama_sys::OPTIONAL_PROBLEM_RESPONSE_OPROBLEM_NONE, + } +} + +fn get_gpg_check(_closure: &F) -> zypp_agama_sys::ZyppDownloadResolvableGpgCheckCallback +where + F: FnMut(String, String, GPGCheckResult) -> Option, +{ + Some(gpg_check::) +} + +unsafe extern "C" fn preload_finish( + url: *const c_char, + local_path: *const c_char, + error: zypp_agama_sys::DownloadResolvableFileError, + details: *const c_char, + user_data: *mut c_void, +) where + F: FnMut(String, String, PreloadError, String), +{ + let user_data = &mut *(user_data as *mut F); + user_data( + string_from_ptr(url), + string_from_ptr(local_path), + error.into(), + string_from_ptr(details), + ); +} + +fn get_preload_finish(_closure: &F) -> zypp_agama_sys::ZyppDownloadResolvableFileFinishCallback +where + F: FnMut(String, String, PreloadError, String), +{ + Some(preload_finish::) +} + +pub(crate) fn with_callback(callback: &impl Callback, block: &mut F) -> R +where + F: FnMut(zypp_agama_sys::DownloadResolvableCallbacks) -> R, +{ + let mut start_call = || callback.start_preload(); + let cb_start = get_start_preload(&start_call); + let mut problem_call = + |name: String, error, description: String| callback.problem(&name, error, &description); + let cb_problem = get_problem(&problem_call); + let mut gpg_check = |name: String, url: String, check_result: GPGCheckResult| { + callback.gpg_check(&name, &url, check_result) + }; + let cb_gpg_check = get_gpg_check(&gpg_check); + let mut finish_call = |url: String, local_path: String, error, details: String| { + callback.finish_preload(&url, &local_path, error, &details) + }; + let cb_finish = get_preload_finish(&finish_call); + + let callbacks = zypp_agama_sys::DownloadResolvableCallbacks { + start_preload: cb_start, + start_preload_data: as_c_void(&mut start_call), + problem: cb_problem, + problem_data: as_c_void(&mut problem_call), + gpg_check: cb_gpg_check, + gpg_check_data: as_c_void(&mut gpg_check), + file_finish: cb_finish, + file_finish_data: as_c_void(&mut finish_call), + }; + block(callbacks) +} diff --git a/rust/zypp-agama/src/helpers.rs b/rust/zypp-agama/src/helpers.rs index fbb71da97e..0169f8bb5e 100644 --- a/rust/zypp-agama/src/helpers.rs +++ b/rust/zypp-agama/src/helpers.rs @@ -1,8 +1,21 @@ +use std::os::raw::c_void; + // Safety requirements: inherited from https://doc.rust-lang.org/std/ffi/struct.CStr.html#method.from_ptr pub(crate) unsafe fn string_from_ptr(c_ptr: *const i8) -> String { String::from_utf8_lossy(std::ffi::CStr::from_ptr(c_ptr).to_bytes()).into_owned() } +/// Helper to wrap data into C to be later used from rust callbacks. +/// +/// It takes a mutable reference to some data and casts it to a raw pointer +/// of type `*mut c_void`. This is useful for passing Rust data (like closures) +/// through a C layer that accepts a `void*` user data pointer, which can then +/// be cast back to its original type in a Rust callback function. +/// see https://adventures.michaelfbryan.com/posts/rust-closures-in-ffi/ +pub(crate) fn as_c_void(data: &mut F) -> *mut c_void { + data as *mut _ as *mut c_void +} + // Safety requirements: ... pub(crate) unsafe fn status_to_result( mut status: zypp_agama_sys::Status, diff --git a/rust/zypp-agama/src/lib.rs b/rust/zypp-agama/src/lib.rs index dba7f8eebd..f6991e5263 100644 --- a/rust/zypp-agama/src/lib.rs +++ b/rust/zypp-agama/src/lib.rs @@ -4,7 +4,6 @@ use std::{ sync::Mutex, }; -pub use callbacks::DownloadProgress; use errors::ZyppResult; use zypp_agama_sys::{ get_patterns_info, PatternNames, ProgressCallback, ProgressData, Status, ZyppProgressCallback, @@ -153,11 +152,13 @@ impl Zypp { } } - pub fn commit(&self) -> ZyppResult { + pub fn commit(&self, report: &impl callbacks::pkg_download::Callback) -> ZyppResult { let mut status: Status = Status::default(); let status_ptr = &mut status as *mut _; unsafe { - let res = zypp_agama_sys::commit(self.ptr, status_ptr); + let mut commit_fn = + |mut callbacks| zypp_agama_sys::commit(self.ptr, status_ptr, &mut callbacks); + let res = callbacks::pkg_download::with_callback(report, &mut commit_fn); helpers::status_to_result(status, res) } } @@ -354,7 +355,7 @@ impl Zypp { pub fn refresh_repository( &self, alias: &str, - progress: &impl DownloadProgress, + progress: &impl callbacks::download_progress::Callback, ) -> ZyppResult<()> { unsafe { let mut status: Status = Status::default(); @@ -368,7 +369,7 @@ impl Zypp { &mut callbacks, ) }; - callbacks::with_c_download_callbacks(progress, &mut refresh_fn); + callbacks::download_progress::with_callback(progress, &mut refresh_fn); helpers::status_to_result_void(status) } @@ -510,7 +511,7 @@ impl Zypp { if !cont { return abort_err; } - self.refresh_repository(&i.alias, &callbacks::EmptyDownloadProgress)?; + self.refresh_repository(&i.alias, &callbacks::download_progress::EmptyCallback)?; percent += percent_step; cont = progress( percent.floor() as i64, diff --git a/rust/zypp-agama/zypp-agama-sys/Cargo.toml b/rust/zypp-agama/zypp-agama-sys/Cargo.toml index 5600e0469e..1f485acc8c 100644 --- a/rust/zypp-agama/zypp-agama-sys/Cargo.toml +++ b/rust/zypp-agama/zypp-agama-sys/Cargo.toml @@ -3,5 +3,8 @@ name = "zypp-agama-sys" version = "0.1.0" edition.workspace = true +[dependencies] +tracing = "0.1.41" + [build-dependencies] bindgen = { version= "0.72.1", features = ["runtime"] } diff --git a/rust/zypp-agama/zypp-agama-sys/c-layer/callbacks.cxx b/rust/zypp-agama/zypp-agama-sys/c-layer/callbacks.cxx index a9eba604bb..63ce17d20d 100644 --- a/rust/zypp-agama/zypp-agama-sys/c-layer/callbacks.cxx +++ b/rust/zypp-agama/zypp-agama-sys/c-layer/callbacks.cxx @@ -1,3 +1,4 @@ +#include "zypp/target/rpm/RpmDb.h" #include #include #include @@ -92,17 +93,10 @@ struct DownloadProgressReceive : public zypp::callback::ReceiveReport< const std::string &description) { if (callbacks != NULL && callbacks->problem != NULL) { PROBLEM_RESPONSE response = - callbacks->problem(file.asString().c_str(), error, + callbacks->problem(file.asString().c_str(), convert_error(error), description.c_str(), callbacks->problem_data); - switch (response) { - case PROBLEM_RETRY: - return zypp::media::DownloadProgressReport::RETRY; - case PROBLEM_ABORT: - return zypp::media::DownloadProgressReport::ABORT; - case PROBLEM_IGNORE: - return zypp::media::DownloadProgressReport::IGNORE; - } + return convert_action(response); } // otherwise return the default value from the parent class return zypp::media::DownloadProgressReport::problem(file, error, @@ -113,14 +107,215 @@ struct DownloadProgressReceive : public zypp::callback::ReceiveReport< zypp::media::DownloadProgressReport::Error error, const std::string &reason) { if (callbacks != NULL && callbacks->finish != NULL) { - callbacks->finish(file.asString().c_str(), error, reason.c_str(), - callbacks->finish_data); + callbacks->finish(file.asString().c_str(), convert_error(error), + reason.c_str(), callbacks->finish_data); + } + } + +private: + inline DownloadProgressError + convert_error(zypp::media::DownloadProgressReport::Error error) noexcept { + switch (error) { + case zypp::media::DownloadProgressReport::NO_ERROR: + return DPE_NO_ERROR; + case zypp::media::DownloadProgressReport::NOT_FOUND: + return DPE_NOT_FOUND; + case zypp::media::DownloadProgressReport::IO: + return DPE_IO; + case zypp::media::DownloadProgressReport::ACCESS_DENIED: + return DPE_ACCESS_DENIED; + case zypp::media::DownloadProgressReport::ERROR: + return DPE_ERROR; + } + return DPE_ERROR; + } + + inline zypp::media::DownloadProgressReport::Action + convert_action(PROBLEM_RESPONSE response) { + switch (response) { + case PROBLEM_RETRY: + return zypp::media::DownloadProgressReport::RETRY; + case PROBLEM_ABORT: + return zypp::media::DownloadProgressReport::ABORT; + case PROBLEM_IGNORE: + return zypp::media::DownloadProgressReport::IGNORE; } + return zypp::media::DownloadProgressReport::ABORT; } }; static DownloadProgressReceive download_progress_receive; +struct DownloadResolvableReport : public zypp::callback::ReceiveReport< + zypp::repo::DownloadResolvableReport> { + struct DownloadResolvableCallbacks *callbacks; + + DownloadResolvableReport() { callbacks = NULL; } + + void set_callbacks(DownloadResolvableCallbacks *callbacks_) { + callbacks = callbacks_; + } + + virtual Action problem(zypp::Resolvable::constPtr resolvable_ptr, Error error, + const std::string &description) { + // return the default value from the parent class if not defined + if (callbacks == NULL || callbacks->problem == NULL) + return zypp::repo::DownloadResolvableReport::problem(resolvable_ptr, + error, description); + + PROBLEM_RESPONSE response = callbacks->problem( + resolvable_ptr->name().c_str(), from_dre_error(error), + description.c_str(), callbacks->problem_data); + return from_response(response); + } + + virtual void pkgGpgCheck(const UserData &userData_r = UserData()) { + if (callbacks == NULL || callbacks->gpg_check == NULL) { + return; + } + zypp::ResObject::constPtr resobject = + userData_r.get("ResObject"); + const zypp::RepoInfo repo = resobject->repoInfo(); + const std::string repo_url = repo.rawUrl().asString(); + enum GPGCheckPackageResult result = from_rpm_result( + userData_r.get( + "CheckPackageResult")); + OPTIONAL_PROBLEM_RESPONSE response = + callbacks->gpg_check(resobject->name().c_str(), repo_url.c_str(), + result, callbacks->gpg_check_data); + set_response(userData_r, response); + } + +private: + inline void set_response(const UserData &userData_r, + OPTIONAL_PROBLEM_RESPONSE response) { + DownloadResolvableReport::Action zypp_action; + switch (response) { + case OPROBLEM_RETRY: + zypp_action = zypp::repo::DownloadResolvableReport::RETRY; + break; + case OPROBLEM_ABORT: + zypp_action = zypp::repo::DownloadResolvableReport::ABORT; + break; + case OPROBLEM_IGNORE: + zypp_action = zypp::repo::DownloadResolvableReport::IGNORE; + break; + // do not set action and it will let fail it later in Done Provide + case OPROBLEM_NONE: + return; + }; + userData_r.set("Action", zypp_action); + } + + inline DownloadResolvableError + from_dre_error(zypp::repo::DownloadResolvableReport::Error error) { + switch (error) { + case zypp::repo::DownloadResolvableReport::NO_ERROR: + return DownloadResolvableError::DRE_NO_ERROR; + case zypp::repo::DownloadResolvableReport::NOT_FOUND: + return DownloadResolvableError::DRE_NOT_FOUND; + case zypp::repo::DownloadResolvableReport::IO: + return DownloadResolvableError::DRE_IO; + case zypp::repo::DownloadResolvableReport::INVALID: + return DownloadResolvableError::DRE_INVALID; + } + // fallback that should not happen + return DownloadResolvableError::DRE_NO_ERROR; + } + + inline Action from_response(PROBLEM_RESPONSE response) { + switch (response) { + case PROBLEM_RETRY: + return zypp::repo::DownloadResolvableReport::RETRY; + case PROBLEM_ABORT: + return zypp::repo::DownloadResolvableReport::ABORT; + case PROBLEM_IGNORE: + return zypp::repo::DownloadResolvableReport::IGNORE; + } + // fallback that should not happen + return zypp::repo::DownloadResolvableReport::ABORT; + } + + inline GPGCheckPackageResult + from_rpm_result(zypp::target::rpm::RpmDb::CheckPackageResult result) { + switch (result) { + case zypp::target::rpm::RpmDb::CHK_OK: + return GPGCheckPackageResult::CHK_OK; + case zypp::target::rpm::RpmDb::CHK_NOTFOUND: + return GPGCheckPackageResult::CHK_NOTFOUND; + case zypp::target::rpm::RpmDb::CHK_FAIL: + return GPGCheckPackageResult::CHK_FAIL; + case zypp::target::rpm::RpmDb::CHK_NOTTRUSTED: + return GPGCheckPackageResult::CHK_NOTTRUSTED; + case zypp::target::rpm::RpmDb::CHK_NOKEY: + return GPGCheckPackageResult::CHK_NOKEY; + case zypp::target::rpm::RpmDb::CHK_ERROR: + return GPGCheckPackageResult::CHK_ERROR; + case zypp::target::rpm::RpmDb::CHK_NOSIG: + return GPGCheckPackageResult::CHK_NOSIG; + } + // fallback that should not happen + return GPGCheckPackageResult::CHK_ERROR; + } +}; + +static DownloadResolvableReport download_resolvable_receive; + +struct CommitPreloadReport + : public zypp::callback::ReceiveReport { + + struct DownloadResolvableCallbacks *callbacks; + + CommitPreloadReport() { callbacks = NULL; } + + void set_callbacks(DownloadResolvableCallbacks *callbacks_) { + callbacks = callbacks_; + } + virtual void start(const UserData &userData = UserData()) { + if (callbacks != NULL && callbacks->start_preload != NULL) { + callbacks->start_preload(callbacks->start_preload_data); + } + } + + virtual void fileDone(const zypp::Pathname &localfile, Error error, + const UserData &userData = UserData()) { + if (callbacks != NULL && callbacks->file_finish != NULL) { + const char *url = ""; + if (userData.hasvalue("url")) { + url = userData.get("url").asString().c_str(); + } + const char *local_path = localfile.c_str(); + const char *error_details = ""; + if (userData.hasvalue("description")) { + error_details = userData.get("description").c_str(); + } + callbacks->file_finish(url, local_path, from_dre_error(error), + error_details, callbacks->file_finish_data); + } + } + +private: + inline DownloadResolvableFileError + from_dre_error(zypp::media::CommitPreloadReport::Error error) { + switch (error) { + case zypp::media::CommitPreloadReport::NO_ERROR: + return DownloadResolvableFileError::DRFE_NO_ERROR; + case zypp::media::CommitPreloadReport::NOT_FOUND: + return DownloadResolvableFileError::DRFE_NOT_FOUND; + case zypp::media::CommitPreloadReport::IO: + return DownloadResolvableFileError::DRFE_IO; + case zypp::media::CommitPreloadReport::ACCESS_DENIED: + return DownloadResolvableFileError::DRFE_ACCESS_DENIED; + case zypp::media::CommitPreloadReport::ERROR: + return DownloadResolvableFileError::DRFE_ERROR; + } + // fallback that should not happen + return DownloadResolvableFileError::DRFE_NO_ERROR; + } +}; + +static CommitPreloadReport commit_preload_report; + extern "C" { void set_zypp_progress_callback(ZyppProgressCallback progress, void *user_data) { @@ -140,6 +335,24 @@ void unset_zypp_download_callbacks() { download_progress_receive.disconnect(); } +// Sets both reports as we consolidate download resolvables +// and commitPreload into one set for easier hooking +void set_zypp_resolvable_download_callbacks( + struct DownloadResolvableCallbacks *callbacks) { + download_resolvable_receive.set_callbacks(callbacks); + download_resolvable_receive.connect(); + commit_preload_report.set_callbacks(callbacks); + commit_preload_report.connect(); +} + +void unset_zypp_resolvable_download_callbacks() { + // NULL pointer to struct to be sure it is not called + download_resolvable_receive.set_callbacks(NULL); + download_resolvable_receive.disconnect(); + commit_preload_report.set_callbacks(NULL); + commit_preload_report.disconnect(); +} + #ifdef __cplusplus bool dynamic_progress_callback(ZyppProgressCallback progress, void *user_data, const zypp::ProgressData &task) { diff --git a/rust/zypp-agama/zypp-agama-sys/c-layer/include/callbacks.h b/rust/zypp-agama/zypp-agama-sys/c-layer/include/callbacks.h index 0413033e9a..84afa17e6f 100644 --- a/rust/zypp-agama/zypp-agama-sys/c-layer/include/callbacks.h +++ b/rust/zypp-agama/zypp-agama-sys/c-layer/include/callbacks.h @@ -25,7 +25,25 @@ typedef bool (*ZyppProgressCallback)(struct ProgressData zypp_data, void *user_data); void set_zypp_progress_callback(ZyppProgressCallback progress, void *user_data); +// keep in sync with below enum to ensure all entries here is also at the below +// one to allow 1:1 matching. enum PROBLEM_RESPONSE { PROBLEM_RETRY, PROBLEM_ABORT, PROBLEM_IGNORE }; +// NOTE: ensure that order are identical as PROBLEM_RESPONSE and NONE is at the +// end +enum OPTIONAL_PROBLEM_RESPONSE { + OPROBLEM_RETRY, + OPROBLEM_ABORT, + OPROBLEM_IGNORE, + OPROBLEM_NONE +}; + +enum DownloadProgressError { + DPE_NO_ERROR, + DPE_NOT_FOUND, // the requested Url was not found + DPE_IO, // IO error + DPE_ACCESS_DENIED, // user authent. failed while accessing restricted file + DPE_ERROR // other error +}; typedef void (*ZyppDownloadStartCallback)(const char *url, const char *localfile, void *user_data); @@ -33,8 +51,10 @@ typedef bool (*ZyppDownloadProgressCallback)(int value, const char *url, double bps_avg, double bps_current, void *user_data); typedef enum PROBLEM_RESPONSE (*ZyppDownloadProblemCallback)( - const char *url, int error, const char *description, void *user_data); -typedef void (*ZyppDownloadFinishCallback)(const char *url, int error, + const char *url, enum DownloadProgressError error, const char *description, + void *user_data); +typedef void (*ZyppDownloadFinishCallback)(const char *url, + enum DownloadProgressError error, const char *reason, void *user_data); // progress for downloading files. There are 4 callbacks: @@ -53,6 +73,67 @@ struct DownloadProgressCallbacks { ZyppDownloadFinishCallback finish; void *finish_data; }; + +enum DownloadResolvableError { + DRE_NO_ERROR, + DRE_NOT_FOUND, // the requested Url was not found + DRE_IO, // IO error + DRE_INVALID // the downloaded file is invalid +}; + +enum DownloadResolvableFileError { + DRFE_NO_ERROR, + DRFE_NOT_FOUND, // the requested Url was not found + DRFE_IO, // IO error + DRFE_ACCESS_DENIED, // user authent. failed while accessing restricted file + DRFE_ERROR // other error +}; + +// keep in sync with +// https://github.com/openSUSE/libzypp/blob/master/zypp-logic/zypp/target/rpm/RpmDb.h#L376 +// maybe there is a better way to export it to C? +enum GPGCheckPackageResult { + CHK_OK = 0, /*!< Signature is OK. */ + CHK_NOTFOUND = 1, /*!< Signature is unknown type. */ + CHK_FAIL = 2, /*!< Signature does not verify. */ + CHK_NOTTRUSTED = 3, /*!< Signature is OK, but key is not trusted. */ + CHK_NOKEY = 4, /*!< Public key is unavailable. */ + CHK_ERROR = 5, /*!< File does not exist or can't be opened. */ + CHK_NOSIG = 6, /*!< File has no gpg signature (only digests). */ +}; + +typedef void (*ZyppDownloadResolvableStartCallback)(void *user_data); +// TODO: do we need more resolvable details? for now just use name and url +typedef enum PROBLEM_RESPONSE (*ZyppDownloadResolvableProblemCallback)( + const char *resolvable_name, enum DownloadResolvableError error, + const char *description, void *user_data); +typedef enum OPTIONAL_PROBLEM_RESPONSE ( + *ZyppDownloadResolvableGpgCheckCallback)( + const char *resolvable_name, const char *repo_url, + enum GPGCheckPackageResult check_result, void *user_data); +typedef void (*ZyppDownloadResolvableFileFinishCallback)( + const char *url, const char *local_path, + enum DownloadResolvableFileError error, const char *error_details, + void *user_data); + +// progress for downloading resolvables (rpms). There are 3 callbacks now ( can +// be extended with progress and finish one): +// 1. start for start of preload +// 2. problem to react when something wrong happen and how to behave +// 3. gpg_check when there is issue with gpg check on resolvable +// 4. finish_file is when preload finish download of package including failed +// NOTE: user_data is separated for each call. +// NOTE: libzypp provides more data, but only those used by agama is used now. +struct DownloadResolvableCallbacks { + ZyppDownloadResolvableStartCallback start_preload; + void *start_preload_data; + ZyppDownloadResolvableProblemCallback problem; + void *problem_data; + ZyppDownloadResolvableGpgCheckCallback gpg_check; + void *gpg_check_data; + ZyppDownloadResolvableFileFinishCallback file_finish; + void *file_finish_data; +}; #ifdef __cplusplus } #endif diff --git a/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h b/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h index d36dc5c85e..6325ff769f 100644 --- a/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h +++ b/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h @@ -61,11 +61,13 @@ void switch_target(struct Zypp *zypp, const char *root, struct Status *status) noexcept; /// Commit zypp settings and install -/// TODO: callbacks +/// TODO: install callbacks /// @param zypp /// @param status +/// @param download_callbacks /// @return true if there is no error -bool commit(struct Zypp *zypp, struct Status *status) noexcept; +bool commit(struct Zypp *zypp, struct Status *status, + struct DownloadResolvableCallbacks *download_callbacks) noexcept; /// Represents a single mount point and its space usage. /// The string pointers are not owned by this struct. diff --git a/rust/zypp-agama/zypp-agama-sys/c-layer/internal/callbacks.hxx b/rust/zypp-agama/zypp-agama-sys/c-layer/internal/callbacks.hxx index 890dda262b..488e1d7335 100644 --- a/rust/zypp-agama/zypp-agama-sys/c-layer/internal/callbacks.hxx +++ b/rust/zypp-agama/zypp-agama-sys/c-layer/internal/callbacks.hxx @@ -13,4 +13,11 @@ create_progress_callback(ZyppProgressCallback progress, void *user_data); void set_zypp_download_callbacks(struct DownloadProgressCallbacks *callbacks); void unset_zypp_download_callbacks(); +// pair of set/unset callbacks used during commit when download packages. +// Uses mixture of ResolvableDownloadReport and also CommitPreloadReport +// to capture related parts of commit download reports. +void set_zypp_resolvable_download_callbacks( + struct DownloadResolvableCallbacks *callbacks); +void unset_zypp_resolvable_download_callbacks(); + #endif diff --git a/rust/zypp-agama/zypp-agama-sys/c-layer/lib.cxx b/rust/zypp-agama/zypp-agama-sys/c-layer/lib.cxx index 4a36578c97..ffa9ae91f6 100644 --- a/rust/zypp-agama/zypp-agama-sys/c-layer/lib.cxx +++ b/rust/zypp-agama/zypp-agama-sys/c-layer/lib.cxx @@ -155,14 +155,18 @@ void switch_target(struct Zypp *zypp, const char *root, STATUS_OK(status); } -bool commit(struct Zypp *zypp, struct Status *status) noexcept { +bool commit(struct Zypp *zypp, struct Status *status, + struct DownloadResolvableCallbacks *download_callbacks) noexcept { try { + set_zypp_resolvable_download_callbacks(download_callbacks); zypp::ZYppCommitPolicy policy; zypp::ZYppCommitResult result = zypp->zypp_pointer->commit(policy); STATUS_OK(status); + unset_zypp_resolvable_download_callbacks(); return result.noError(); } catch (zypp::Exception &excpt) { STATUS_EXCEPT(status, excpt); + unset_zypp_resolvable_download_callbacks(); return false; } } @@ -666,5 +670,4 @@ void get_space_usage(struct Zypp *zypp, struct Status *status, STATUS_EXCEPT(status, excpt); } } - -} \ No newline at end of file +} diff --git a/rust/zypp-agama/zypp-agama-sys/src/bindings.rs b/rust/zypp-agama/zypp-agama-sys/src/bindings.rs index b80b0faa5d..b6b58c7dbf 100644 --- a/rust/zypp-agama/zypp-agama-sys/src/bindings.rs +++ b/rust/zypp-agama/zypp-agama-sys/src/bindings.rs @@ -24,6 +24,17 @@ pub const PROBLEM_RESPONSE_PROBLEM_RETRY: PROBLEM_RESPONSE = 0; pub const PROBLEM_RESPONSE_PROBLEM_ABORT: PROBLEM_RESPONSE = 1; pub const PROBLEM_RESPONSE_PROBLEM_IGNORE: PROBLEM_RESPONSE = 2; pub type PROBLEM_RESPONSE = ::std::os::raw::c_uint; +pub const OPTIONAL_PROBLEM_RESPONSE_OPROBLEM_RETRY: OPTIONAL_PROBLEM_RESPONSE = 0; +pub const OPTIONAL_PROBLEM_RESPONSE_OPROBLEM_ABORT: OPTIONAL_PROBLEM_RESPONSE = 1; +pub const OPTIONAL_PROBLEM_RESPONSE_OPROBLEM_IGNORE: OPTIONAL_PROBLEM_RESPONSE = 2; +pub const OPTIONAL_PROBLEM_RESPONSE_OPROBLEM_NONE: OPTIONAL_PROBLEM_RESPONSE = 3; +pub type OPTIONAL_PROBLEM_RESPONSE = ::std::os::raw::c_uint; +pub const DownloadProgressError_DPE_NO_ERROR: DownloadProgressError = 0; +pub const DownloadProgressError_DPE_NOT_FOUND: DownloadProgressError = 1; +pub const DownloadProgressError_DPE_IO: DownloadProgressError = 2; +pub const DownloadProgressError_DPE_ACCESS_DENIED: DownloadProgressError = 3; +pub const DownloadProgressError_DPE_ERROR: DownloadProgressError = 4; +pub type DownloadProgressError = ::std::os::raw::c_uint; pub type ZyppDownloadStartCallback = ::std::option::Option< unsafe extern "C" fn( url: *const ::std::os::raw::c_char, @@ -43,7 +54,7 @@ pub type ZyppDownloadProgressCallback = ::std::option::Option< pub type ZyppDownloadProblemCallback = ::std::option::Option< unsafe extern "C" fn( url: *const ::std::os::raw::c_char, - error: ::std::os::raw::c_int, + error: DownloadProgressError, description: *const ::std::os::raw::c_char, user_data: *mut ::std::os::raw::c_void, ) -> PROBLEM_RESPONSE, @@ -51,7 +62,7 @@ pub type ZyppDownloadProblemCallback = ::std::option::Option< pub type ZyppDownloadFinishCallback = ::std::option::Option< unsafe extern "C" fn( url: *const ::std::os::raw::c_char, - error: ::std::os::raw::c_int, + error: DownloadProgressError, reason: *const ::std::os::raw::c_char, user_data: *mut ::std::os::raw::c_void, ), @@ -91,6 +102,94 @@ const _: () = { ["Offset of field: DownloadProgressCallbacks::finish_data"] [::std::mem::offset_of!(DownloadProgressCallbacks, finish_data) - 56usize]; }; +pub const DownloadResolvableError_DRE_NO_ERROR: DownloadResolvableError = 0; +pub const DownloadResolvableError_DRE_NOT_FOUND: DownloadResolvableError = 1; +pub const DownloadResolvableError_DRE_IO: DownloadResolvableError = 2; +pub const DownloadResolvableError_DRE_INVALID: DownloadResolvableError = 3; +pub type DownloadResolvableError = ::std::os::raw::c_uint; +pub const DownloadResolvableFileError_DRFE_NO_ERROR: DownloadResolvableFileError = 0; +pub const DownloadResolvableFileError_DRFE_NOT_FOUND: DownloadResolvableFileError = 1; +pub const DownloadResolvableFileError_DRFE_IO: DownloadResolvableFileError = 2; +pub const DownloadResolvableFileError_DRFE_ACCESS_DENIED: DownloadResolvableFileError = 3; +pub const DownloadResolvableFileError_DRFE_ERROR: DownloadResolvableFileError = 4; +pub type DownloadResolvableFileError = ::std::os::raw::c_uint; +#[doc = "< Signature is OK."] +pub const GPGCheckPackageResult_CHK_OK: GPGCheckPackageResult = 0; +#[doc = "< Signature is unknown type."] +pub const GPGCheckPackageResult_CHK_NOTFOUND: GPGCheckPackageResult = 1; +#[doc = "< Signature does not verify."] +pub const GPGCheckPackageResult_CHK_FAIL: GPGCheckPackageResult = 2; +#[doc = "< Signature is OK, but key is not trusted."] +pub const GPGCheckPackageResult_CHK_NOTTRUSTED: GPGCheckPackageResult = 3; +#[doc = "< Public key is unavailable."] +pub const GPGCheckPackageResult_CHK_NOKEY: GPGCheckPackageResult = 4; +#[doc = "< File does not exist or can't be opened."] +pub const GPGCheckPackageResult_CHK_ERROR: GPGCheckPackageResult = 5; +#[doc = "< File has no gpg signature (only digests)."] +pub const GPGCheckPackageResult_CHK_NOSIG: GPGCheckPackageResult = 6; +pub type GPGCheckPackageResult = ::std::os::raw::c_uint; +pub type ZyppDownloadResolvableStartCallback = + ::std::option::Option; +pub type ZyppDownloadResolvableProblemCallback = ::std::option::Option< + unsafe extern "C" fn( + resolvable_name: *const ::std::os::raw::c_char, + error: DownloadResolvableError, + description: *const ::std::os::raw::c_char, + user_data: *mut ::std::os::raw::c_void, + ) -> PROBLEM_RESPONSE, +>; +pub type ZyppDownloadResolvableGpgCheckCallback = ::std::option::Option< + unsafe extern "C" fn( + resolvable_name: *const ::std::os::raw::c_char, + repo_url: *const ::std::os::raw::c_char, + check_result: GPGCheckPackageResult, + user_data: *mut ::std::os::raw::c_void, + ) -> OPTIONAL_PROBLEM_RESPONSE, +>; +pub type ZyppDownloadResolvableFileFinishCallback = ::std::option::Option< + unsafe extern "C" fn( + url: *const ::std::os::raw::c_char, + local_path: *const ::std::os::raw::c_char, + error: DownloadResolvableFileError, + error_details: *const ::std::os::raw::c_char, + user_data: *mut ::std::os::raw::c_void, + ), +>; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct DownloadResolvableCallbacks { + pub start_preload: ZyppDownloadResolvableStartCallback, + pub start_preload_data: *mut ::std::os::raw::c_void, + pub problem: ZyppDownloadResolvableProblemCallback, + pub problem_data: *mut ::std::os::raw::c_void, + pub gpg_check: ZyppDownloadResolvableGpgCheckCallback, + pub gpg_check_data: *mut ::std::os::raw::c_void, + pub file_finish: ZyppDownloadResolvableFileFinishCallback, + pub file_finish_data: *mut ::std::os::raw::c_void, +} +#[allow(clippy::unnecessary_operation, clippy::identity_op)] +const _: () = { + ["Size of DownloadResolvableCallbacks"] + [::std::mem::size_of::() - 64usize]; + ["Alignment of DownloadResolvableCallbacks"] + [::std::mem::align_of::() - 8usize]; + ["Offset of field: DownloadResolvableCallbacks::start_preload"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, start_preload) - 0usize]; + ["Offset of field: DownloadResolvableCallbacks::start_preload_data"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, start_preload_data) - 8usize]; + ["Offset of field: DownloadResolvableCallbacks::problem"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, problem) - 16usize]; + ["Offset of field: DownloadResolvableCallbacks::problem_data"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, problem_data) - 24usize]; + ["Offset of field: DownloadResolvableCallbacks::gpg_check"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, gpg_check) - 32usize]; + ["Offset of field: DownloadResolvableCallbacks::gpg_check_data"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, gpg_check_data) - 40usize]; + ["Offset of field: DownloadResolvableCallbacks::file_finish"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, file_finish) - 48usize]; + ["Offset of field: DownloadResolvableCallbacks::file_finish_data"] + [::std::mem::offset_of!(DownloadResolvableCallbacks, file_finish_data) - 56usize]; +}; #[doc = " status struct to pass and obtain from calls that can fail.\n After usage free with \\ref free_status function.\n\n Most functions act as *constructors* for this, taking a pointer\n to it as an output parameter, disregarding the struct current contents\n and filling it in. Thus, if you reuse a `Status` without \\ref free_status\n in between, `error` will leak."] #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -280,8 +379,12 @@ unsafe extern "C" { ) -> *mut Zypp; #[doc = " Switch Zypp target (where to install packages to).\n @param root\n @param[out] status"] pub fn switch_target(zypp: *mut Zypp, root: *const ::std::os::raw::c_char, status: *mut Status); - #[doc = " Commit zypp settings and install\n TODO: callbacks\n @param zypp\n @param status\n @return true if there is no error"] - pub fn commit(zypp: *mut Zypp, status: *mut Status) -> bool; + #[doc = " Commit zypp settings and install\n TODO: install callbacks\n @param zypp\n @param status\n @param download_callbacks\n @return true if there is no error"] + pub fn commit( + zypp: *mut Zypp, + status: *mut Status, + download_callbacks: *mut DownloadResolvableCallbacks, + ) -> bool; #[doc = " Calculates the space usage for a given list of mount points.\n This function populates the `used_size` field for each element in the\n provided `mount_points` array.\n\n @param zypp The Zypp context.\n @param[out] status Output status object.\n @param[in,out] mount_points An array of mount points to be evaluated.\n @param mount_points_size The number of elements in the `mount_points` array."] pub fn get_space_usage( zypp: *mut Zypp,