diff --git a/appmgr/Cargo.lock b/appmgr/Cargo.lock index 023773bc5..bfb6c2046 100644 --- a/appmgr/Cargo.lock +++ b/appmgr/Cargo.lock @@ -829,9 +829,9 @@ dependencies = [ [[package]] name = "emver" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b2c25504998d1069f80a8082e3c558acd7c103a2f2399306b12ed06fec5db8" +checksum = "ed260c4d7efaec031b9c4f6c4d3cf136e3df2bbfe50925800236f5e847f28704" dependencies = [ "either", "fp-core", diff --git a/appmgr/Cargo.toml b/appmgr/Cargo.toml index 2f234794e..acf0c061d 100644 --- a/appmgr/Cargo.toml +++ b/appmgr/Cargo.toml @@ -57,7 +57,7 @@ cookie_store = "0.15.0" digest = "0.9.0" divrem = "1.0.0" ed25519-dalek = { version = "1.0.1", features = ["serde"] } -emver = { version = "0.1.2", features = ["serde"] } +emver = { version = "0.1.6", features = ["serde"] } fd-lock-rs = "0.1.3" futures = "0.3.17" git-version = "0.3.5" diff --git a/appmgr/src/auth.rs b/appmgr/src/auth.rs index dba55d0a7..bc933a40d 100644 --- a/appmgr/src/auth.rs +++ b/appmgr/src/auth.rs @@ -117,7 +117,7 @@ pub async fn login( res.headers.insert( "set-cookie", HeaderValue::from_str(&format!( - "session={}; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", + "session={}; Path=/; SameSite=Lax; Expires=Fri, 31 Dec 9999 23:59:59 GMT;", token )) .with_kind(crate::ErrorKind::Unknown)?, // Should be impossible, but don't want to panic diff --git a/appmgr/src/bin/embassy-init.rs b/appmgr/src/bin/embassy-init.rs index 4ac600c4d..1e7d3ab30 100644 --- a/appmgr/src/bin/embassy-init.rs +++ b/appmgr/src/bin/embassy-init.rs @@ -63,6 +63,9 @@ async fn init(cfg_path: Option<&str>) -> Result<(), Error> { }) .await .with_kind(embassy::ErrorKind::Network)?; + let pool_name = ctx.zfs_pool_name.clone(); + drop(ctx); + embassy::disk::main::export(&*pool_name).await?; } embassy::disk::main::load( diff --git a/appmgr/src/config/mod.rs b/appmgr/src/config/mod.rs index c3b11acb7..291b24cd5 100644 --- a/appmgr/src/config/mod.rs +++ b/appmgr/src/config/mod.rs @@ -19,11 +19,11 @@ use crate::db::model::{ }; use crate::db::util::WithRevision; use crate::dependencies::{ - update_current_dependents, BreakageRes, DependencyError, TaggedDependencyError, + break_transitive, update_current_dependents, BreakageRes, DependencyError, DependencyErrors, + TaggedDependencyError, }; -use crate::install::cleanup::update_dependents; +use crate::install::cleanup::{remove_current_dependents, update_dependents}; use crate::s9pk::manifest::{Manifest, ManifestModel, PackageId}; -use crate::status::{handle_broken_dependents, DependencyErrors}; use crate::util::{ display_none, display_serializable, parse_duration, parse_stdin_deserializable, IoFormat, }; @@ -414,15 +414,16 @@ pub fn configure<'a, Db: DbHandle>( } }) .collect(); - let mut deps = pkg_model.clone().current_dependencies().get_mut(db).await?; - *deps = current_dependencies.clone(); - deps.save(db).await?; res.signal } else { None }; // update dependencies + let mut deps = pkg_model.clone().current_dependencies().get_mut(db).await?; + remove_current_dependents(db, id, deps.keys()).await?; + *deps = current_dependencies.clone(); + deps.save(db).await?; update_current_dependents(db, id, ¤t_dependencies).await?; let mut errs = pkg_model .clone() @@ -431,7 +432,7 @@ pub fn configure<'a, Db: DbHandle>( .get_mut(db) .await?; *errs = DependencyErrors::init(ctx, db, &*manifest, ¤t_dependencies).await?; - errs.save(db).await; + errs.save(db).await?; // cache current config for dependents overrides.insert(id.clone(), config.clone()); @@ -471,15 +472,7 @@ pub fn configure<'a, Db: DbHandle>( .await? { let dep_err = DependencyError::ConfigUnsatisfied { error }; - handle_broken_dependents( - db, - dependent, - id, - dependent_model, - dep_err, - breakages, - ) - .await?; + break_transitive(db, dependent, id, dep_err, breakages).await?; } // handle backreferences @@ -492,17 +485,10 @@ pub fn configure<'a, Db: DbHandle>( .await { if e.kind == crate::ErrorKind::ConfigRulesViolation { - let dependent_model = crate::db::DatabaseModel::new() - .package_data() - .idx_model(dependent) - .and_then(|pkg| pkg.installed()) - .expect(db) - .await?; - handle_broken_dependents( + break_transitive( db, dependent, id, - dependent_model, DependencyError::ConfigUnsatisfied { error: format!("{}", e), }, diff --git a/appmgr/src/control.rs b/appmgr/src/control.rs index b67deeac2..f42352098 100644 --- a/appmgr/src/control.rs +++ b/appmgr/src/control.rs @@ -7,9 +7,13 @@ use rpc_toolkit::command; use crate::context::RpcContext; use crate::db::util::WithRevision; +use crate::dependencies::{ + break_all_dependents_transitive, heal_all_dependents_transitive, BreakageRes, DependencyError, + TaggedDependencyError, +}; use crate::s9pk::manifest::PackageId; use crate::status::MainStatus; -use crate::util::display_none; +use crate::util::{display_none, display_serializable}; use crate::{Error, ResultExt}; #[command(display(display_none))] @@ -46,12 +50,17 @@ pub async fn start( }; status .synchronize( - &*ctx.managers.get(&(id, version)).await.ok_or_else(|| { - Error::new(anyhow!("Manager not found"), crate::ErrorKind::Docker) - })?, + &*ctx + .managers + .get(&(id.clone(), version)) + .await + .ok_or_else(|| { + Error::new(anyhow!("Manager not found"), crate::ErrorKind::Docker) + })?, ) .await?; status.save(&mut tx).await?; + heal_all_dependents_transitive(&ctx, &mut tx, &id).await?; Ok(WithRevision { revision: tx.commit(None).await?, @@ -59,19 +68,16 @@ pub async fn start( }) } -#[command(display(display_none))] -pub async fn stop( - #[context] ctx: RpcContext, - #[arg] id: PackageId, -) -> Result, Error> { - let mut db = ctx.db.handle(); - let mut tx = db.begin().await?; - +async fn stop_common( + db: &mut Db, + id: &PackageId, + breakages: &mut BTreeMap, +) -> Result<(), Error> { let mut status = crate::db::DatabaseModel::new() .package_data() .idx_model(&id) .and_then(|pkg| pkg.installed()) - .expect(&mut tx) + .expect(db) .await .with_ctx(|_| { ( @@ -81,11 +87,43 @@ pub async fn stop( })? .status() .main() - .get_mut(&mut tx) + .get_mut(db) .await?; *status = MainStatus::Stopping; - status.save(&mut tx).await?; + status.save(db).await?; + break_all_dependents_transitive(db, &id, DependencyError::NotRunning, breakages).await?; + + Ok(()) +} + +#[command(subcommands(self(stop_impl(async)), stop_dry), display(display_none))] +pub fn stop(#[arg] id: PackageId) -> Result { + Ok(id) +} + +#[command(rename = "dry", display(display_serializable))] +pub async fn stop_dry( + #[context] ctx: RpcContext, + #[parent_data] id: PackageId, +) -> Result { + let mut db = ctx.db.handle(); + let mut tx = db.begin().await?; + + let mut breakages = BTreeMap::new(); + stop_common(&mut tx, &id, &mut breakages).await?; + + Ok(BreakageRes { + breakages, + patch: tx.abort().await?, + }) +} + +pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result, Error> { + let mut db = ctx.db.handle(); + let mut tx = db.begin().await?; + + stop_common(&mut tx, &id, &mut BTreeMap::new()).await?; Ok(WithRevision { revision: tx.commit(None).await?, diff --git a/appmgr/src/db/mod.rs b/appmgr/src/db/mod.rs index ac6fca35e..6225fa0aa 100644 --- a/appmgr/src/db/mod.rs +++ b/appmgr/src/db/mod.rs @@ -1,10 +1,12 @@ pub mod model; pub mod util; +use std::borrow::Cow; use std::future::Future; use std::sync::Arc; use std::time::Duration; +use anyhow::anyhow; use futures::{FutureExt, SinkExt, StreamExt}; use patch_db::json_ptr::JsonPointer; use patch_db::{Dump, Revision}; @@ -21,6 +23,7 @@ use tokio_tungstenite::WebSocketStream; pub use self::model::DatabaseModel; use self::util::WithRevision; use crate::context::RpcContext; +use crate::middleware::auth::hash_token; use crate::util::{display_serializable, GeneralGuard, IoFormat}; use crate::{Error, ResultExt}; @@ -39,7 +42,7 @@ async fn ws_handler< // add 1 to the session counter and issue an RAII guard to subtract 1 on drop ctx.websocket_count .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - let decrementer = GeneralGuard::new(|| { + let _decrementer = GeneralGuard::new(|| { let new_count = ctx .websocket_count .fetch_sub(1, std::sync::atomic::Ordering::SeqCst); @@ -51,13 +54,22 @@ async fn ws_handler< }); loop { - if let Some(Message::Text(_)) = stream + if let Some(Message::Text(cookie)) = stream .next() .await .transpose() .with_kind(crate::ErrorKind::Network)? { - // TODO: check auth + let cookie_str = serde_json::from_str::>(&cookie) + .with_kind(crate::ErrorKind::Deserialization)?; + let id = basic_cookies::Cookie::parse(&cookie_str) + .with_kind(crate::ErrorKind::Authorization)? + .into_iter() + .find(|c| c.get_name() == "session") + .ok_or_else(|| { + Error::new(anyhow!("UNAUTHORIZED"), crate::ErrorKind::Authorization) + })?; + crate::middleware::auth::is_authed(&ctx, &hash_token(id.get_value())).await?; break; } } diff --git a/appmgr/src/dependencies.rs b/appmgr/src/dependencies.rs index 8e9d2edb7..0f3c25d73 100644 --- a/appmgr/src/dependencies.rs +++ b/appmgr/src/dependencies.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use anyhow::anyhow; use emver::VersionRange; +use futures::future::BoxFuture; +use futures::FutureExt; use patch_db::{DbHandle, DiffPatch, HasModel, Map, MapModel}; use serde::{Deserialize, Serialize}; @@ -9,9 +11,9 @@ use crate::action::{ActionImplementation, NoOutput}; use crate::config::Config; use crate::context::RpcContext; use crate::db::model::CurrentDependencyInfo; -use crate::s9pk::manifest::PackageId; +use crate::s9pk::manifest::{Manifest, PackageId}; use crate::status::health_check::{HealthCheckId, HealthCheckResult, HealthCheckResultVariant}; -use crate::status::MainStatus; +use crate::status::{MainStatus, Status}; use crate::util::Version; use crate::volume::Volumes; use crate::Error; @@ -21,59 +23,278 @@ use crate::Error; #[serde(tag = "type")] pub enum DependencyError { NotInstalled, // { "type": "not-installed" } + #[serde(rename_all = "kebab-case")] IncorrectVersion { expected: VersionRange, received: Version, }, // { "type": "incorrect-version", "expected": "0.1.0", "received": "^0.2.0" } + #[serde(rename_all = "kebab-case")] ConfigUnsatisfied { error: String, }, // { "type": "config-unsatisfied", "error": "Bitcoin Core must have pruning set to manual." } NotRunning, // { "type": "not-running" } + #[serde(rename_all = "kebab-case")] HealthChecksFailed { failures: BTreeMap, - }, // { "type": "health-checks-failed", "checks": { "rpc": { "time": "2021-05-11T18:21:29Z", "result": "warming-up" } } } + }, // { "type": "health-checks-failed", "checks": { "rpc": { "time": "2021-05-11T18:21:29Z", "result": "starting" } } } + #[serde(rename_all = "kebab-case")] + Transitive, // { "type": "transitive" } } impl DependencyError { pub fn merge_with(self, other: DependencyError) -> DependencyError { - use DependencyError::*; match (self, other) { - (NotInstalled, _) => NotInstalled, - (_, NotInstalled) => NotInstalled, - (IncorrectVersion { expected, received }, _) => IncorrectVersion { expected, received }, - (_, IncorrectVersion { expected, received }) => IncorrectVersion { expected, received }, - (ConfigUnsatisfied { error: e0 }, ConfigUnsatisfied { error: e1 }) => { - ConfigUnsatisfied { - error: e0 + "\n" + &e1, - } + (DependencyError::NotInstalled, _) | (_, DependencyError::NotInstalled) => { + DependencyError::NotInstalled } - (ConfigUnsatisfied { error }, _) => ConfigUnsatisfied { error }, - (_, ConfigUnsatisfied { error }) => ConfigUnsatisfied { error }, - (NotRunning, _) => NotRunning, - (_, NotRunning) => NotRunning, - (HealthChecksFailed { failures: f0 }, HealthChecksFailed { failures: f1 }) => { - HealthChecksFailed { - failures: f0.into_iter().chain(f1.into_iter()).collect(), - } + (DependencyError::IncorrectVersion { expected, received }, _) + | (_, DependencyError::IncorrectVersion { expected, received }) => { + DependencyError::IncorrectVersion { expected, received } + } + ( + DependencyError::ConfigUnsatisfied { error: e0 }, + DependencyError::ConfigUnsatisfied { error: e1 }, + ) => DependencyError::ConfigUnsatisfied { + error: e0 + "\n" + &e1, + }, + (DependencyError::ConfigUnsatisfied { error }, _) + | (_, DependencyError::ConfigUnsatisfied { error }) => { + DependencyError::ConfigUnsatisfied { error } + } + (DependencyError::NotRunning, _) | (_, DependencyError::NotRunning) => { + DependencyError::NotRunning + } + ( + DependencyError::HealthChecksFailed { failures: f0 }, + DependencyError::HealthChecksFailed { failures: f1 }, + ) => DependencyError::HealthChecksFailed { + failures: f0.into_iter().chain(f1.into_iter()).collect(), + }, + (DependencyError::HealthChecksFailed { failures }, _) + | (_, DependencyError::HealthChecksFailed { failures }) => { + DependencyError::HealthChecksFailed { failures } + } + (DependencyError::Transitive, _) | (_, DependencyError::Transitive) => { + DependencyError::Transitive } } } + pub fn try_heal<'a, Db: DbHandle>( + self, + ctx: &'a RpcContext, + db: &'a mut Db, + id: &'a PackageId, + dependency: &'a PackageId, + mut dependency_config: Option, + info: &'a DepInfo, + ) -> BoxFuture<'a, Result, Error>> { + async move { + Ok(match self { + DependencyError::NotInstalled => { + if crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency) + .and_then(|m| m.installed()) + .exists(db, true) + .await? + { + DependencyError::IncorrectVersion { + expected: info.version.clone(), + received: Default::default(), + } + .try_heal(ctx, db, id, dependency, dependency_config, info) + .await? + } else { + Some(DependencyError::NotInstalled) + } + } + DependencyError::IncorrectVersion { expected, .. } => { + let version: Version = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency) + .and_then(|m| m.installed()) + .map(|m| m.manifest().version()) + .get(db, true) + .await? + .into_owned() + .unwrap_or_default(); + if version.satisfies(&expected) { + DependencyError::ConfigUnsatisfied { + error: String::new(), + } + .try_heal(ctx, db, id, dependency, dependency_config, info) + .await? + } else { + Some(DependencyError::IncorrectVersion { + expected, + received: version, + }) + } + } + DependencyError::ConfigUnsatisfied { .. } => { + let dependent_manifest = crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .map::<_, Manifest>(|m| m.manifest()) + .expect(db) + .await? + .get(db, true) + .await?; + let dependency_manifest = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency) + .and_then(|m| m.installed()) + .map::<_, Manifest>(|m| m.manifest()) + .expect(db) + .await? + .get(db, true) + .await?; + let dependency_config = if let Some(cfg) = dependency_config.take() { + cfg + } else if let Some(cfg_info) = &dependency_manifest.config { + cfg_info + .get( + ctx, + dependency, + &dependency_manifest.version, + &dependency_manifest.volumes, + ) + .await? + .config + .unwrap_or_default() + } else { + Config::default() + }; + if let Some(cfg_req) = &info.config { + if let Err(e) = cfg_req + .check( + ctx, + id, + &dependent_manifest.version, + &dependent_manifest.volumes, + &dependency_config, + ) + .await + { + if e.kind == crate::ErrorKind::ConfigRulesViolation { + return Ok(Some(DependencyError::ConfigUnsatisfied { + error: format!("{}", e), + })); + } else { + return Err(e); + } + } + } + DependencyError::NotRunning + .try_heal(ctx, db, id, dependency, Some(dependency_config), info) + .await? + } + DependencyError::NotRunning => { + let status = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency) + .and_then(|m| m.installed()) + .map::<_, Status>(|m| m.status()) + .expect(db) + .await? + .get(db, true) + .await?; + if status.main.running() { + DependencyError::HealthChecksFailed { + failures: BTreeMap::new(), + } + .try_heal(ctx, db, id, dependency, dependency_config, info) + .await? + } else { + Some(DependencyError::NotRunning) + } + } + DependencyError::HealthChecksFailed { .. } => { + let status = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency) + .and_then(|m| m.installed()) + .map::<_, Status>(|m| m.status()) + .expect(db) + .await? + .get(db, true) + .await? + .into_owned(); + match status.main { + MainStatus::BackingUp { + started: Some(_), + health, + } + | MainStatus::Running { health, .. } => { + let mut failures = BTreeMap::new(); + for (check, res) in health { + if !matches!(res.result, HealthCheckResultVariant::Success) + && crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .and_then::<_, CurrentDependencyInfo>(|m| { + m.current_dependencies().idx_model(dependency) + }) + .get(db, true) + .await? + .into_owned() + .map(|i| i.health_checks) + .unwrap_or_default() + .contains(&check) + { + failures.insert(check.clone(), res.clone()); + } + } + if !failures.is_empty() { + Some(DependencyError::HealthChecksFailed { failures }) + } else { + DependencyError::Transitive + .try_heal(ctx, db, id, dependency, dependency_config, info) + .await? + } + } + _ => return Ok(Some(DependencyError::NotRunning)), + } + } + DependencyError::Transitive => { + if crate::db::DatabaseModel::new() + .package_data() + .idx_model(dependency) + .and_then(|m| m.installed()) + .map::<_, DependencyErrors>(|m| m.status().dependency_errors()) + .get(db, true) + .await? + .into_owned() + .unwrap_or_default() + .0 + .is_empty() + { + None + } else { + Some(DependencyError::Transitive) + } + } + }) + } + .boxed() + } } impl std::fmt::Display for DependencyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use DependencyError::*; match self { - NotInstalled => write!(f, "Not Installed"), - IncorrectVersion { expected, received } => write!( + DependencyError::NotInstalled => write!(f, "Not Installed"), + DependencyError::IncorrectVersion { expected, received } => write!( f, "Incorrect Version: Expected {}, Received {}", expected, received.as_str() ), - ConfigUnsatisfied { error } => { + DependencyError::ConfigUnsatisfied { error } => { write!(f, "Configuration Requirements Not Satisfied: {}", error) } - NotRunning => write!(f, "Not Running"), - HealthChecksFailed { failures } => { + DependencyError::NotRunning => write!(f, "Not Running"), + DependencyError::HealthChecksFailed { failures } => { write!(f, "Failed Health Check(s): ")?; let mut comma = false; for (check, res) in failures { @@ -86,6 +307,9 @@ impl std::fmt::Display for DependencyError { } Ok(()) } + DependencyError::Transitive => { + write!(f, "Dependency Error(s)") + } } } } @@ -150,79 +374,24 @@ impl DepInfo { dependency_id: &PackageId, dependency_config: Option, // fetch if none dependent_id: &PackageId, - dependent_version: &Version, - dependent_volumes: &Volumes, ) -> Result, Error> { - let (manifest, info) = if let Some(dep_model) = crate::db::DatabaseModel::new() - .package_data() - .idx_model(dependency_id) - .and_then(|pde| pde.installed()) - .check(db) - .await? - { - ( - dep_model.clone().manifest().get(db, true).await?, - dep_model.get(db, true).await?, - ) - } else { - return Ok(Err(DependencyError::NotInstalled)); - }; - if !&manifest.version.satisfies(&self.version) { - return Ok(Err(DependencyError::IncorrectVersion { - expected: self.version.clone(), - received: manifest.version.clone(), - })); - } - let dependency_config = if let Some(cfg) = dependency_config { - cfg - } else if let Some(cfg_info) = &manifest.config { - cfg_info - .get(ctx, dependency_id, &manifest.version, &manifest.volumes) - .await? - .config - .unwrap_or_default() - } else { - Config::default() - }; - if let Some(cfg_req) = &self.config { - if let Err(e) = cfg_req - .check( + Ok( + if let Some(err) = DependencyError::NotInstalled + .try_heal( ctx, + db, dependent_id, - dependent_version, - dependent_volumes, - &dependency_config, + dependency_id, + dependency_config, + self, ) - .await + .await? { - if e.kind == crate::ErrorKind::ConfigRulesViolation { - return Ok(Err(DependencyError::ConfigUnsatisfied { - error: format!("{}", e), - })); - } else { - return Err(e); - } - } - } - match &info.status.main { - MainStatus::BackingUp { - started: Some(_), - health, - } - | MainStatus::Running { health, .. } => { - let mut failures = BTreeMap::new(); - for (check, res) in health { - if !matches!(res.result, HealthCheckResultVariant::Success) { - failures.insert(check.clone(), res.clone()); - } - } - if !failures.is_empty() { - return Ok(Err(DependencyError::HealthChecksFailed { failures })); - } - } - _ => return Ok(Err(DependencyError::NotRunning)), - } - Ok(Ok(())) + Err(err) + } else { + Ok(()) + }, + ) } } @@ -303,3 +472,202 @@ pub async fn update_current_dependents< } Ok(()) } + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct DependencyErrors(pub BTreeMap); +impl Map for DependencyErrors { + type Key = PackageId; + type Value = DependencyError; + fn get(&self, key: &Self::Key) -> Option<&Self::Value> { + self.0.get(key) + } +} +impl HasModel for DependencyErrors { + type Model = MapModel; +} +impl DependencyErrors { + pub async fn init( + ctx: &RpcContext, + db: &mut Db, + manifest: &Manifest, + current_dependencies: &BTreeMap, + ) -> Result { + let mut res = BTreeMap::new(); + for (dependency_id, info) in current_dependencies.keys().filter_map(|dependency_id| { + manifest + .dependencies + .0 + .get(dependency_id) + .map(|info| (dependency_id, info)) + }) { + if let Err(e) = info + .satisfied(ctx, db, dependency_id, None, &manifest.id) + .await? + { + res.insert(dependency_id.clone(), e); + } + } + Ok(DependencyErrors(res)) + } +} +impl std::fmt::Display for DependencyErrors { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ ")?; + for (idx, (id, err)) in self.0.iter().enumerate() { + write!(f, "{}: {}", id, err)?; + if idx < self.0.len() - 1 { + // not last + write!(f, ", ")?; + } + } + write!(f, " }}") + } +} + +pub async fn break_all_dependents_transitive<'a, Db: DbHandle>( + db: &'a mut Db, + id: &'a PackageId, + error: DependencyError, + breakages: &'a mut BTreeMap, +) -> Result<(), Error> { + for dependent in crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .expect(db) + .await? + .current_dependents() + .keys(db, true) + .await? + { + break_transitive(db, &dependent, id, error.clone(), breakages).await?; + } + Ok(()) +} + +pub fn break_transitive<'a, Db: DbHandle>( + db: &'a mut Db, + id: &'a PackageId, + dependency: &'a PackageId, + error: DependencyError, + breakages: &'a mut BTreeMap, +) -> BoxFuture<'a, Result<(), Error>> { + async move { + let model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .expect(db) + .await?; + let mut status = model.clone().status().get_mut(db).await?; + + let old = status.dependency_errors.0.remove(dependency); + let newly_broken = old.is_none(); + status.dependency_errors.0.insert( + dependency.clone(), + if let Some(old) = old { + old.merge_with(error.clone()) + } else { + error.clone() + }, + ); + if newly_broken { + breakages.insert( + id.clone(), + TaggedDependencyError { + dependency: dependency.clone(), + error: error.clone(), + }, + ); + if status.main.running() { + let transitive_error = if model + .clone() + .manifest() + .dependencies() + .idx_model(dependency) + .get(db, true) + .await? + .into_owned() + .ok_or_else(|| { + Error::new( + anyhow!("{} not in listed dependencies", dependency), + crate::ErrorKind::Database, + ) + })? + .critical + { + status.main.stop(); + DependencyError::NotRunning + } else { + DependencyError::Transitive + }; + break_all_dependents_transitive(db, id, transitive_error, breakages).await?; + } + } + + status.save(db).await?; + + Ok(()) + } + .boxed() +} + +pub async fn heal_all_dependents_transitive<'a, Db: DbHandle>( + ctx: &'a RpcContext, + db: &'a mut Db, + id: &'a PackageId, +) -> Result<(), Error> { + for dependent in crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .expect(db) + .await? + .current_dependents() + .keys(db, true) + .await? + { + heal_transitive(ctx, db, &dependent, id).await?; + } + Ok(()) +} + +pub fn heal_transitive<'a, Db: DbHandle>( + ctx: &'a RpcContext, + db: &'a mut Db, + id: &'a PackageId, + dependency: &'a PackageId, +) -> BoxFuture<'a, Result<(), Error>> { + async move { + let model = crate::db::DatabaseModel::new() + .package_data() + .idx_model(id) + .and_then(|m| m.installed()) + .expect(db) + .await?; + let mut status = model.clone().status().get_mut(db).await?; + + let old = status.dependency_errors.0.remove(dependency); + + if let Some(old) = old { + let info = model + .manifest() + .dependencies() + .idx_model(dependency) + .expect(db) + .await? + .get(db, true) + .await?; + if let Some(new) = old.try_heal(ctx, db, id, dependency, None, &*info).await? { + status.dependency_errors.0.insert(dependency.clone(), new); + } else { + heal_all_dependents_transitive(ctx, db, id).await?; + } + } + + status.save(db).await?; + + Ok(()) + } + .boxed() +} diff --git a/appmgr/src/hostname.rs b/appmgr/src/hostname.rs index bfe3658c2..8eb1ceee7 100644 --- a/appmgr/src/hostname.rs +++ b/appmgr/src/hostname.rs @@ -38,8 +38,13 @@ pub async fn get_id() -> Result { Ok(hex::encode(&res[0..4])) } -// cat /embassy-os/product_key.txt | shasum -a 256 | head -c 8 | awk '{print "start9-"$1}' | xargs hostnamectl set-hostname +// cat /embassy-os/product_key.txt | shasum -a 256 | head -c 8 | awk '{print "start9-"$1}' | xargs hostnamectl set-hostname && systemctl restart avahi-daemon pub async fn sync_hostname() -> Result<(), Error> { set_hostname(&format!("start9-{}", get_id().await?)).await?; + Command::new("systemctl") + .arg("restart") + .arg("avahi-daemon") + .invoke(crate::ErrorKind::Network) + .await?; Ok(()) } diff --git a/appmgr/src/install/cleanup.rs b/appmgr/src/install/cleanup.rs index cef926498..d84151cca 100644 --- a/appmgr/src/install/cleanup.rs +++ b/appmgr/src/install/cleanup.rs @@ -1,11 +1,12 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use bollard::image::ListImagesOptions; use patch_db::{DbHandle, PatchDbHandle}; use super::PKG_DOCKER_DIR; use crate::context::RpcContext; -use crate::db::model::{InstalledPackageDataEntry, PackageDataEntry}; +use crate::db::model::{CurrentDependencyInfo, InstalledPackageDataEntry, PackageDataEntry}; +use crate::dependencies::update_current_dependents; use crate::s9pk::manifest::PackageId; use crate::util::Version; use crate::Error; @@ -29,8 +30,7 @@ pub async fn update_dependents<'a, Db: DbHandle, I: IntoIterator( Ok(()) } +pub async fn remove_current_dependents<'a, Db: DbHandle, I: IntoIterator>( + db: &mut Db, + id: &PackageId, + current_dependencies: I, +) -> Result<(), Error> { + for dep in current_dependencies { + if let Some(current_dependents) = crate::db::DatabaseModel::new() + .package_data() + .idx_model(dep) + .and_then(|m| m.installed()) + .map::<_, BTreeMap>(|m| m.current_dependents()) + .check(db) + .await? + { + current_dependents.remove(db, id).await? + } + } + Ok(()) +} + pub async fn uninstall( ctx: &RpcContext, db: &mut PatchDbHandle, @@ -178,6 +198,12 @@ pub async fn uninstall( .package_data() .remove(&mut tx, &entry.manifest.id) .await?; + remove_current_dependents( + &mut tx, + &entry.manifest.id, + entry.current_dependencies.keys(), + ) + .await?; update_dependents( ctx, &mut tx, diff --git a/appmgr/src/install/mod.rs b/appmgr/src/install/mod.rs index a91155d01..2587ad989 100644 --- a/appmgr/src/install/mod.rs +++ b/appmgr/src/install/mod.rs @@ -24,12 +24,15 @@ use crate::db::model::{ StaticFiles, }; use crate::db::util::WithRevision; -use crate::dependencies::{update_current_dependents, BreakageRes, DependencyError}; +use crate::dependencies::{ + break_all_dependents_transitive, update_current_dependents, BreakageRes, DependencyError, + DependencyErrors, +}; use crate::install::cleanup::{cleanup, update_dependents}; use crate::install::progress::{InstallProgress, InstallProgressTracker}; use crate::s9pk::manifest::{Manifest, PackageId}; use crate::s9pk::reader::S9pkReader; -use crate::status::{handle_broken_dependents, DependencyErrors, MainStatus, Status}; +use crate::status::{MainStatus, Status}; use crate::util::io::copy_and_shutdown; use crate::util::{display_none, display_serializable, AsyncFileExt, Version}; use crate::volume::asset_dir; @@ -134,38 +137,10 @@ pub async fn uninstall_dry( #[parent_data] id: PackageId, ) -> Result { let mut db = ctx.db.handle(); - let deps = crate::db::DatabaseModel::new() - .package_data() - .idx_model(&id) - .expect(&mut db) - .await? - .installed() - .expect(&mut db) - .await? - .current_dependents() - .get(&mut db, true) - .await?; let mut tx = db.begin().await?; let mut breakages = BTreeMap::new(); - for dep_id in deps.keys() { - let model = crate::db::DatabaseModel::new() - .package_data() - .idx_model(&dep_id) - .expect(&mut tx) - .await? - .installed() - .expect(&mut tx) - .await?; - handle_broken_dependents( - &mut tx, - dep_id, - &id, - model, - DependencyError::NotInstalled, - &mut breakages, - ) + break_all_dependents_transitive(&mut tx, &id, DependencyError::NotInstalled, &mut breakages) .await?; - } Ok(BreakageRes { breakages, @@ -575,7 +550,7 @@ pub async fn install_s9pk( log::info!("Install {}@{}: Created manager", pkg_id, version); let static_files = StaticFiles::local(pkg_id, version, manifest.assets.icon_type()); - let current_dependencies = manifest + let current_dependencies: BTreeMap<_, _> = manifest .dependencies .0 .iter() @@ -616,22 +591,21 @@ pub async fn install_s9pk( status: Status { configured: manifest.config.is_none(), main: MainStatus::Stopped, - dependency_errors: DependencyErrors::init( - ctx, - &mut tx, - &manifest, - ¤t_dependencies, - ) - .await?, + dependency_errors: DependencyErrors::default(), }, manifest: manifest.clone(), system_pointers: Vec::new(), dependency_info, current_dependents: current_dependents.clone(), - current_dependencies, + current_dependencies: current_dependencies.clone(), interface_addresses, }; - let mut pde = model.expect(&mut tx).await?.get_mut(&mut tx).await?; + let mut pde = model + .clone() + .expect(&mut tx) + .await? + .get_mut(&mut tx) + .await?; let prev = std::mem::replace( &mut *pde, PackageDataEntry::Installed { @@ -641,6 +615,18 @@ pub async fn install_s9pk( }, ); pde.save(&mut tx).await?; + let mut dep_errs = model + .expect(&mut tx) + .await? + .installed() + .expect(&mut tx) + .await? + .status() + .dependency_errors() + .get_mut(&mut tx) + .await?; + *dep_errs = DependencyErrors::init(ctx, &mut tx, &manifest, ¤t_dependencies).await?; + dep_errs.save(&mut tx).await?; if let PackageDataEntry::Updating { installed: prev, diff --git a/appmgr/src/middleware/auth.rs b/appmgr/src/middleware/auth.rs index 84d8c04ee..0b4ceffab 100644 --- a/appmgr/src/middleware/auth.rs +++ b/appmgr/src/middleware/auth.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use basic_cookies::Cookie; use digest::Digest; use futures::future::BoxFuture; -use futures::FutureExt; +use futures::{FutureExt, TryFutureExt}; use http::StatusCode; use rpc_toolkit::command_helpers::prelude::RequestParts; use rpc_toolkit::hyper::header::COOKIE; @@ -44,8 +44,7 @@ pub fn hash_token(token: &str) -> String { .to_lowercase() } -pub async fn is_authed(ctx: &RpcContext, req: &RequestParts) -> Result<(), Error> { - let id = get_id(req)?; +pub async fn is_authed(ctx: &RpcContext, id: &str) -> Result<(), Error> { let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", id) .execute(&mut ctx.secret_store.acquire().await?) .await?; @@ -73,7 +72,10 @@ pub fn auth(ctx: RpcContext) -> DynMiddleware { .get(rpc_req.method.as_str(), "authenticated") .unwrap_or(true) { - if let Err(e) = is_authed(&ctx, req).await { + if let Err(e) = async { get_id(req) } + .and_then(|id| async move { is_authed(&ctx, &id).await }) + .await + { let (res_parts, _) = Response::new(()).into_parts(); return Ok(Err(to_response( &req.headers, diff --git a/appmgr/src/setup.rs b/appmgr/src/setup.rs index 9d5cd3c7a..613e6062b 100644 --- a/appmgr/src/setup.rs +++ b/appmgr/src/setup.rs @@ -103,7 +103,7 @@ pub async fn execute_inner( let mut guid_file = File::create("/embassy-os/disk.guid").await?; guid_file.write_all(guid.as_bytes()).await?; guid_file.sync_all().await?; - crate::disk::main::export(&ctx.zfs_pool_name).await?; + sqlite_pool.close().await; ctx.shutdown.send(()).expect("failed to shutdown"); diff --git a/appmgr/src/status/mod.rs b/appmgr/src/status/mod.rs index fd9e86708..57cbfabd4 100644 --- a/appmgr/src/status/mod.rs +++ b/appmgr/src/status/mod.rs @@ -11,7 +11,9 @@ use serde::{Deserialize, Serialize}; use self::health_check::{HealthCheckId, HealthCheckResult}; use crate::context::RpcContext; use crate::db::model::{CurrentDependencyInfo, InstalledPackageDataEntryModel}; -use crate::dependencies::{DependencyError, TaggedDependencyError}; +use crate::dependencies::{ + break_transitive, DependencyError, DependencyErrors, TaggedDependencyError, +}; use crate::manager::{Manager, Status as ManagerStatus}; use crate::notifications::{notify, NotificationLevel, NotificationSubtype}; use crate::s9pk::manifest::{Manifest, PackageId}; @@ -212,15 +214,7 @@ pub async fn check_all(ctx: &RpcContext) -> Result<(), Error> { } _ => Some(DependencyError::NotRunning), } { - handle_broken_dependents( - &mut db, - id, - dep_id, - model.clone(), - err, - &mut BTreeMap::new(), - ) - .await?; + break_transitive(&mut db, id, dep_id, err, &mut BTreeMap::new()).await?; } else { let mut errs = model .clone() @@ -262,6 +256,7 @@ pub async fn check_all(ctx: &RpcContext) -> Result<(), Error> { pub struct Status { pub configured: bool, pub main: MainStatus, + #[model] pub dependency_errors: DependencyErrors, } @@ -391,131 +386,3 @@ impl MainStatus { } } } - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct DependencyErrors(pub BTreeMap); -impl Map for DependencyErrors { - type Key = PackageId; - type Value = DependencyError; - fn get(&self, key: &Self::Key) -> Option<&Self::Value> { - self.0.get(key) - } -} -impl HasModel for DependencyErrors { - type Model = MapModel; -} -impl DependencyErrors { - pub async fn init( - ctx: &RpcContext, - db: &mut Db, - manifest: &Manifest, - current_dependencies: &BTreeMap, - ) -> Result { - let mut res = BTreeMap::new(); - for (dep_id, info) in current_dependencies.keys().filter_map(|dep_id| { - manifest - .dependencies - .0 - .get(dep_id) - .map(|info| (dep_id, info)) - }) { - if let Err(e) = info - .satisfied( - ctx, - db, - dep_id, - None, - &manifest.id, - &manifest.version, - &manifest.volumes, - ) - .await? - { - res.insert(dep_id.clone(), e); - } - } - Ok(DependencyErrors(res)) - } -} - -pub fn handle_broken_dependents<'a, Db: DbHandle>( - db: &'a mut Db, - id: &'a PackageId, - dependency: &'a PackageId, - model: InstalledPackageDataEntryModel, - error: DependencyError, - breakages: &'a mut BTreeMap, -) -> BoxFuture<'a, Result<(), Error>> { - async move { - let mut status = model.clone().status().get_mut(db).await?; - - let old = status.dependency_errors.0.remove(id); - let newly_broken = old.is_none(); - status.dependency_errors.0.insert( - id.clone(), - if let Some(old) = old { - old.merge_with(error.clone()) - } else { - error.clone() - }, - ); - if newly_broken { - breakages.insert( - id.clone(), - TaggedDependencyError { - dependency: dependency.clone(), - error: error.clone(), - }, - ); - if status.main.running() { - if model - .clone() - .manifest() - .dependencies() - .idx_model(dependency) - .get(db, true) - .await? - .into_owned() - .ok_or_else(|| { - Error::new( - anyhow!("{} not in listed dependencies", dependency), - crate::ErrorKind::Database, - ) - })? - .critical - { - status.main.stop(); - let dependents = model.current_dependents().get(db, true).await?; - for (dependent, _) in &*dependents { - let dependent_model = crate::db::DatabaseModel::new() - .package_data() - .idx_model(dependent) - .and_then(|pkg| pkg.installed()) - .check(db) - .await? - .ok_or_else(|| { - Error::new( - anyhow!("{} is not installed", dependent), - crate::ErrorKind::NotFound, - ) - })?; - handle_broken_dependents( - db, - dependent, - id, - dependent_model, - DependencyError::NotRunning, - breakages, - ) - .await?; - } - } - } - } - - status.save(db).await?; - - Ok(()) - } - .boxed() -} diff --git a/appmgr/src/util/mod.rs b/appmgr/src/util/mod.rs index ee70c3fce..989d3a3fb 100644 --- a/appmgr/src/util/mod.rs +++ b/appmgr/src/util/mod.rs @@ -365,6 +365,11 @@ impl From for emver::Version { v.version } } +impl Default for Version { + fn default() -> Self { + Self::from(emver::Version::default()) + } +} impl Deref for Version { type Target = emver::Version; fn deref(&self) -> &Self::Target { diff --git a/patch-db b/patch-db index f0e6968a7..44f7150ba 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit f0e6968a79e021c65e4c0e29a667b110c830237e +Subproject commit 44f7150bad20004587b9cb75eb415d759dca2cf8