use hardware requirements to display conflicts and prevent install (#2700)

* use hardware requirements to display conflicts and prevent install

* better messaging and also consider OS compatibility

* wip: backend hw requirements

* update backend components

* migration

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2024-10-29 13:48:03 -06:00
committed by GitHub
parent e1a91a7e53
commit 1be9cdae67
35 changed files with 725 additions and 500 deletions

676
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,8 +39,7 @@ use crate::service::action::update_requested_actions;
use crate::service::effects::callbacks::ServiceCallbacks; use crate::service::effects::callbacks::ServiceCallbacks;
use crate::service::ServiceMap; use crate::service::ServiceMap;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::system::get_mem_info; use crate::util::lshw::LshwDevice;
use crate::util::lshw::{lshw, LshwDevice};
use crate::util::sync::SyncMutex; use crate::util::sync::SyncMutex;
pub struct RpcContextSeed { pub struct RpcContextSeed {
@@ -67,7 +66,6 @@ pub struct RpcContextSeed {
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>, pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
pub current_secret: Arc<Jwk>, pub current_secret: Arc<Jwk>,
pub client: Client, pub client: Client,
pub hardware: Hardware,
pub start_time: Instant, pub start_time: Instant,
pub crons: SyncMutex<BTreeMap<Guid, NonDetachingJoinHandle<()>>>, pub crons: SyncMutex<BTreeMap<Guid, NonDetachingJoinHandle<()>>>,
// #[cfg(feature = "dev")] // #[cfg(feature = "dev")]
@@ -86,7 +84,6 @@ pub struct Hardware {
pub struct InitRpcContextPhases { pub struct InitRpcContextPhases {
load_db: PhaseProgressTrackerHandle, load_db: PhaseProgressTrackerHandle,
init_net_ctrl: PhaseProgressTrackerHandle, init_net_ctrl: PhaseProgressTrackerHandle,
read_device_info: PhaseProgressTrackerHandle,
cleanup_init: CleanupInitPhases, cleanup_init: CleanupInitPhases,
// TODO: migrations // TODO: migrations
} }
@@ -95,7 +92,6 @@ impl InitRpcContextPhases {
Self { Self {
load_db: handle.add_phase("Loading database".into(), Some(5)), load_db: handle.add_phase("Loading database".into(), Some(5)),
init_net_ctrl: handle.add_phase("Initializing network".into(), Some(1)), init_net_ctrl: handle.add_phase("Initializing network".into(), Some(1)),
read_device_info: handle.add_phase("Reading device information".into(), Some(1)),
cleanup_init: CleanupInitPhases::new(handle), cleanup_init: CleanupInitPhases::new(handle),
} }
} }
@@ -127,7 +123,6 @@ impl RpcContext {
InitRpcContextPhases { InitRpcContextPhases {
mut load_db, mut load_db,
mut init_net_ctrl, mut init_net_ctrl,
mut read_device_info,
cleanup_init, cleanup_init,
}: InitRpcContextPhases, }: InitRpcContextPhases,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
@@ -179,11 +174,6 @@ impl RpcContext {
let metrics_cache = RwLock::<Option<crate::system::Metrics>>::new(None); let metrics_cache = RwLock::<Option<crate::system::Metrics>>::new(None);
let tor_proxy_url = format!("socks5h://{tor_proxy}"); let tor_proxy_url = format!("socks5h://{tor_proxy}");
read_device_info.start();
let devices = lshw().await?;
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
read_device_info.complete();
let crons = SyncMutex::new(BTreeMap::new()); let crons = SyncMutex::new(BTreeMap::new());
if !db if !db
@@ -275,7 +265,6 @@ impl RpcContext {
})) }))
.build() .build()
.with_kind(crate::ErrorKind::ParseUrl)?, .with_kind(crate::ErrorKind::ParseUrl)?,
hardware: Hardware { devices, ram },
start_time: Instant::now(), start_time: Instant::now(),
crons, crons,
// #[cfg(feature = "dev")] // #[cfg(feature = "dev")]

View File

@@ -22,6 +22,7 @@ use crate::prelude::*;
use crate::progress::FullProgress; use crate::progress::FullProgress;
use crate::system::SmtpValue; use crate::system::SmtpValue;
use crate::util::cpupower::Governor; use crate::util::cpupower::Governor;
use crate::util::lshw::LshwDevice;
use crate::version::{Current, VersionT}; use crate::version::{Current, VersionT};
use crate::{ARCH, PLATFORM}; use crate::{ARCH, PLATFORM};
@@ -46,7 +47,7 @@ impl Public {
version: Current::default().semver(), version: Current::default().semver(),
hostname: account.hostname.no_dot_host_name(), hostname: account.hostname.no_dot_host_name(),
last_backup: None, last_backup: None,
version_compat: Current::default().compat().clone(), package_version_compat: Current::default().compat().clone(),
post_init_migration_todos: BTreeSet::new(), post_init_migration_todos: BTreeSet::new(),
lan_address, lan_address,
onion_address: account.tor_key.public().get_onion_address(), onion_address: account.tor_key.public().get_onion_address(),
@@ -78,6 +79,8 @@ impl Public {
zram: true, zram: true,
governor: None, governor: None,
smtp: None, smtp: None,
ram: 0,
devices: Vec::new(),
}, },
package_data: AllPackageData::default(), package_data: AllPackageData::default(),
ui: serde_json::from_str(include_str!(concat!( ui: serde_json::from_str(include_str!(concat!(
@@ -114,7 +117,7 @@ pub struct ServerInfo {
#[ts(type = "string")] #[ts(type = "string")]
pub version: Version, pub version: Version,
#[ts(type = "string")] #[ts(type = "string")]
pub version_compat: VersionRange, pub package_version_compat: VersionRange,
#[ts(type = "string[]")] #[ts(type = "string[]")]
pub post_init_migration_todos: BTreeSet<Version>, pub post_init_migration_todos: BTreeSet<Version>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
@@ -141,6 +144,9 @@ pub struct ServerInfo {
pub zram: bool, pub zram: bool,
pub governor: Option<Governor>, pub governor: Option<Governor>,
pub smtp: Option<SmtpValue>, pub smtp: Option<SmtpValue>,
#[ts(type = "number")]
pub ram: u64,
pub devices: Vec<LshwDevice>,
} }
#[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[derive(Debug, Deserialize, Serialize, HasModel, TS)]

View File

@@ -32,7 +32,9 @@ use crate::progress::{
use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL}; use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
use crate::ssh::SSH_AUTHORIZED_KEYS_FILE; use crate::ssh::SSH_AUTHORIZED_KEYS_FILE;
use crate::system::get_mem_info;
use crate::util::io::{create_file, IOHook}; use crate::util::io::{create_file, IOHook};
use crate::util::lshw::lshw;
use crate::util::net::WebSocketExt; use crate::util::net::WebSocketExt;
use crate::util::{cpupower, Invoke}; use crate::util::{cpupower, Invoke};
use crate::Error; use crate::Error;
@@ -508,6 +510,8 @@ pub async fn init(
update_server_info.start(); update_server_info.start();
server_info.ip_info = crate::net::dhcp::init_ips().await?; server_info.ip_info = crate::net::dhcp::init_ips().await?;
server_info.ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
server_info.devices = lshw().await?;
server_info.status_info = ServerStatus { server_info.status_info = ServerStatus {
updated: false, updated: false,
update_progress: None, update_progress: None,

View File

@@ -255,7 +255,7 @@ impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
.header(CONTENT_TYPE, "application/json") .header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json") .header(ACCEPT, "application/json")
.header(CONTENT_LENGTH, body.len()) .header(CONTENT_LENGTH, body.len())
.header(DEVICE_INFO_HEADER, DeviceInfo::from(self).to_header_value()) // .header(DEVICE_INFO_HEADER, DeviceInfo::from(self).to_header_value())
.body(body) .body(body)
.send() .send()
.await?; .await?;

View File

@@ -15,6 +15,7 @@ use url::Url;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::prelude::*; use crate::prelude::*;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::VersionString; use crate::util::VersionString;
use crate::version::VersionT; use crate::version::VersionT;
@@ -26,12 +27,12 @@ pub struct DeviceInfo {
pub os: OsInfo, pub os: OsInfo,
pub hardware: HardwareInfo, pub hardware: HardwareInfo,
} }
impl From<&RpcContext> for DeviceInfo { impl DeviceInfo {
fn from(value: &RpcContext) -> Self { pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
Self { Ok(Self {
os: OsInfo::from(value), os: OsInfo::from(ctx),
hardware: HardwareInfo::from(value), hardware: HardwareInfo::load(ctx).await?,
} })
} }
} }
impl DeviceInfo { impl DeviceInfo {
@@ -44,11 +45,11 @@ impl DeviceInfo {
.append_pair("hardware.arch", &*self.hardware.arch) .append_pair("hardware.arch", &*self.hardware.arch)
.append_pair("hardware.ram", &self.hardware.ram.to_string()); .append_pair("hardware.ram", &self.hardware.ram.to_string());
for (class, products) in &self.hardware.devices { for device in &self.hardware.devices {
for product in products { url.query_pairs_mut().append_pair(
url.query_pairs_mut() &format!("hardware.device.{}", device.class()),
.append_pair(&format!("hardware.device.{}", class), product); device.product(),
} );
} }
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
@@ -80,16 +81,20 @@ impl DeviceInfo {
devices: identity(query) devices: identity(query)
.split_off("hardware.device.") .split_off("hardware.device.")
.into_iter() .into_iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| match k.strip_prefix("hardware.device.") {
k.strip_prefix("hardware.device.") Some("processor") => Some(LshwDevice::Processor(LshwProcessor {
.map(|k| (k.into(), v.into_owned())) product: v.into_owned(),
})),
Some("display") => Some(LshwDevice::Display(LshwDisplay {
product: v.into_owned(),
})),
Some(class) => {
tracing::warn!("unknown device class: {class}");
None
}
_ => None,
}) })
.fold(BTreeMap::new(), |mut acc, (k, v)| { .collect(),
let mut devs = acc.remove(&k).unwrap_or_default();
devs.push(v);
acc.insert(k, devs);
acc
}),
}, },
}) })
} }
@@ -122,26 +127,16 @@ pub struct HardwareInfo {
pub arch: InternedString, pub arch: InternedString,
#[ts(type = "number")] #[ts(type = "number")]
pub ram: u64, pub ram: u64,
#[ts(as = "BTreeMap::<String, Vec<String>>")] pub devices: Vec<LshwDevice>,
pub devices: BTreeMap<InternedString, Vec<String>>,
} }
impl HardwareInfo {
impl From<&RpcContext> for HardwareInfo { pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
fn from(value: &RpcContext) -> Self { let s = ctx.db.peek().await.into_public().into_server_info();
Self { Ok(Self {
arch: InternedString::intern(crate::ARCH), arch: s.as_arch().de()?,
ram: value.hardware.ram, ram: s.as_ram().de()?,
devices: value devices: s.as_devices().de()?,
.hardware })
.devices
.iter()
.fold(BTreeMap::new(), |mut acc, dev| {
let mut devs = acc.remove(dev.class()).unwrap_or_default();
devs.push(dev.product().to_owned());
acc.insert(dev.class().into(), devs);
acc
}),
}
} }
} }

