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
20 changes: 19 additions & 1 deletion rust/agama-manager/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

use agama_utils::{
actor::Message,
api::{Action, Config, IssueMap, Proposal, Status, SystemInfo},
api::{
manager::{LanguageTag, LicenseContent},
Action, Config, IssueMap, Proposal, Status, SystemInfo,
},
};
use serde_json::Value;

Expand Down Expand Up @@ -104,6 +107,21 @@ impl Message for GetIssues {
type Reply = IssueMap;
}

pub struct GetLicense {
pub id: String,
pub lang: LanguageTag,
}

impl Message for GetLicense {
type Reply = Option<LicenseContent>;
}

impl GetLicense {
pub fn new(id: String, lang: LanguageTag) -> Self {
Self { id, lang }
}
}

/// Runs the given action.
#[derive(Debug)]
pub struct RunAction {
Expand Down
18 changes: 16 additions & 2 deletions rust/agama-manager/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ use crate::{l10n, message, network, software, storage};
use agama_utils::{
actor::{self, Actor, Handler, MessageHandler},
api::{
self, event, manager, status::State, Action, Config, Event, Issue, IssueMap, IssueSeverity,
Proposal, Scope, Status, SystemInfo,
self, event,
manager::{self, LicenseContent},
status::State,
Action, Config, Event, Issue, IssueMap, IssueSeverity, Proposal, Scope, Status, SystemInfo,
},
issue, licenses,
products::{self, ProductSpec},
Expand Down Expand Up @@ -325,11 +327,13 @@ impl MessageHandler<message::GetSystem> for Service {
let manager = self.system.clone();
let storage = self.storage.call(storage::message::GetSystem).await?;
let network = self.network.get_system().await?;
let software = self.software.call(software::message::GetSystem).await?;
Ok(SystemInfo {
l10n,
manager,
network,
storage,
software,
})
}
}
Expand Down Expand Up @@ -425,6 +429,16 @@ impl MessageHandler<message::GetIssues> for Service {
}
}

#[async_trait]
impl MessageHandler<message::GetLicense> for Service {
async fn handle(
&mut self,
message: message::GetLicense,
) -> Result<Option<LicenseContent>, Error> {
Ok(self.licenses.find(&message.id, &message.lang))
}
}

#[async_trait]
impl MessageHandler<message::RunAction> for Service {
/// It runs the given action.
Expand Down
49 changes: 47 additions & 2 deletions rust/agama-server/src/server/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use agama_software::Resolvable;
use agama_utils::{
actor::Handler,
api::{
event, query,
event,
manager::LicenseContent,
query,
question::{Question, QuestionSpec, UpdateQuestion},
Action, Config, IssueMap, Patch, Status, SystemInfo,
},
Expand All @@ -40,7 +42,7 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -106,6 +108,7 @@ pub async fn server_service(
"/questions",
get(get_questions).post(ask_question).patch(update_question),
)
.route("/licenses/:id", get(get_license))
.route(
"/private/storage_model",
get(get_storage_model).put(set_storage_model),
Expand Down Expand Up @@ -324,6 +327,48 @@ async fn update_question(
Ok(())
}

#[derive(Deserialize, utoipa::IntoParams)]
struct LicenseQuery {
lang: Option<String>,
}

/// Returns the license content.
///
/// Optionally it can receive a language tag (RFC 5646). Otherwise, it returns
/// the license in English.
#[utoipa::path(
get,
path = "/licenses/:id",
context_path = "/api/software",
params(LicenseQuery),
responses(
(status = 200, description = "License with the given ID", body = LicenseContent),
(status = 400, description = "The specified language tag is not valid"),
(status = 404, description = "There is not license with the given ID")
)
)]
async fn get_license(
State(state): State<ServerState>,
Path(id): Path<String>,
Query(query): Query<LicenseQuery>,
) -> Result<Response, Error> {
let lang = query.lang.unwrap_or("en".to_string());

let Ok(lang) = lang.as_str().try_into() else {
return Ok(StatusCode::BAD_REQUEST.into_response());
};

let license = state
.manager
.call(message::GetLicense::new(id.to_string(), lang))
.await?;
if let Some(license) = license {
Ok(Json(license).into_response())
} else {
Ok(StatusCode::NOT_FOUND.into_response())
}
}

#[utoipa::path(
post,
path = "/actions",
Expand Down
53 changes: 31 additions & 22 deletions rust/agama-software/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use agama_utils::{
actor::Handler,
api::{
software::{Pattern, SoftwareProposal},
software::{Pattern, SoftwareProposal, SystemInfo},
Issue,
},
products::{ProductSpec, UserPattern},
Expand All @@ -47,8 +47,8 @@ pub use packages::{Resolvable, ResolvableType};
/// tests.
#[async_trait]
pub trait ModelAdapter: Send + Sync + 'static {
/// List of available patterns.
async fn patterns(&self) -> Result<Vec<Pattern>, service::Error>;
/// Returns the software system information.
async fn system_info(&self) -> Result<SystemInfo, service::Error>;

async fn compute_proposal(&self) -> Result<SoftwareProposal, service::Error>;

Expand Down Expand Up @@ -97,6 +97,27 @@ impl Model {
question,
})
}