View File

@@ -180,14 +180,13 @@ impl Model<PackageVersionInfo> {
return Ok(false); return Ok(false);
} }
} }
for (class, regex) in hw.device { for device_filter in hw.device {
if !device_info if !device_info
.hardware .hardware
.devices .devices
.get(&*class)
.unwrap_or(&Vec::new())
.iter() .iter()
.any(|product| regex.as_ref().is_match(product)) .filter(|d| d.class() == &*device_filter.class)
.any(|d| device_filter.pattern.as_ref().is_match(d.product()))
{ {
return Ok(false); return Ok(false);
} }

View File

@@ -1,7 +1,8 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use exver::{Version, VersionRange}; use exver::{Version, VersionRange};
use imbl_value::InternedString;
use indexmap::IndexMap; use indexmap::IndexMap;
pub use models::PackageId; pub use models::PackageId;
use models::{ActionId, HealthCheckId, ImageId, VolumeId}; use models::{ActionId, HealthCheckId, ImageId, VolumeId};
@@ -10,8 +11,8 @@ use url::Url;
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::git_hash::GitHash; use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; use crate::s9pk::manifest::{Alerts, Description};
use crate::util::serde::{Duration, IoFormat}; use crate::util::serde::{Duration, IoFormat, Regex};
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
@@ -192,6 +193,15 @@ impl DependencyRequirement {
} }
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HardwareRequirements {
#[serde(default)]
pub device: BTreeMap<InternedString, Regex>,
pub ram: Option<u64>,
pub arch: Option<BTreeSet<InternedString>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Assets { pub struct Assets {

View File

@@ -10,7 +10,7 @@ use tokio::process::Command;
use crate::dependencies::{DepInfo, Dependencies}; use crate::dependencies::{DepInfo, Dependencies};
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::manifest::Manifest; use crate::s9pk::manifest::{DeviceFilter, Manifest};
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::source::TmpSource; use crate::s9pk::merkle_archive::source::TmpSource;
use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
@@ -246,7 +246,23 @@ impl TryFrom<ManifestV1> for Manifest {
}) })
.collect(), .collect(),
), ),
hardware_requirements: value.hardware_requirements, hardware_requirements: super::manifest::HardwareRequirements {
arch: value.hardware_requirements.arch,
ram: value.hardware_requirements.ram,
device: value
.hardware_requirements
.device
.into_iter()
.map(|(class, product)| DeviceFilter {
pattern_description: format!(
"a {class} device matching the expression {}",
product.as_ref()
),
class,
pattern: product,
})
.collect(),
},
git_hash: value.git_hash, git_hash: value.git_hash,
os_version: value.eos_version, os_version: value.eos_version,
}) })

View File

@@ -161,14 +161,24 @@ impl Manifest {
#[ts(export)] #[ts(export)]
pub struct HardwareRequirements { pub struct HardwareRequirements {
#[serde(default)] #[serde(default)]
#[ts(type = "{ display?: string, processor?: string }")] pub device: Vec<DeviceFilter>,
pub device: BTreeMap<String, Regex>, // TODO: array
#[ts(type = "number | null")] #[ts(type = "number | null")]
pub ram: Option<u64>, pub ram: Option<u64>,
#[ts(type = "string[] | null")] #[ts(type = "string[] | null")]
pub arch: Option<BTreeSet<InternedString>>, pub arch: Option<BTreeSet<InternedString>>,
} }
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DeviceFilter {
#[ts(type = "\"processor\" | \"display\"")]
pub class: InternedString,
#[ts(type = "string")]
pub pattern: Regex,
pub pattern_description: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)] #[ts(export)]
pub struct Description { pub struct Description {

View File

@@ -10,6 +10,7 @@ const KNOWN_CLASSES: &[&str] = &["processor", "display"];
#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(tag = "class")] #[serde(tag = "class")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum LshwDevice { pub enum LshwDevice {
Processor(LshwProcessor), Processor(LshwProcessor),
Display(LshwDisplay), Display(LshwDisplay),

View File

@@ -25,8 +25,9 @@ mod v0_3_6_alpha_3;
mod v0_3_6_alpha_4; mod v0_3_6_alpha_4;
mod v0_3_6_alpha_5; mod v0_3_6_alpha_5;
mod v0_3_6_alpha_6; mod v0_3_6_alpha_6;
mod v0_3_6_alpha_7;
pub type Current = v0_3_6_alpha_6::Version; // VERSION_BUMP pub type Current = v0_3_6_alpha_7::Version; // VERSION_BUMP
impl Current { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -102,6 +103,7 @@ enum Version {
V0_3_6_alpha_4(Wrapper<v0_3_6_alpha_4::Version>), V0_3_6_alpha_4(Wrapper<v0_3_6_alpha_4::Version>),
V0_3_6_alpha_5(Wrapper<v0_3_6_alpha_5::Version>), V0_3_6_alpha_5(Wrapper<v0_3_6_alpha_5::Version>),
V0_3_6_alpha_6(Wrapper<v0_3_6_alpha_6::Version>), V0_3_6_alpha_6(Wrapper<v0_3_6_alpha_6::Version>),
V0_3_6_alpha_7(Wrapper<v0_3_6_alpha_7::Version>),
Other(exver::Version), Other(exver::Version),
} }
@@ -132,6 +134,7 @@ impl Version {
Self::V0_3_6_alpha_4(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_4(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_5(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_5(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_6(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_6(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_7(v) => DynVersion(Box::new(v.0)),
Self::Other(v) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), eyre!("unknown version {v}"),
@@ -154,6 +157,7 @@ impl Version {
Version::V0_3_6_alpha_4(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_4(Wrapper(x)) => x.semver(),
Version::V0_3_6_alpha_5(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_5(Wrapper(x)) => x.semver(),
Version::V0_3_6_alpha_6(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_6(Wrapper(x)) => x.semver(),
Version::V0_3_6_alpha_7(Wrapper(x)) => x.semver(),
Version::Other(x) => x.clone(), Version::Other(x) => x.clone(),
} }
} }
@@ -172,15 +176,19 @@ fn version_accessor(db: &mut Value) -> Option<&mut Value> {
fn version_compat_accessor(db: &mut Value) -> Option<&mut Value> { fn version_compat_accessor(db: &mut Value) -> Option<&mut Value> {
if db.get("public").is_some() { if db.get("public").is_some() {
let server_info = db.get_mut("public")?.get_mut("serverInfo")?; let server_info = db.get_mut("public")?.get_mut("serverInfo")?;
if server_info.get("versionCompat").is_some() { if server_info.get("packageVersionCompat").is_some() {
server_info.get_mut("versionCompat") server_info.get_mut("packageVersionCompat")
} else { } else {
if let Some(prev) = server_info.get("eosVersionCompat").cloned() { if let Some(prev) = server_info.get("eosVersionCompat").cloned() {
server_info server_info
.as_object_mut()? .as_object_mut()?
.insert("versionCompat".into(), prev); .insert("packageVersionCompat".into(), prev);
} else if let Some(prev) = server_info.get("versionCompat").cloned() {
server_info
.as_object_mut()?
.insert("packageVersionCompat".into(), prev);
} }
server_info.get_mut("versionCompat") server_info.get_mut("packageVersionCompat")
} }
} else { } else {
db.get_mut("server-info")?.get_mut("eos-version-compat") db.get_mut("server-info")?.get_mut("eos-version-compat")

View File

@@ -0,0 +1,50 @@
use exver::{PreReleaseSegment, VersionRange};
use imbl_value::{json, InOMap};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_3_6_alpha_6, VersionT};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_3_6_alpha_7: exver::Version = exver::Version::new(
[0, 3, 6],
[PreReleaseSegment::String("alpha".into()), 7.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_3_6_alpha_6::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_3_6_alpha_7.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> {
let server_info = db["public"]["serverInfo"]
.as_object_mut()
.or_not_found("public.serverInfo")?;
server_info.insert("ram".into(), json!(0));
server_info.insert("devices".into(), json!([]));
let package_data = db["public"]["packageData"]
.as_object_mut()
.or_not_found("public.packageData")?;
for (_, pde) in package_data.iter_mut() {
if let Some(manifest) = pde["stateInfo"].get_mut("manifest") {
manifest["hardwareRequirements"]["device"] = json!([]);
}
}
Ok(())
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DeviceFilter = {
class: "processor" | "display"
pattern: string
patternDescription: string
}

View File

@@ -1,7 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DeviceFilter } from "./DeviceFilter"
export type HardwareRequirements = { export type HardwareRequirements = {
device: { display?: string; processor?: string } device: Array<DeviceFilter>
ram: number | null ram: number | null
arch: string[] | null arch: string[] | null
} }

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LshwDisplay } from "./LshwDisplay"
import type { LshwProcessor } from "./LshwProcessor"
export type LshwDevice =
| ({ class: "processor" } & LshwProcessor)
| ({ class: "display" } & LshwDisplay)

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwDisplay = { product: string }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LshwProcessor = { product: string }

View File

@@ -1,6 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Governor } from "./Governor" import type { Governor } from "./Governor"
import type { IpInfo } from "./IpInfo" import type { IpInfo } from "./IpInfo"
import type { LshwDevice } from "./LshwDevice"
import type { ServerStatus } from "./ServerStatus" import type { ServerStatus } from "./ServerStatus"
import type { SmtpValue } from "./SmtpValue" import type { SmtpValue } from "./SmtpValue"
import type { WifiInfo } from "./WifiInfo" import type { WifiInfo } from "./WifiInfo"
@@ -11,7 +12,7 @@ export type ServerInfo = {
id: string id: string
hostname: string hostname: string
version: string version: string
versionCompat: string packageVersionCompat: string
postInitMigrationTodos: string[] postInitMigrationTodos: string[]
lastBackup: string | null lastBackup: string | null
lanAddress: string lanAddress: string
@@ -31,4 +32,6 @@ export type ServerInfo = {
zram: boolean zram: boolean
governor: Governor | null governor: Governor | null
smtp: SmtpValue | null smtp: SmtpValue | null
ram: number
devices: Array<LshwDevice>
} }

View File

@@ -62,6 +62,7 @@ export { DependencyRequirement } from "./DependencyRequirement"
export { DepInfo } from "./DepInfo" export { DepInfo } from "./DepInfo"
export { Description } from "./Description" export { Description } from "./Description"
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams" export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
export { DeviceFilter } from "./DeviceFilter"
export { Duration } from "./Duration" export { Duration } from "./Duration"
export { EchoParams } from "./EchoParams" export { EchoParams } from "./EchoParams"
export { EncryptedWire } from "./EncryptedWire" export { EncryptedWire } from "./EncryptedWire"
@@ -111,6 +112,9 @@ export { LanInfo } from "./LanInfo"
export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams" export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams"
export { ListVersionSignersParams } from "./ListVersionSignersParams" export { ListVersionSignersParams } from "./ListVersionSignersParams"
export { LoginParams } from "./LoginParams" export { LoginParams } from "./LoginParams"
export { LshwDevice } from "./LshwDevice"
export { LshwDisplay } from "./LshwDisplay"
export { LshwProcessor } from "./LshwProcessor"
export { MainStatus } from "./MainStatus" export { MainStatus } from "./MainStatus"
export { Manifest } from "./Manifest" export { Manifest } from "./Manifest"
export { MaybeUtf8String } from "./MaybeUtf8String" export { MaybeUtf8String } from "./MaybeUtf8String"

View File

@@ -144,7 +144,7 @@ export type SDKManifest = {
* ``` * ```
*/ */
readonly hardwareRequirements?: { readonly hardwareRequirements?: {
readonly device?: { display?: RegExp; processor?: RegExp } readonly device?: T.DeviceFilter[]
readonly ram?: number | null readonly ram?: number | null
readonly arch?: string[] | null readonly arch?: string[] | null
} }

View File

@@ -73,11 +73,7 @@ export function buildManifest<
stop: manifest.alerts?.stop || null, stop: manifest.alerts?.stop || null,
}, },
hardwareRequirements: { hardwareRequirements: {
device: Object.fromEntries( device: manifest.hardwareRequirements?.device || [],
Object.entries(manifest.hardwareRequirements?.device || {}).map(
([k, v]) => [k, v.source],
),
),
ram: manifest.hardwareRequirements?.ram || null, ram: manifest.hardwareRequirements?.ram || null,
arch: arch:
manifest.hardwareRequirements?.arch === undefined manifest.hardwareRequirements?.arch === undefined

View File

@@ -27,8 +27,8 @@
} }
.published { .published {
margin: 0; margin: 0px;
padding: 4px 0 12px 0; padding: 8px 0 8px 0;
font-style: italic; font-style: italic;
} }

View File

@@ -6,15 +6,19 @@ import { Pipe, PipeTransform } from '@angular/core'
}) })
export class ConvertBytesPipe implements PipeTransform { export class ConvertBytesPipe implements PipeTransform {
transform(bytes: number): string { transform(bytes: number): string {
if (bytes === 0) return '0 Bytes' return convertBytes(bytes)
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
} }
} }
export function convertBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes'
const k = 1024
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
@Pipe({ @Pipe({
name: 'durationToSeconds', name: 'durationToSeconds',
}) })

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { VersionRange, ExtendedVersion, Version } from '@start9labs/start-sdk' import { ExtendedVersion, VersionRange } from '@start9labs/start-sdk'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -29,12 +29,8 @@ export class Exver {
} }
} }
compareOsVersion(current: string, other: string) {
return Version.parse(current).compare(Version.parse(other))
}
satisfies(version: string, range: string): boolean { satisfies(version: string, range: string): boolean {
return VersionRange.parse(range).satisfiedBy(ExtendedVersion.parse(version)) return ExtendedVersion.parse(version).satisfies(VersionRange.parse(range))
} }
getFlavor(version: string): string | null { getFlavor(version: string): string | null {

View File

@@ -7,7 +7,7 @@ import {
DiskBackupTarget, DiskBackupTarget,
} from 'src/app/services/api/api.types' } from 'src/app/services/api/api.types'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { Exver, getErrorMessage } from '@start9labs/shared' import { getErrorMessage } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk' import { Version } from '@start9labs/start-sdk'
@Injectable({ @Injectable({
@@ -19,10 +19,7 @@ export class BackupService {
loading = true loading = true
loadingError: string | IonicSafeString = '' loadingError: string | IonicSafeString = ''
constructor( constructor(private readonly embassyApi: ApiService) {}
private readonly embassyApi: ApiService,
private readonly exver: Exver,
) {}
async getBackupTargets(): Promise<void> { async getBackupTargets(): Promise<void> {
this.loading = true this.loading = true
@@ -58,15 +55,16 @@ export class BackupService {
hasAnyBackup(target: BackupTarget): boolean { hasAnyBackup(target: BackupTarget): boolean {
return Object.values(target.startOs).some( return Object.values(target.startOs).some(
s => this.exver.compareOsVersion(s.version, '0.3.6') !== 'less', s => Version.parse(s.version).compare(Version.parse('0.3.6')) !== 'less',
) )
} }
hasThisBackup(target: BackupTarget, id: string): boolean { hasThisBackup(target: BackupTarget, id: string): boolean {
return ( return (
target.startOs[id] && target.startOs[id] &&
this.exver.compareOsVersion(target.startOs[id].version, '0.3.6') !== Version.parse(target.startOs[id].version).compare(
'less' Version.parse('0.3.6'),
) !== 'less'
) )
} }
} }

View File

@@ -1,24 +1,24 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs' import { endWith, Observable } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { Exver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { ConfigService } from '../../../services/config.service' import { ConfigService } from '../../../services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { Version } from '@start9labs/start-sdk'
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class RefreshAlertService extends Observable<boolean> { export class RefreshAlertService extends Observable<boolean> {
private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe( private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe(
map( map(
version => version =>
this.exver.compareOsVersion(this.config.version, version) !== 'equal', Version.parse(this.config.version).compare(Version.parse(version)) !==
'equal',
), ),
endWith(false), endWith(false),
) )
constructor( constructor(
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
private readonly exver: Exver,
private readonly config: ConfigService, private readonly config: ConfigService,
) { ) {
super(subscriber => this.stream$.subscribe(subscriber)) super(subscriber => this.stream$.subscribe(subscriber))

View File

@@ -5,6 +5,7 @@ import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { Version } from '@start9labs/start-sdk'
export interface AppRecoverOption extends PackageBackupInfo { export interface AppRecoverOption extends PackageBackupInfo {
id: string id: string
@@ -34,7 +35,10 @@ export class ToOptionsPipe implements PipeTransform {
id, id,
installed: !!packageData[id], installed: !!packageData[id],
checked: false, checked: false,
newerOS: this.compare(packageBackups[id].osVersion), newerOS:
Version.parse(packageBackups[id].osVersion).compare(
Version.parse(this.config.version),
) === 'greater',
})) }))
.sort((a, b) => .sort((a, b) =>
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1, b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
@@ -42,11 +46,4 @@ export class ToOptionsPipe implements PipeTransform {
), ),
) )
} }
private compare(version: string): boolean {
// checks to see if backup was made on a newer version of startOS
return (
this.exver.compareOsVersion(version, this.config.version) === 'greater'
)
}
} }

View File

@@ -11,49 +11,51 @@
: 'View Installing' : 'View Installing'
}} }}
</ion-button> </ion-button>
<ng-container *ngIf="localPkg; else install"> <ng-container *ngIf="!conflict">
<ng-container <ng-container *ngIf="localPkg; else install">
*ngIf=" <ng-container
localPkg.stateInfo.state === 'installed' && *ngIf="
(localPkg | toManifest) as manifest localPkg.stateInfo.state === 'installed' &&
" (localPkg | toManifest) as manifest
> "
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === -1"
expand="block"
color="success"
(click)="tryInstall()"
> >
Update
</ion-button>
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
>
Downgrade
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button <ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 0" *ngIf="(manifest.version | compareExver : pkg.version) === -1"
expand="block" expand="block"
color="success" color="success"
(click)="tryInstall()" (click)="tryInstall()"
> >
Reinstall Update
</ion-button> </ion-button>
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
>
Downgrade
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button
*ngIf="(manifest.version | compareExver : pkg.version) === 0"
expand="block"
color="success"
(click)="tryInstall()"
>
Reinstall
</ion-button>
</ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-template #install>
<ion-button
expand="block"
[color]="localFlavor ? 'warning' : 'success'"
(click)="tryInstall()"
>
{{ localFlavor ? 'Switch' : 'Install' }}
</ion-button>
</ng-template>
</ng-container> </ng-container>
<ng-template #install>
<ion-button
expand="block"
[color]="localFlavor ? 'warning' : 'success'"
(click)="tryInstall()"
>
{{ localFlavor ? 'Switch' : 'Install' }}
</ion-button>
</ng-template>
</div> </div>

View File

@@ -47,6 +47,9 @@ export class MarketplaceShowControlsComponent {
@Input() @Input()
localFlavor!: boolean localFlavor!: boolean
@Input()
conflict?: string | null
readonly showDevTools$ = this.ClientStorageService.showDevTools$ readonly showDevTools$ = this.ClientStorageService.showDevTools$
constructor( constructor(

View File

@@ -13,12 +13,20 @@
</ng-container> </ng-container>
<ng-template #show> <ng-template #show>
<marketplace-package [pkg]="pkg"></marketplace-package> <marketplace-package [pkg]="pkg">
<ion-item *ngIf="conflict$ | async as conflict" color="warning" class="ion-margin-top">
<ion-icon slot="start" name="warning"></ion-icon>
<ion-label>
<h2 style="font-weight: 600" [innerHTML]="conflict"></h2>
</ion-label>
</ion-item>
</marketplace-package>
<marketplace-show-controls <marketplace-show-controls
[url]="url" [url]="url"
[pkg]="pkg" [pkg]="pkg"
[localPkg]="localPkg$ | async" [localPkg]="localPkg$ | async"
[localFlavor]="!!(localFlavor$ | async)" [localFlavor]="!!(localFlavor$ | async)"
[conflict]="conflict$ | async"
></marketplace-show-controls> ></marketplace-show-controls>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent> <marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>

View File

@@ -1,15 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core' import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { Exver, getPkgId } from '@start9labs/shared' import { convertBytes, Exver, getPkgId } from '@start9labs/shared'
import { import {
AbstractMarketplaceService, AbstractMarketplaceService,
MarketplacePkg, MarketplacePkg,
} from '@start9labs/marketplace' } from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { combineLatest, Observable } from 'rxjs' import { combineLatest, Observable } from 'rxjs'
import { filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators' import {
filter,
first,
map,
pairwise,
shareReplay,
startWith,
switchMap,
} from 'rxjs/operators'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/util/get-package-data' import { getManifest } from 'src/app/util/get-package-data'
import { Version, VersionRange } from '@start9labs/start-sdk'
@Component({ @Component({
selector: 'marketplace-show', selector: 'marketplace-show',
@@ -25,9 +34,10 @@ export class MarketplaceShowPage {
this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)), this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)),
this.route.queryParamMap, this.route.queryParamMap,
]).pipe( ]).pipe(
map(([pkg, paramMap]) => map(([localPkg, paramMap]) =>
this.exver.getFlavor(getManifest(pkg).version) === paramMap.get('flavor') this.exver.getFlavor(getManifest(localPkg).version) ===
? pkg paramMap.get('flavor')
? localPkg
: null, : null,
), ),
shareReplay({ bufferSize: 1, refCount: true }), shareReplay({ bufferSize: 1, refCount: true }),
@@ -49,6 +59,83 @@ export class MarketplaceShowPage {
), ),
) )
readonly conflict$: Observable<string> = combineLatest([
this.pkg$,
this.patch.watch$('packageData', this.pkgId).pipe(
map(pkg => getManifest(pkg).version),
pairwise(),
filter(([prev, curr]) => prev !== curr),
map(([_, curr]) => curr),
),
this.patch.watch$('serverInfo').pipe(first()),
]).pipe(
map(([pkg, localVersion, server]) => {
let conflicts: string[] = []
// OS version
if (
!Version.parse(pkg.osVersion).satisfies(
VersionRange.parse(server.packageVersionCompat),
)
) {
const compare = Version.parse(pkg.osVersion).compare(
Version.parse(server.version),
)
conflicts.push(
compare === 'greater'
? `Minimum StartOS version ${pkg.osVersion}. Detected ${server.version}`
: `Version ${pkg.version} is outdated and cannot run newer versions of StartOS`,
)
}
// package version
if (
localVersion &&
pkg.sourceVersion &&
!this.exver.satisfies(localVersion, pkg.sourceVersion)
) {
conflicts.push(
`Currently installed version ${localVersion} cannot be upgraded to version ${pkg.version}. Try installing an older version first.`,
)
}
const { arch, ram, device } = pkg.hardwareRequirements
// arch
if (arch && !arch.includes(server.arch)) {
conflicts.push(
`Arch ${server.arch} is not supported. Supported: ${arch.join(
', ',
)}.`,
)
}
// ram
if (ram && ram > server.ram) {
conflicts.push(
`Minimum ${convertBytes(
ram,
)} of RAM required, detected ${convertBytes(server.ram)}.`,
)
}
// devices
conflicts.concat(
device
.filter(d =>
server.devices.some(
sd =>
d.class === sd.class && !new RegExp(d.pattern).test(sd.product),
),
)
.map(d => d.patternDescription),
)
return conflicts.join(' ')
}),
shareReplay({ bufferSize: 1, refCount: true }),
)
readonly flavors$ = this.route.queryParamMap.pipe( readonly flavors$ = this.route.queryParamMap.pipe(
switchMap(paramMap => switchMap(paramMap =>
this.marketplaceService.getSelectedStore$().pipe( this.marketplaceService.getSelectedStore$().pipe(

View File

@@ -117,7 +117,7 @@ export module Mock {
assets: [], assets: [],
volumes: ['main'], volumes: ['main'],
hardwareRequirements: { hardwareRequirements: {
device: {}, device: [],
arch: null, arch: null,
ram: null, ram: null,
}, },
@@ -174,7 +174,7 @@ export module Mock {
assets: [], assets: [],
volumes: ['main'], volumes: ['main'],
hardwareRequirements: { hardwareRequirements: {
device: {}, device: [],
arch: null, arch: null,
ram: null, ram: null,
}, },
@@ -224,7 +224,7 @@ export module Mock {
assets: [], assets: [],
volumes: ['main'], volumes: ['main'],
hardwareRequirements: { hardwareRequirements: {
device: {}, device: [],
arch: null, arch: null,
ram: null, ram: null,
}, },
@@ -253,7 +253,7 @@ export module Mock {
'26.1.0:0.1.0': { '26.1.0:0.1.0': {
title: 'Bitcoin Core', title: 'Bitcoin Core',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin', upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -286,7 +286,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin', short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
}, },
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -329,7 +329,7 @@ export module Mock {
'26.1.0:0.1.0': { '26.1.0:0.1.0': {
title: 'Bitcoin Core', title: 'Bitcoin Core',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin', upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -362,7 +362,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin', short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
}, },
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -407,7 +407,7 @@ export module Mock {
'0.17.5:0': { '0.17.5:0': {
title: 'LND', title: 'LND',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos', wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd', upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -463,7 +463,7 @@ export module Mock {
'0.17.4-beta:1.0-alpha': { '0.17.4-beta:1.0-alpha': {
title: 'LND', title: 'LND',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos', wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd', upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -521,7 +521,7 @@ export module Mock {
'0.3.2.6:0': { '0.3.2.6:0': {
title: 'Bitcoin Proxy', title: 'Bitcoin Proxy',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
@@ -565,7 +565,7 @@ export module Mock {
'27.0.0:1.0.0': { '27.0.0:1.0.0': {
title: 'Bitcoin Core', title: 'Bitcoin Core',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin', upstreamRepo: 'https://github.com/bitcoin/bitcoin',
@@ -598,7 +598,7 @@ export module Mock {
short: 'An alternate fully verifying implementation of Bitcoin', short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
}, },
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
@@ -641,7 +641,7 @@ export module Mock {
'0.18.0:0.0.1': { '0.18.0:0.0.1': {
title: 'LND', title: 'LND',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos', wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd', upstreamRepo: 'https://github.com/lightningnetwork/lnd',
@@ -697,7 +697,7 @@ export module Mock {
'0.3.2.7:0': { '0.3.2.7:0': {
title: 'Bitcoin Proxy', title: 'Bitcoin Proxy',
description: mockDescription, description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null }, hardwareRequirements: { arch: null, device: [], ram: null },
license: 'mit', license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',

View File

@@ -60,7 +60,7 @@ export const mockPatchData: DataModel = {
// password is asdfasdf // password is asdfasdf
passwordHash: passwordHash:
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
versionCompat: '>=0.3.0 <=0.3.6', packageVersionCompat: '>=0.3.0 <=0.3.6',
postInitMigrationTodos: [], postInitMigrationTodos: [],
statusInfo: { statusInfo: {
backupProgress: null, backupProgress: null,
@@ -83,6 +83,8 @@ export const mockPatchData: DataModel = {
selected: null, selected: null,
lastRegion: null, lastRegion: null,
}, },
ram: 8 * 1024 * 1024 * 1024,
devices: [],
}, },
packageData: { packageData: {
bitcoind: { bitcoind: {

View File

@@ -6,7 +6,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { getServerInfo } from 'src/app/util/get-server-info' import { getServerInfo } from 'src/app/util/get-server-info'
import { DataModel } from './patch-db/data-model' import { DataModel } from './patch-db/data-model'
import { Exver } from '@start9labs/shared' import { Version } from '@start9labs/start-sdk'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -48,14 +48,14 @@ export class EOSService {
constructor( constructor(
private readonly api: ApiService, private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>, private readonly patch: PatchDB<DataModel>,
private readonly exver: Exver,
) {} ) {}
async loadEos(): Promise<void> { async loadEos(): Promise<void> {
const { version, id } = await getServerInfo(this.patch) const { version, id } = await getServerInfo(this.patch)
this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
const updateAvailable = const updateAvailable =
this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater' Version.parse(this.osUpdate.version).compare(Version.parse(version)) ===
'greater'
this.updateAvailable$.next(updateAvailable) this.updateAvailable$.next(updateAvailable)
} }
} }