async fn patterns(&self) -> Result<Vec<Pattern>, service::Error> {
let Some(product) = &self.selected_product else {
return Err(service::Error::MissingProduct);
};

let names = product
.software
.user_patterns
.iter()
.map(|user_pattern| match user_pattern {
UserPattern::Plain(name) => name.clone(),
UserPattern::Preselected(preselected) => preselected.name.clone(),
})
.collect();

let (tx, rx) = oneshot::channel();
self.zypp_sender
.send(SoftwareAction::GetPatternsMetadata(names, tx))?;
Ok(rx.await??)
}
}

#[async_trait]
Expand All @@ -119,25 +140,13 @@ impl ModelAdapter for Model {
Ok(rx.await??)
}

async fn patterns(&self) -> Result<Vec<Pattern>, service::Error> {
let Some(product) = &self.selected_product else {
return Err(service::Error::MissingProduct);
};

let names = product
.software
.user_patterns
.iter()
.map(|user_pattern| match user_pattern {
UserPattern::Plain(name) => name.clone(),
UserPattern::Preselected(preselected) => preselected.name.clone(),
})
.collect();

let (tx, rx) = oneshot::channel();
self.zypp_sender
.send(SoftwareAction::GetPatternsMetadata(names, tx))?;
Ok(rx.await??)
/// Returns the software system information.
async fn system_info(&self) -> Result<SystemInfo, service::Error> {
Ok(SystemInfo {
patterns: self.patterns().await?,
repositories: vec![],
addons: vec![],
})
}

async fn refresh(&mut self) -> Result<(), service::Error> {
Expand Down
76 changes: 44 additions & 32 deletions rust/agama-software/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ pub struct Service {
issues: Handler<issue::Service>,
progress: Handler<progress::Service>,
events: event::Sender,
state: State,
state: Arc<RwLock<ServiceState>>,
selection: SoftwareSelection,
}

#[derive(Default)]
struct State {
struct ServiceState {
config: Config,
system: SystemInfo,
proposal: Arc<RwLock<Proposal>>,
proposal: Proposal,
}

impl Service {
Expand All @@ -94,20 +94,22 @@ impl Service {
progress: Handler<progress::Service>,
events: event::Sender,
) -> Service {
let state = Arc::new(RwLock::new(Default::default()));
Self {
model: Arc::new(Mutex::new(model)),
issues,
progress,
events,
state: Default::default(),
state,
selection: Default::default(),
}
}

pub async fn setup(&mut self) -> Result<(), Error> {
if let Some(install_repo) = find_install_repository() {
tracing::info!("Found repository at {}", install_repo.url);
self.state.system.repositories.push(install_repo);
let mut state = self.state.write().await;
state.system.repositories.push(install_repo);
}
Ok(())
}
Expand All @@ -129,14 +131,16 @@ impl Actor for Service {
#[async_trait]
impl MessageHandler<message::GetSystem> for Service {
async fn handle(&mut self, _message: message::GetSystem) -> Result<SystemInfo, Error> {
Ok(self.state.system.clone())
let state = self.state.read().await;
Ok(state.system.clone())
}
}

#[async_trait]
impl MessageHandler<message::GetConfig> for Service {
async fn handle(&mut self, _message: message::GetConfig) -> Result<Config, Error> {
Ok(self.state.config.clone())
let state = self.state.read().await;
Ok(state.config.clone())
}
}

Expand All @@ -145,40 +149,46 @@ impl MessageHandler<message::SetConfig<Config>> for Service {
async fn handle(&mut self, message: message::SetConfig<Config>) -> Result<(), Error> {
let product = message.product.read().await;

self.state.config = message.config.clone().unwrap_or_default();
let software = {
let mut state = self.state.write().await;
state.config = message.config.clone().unwrap_or_default();
SoftwareState::build_from(&product, &state.config, &state.system, &self.selection)
};

self.events.send(Event::ConfigChanged {
scope: Scope::Software,
})?;

let software = SoftwareState::build_from(
&product,
&self.state.config,
&self.state.system,
&self.selection,
);
tracing::info!("Wanted software state: {software:?}");

let model = self.model.clone();
let issues = self.issues.clone();
let events = self.events.clone();
let progress = self.progress.clone();
let proposal = self.state.proposal.clone();
let product_spec = product.clone();
let state = self.state.clone();
tokio::task::spawn(async move {
let (new_proposal, found_issues) =
match compute_proposal(model, product_spec, software, progress).await {
Ok((new_proposal, found_issues)) => (Some(new_proposal), found_issues),
Err(error) => {
let new_issue = Issue::new(
"software.proposal_failed",
"It was not possible to create a software proposal",
IssueSeverity::Error,
)
.with_details(&error.to_string());
(None, vec![new_issue])
}
};
proposal.write().await.software = new_proposal;
let found_issues = match compute_proposal(model, product_spec, software, progress).await
{
Ok((new_proposal, system_info, found_issues)) => {
let mut state = state.write().await;
state.proposal.software = Some(new_proposal);
state.system = system_info;
found_issues
}
Err(error) => {
let new_issue = Issue::new(
"software.proposal_failed",
"It was not possible to create a software proposal",
IssueSeverity::Error,
)
.with_details(&error.to_string());
let mut state = state.write().await;
state.proposal.software = None;
vec![new_issue]
}
};

_ = issues.cast(issue::message::Set::new(Scope::Software, found_issues));
_ = events.send(Event::ProposalChanged {
scope: Scope::Software,
Expand All @@ -194,18 +204,20 @@ async fn compute_proposal(
product_spec: ProductSpec,
wanted: SoftwareState,
progress: Handler<progress::Service>,
) -> Result<(SoftwareProposal, Vec<Issue>), Error> {
) -> Result<(SoftwareProposal, SystemInfo, Vec<Issue>), Error> {
let mut my_model = model.lock().await;
my_model.set_product(product_spec);
let issues = my_model.write(wanted, progress).await?;
let proposal = my_model.compute_proposal().await?;
Ok((proposal, issues))
let system = my_model.system_info().await?;
Ok((proposal, system, issues))
}

#[async_trait]
impl MessageHandler<message::GetProposal> for Service {
async fn handle(&mut self, _message: message::GetProposal) -> Result<Option<Proposal>, Error> {
Ok(self.state.proposal.read().await.clone().into_option())
let state = self.state.read().await;
Ok(state.proposal.clone().into_option())
}
}

Expand Down
Loading
Loading