diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index 4c2dbc557..5b5821569 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -43,6 +43,7 @@ podman postgresql psmisc qemu-guest-agent +rfkill rsync samba-common-bin smartmontools diff --git a/core/Cargo.lock b/core/Cargo.lock index db92c1ff9..87edff8ac 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -5952,7 +5952,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "start-os" -version = "0.3.6-alpha.15" +version = "0.4.0-alpha.0" dependencies = [ "aes 0.7.5", "async-acme", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index dbcaf2a78..2801fc667 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.6-alpha.15" # VERSION_BUMP +version = "0.4.0-alpha.0" # VERSION_BUMP license = "MIT" [lib] diff --git a/core/startos/src/account.rs b/core/startos/src/account.rs index 3de55dfb3..46fd2c1f9 100644 --- a/core/startos/src/account.rs +++ b/core/startos/src/account.rs @@ -59,7 +59,13 @@ impl AccountInfo { let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let password = db.as_private().as_password().de()?; let key_store = db.as_private().as_key_store(); - let tor_addrs = db.as_public().as_server_info().as_host().as_onions().de()?; + let tor_addrs = db + .as_public() + .as_server_info() + .as_network() + .as_host() + .as_onions() + .de()?; let tor_keys = tor_addrs .into_iter() .map(|tor_addr| key_store.as_onion().get_key(&tor_addr)) @@ -89,13 +95,17 @@ impl AccountInfo { server_info .as_pubkey_mut() .ser(&self.ssh_key.public_key().to_openssh()?)?; - server_info.as_host_mut().as_onions_mut().ser( - &self - .tor_keys - .iter() - .map(|tor_key| tor_key.public().get_onion_address()) - .collect(), - )?; + server_info + .as_network_mut() + .as_host_mut() + .as_onions_mut() + .ser( + &self + .tor_keys + .iter() + .map(|tor_key| tor_key.public().get_onion_address()) + .collect(), + )?; db.as_private_mut().as_password_mut().ser(&self.password)?; db.as_private_mut() .as_ssh_privkey_mut() diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index 01e6ac916..0ec28d67d 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -1,6 +1,5 @@ use std::cmp::max; use std::ffi::OsString; -use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; @@ -14,7 +13,6 @@ use crate::context::config::ServerConfig; use crate::context::rpc::InitRpcContextPhases; use crate::context::{DiagnosticContext, InitContext, RpcContext}; use crate::net::network_interface::SelfContainedNetworkInterfaceListener; -use crate::net::utils::ipv6_is_local; use crate::net::web_server::{Acceptor, UpgradableListener, WebServer}; use crate::shutdown::Shutdown; use crate::system::launch_metrics_task; diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 96ca3ca63..927fc36ed 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -1,4 +1,3 @@ -use std::backtrace; use std::collections::{BTreeMap, BTreeSet}; use std::future::Future; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; @@ -41,7 +40,7 @@ use crate::service::effects::callbacks::ServiceCallbacks; use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::util::lshw::LshwDevice; -use crate::util::sync::SyncMutex; +use crate::util::sync::{SyncMutex, Watch}; pub struct RpcContextSeed { is_closed: AtomicBool, @@ -57,14 +56,14 @@ pub struct RpcContextSeed { pub os_net_service: NetService, pub s9pk_arch: Option<&'static str>, pub services: ServiceMap, - pub metrics_cache: RwLock>, + pub metrics_cache: Watch>, pub shutdown: broadcast::Sender>, pub tor_socks: SocketAddr, pub lxc_manager: Arc, pub open_authed_continuations: OpenAuthedContinuations>, pub rpc_continuations: RpcContinuations, pub callbacks: ServiceCallbacks, - pub wifi_manager: Option>>, + pub wifi_manager: Arc>>, pub current_secret: Arc, pub client: Client, pub start_time: Instant, @@ -174,7 +173,7 @@ impl RpcContext { tracing::info!("Initialized Net Controller"); let services = ServiceMap::default(); - let metrics_cache = RwLock::>::new(None); + let metrics_cache = Watch::>::new(None); let tor_proxy_url = format!("socks5h://{tor_proxy}"); let crons = SyncMutex::new(BTreeMap::new()); @@ -245,9 +244,7 @@ impl RpcContext { open_authed_continuations: OpenAuthedContinuations::new(), rpc_continuations: RpcContinuations::new(), callbacks: Default::default(), - wifi_manager: wifi_interface - .clone() - .map(|i| Arc::new(RwLock::new(WpaCli::init(i)))), + wifi_manager: Arc::new(RwLock::new(wifi_interface.clone().map(|i| WpaCli::init(i)))), current_secret: Arc::new( Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| { tracing::debug!("{:?}", e); @@ -488,7 +485,7 @@ impl Drop for RpcContext { let count = Arc::strong_count(&self.0) - 1; tracing::info!("RpcContext dropped. {} left.", count); if count > 0 { - tracing::debug!("{}", backtrace::Backtrace::force_capture()); + tracing::debug!("{}", std::backtrace::Backtrace::force_capture()); tracing::debug!("{:?}", eyre!("")) } } diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 1f764e0c9..2f7852ee1 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -48,44 +48,50 @@ impl Public { id: account.server_id.clone(), version: Current::default().semver(), hostname: account.hostname.no_dot_host_name(), - host: Host { - bindings: [( - 80, - BindInfo { - enabled: false, - options: BindOptions { - preferred_external_port: 80, - add_ssl: Some(AddSslOptions { - preferred_external_port: 443, - alpn: Some(AlpnInfo::Specified(vec![ - MaybeUtf8String("http/1.1".into()), - MaybeUtf8String("h2".into()), - ])), - }), - secure: None, - }, - net: NetInfo { - assigned_port: None, - assigned_ssl_port: Some(443), - public: false, - }, - }, - )] - .into_iter() - .collect(), - onions: account - .tor_keys - .iter() - .map(|k| k.public().get_onion_address()) - .collect(), - domains: BTreeMap::new(), - hostname_info: BTreeMap::new(), - }, last_backup: None, package_version_compat: Current::default().compat().clone(), post_init_migration_todos: BTreeSet::new(), - network_interfaces: BTreeMap::new(), - acme: BTreeMap::new(), + network: NetworkInfo { + host: Host { + bindings: [( + 80, + BindInfo { + enabled: false, + options: BindOptions { + preferred_external_port: 80, + add_ssl: Some(AddSslOptions { + preferred_external_port: 443, + alpn: Some(AlpnInfo::Specified(vec![ + MaybeUtf8String("http/1.1".into()), + MaybeUtf8String("h2".into()), + ])), + }), + secure: None, + }, + net: NetInfo { + assigned_port: None, + assigned_ssl_port: Some(443), + public: false, + }, + }, + )] + .into_iter() + .collect(), + onions: account + .tor_keys + .iter() + .map(|k| k.public().get_onion_address()) + .collect(), + domains: BTreeMap::new(), + hostname_info: BTreeMap::new(), + }, + wifi: WifiInfo { + enabled: true, + ..Default::default() + }, + network_interfaces: BTreeMap::new(), + acme: BTreeMap::new(), + }, status_info: ServerStatus { backup_progress: None, updated: false, @@ -93,7 +99,6 @@ impl Public { shutting_down: false, restarting: false, }, - wifi: WifiInfo::default(), unread_notification_count: 0, password_hash: account.password.clone(), pubkey: ssh_key::PublicKey::from(&account.ssh_key) @@ -145,7 +150,6 @@ pub struct ServerInfo { pub id: String, #[ts(type = "string")] pub hostname: InternedString, - pub host: Host, #[ts(type = "string")] pub version: Version, #[ts(type = "string")] @@ -154,14 +158,9 @@ pub struct ServerInfo { pub post_init_migration_todos: BTreeSet, #[ts(type = "string | null")] pub last_backup: Option>, - #[ts(as = "BTreeMap::")] - #[serde(default)] - pub network_interfaces: BTreeMap, - #[serde(default)] - pub acme: BTreeMap, + pub network: NetworkInfo, #[serde(default)] pub status_info: ServerStatus, - pub wifi: WifiInfo, #[ts(type = "number")] pub unread_notification_count: u64, pub password_hash: String, @@ -178,17 +177,32 @@ pub struct ServerInfo { pub devices: Vec, } +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct NetworkInfo { + pub wifi: WifiInfo, + pub host: Host, + #[ts(as = "BTreeMap::")] + #[serde(default)] + pub network_interfaces: BTreeMap, + #[serde(default)] + pub acme: BTreeMap, +} + #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct NetworkInterfaceInfo { - pub public: Option, + pub inbound: Option, + pub outbound: Option, pub ip_info: Option, } impl NetworkInterfaceInfo { - pub fn public(&self) -> bool { - self.public.unwrap_or_else(|| { + pub fn inbound(&self) -> bool { + self.inbound.unwrap_or_else(|| { !self.ip_info.as_ref().map_or(true, |ip_info| { let ip4s = ip_info .subnets @@ -224,6 +238,8 @@ impl NetworkInterfaceInfo { #[ts(export)] #[serde(rename_all = "camelCase")] pub struct IpInfo { + #[ts(type = "string")] + pub name: InternedString, pub scope_id: u32, pub device_type: Option, #[ts(type = "string[]")] @@ -276,6 +292,7 @@ pub struct ServerStatus { #[model = "Model"] #[ts(export)] pub struct WifiInfo { + pub enabled: bool, pub interface: Option, pub ssids: BTreeSet, pub selected: Option, diff --git a/core/startos/src/db/prelude.rs b/core/startos/src/db/prelude.rs index 419b356ef..a6883658b 100644 --- a/core/startos/src/db/prelude.rs +++ b/core/startos/src/db/prelude.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::marker::PhantomData; use std::str::FromStr; @@ -267,7 +267,7 @@ where T::Key: FromStr + Ord + Clone, Error: From<::Err>, { - pub fn keys(&self) -> Result, Error> { + pub fn keys(&self) -> Result, Error> { use serde::de::Error; match &self.value { Value::Object(o) => o diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 63642dfb3..f1213369f 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -415,7 +415,11 @@ pub async fn init( let wifi_interface = find_wifi_iface().await?; let wifi = db .mutate(|db| { - let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + let wifi = db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_wifi_mut(); wifi.as_interface_mut().ser(&wifi_interface)?; wifi.de() }) diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index bcdcb7f91..2415961e6 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -89,7 +89,7 @@ use crate::context::{ use crate::disk::fsck::RequiresReboot; use crate::net::net; use crate::registry::context::{RegistryContext, RegistryUrlParams}; -use crate::util::serde::HandlerExtSerde; +use crate::util::serde::{HandlerExtSerde, WithIoFormat}; #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] @@ -242,10 +242,18 @@ pub fn server() -> ParentHandler { ) .subcommand( "metrics", - from_fn_async(system::metrics) - .with_display_serializable() - .with_about("Display information about the server i.e. temperature, RAM, CPU, and disk usage") - .with_call_remote::() + ParentHandler::>::new() + .root_handler( + from_fn_async(system::metrics) + .with_display_serializable() + .with_about("Display information about the server i.e. temperature, RAM, CPU, and disk usage") + .with_call_remote::() + ) + .subcommand( + "follow", + from_fn_async(system::metrics_follow) + .no_cli() + ) ) .subcommand( "shutdown", diff --git a/core/startos/src/net/acme.rs b/core/startos/src/net/acme.rs index 7e2dd23de..49ec7cb40 100644 --- a/core/startos/src/net/acme.rs +++ b/core/startos/src/net/acme.rs @@ -255,7 +255,7 @@ pub async fn init( ctx.db .mutate(|db| { db.as_public_mut() - .as_server_info_mut() + .as_server_info_mut().as_network_mut() .as_acme_mut() .insert(&provider, &AcmeSettings { contact }) }) @@ -276,7 +276,7 @@ pub async fn remove( ctx.db .mutate(|db| { db.as_public_mut() - .as_server_info_mut() + .as_server_info_mut().as_network_mut() .as_acme_mut() .remove(&provider) }) diff --git a/core/startos/src/net/forward.rs b/core/startos/src/net/forward.rs index 6f3abee15..b7ad33318 100644 --- a/core/startos/src/net/forward.rs +++ b/core/startos/src/net/forward.rs @@ -155,7 +155,7 @@ impl LanPortForwardController { let mut interfaces = ip_info.peek_and_mark_seen(|ip_info| { ip_info .iter() - .map(|(iface, info)| (iface.clone(), info.public())) + .map(|(iface, info)| (iface.clone(), info.inbound())) .collect() }); let mut reply: Option>> = None; @@ -175,7 +175,7 @@ impl LanPortForwardController { interfaces = ip_info.peek(|ip_info| { ip_info .iter() - .map(|(iface, info)| (iface.clone(), info.public())) + .map(|(iface, info)| (iface.clone(), info.inbound())) .collect() }); } diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs index b75734a13..632952c0e 100644 --- a/core/startos/src/net/host/address.rs +++ b/core/startos/src/net/host/address.rs @@ -186,7 +186,7 @@ pub async fn add_domain( ctx.db .mutate(|db| { if let Some(acme) = &acme { - if !db.as_public().as_server_info().as_acme().contains_key(&acme)? { + if !db.as_public().as_server_info().as_network().as_acme().contains_key(&acme)? { return Err(Error::new(eyre!("unknown acme provider {}, please run acme.init for this provider first", acme.0), ErrorKind::InvalidRequest)); } } diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs index df209cef5..37765a257 100644 --- a/core/startos/src/net/host/mod.rs +++ b/core/startos/src/net/host/mod.rs @@ -85,7 +85,11 @@ pub fn host_for<'a>( host_id: &HostId, ) -> Result<&'a mut Model, Error> { let Some(package_id) = package_id else { - return Ok(db.as_public_mut().as_server_info_mut().as_host_mut()); + return Ok(db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_host_mut()); }; fn host_info<'a>( db: &'a mut DatabaseModel, @@ -122,7 +126,7 @@ pub fn host_for<'a>( } pub fn all_hosts(db: &DatabaseModel) -> impl Iterator, Error>> { - [Ok(db.as_public().as_server_info().as_host())] + [Ok(db.as_public().as_server_info().as_network().as_host())] .into_iter() .chain( [db.as_public().as_package_data().as_entries()] @@ -255,7 +259,7 @@ pub async fn list_hosts( ctx: RpcContext, _: Empty, package: PackageId, -) -> Result, Error> { +) -> Result, Error> { ctx.db .peek() .await diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index c3d0e8676..2deeacf0f 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -188,7 +188,11 @@ impl NetServiceData { let host = ctrl .db .mutate(|db| { - let host = db.as_public_mut().as_server_info_mut().as_host_mut(); + let host = db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_host_mut(); host.as_bindings_mut().mutate(|b| { for (internal_port, info) in b { if !except.contains(&BindId { @@ -326,7 +330,7 @@ impl NetServiceData { for (interface, public, ip_info) in net_ifaces.iter().filter_map(|(interface, info)| { if let Some(ip_info) = &info.ip_info { - Some((interface, info.public(), ip_info)) + Some((interface, info.inbound(), ip_info)) } else { None } @@ -612,6 +616,7 @@ impl NetServiceData { .await .as_public() .as_server_info() + .as_network() .as_host() .de()?, ) diff --git a/core/startos/src/net/network_interface.rs b/core/startos/src/net/network_interface.rs index 8c99f0de9..00c5fc56b 100644 --- a/core/startos/src/net/network_interface.rs +++ b/core/startos/src/net/network_interface.rs @@ -58,7 +58,7 @@ pub fn network_interface_api() -> ParentHandler { info.ip_info.as_ref() .and_then(|ip_info| ip_info.device_type) .map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")), - info.public(), + info.inbound(), info.ip_info.as_ref().map_or_else( || "".to_owned(), |ip_info| ip_info.subnets @@ -86,18 +86,18 @@ pub fn network_interface_api() -> ParentHandler { .with_call_remote::(), ) .subcommand( - "set-public", - from_fn_async(set_public) + "set-inbound", + from_fn_async(set_inbound) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Indicate whether this interface is publicly addressable") + .with_about("Indicate whether this interface has inbound access from the WAN") .with_call_remote::(), ).subcommand( - "unset-public", - from_fn_async(unset_public) + "unset-inbound", + from_fn_async(unset_inbound) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_about("Allow this interface to infer whether it is publicly addressable based on its IPv4 address") + .with_about("Allow this interface to infer whether it has inbound access from the WAN based on its IPv4 address") .with_call_remote::(), ).subcommand("forget", from_fn_async(forget_iface) @@ -116,36 +116,36 @@ async fn list_interfaces( #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[ts(export)] -struct NetworkInterfaceSetPublicParams { +struct NetworkInterfaceSetInboundParams { #[ts(type = "string")] interface: InternedString, - public: Option, + inbound: Option, } -async fn set_public( +async fn set_inbound( ctx: RpcContext, - NetworkInterfaceSetPublicParams { interface, public }: NetworkInterfaceSetPublicParams, + NetworkInterfaceSetInboundParams { interface, inbound }: NetworkInterfaceSetInboundParams, ) -> Result<(), Error> { ctx.net_controller .net_iface - .set_public(&interface, Some(public.unwrap_or(true))) + .set_inbound(&interface, Some(inbound.unwrap_or(true))) .await } #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[ts(export)] -struct UnsetPublicParams { +struct UnsetInboundParams { #[ts(type = "string")] interface: InternedString, } -async fn unset_public( +async fn unset_inbound( ctx: RpcContext, - UnsetPublicParams { interface }: UnsetPublicParams, + UnsetInboundParams { interface }: UnsetInboundParams, ) -> Result<(), Error> { ctx.net_controller .net_iface - .set_public(&interface, None) + .set_inbound(&interface, None) .await } @@ -193,6 +193,9 @@ mod active_connection { default_service = "org.freedesktop.NetworkManager" )] pub trait ActiveConnection { + #[zbus(property)] + fn id(&self) -> Result; + #[zbus(property)] fn state_flags(&self) -> Result; @@ -511,6 +514,8 @@ async fn watch_ip( _ => None, }; + let name = InternedString::from(active_connection_proxy.id().await?); + let dhcp4_config = active_connection_proxy.dhcp4_config().await?; let ip4_proxy = Ip4ConfigProxy::new(&connection, ip4_config.clone()).await?; @@ -568,6 +573,7 @@ async fn watch_ip( } }; Some(IpInfo { + name: name.clone(), scope_id, device_type, subnets, @@ -579,11 +585,14 @@ async fn watch_ip( }; write_to.send_if_modified(|m| { - let public = m.get(&iface).map_or(None, |i| i.public); + let (inbound, outbound) = m + .get(&iface) + .map_or((None, None), |i| (i.inbound, i.outbound)); m.insert( iface.clone(), NetworkInterfaceInfo { - public, + inbound, + outbound, ip_info: ip_info.clone(), }, ) @@ -663,6 +672,7 @@ impl NetworkInterfaceController { db.mutate(|db| { db.as_public_mut() .as_server_info_mut() + .as_network_mut() .as_network_interfaces_mut() .ser(info) }) @@ -731,6 +741,7 @@ impl NetworkInterfaceController { .await .as_public() .as_server_info() + .as_network() .as_network_interfaces() .de() { @@ -820,7 +831,7 @@ impl NetworkInterfaceController { Ok(listener) } - pub async fn set_public( + pub async fn set_inbound( &self, interface: &InternedString, public: Option, @@ -828,7 +839,7 @@ impl NetworkInterfaceController { let mut sub = self .db .subscribe( - "/public/serverInfo/networkInterfaces" + "/public/serverInfo/network/networkInterfaces" .parse::>() .with_kind(ErrorKind::Database)?, ) @@ -843,7 +854,7 @@ impl NetworkInterfaceController { return false; } } - .public, + .inbound, public, ); prev != public @@ -861,7 +872,7 @@ impl NetworkInterfaceController { let mut sub = self .db .subscribe( - "/public/serverInfo/networkInterfaces" + "/public/serverInfo/network/networkInterfaces" .parse::>() .with_kind(ErrorKind::Database)?, ) @@ -955,8 +966,10 @@ impl ListenerMap { ) -> Result<(), Error> { let mut keep = BTreeSet::::new(); for info in ip_info.values().chain([&NetworkInterfaceInfo { - public: Some(false), + inbound: Some(false), + outbound: Some(false), ip_info: Some(IpInfo { + name: "lo".into(), scope_id: 1, device_type: None, subnets: [ @@ -969,7 +982,7 @@ impl ListenerMap { ntp_servers: Default::default(), }), }]) { - if public || !info.public() { + if public || !info.inbound() { if let Some(ip_info) = &info.ip_info { for ipnet in &ip_info.subnets { let addr = match ipnet.addr() { @@ -988,7 +1001,7 @@ impl ListenerMap { }; keep.insert(addr); if let Some((_, is_public, wan_ip)) = self.listeners.get_mut(&addr) { - *is_public = info.public(); + *is_public = info.inbound(); *wan_ip = info.ip_info.as_ref().and_then(|i| i.wan_ip); continue; } @@ -1006,7 +1019,7 @@ impl ListenerMap { .into(), ) .with_kind(ErrorKind::Network)?, - info.public(), + info.inbound(), info.ip_info.as_ref().and_then(|i| i.wan_ip), ), ); diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index bba50c371..44e0ae5e6 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::net::SocketAddr; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Weak}; @@ -179,7 +179,7 @@ pub async fn add_key( Ok(key.public().get_onion_address()) } -pub async fn list_keys(ctx: RpcContext) -> Result, Error> { +pub async fn list_keys(ctx: RpcContext) -> Result, Error> { ctx.db .peek() .await diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index cbc1cc916..3a4329693 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -461,6 +461,7 @@ impl VHostServer { target.acme.as_ref().and_then(|a| { peek.as_public() .as_server_info() + .as_network() .as_acme() .as_idx(a) .map(|s| (domain, a, s)) diff --git a/core/startos/src/net/wifi.rs b/core/startos/src/net/wifi.rs index a70fbfc73..67eeaad82 100644 --- a/core/startos/src/net/wifi.rs +++ b/core/startos/src/net/wifi.rs @@ -24,21 +24,28 @@ use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::Invoke; use crate::{Error, ErrorKind}; -type WifiManager = Arc>; +type WifiManager = Arc>>; -pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> { - if let Some(wifi_manager) = ctx.wifi_manager.as_ref() { - Ok(wifi_manager) - } else { - Err(Error::new( - color_eyre::eyre::eyre!("No WiFi interface available"), - ErrorKind::Wifi, - )) - } -} +// pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> { +// if let Some(wifi_manager) = ctx.wifi_manager.as_ref() { +// Ok(wifi_manager) +// } else { +// Err(Error::new( +// color_eyre::eyre::eyre!("No WiFi interface available"), +// ErrorKind::Wifi, +// )) +// } +// } pub fn wifi() -> ParentHandler { ParentHandler::new() + .subcommand( + "set-enabled", + from_fn_async(set_enabled) + .no_display() + .with_about("Enable or disable wifi") + .with_call_remote::(), + ) .subcommand( "add", from_fn_async(add) @@ -80,6 +87,48 @@ pub fn wifi() -> ParentHandler { ) } +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct SetWifiEnabledParams { + pub enabled: bool, +} + +pub async fn set_enabled( + ctx: RpcContext, + SetWifiEnabledParams { enabled }: SetWifiEnabledParams, +) -> Result<(), Error> { + if enabled { + Command::new("rfkill") + .arg("unblock") + .arg("all") + .invoke(ErrorKind::Wifi) + .await?; + } else { + Command::new("rfkill") + .arg("block") + .arg("all") + .invoke(ErrorKind::Wifi) + .await?; + } + let iface = if let Some(man) = ctx.wifi_manager.read().await.as_ref().filter(|_| enabled) { + Some(man.interface.clone()) + } else { + None + }; + ctx.db + .mutate(|d| { + d.as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_wifi_mut() + .as_interface_mut() + .ser(&iface) + }) + .await?; + Ok(()) +} + pub fn available() -> ParentHandler { ParentHandler::new().subcommand( "get", @@ -110,7 +159,7 @@ pub struct AddParams { } #[instrument(skip_all)] pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Result<(), Error> { - let wifi_manager = wifi_manager(&ctx)?; + let wifi_manager = ctx.wifi_manager.clone(); if !ssid.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("SSID may not have special characters"), @@ -130,9 +179,14 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re password: &Psk, ) -> Result<(), Error> { tracing::info!("Adding new WiFi network: '{}'", ssid.0); - let mut wpa_supplicant = wifi_manager.write().await; + let mut wpa_supplicant = wifi_manager.write_owned().await; + let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("No WiFi interface available"), + ErrorKind::Wifi, + ) + })?; wpa_supplicant.add_network(db, ssid, password).await?; - drop(wpa_supplicant); Ok(()) } if let Err(err) = add_procedure( @@ -154,6 +208,7 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re .mutate(|db| { db.as_public_mut() .as_server_info_mut() + .as_network_mut() .as_wifi_mut() .as_ssids_mut() .mutate(|s| { @@ -173,7 +228,7 @@ pub struct SsidParams { #[instrument(skip_all)] pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { - let wifi_manager = wifi_manager(&ctx)?; + let wifi_manager = ctx.wifi_manager.clone(); if !ssid.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("SSID may not have special characters"), @@ -185,10 +240,14 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result wifi_manager: WifiManager, ssid: &Ssid, ) -> Result<(), Error> { - let wpa_supplicant = wifi_manager.read().await; + let mut wpa_supplicant = wifi_manager.write_owned().await; + let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("No WiFi interface available"), + ErrorKind::Wifi, + ) + })?; let current = wpa_supplicant.get_current_network().await?; - drop(wpa_supplicant); - let mut wpa_supplicant = wifi_manager.write().await; let connected = wpa_supplicant.select_network(db.clone(), ssid).await?; if connected { tracing::info!("Successfully connected to WiFi: '{}'", ssid.0); @@ -218,7 +277,11 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result ctx.db .mutate(|db| { - let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + let wifi = db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_wifi_mut(); wifi.as_ssids_mut().mutate(|s| { s.insert(ssid.clone()); Ok(()) @@ -231,17 +294,22 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result #[instrument(skip_all)] pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { - let wifi_manager = wifi_manager(&ctx)?; + let wifi_manager = ctx.wifi_manager.clone(); if !ssid.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("SSID may not have special characters"), ErrorKind::Wifi, )); } - let wpa_supplicant = wifi_manager.read().await; + + let mut wpa_supplicant = wifi_manager.write_owned().await; + let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("No WiFi interface available"), + ErrorKind::Wifi, + ) + })?; let current = wpa_supplicant.get_current_network().await?; - drop(wpa_supplicant); - let mut wpa_supplicant = wifi_manager.write().await; let ssid = Ssid(ssid); let is_current_being_removed = matches!(current, Some(current) if current == ssid); let is_current_removed_and_no_hardwire = @@ -254,7 +322,11 @@ pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result< ctx.db .mutate(|db| { - let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + let wifi = db + .as_public_mut() + .as_server_info_mut() + .as_network_mut() + .as_wifi_mut(); wifi.as_ssids_mut().mutate(|s| { s.remove(&ssid.0); Ok(()) @@ -379,8 +451,14 @@ fn display_wifi_list(params: WithIoFormat, info: Vec) { // #[command(display(display_wifi_info))] #[instrument(skip_all)] pub async fn get(ctx: RpcContext, _: Empty) -> Result { - let wifi_manager = wifi_manager(&ctx)?; - let wpa_supplicant = wifi_manager.read().await; + let wifi_manager = ctx.wifi_manager.clone(); + let wpa_supplicant = wifi_manager.read_owned().await; + let wpa_supplicant = wpa_supplicant.as_ref().ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("No WiFi interface available"), + ErrorKind::Wifi, + ) + })?; let (list_networks, current_res, country_res, ethernet_res, signal_strengths) = tokio::join!( wpa_supplicant.list_networks_low(), wpa_supplicant.get_current_network(), @@ -427,8 +505,14 @@ pub async fn get(ctx: RpcContext, _: Empty) -> Result { #[instrument(skip_all)] pub async fn get_available(ctx: RpcContext, _: Empty) -> Result, Error> { - let wifi_manager = wifi_manager(&ctx)?; - let wpa_supplicant = wifi_manager.read().await; + let wifi_manager = ctx.wifi_manager.clone(); + let wpa_supplicant = wifi_manager.read_owned().await; + let wpa_supplicant = wpa_supplicant.as_ref().ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("No WiFi interface available"), + ErrorKind::Wifi, + ) + })?; let (wifi_list, network_list) = tokio::join!( wpa_supplicant.list_wifi_low(), wpa_supplicant.list_networks_low() @@ -463,14 +547,20 @@ pub async fn set_country( ctx: RpcContext, SetCountryParams { country }: SetCountryParams, ) -> Result<(), Error> { - let wifi_manager = wifi_manager(&ctx)?; + let wifi_manager = ctx.wifi_manager.clone(); if !interface_connected(&ctx.ethernet_interface).await? { return Err(Error::new( color_eyre::eyre::eyre!("Won't change country without hardwire connection"), crate::ErrorKind::Wifi, )); } - let mut wpa_supplicant = wifi_manager.write().await; + let mut wpa_supplicant = wifi_manager.write_owned().await; + let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { + Error::new( + color_eyre::eyre::eyre!("No WiFi interface available"), + ErrorKind::Wifi, + ) + })?; wpa_supplicant.set_country_low(country.alpha2()).await?; for (network_id, _wifi_info) in wpa_supplicant.list_networks_low().await? { wpa_supplicant.remove_network_low(network_id).await?; @@ -734,6 +824,7 @@ impl WpaCli { db.mutate(|d| { d.as_public_mut() .as_server_info_mut() + .as_network_mut() .as_wifi_mut() .as_last_region_mut() .ser(&new_country) @@ -909,13 +1000,21 @@ pub async fn synchronize_network_manager>( crate::disk::mount::util::bind(&persistent, "/etc/NetworkManager/system-connections", false) .await?; + if !wifi.enabled { + Command::new("rfkill") + .arg("block") + .arg("all") + .invoke(ErrorKind::Wifi) + .await?; + } + Command::new("systemctl") .arg("restart") .arg("NetworkManager") .invoke(ErrorKind::Wifi) .await?; - let Some(wifi_iface) = &wifi.interface else { + let Some(wifi_iface) = wifi.interface.as_ref().filter(|_| wifi.enabled) else { return Ok(()); }; diff --git a/core/startos/src/notifications.rs b/core/startos/src/notifications.rs index 48444020a..c8ff08409 100644 --- a/core/startos/src/notifications.rs +++ b/core/startos/src/notifications.rs @@ -6,6 +6,7 @@ use chrono::{DateTime, Utc}; use clap::builder::ValueParserFactory; use clap::Parser; use color_eyre::eyre::eyre; +use helpers::const_true; use imbl_value::InternedString; use models::{FromStrParser, PackageId}; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; @@ -15,7 +16,7 @@ use ts_rs::TS; use crate::backup::BackupReport; use crate::context::{CliContext, RpcContext}; -use crate::db::model::{Database, DatabaseModel}; +use crate::db::model::DatabaseModel; use crate::prelude::*; use crate::util::serde::HandlerExtSerde; @@ -33,14 +34,35 @@ pub fn notification() -> ParentHandler { "remove", from_fn_async(remove) .no_display() - .with_about("Delete notification for a given id") + .with_about("Remove notification for given ids") .with_call_remote::(), ) .subcommand( "remove-before", from_fn_async(remove_before) .no_display() - .with_about("Delete notifications preceding a given id") + .with_about("Remove notifications preceding a given id") + .with_call_remote::(), + ) + .subcommand( + "mark-seen", + from_fn_async(mark_seen) + .no_display() + .with_about("Mark given notifications as seen") + .with_call_remote::(), + ) + .subcommand( + "mark-seen-before", + from_fn_async(mark_seen_before) + .no_display() + .with_about("Mark notifications preceding a given id as seen") + .with_call_remote::(), + ) + .subcommand( + "mark-unseen", + from_fn_async(mark_unseen) + .no_display() + .with_about("Mark given notifications as unseen") .with_call_remote::(), ) .subcommand( @@ -55,7 +77,7 @@ pub fn notification() -> ParentHandler { #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] -pub struct ListParams { +pub struct ListNotificationParams { #[ts(type = "number | null")] before: Option, #[ts(type = "number | null")] @@ -65,7 +87,7 @@ pub struct ListParams { #[instrument(skip_all)] pub async fn list( ctx: RpcContext, - ListParams { before, limit }: ListParams, + ListNotificationParams { before, limit }: ListNotificationParams, ) -> Result, Error> { ctx.db .mutate(|db| { @@ -121,38 +143,124 @@ pub async fn list( #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] -pub struct DeleteParams { - #[ts(type = "number")] - id: u32, +pub struct ModifyNotificationParams { + #[ts(type = "number[]")] + ids: Vec, } -pub async fn remove(ctx: RpcContext, DeleteParams { id }: DeleteParams) -> Result<(), Error> { +pub async fn remove( + ctx: RpcContext, + ModifyNotificationParams { ids }: ModifyNotificationParams, +) -> Result<(), Error> { ctx.db .mutate(|db| { - db.as_private_mut().as_notifications_mut().remove(&id)?; + let n = db.as_private_mut().as_notifications_mut(); + for id in ids { + n.remove(&id)?; + } Ok(()) }) .await } + #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] -pub struct DeleteBeforeParams { +pub struct ModifyNotificationBeforeParams { #[ts(type = "number")] before: u32, } pub async fn remove_before( ctx: RpcContext, - DeleteBeforeParams { before }: DeleteBeforeParams, + ModifyNotificationBeforeParams { before }: ModifyNotificationBeforeParams, ) -> Result<(), Error> { ctx.db .mutate(|db| { - for id in db.as_private().as_notifications().keys()? { - if id < before { - db.as_private_mut().as_notifications_mut().remove(&id)?; + let n = db.as_private_mut().as_notifications_mut(); + for id in n.keys()?.range(..before) { + n.remove(&id)?; + } + Ok(()) + }) + .await +} + +pub async fn mark_seen( + ctx: RpcContext, + ModifyNotificationParams { ids }: ModifyNotificationParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let mut diff = 0; + let n = db.as_private_mut().as_notifications_mut(); + for id in ids { + if !n + .as_idx_mut(&id) + .or_not_found(lazy_format!("Notification #{id}"))? + .as_seen_mut() + .replace(&true)? + { + diff += 1; } } + db.as_public_mut() + .as_server_info_mut() + .as_unread_notification_count_mut() + .mutate(|n| Ok(*n -= diff))?; + Ok(()) + }) + .await +} + +pub async fn mark_seen_before( + ctx: RpcContext, + ModifyNotificationBeforeParams { before }: ModifyNotificationBeforeParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let mut diff = 0; + let n = db.as_private_mut().as_notifications_mut(); + for id in n.keys()?.range(..before) { + if !n + .as_idx_mut(&id) + .or_not_found(lazy_format!("Notification #{id}"))? + .as_seen_mut() + .replace(&true)? + { + diff += 1; + } + } + db.as_public_mut() + .as_server_info_mut() + .as_unread_notification_count_mut() + .mutate(|n| Ok(*n -= diff))?; + Ok(()) + }) + .await +} + +pub async fn mark_unseen( + ctx: RpcContext, + ModifyNotificationParams { ids }: ModifyNotificationParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let mut diff = 0; + let n = db.as_private_mut().as_notifications_mut(); + for id in ids { + if n.as_idx_mut(&id) + .or_not_found(lazy_format!("Notification #{id}"))? + .as_seen_mut() + .replace(&false)? + { + diff += 1; + } + } + db.as_public_mut() + .as_server_info_mut() + .as_unread_notification_count_mut() + .mutate(|n| Ok(*n += diff))?; Ok(()) }) .await @@ -253,8 +361,9 @@ impl Map for Notifications { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, HasModel)] #[serde(rename_all = "camelCase")] +#[model = "Model"] pub struct Notification { pub package_id: Option, pub created_at: DateTime, @@ -263,6 +372,8 @@ pub struct Notification { pub title: String, pub message: String, pub data: Value, + #[serde(default = "const_true")] + pub seen: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -323,6 +434,7 @@ pub fn notify( title, message, data, + seen: false, }, ) } diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index eb3aaa186..7117b4c3b 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -217,7 +217,7 @@ impl PackParams { }) .map_err(Error::from) .try_fold( - Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + Err(Error::new(eyre!("license not found"), ErrorKind::NotFound)), |acc, x| async move { match acc { Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)), diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index 2a16b4155..7fff3b1d7 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -88,7 +88,7 @@ pub async fn mount( Ok(()) } -pub async fn get_installed_packages(context: EffectContext) -> Result, Error> { +pub async fn get_installed_packages(context: EffectContext) -> Result, Error> { context .deref()? .seed diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 68f6ad7f9..9ecb27365 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -1,19 +1,20 @@ use std::collections::BTreeSet; use std::fmt; use std::sync::Arc; +use std::time::Duration; use chrono::Utc; use clap::Parser; use color_eyre::eyre::eyre; -use futures::FutureExt; +use futures::{FutureExt, TryStreamExt}; use imbl::vector; +use imbl_value::InternedString; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use rustls::RootCertStore; use rustls_pki_types::CertificateDer; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::process::Command; use tokio::sync::broadcast::Receiver; -use tokio::sync::RwLock; use tracing::instrument; use ts_rs::TS; @@ -21,11 +22,13 @@ use crate::context::{CliContext, RpcContext}; use crate::disk::util::{get_available, get_used}; use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT}; use crate::prelude::*; -use crate::rpc_continuations::RpcContinuations; +use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::shutdown::Shutdown; use crate::util::cpupower::{get_available_governors, set_governor, Governor}; use crate::util::io::open_file; +use crate::util::net::WebSocketExt; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; +use crate::util::sync::Watch; use crate::util::Invoke; use crate::{MAIN_DATA, PACKAGE_DATA}; @@ -249,7 +252,7 @@ pub struct MetricLeaf { unit: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, TS)] pub struct Celsius(f64); impl fmt::Display for Celsius { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -277,7 +280,7 @@ impl<'de> Deserialize<'de> for Celsius { Ok(Celsius(s.value.parse().map_err(serde::de::Error::custom)?)) } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, PartialOrd, TS)] pub struct Percentage(f64); impl Serialize for Percentage { fn serialize(&self, serializer: S) -> Result @@ -303,7 +306,7 @@ impl<'de> Deserialize<'de> for Percentage { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, TS)] pub struct MebiBytes(pub f64); impl Serialize for MebiBytes { fn serialize(&self, serializer: S) -> Result @@ -329,7 +332,7 @@ impl<'de> Deserialize<'de> for MebiBytes { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, PartialOrd, TS)] pub struct GigaBytes(f64); impl Serialize for GigaBytes { fn serialize(&self, serializer: S) -> Result @@ -355,12 +358,13 @@ impl<'de> Deserialize<'de> for GigaBytes { } } -#[derive(Deserialize, Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug, TS)] #[serde(rename_all = "camelCase")] pub struct MetricsGeneral { pub temperature: Option, } -#[derive(Deserialize, Serialize, Clone, Debug)] + +#[derive(Deserialize, Serialize, Clone, Debug, TS)] #[serde(rename_all = "camelCase")] pub struct MetricsMemory { pub percentage_used: Percentage, @@ -371,7 +375,8 @@ pub struct MetricsMemory { pub zram_available: MebiBytes, pub zram_used: MebiBytes, } -#[derive(Deserialize, Serialize, Clone, Debug)] + +#[derive(Deserialize, Serialize, Clone, Debug, TS)] #[serde(rename_all = "camelCase")] pub struct MetricsCpu { percentage_used: Percentage, @@ -380,7 +385,8 @@ pub struct MetricsCpu { kernel_space: Percentage, wait: Percentage, } -#[derive(Deserialize, Serialize, Clone, Debug)] + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, TS)] #[serde(rename_all = "camelCase")] pub struct MetricsDisk { percentage_used: Percentage, @@ -388,8 +394,10 @@ pub struct MetricsDisk { available: GigaBytes, capacity: GigaBytes, } -#[derive(Deserialize, Serialize, Clone, Debug)] + +#[derive(Deserialize, Serialize, Clone, Debug, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct Metrics { general: MetricsGeneral, memory: MetricsMemory, @@ -398,19 +406,74 @@ pub struct Metrics { } // #[command(display(display_serializable))] -pub async fn metrics(ctx: RpcContext, _: Empty) -> Result { - match ctx.metrics_cache.read().await.clone() { - None => Err(Error { - source: color_eyre::eyre::eyre!("No Metrics Found"), - kind: ErrorKind::NotFound, - revision: None, - }), - Some(metrics_val) => Ok(metrics_val), - } +pub async fn metrics(ctx: RpcContext) -> Result { + ctx.metrics_cache.read().or_not_found("No Metrics Found") +} + +#[derive(Deserialize, Serialize, Clone, Debug, TS)] +#[serde(rename_all = "camelCase")] +pub struct MetricsFollowResponse { + pub guid: Guid, + pub metrics: Metrics, +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct MetricsFollowParams { + #[ts(skip)] + #[serde(rename = "__auth_session")] // from Auth middleware + session: Option, +} + +pub async fn metrics_follow( + ctx: RpcContext, + MetricsFollowParams { session }: MetricsFollowParams, +) -> Result { + let mut local_cache = ctx.metrics_cache.clone(); + let metrics = local_cache + .peek_and_mark_seen(|m| m.clone()) + .or_not_found("No Metrics Found")?; + let guid = Guid::new(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws_authed( + ctx.clone(), + session, + |mut ws| async move { + let res = async { + loop { + use axum::extract::ws::Message; + tokio::select! { + _ = local_cache.changed() => { + ws.send(Message::Text( + local_cache + .peek(|m| serde_json::to_string(&m)) + .with_kind(ErrorKind::Serialization)? + )).await.with_kind(ErrorKind::Network)?; + } + msg = ws.try_next() => { + if msg.with_kind(crate::ErrorKind::Network)?.is_none() { + break; + } + } + } + } + Ok::<_, Error>("complete") + } + .await; + ws.close_result(res).await.log_err(); + }, + Duration::from_secs(30), + ), + ) + .await; + Ok(MetricsFollowResponse { guid, metrics }) } pub async fn launch_metrics_task Receiver>>( - cache: &RwLock>, + cache: &Watch>, mut mk_shutdown: F, ) { // fetch init temp @@ -475,31 +538,21 @@ pub async fn launch_metrics_task Receiver>>( } tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; } - { - // lock for writing - let mut guard = cache.write().await; - // write - *guard = Some(Metrics { - general: MetricsGeneral { - temperature: init_temp, - }, - memory: init_mem, - cpu: init_cpu, - disk: init_disk, - }) - } + + let should_launch_temp_task = init_temp.is_some(); + + cache.send(Some(Metrics { + general: MetricsGeneral { + temperature: init_temp, + }, + memory: init_mem, + cpu: init_cpu, + disk: init_disk, + })); let mut task_vec = Vec::new(); // launch persistent temp task - if cache - .read() - .await - .as_ref() - .unwrap() - .general - .temperature - .is_some() - { + if should_launch_temp_task { task_vec.push(launch_temp_task(cache, mk_shutdown()).boxed()); } // launch persistent cpu task @@ -513,14 +566,15 @@ pub async fn launch_metrics_task Receiver>>( } async fn launch_temp_task( - cache: &RwLock>, + cache: &Watch>, mut shutdown: Receiver>, ) { loop { match get_temp().await { Ok(a) => { - let mut lock = cache.write().await; - (*lock).as_mut().unwrap().general.temperature = Some(a) + cache.send_if_modified(|c| { + c.as_mut().unwrap().general.temperature.replace(a) != Some(a) + }); } Err(e) => { tracing::error!("Could not get new temperature: {}", e); @@ -535,7 +589,7 @@ async fn launch_temp_task( } async fn launch_cpu_task( - cache: &RwLock>, + cache: &Watch>, mut init: ProcStat, mut shutdown: Receiver>, ) { @@ -543,8 +597,7 @@ async fn launch_cpu_task( // read /proc/stat, diff against previous metrics, compute cpu load match get_cpu_info(&mut init).await { Ok(info) => { - let mut lock = cache.write().await; - (*lock).as_mut().unwrap().cpu = info; + cache.send_modify(|c| c.as_mut().unwrap().cpu = info); } Err(e) => { tracing::error!("Could not get new CPU Metrics: {}", e); @@ -558,16 +611,12 @@ async fn launch_cpu_task( } } -async fn launch_mem_task( - cache: &RwLock>, - mut shutdown: Receiver>, -) { +async fn launch_mem_task(cache: &Watch>, mut shutdown: Receiver>) { loop { // read /proc/meminfo match get_mem_info().await { Ok(a) => { - let mut lock = cache.write().await; - (*lock).as_mut().unwrap().memory = a; + cache.send_modify(|c| c.as_mut().unwrap().memory = a); } Err(e) => { tracing::error!("Could not get new Memory Metrics: {}", e); @@ -581,15 +630,22 @@ async fn launch_mem_task( } } async fn launch_disk_task( - cache: &RwLock>, + cache: &Watch>, mut shutdown: Receiver>, ) { loop { // run df and capture output match get_disk_info().await { Ok(a) => { - let mut lock = cache.write().await; - (*lock).as_mut().unwrap().disk = a; + cache.send_if_modified(|c| { + let c = c.as_mut().unwrap(); + if c.disk != a { + c.disk = a; + true + } else { + false + } + }); } Err(e) => { tracing::error!("Could not get new Disk Metrics: {}", e); diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index eeed45ee5..c328f2689 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -36,7 +36,9 @@ mod v0_3_6_alpha_13; mod v0_3_6_alpha_14; mod v0_3_6_alpha_15; -pub type Current = v0_3_6_alpha_15::Version; // VERSION_BUMP +mod v0_4_0_alpha_0; + +pub type Current = v0_4_0_alpha_0::Version; // VERSION_BUMP impl Current { #[instrument(skip(self, db))] @@ -133,7 +135,8 @@ enum Version { V0_3_6_alpha_12(Wrapper), V0_3_6_alpha_13(Wrapper), V0_3_6_alpha_14(Wrapper), - V0_3_6_alpha_15(Wrapper), // VERSION_BUMP + V0_3_6_alpha_15(Wrapper), + V0_4_0_alpha_0(Wrapper), // VERSION_BUMP Other(exver::Version), } @@ -172,7 +175,8 @@ impl Version { Self::V0_3_6_alpha_12(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_13(v) => DynVersion(Box::new(v.0)), Self::V0_3_6_alpha_14(v) => DynVersion(Box::new(v.0)), - Self::V0_3_6_alpha_15(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP + Self::V0_3_6_alpha_15(v) => DynVersion(Box::new(v.0)), + Self::V0_4_0_alpha_0(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( eyre!("unknown version {v}"), @@ -203,7 +207,8 @@ impl Version { Version::V0_3_6_alpha_12(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_13(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_14(Wrapper(x)) => x.semver(), - Version::V0_3_6_alpha_15(Wrapper(x)) => x.semver(), // VERSION_BUMP + Version::V0_3_6_alpha_15(Wrapper(x)) => x.semver(), + Version::V0_4_0_alpha_0(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } diff --git a/core/startos/src/version/v0_3_6_alpha_0.rs b/core/startos/src/version/v0_3_6_alpha_0.rs index 8115aad1e..2be315327 100644 --- a/core/startos/src/version/v0_3_6_alpha_0.rs +++ b/core/startos/src/version/v0_3_6_alpha_0.rs @@ -6,7 +6,6 @@ use const_format::formatcp; use ed25519_dalek::SigningKey; use exver::{PreReleaseSegment, VersionRange}; use imbl_value::{json, InternedString}; -use models::PackageId; use openssl::pkey::PKey; use openssl::x509::X509; use sqlx::postgres::PgConnectOptions; @@ -25,12 +24,12 @@ use crate::disk::mount::util::unmount; use crate::hostname::Hostname; use crate::net::forward::AvailablePorts; use crate::net::keys::KeyStore; -use crate::notifications::{Notification, Notifications}; +use crate::notifications::Notifications; use crate::prelude::*; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::ssh::{SshKeys, SshPubKey}; use crate::util::crypto::ed25519_expand_key; -use crate::util::serde::{Pem, PemEncoding}; +use crate::util::serde::Pem; use crate::util::Invoke; use crate::{DATA_DIR, PACKAGE_DATA}; diff --git a/core/startos/src/version/v0_3_6_alpha_13.rs b/core/startos/src/version/v0_3_6_alpha_13.rs index da34f7743..a5ecc5aad 100644 --- a/core/startos/src/version/v0_3_6_alpha_13.rs +++ b/core/startos/src/version/v0_3_6_alpha_13.rs @@ -1,7 +1,4 @@ -use std::collections::BTreeMap; - use exver::{PreReleaseSegment, VersionRange}; -use imbl_value::json; use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_6_alpha_12, VersionT}; @@ -30,7 +27,7 @@ impl VersionT for Version { fn compat(self) -> &'static VersionRange { &V0_3_0_COMPAT } - fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { Ok(()) } fn down(self, _db: &mut Value) -> Result<(), Error> { diff --git a/core/startos/src/version/v0_3_6_alpha_14.rs b/core/startos/src/version/v0_3_6_alpha_14.rs index 219b0d6d2..69422051f 100644 --- a/core/startos/src/version/v0_3_6_alpha_14.rs +++ b/core/startos/src/version/v0_3_6_alpha_14.rs @@ -1,7 +1,4 @@ -use std::collections::BTreeMap; - use exver::{PreReleaseSegment, VersionRange}; -use imbl_value::json; use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_6_alpha_13, VersionT}; @@ -30,7 +27,7 @@ impl VersionT for Version { fn compat(self) -> &'static VersionRange { &V0_3_0_COMPAT } - fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { Ok(()) } fn down(self, _db: &mut Value) -> Result<(), Error> { diff --git a/core/startos/src/version/v0_3_6_alpha_15.rs b/core/startos/src/version/v0_3_6_alpha_15.rs index eef6c5993..7c7425b60 100644 --- a/core/startos/src/version/v0_3_6_alpha_15.rs +++ b/core/startos/src/version/v0_3_6_alpha_15.rs @@ -1,7 +1,4 @@ -use std::collections::BTreeMap; - use exver::{PreReleaseSegment, VersionRange}; -use imbl_value::json; use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_6_alpha_14, VersionT}; @@ -30,7 +27,7 @@ impl VersionT for Version { fn compat(self) -> &'static VersionRange { &V0_3_0_COMPAT } - fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { Ok(()) } fn down(self, _db: &mut Value) -> Result<(), Error> { diff --git a/core/startos/src/version/v0_4_0.rs b/core/startos/src/version/v0_4_0.rs deleted file mode 100644 index bcf5b57a4..000000000 --- a/core/startos/src/version/v0_4_0.rs +++ /dev/null @@ -1,36 +0,0 @@ -use async_trait::async_trait; -use emver::VersionRange; -use lazy_static::lazy_static; - -use super::*; - -const V0_4_0: emver::Version = emver::Version::new(0, 4, 0, 0); -lazy_static! { - pub static ref V0_4_0_COMPAT: VersionRange = VersionRange::Conj( - Box::new(VersionRange::Anchor(emver::GTE, V0_4_0)), - Box::new(VersionRange::Anchor(emver::LTE, Current::new().semver())), - ); -} - -#[derive(Clone, Debug)] -pub struct Version; - -#[async_trait] -impl VersionT for Version { - type Previous = v0_3_5::Version; - fn new() -> Self { - Version - } - fn semver(&self) -> emver::Version { - V0_4_0 - } - fn compat(&self) -> &'static VersionRange { - &*V0_4_0_COMPAT - } - async fn up(&self, _db: &PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } - async fn down(&self, _db: &PatchDb, _secrets: &PgPool) -> Result<(), Error> { - Ok(()) - } -} diff --git a/core/startos/src/version/v0_4_0_alpha_0.rs b/core/startos/src/version/v0_4_0_alpha_0.rs new file mode 100644 index 000000000..460f6e2fe --- /dev/null +++ b/core/startos/src/version/v0_4_0_alpha_0.rs @@ -0,0 +1,58 @@ +use exver::{PreReleaseSegment, VersionRange}; +use imbl_value::json; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_15, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_4_0_alpha_0: exver::Version = exver::Version::new( + [0, 4, 0], + [PreReleaseSegment::String("alpha".into()), 0.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_15::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_4_0_alpha_0.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + let host = db["public"]["serverInfo"]["host"].clone(); + let mut wifi = db["public"]["serverInfo"]["wifi"].clone(); + wifi["enabled"] = Value::Bool(true); + let mut network_interfaces = db["public"]["serverInfo"]["networkInterfaces"].clone(); + for (k, v) in network_interfaces + .as_object_mut() + .into_iter() + .flat_map(|m| m.iter_mut()) + { + v["inbound"] = v["public"].clone(); + if v["ipInfo"].is_object() { + v["ipInfo"]["name"] = Value::String((&**k).to_owned().into()); + } + } + let acme = db["public"]["serverInfo"]["acme"].clone(); + db["public"]["serverInfo"]["network"] = json!({ + "host": host, + "wifi": wifi, + "networkInterfaces": network_interfaces, + "acme": acme, + }); + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/sdk/base/lib/osBindings/UnsetPublicParams.ts b/sdk/base/lib/osBindings/Celsius.ts similarity index 67% rename from sdk/base/lib/osBindings/UnsetPublicParams.ts rename to sdk/base/lib/osBindings/Celsius.ts index db8f730e1..298f97f5e 100644 --- a/sdk/base/lib/osBindings/UnsetPublicParams.ts +++ b/sdk/base/lib/osBindings/Celsius.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type UnsetPublicParams = { interface: string } +export type Celsius = number diff --git a/sdk/base/lib/osBindings/GigaBytes.ts b/sdk/base/lib/osBindings/GigaBytes.ts new file mode 100644 index 000000000..9e5e4ed7b --- /dev/null +++ b/sdk/base/lib/osBindings/GigaBytes.ts @@ -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 GigaBytes = number diff --git a/sdk/base/lib/osBindings/IpInfo.ts b/sdk/base/lib/osBindings/IpInfo.ts index 260add9e6..28d052048 100644 --- a/sdk/base/lib/osBindings/IpInfo.ts +++ b/sdk/base/lib/osBindings/IpInfo.ts @@ -2,6 +2,7 @@ import type { NetworkInterfaceType } from "./NetworkInterfaceType" export type IpInfo = { + name: string scopeId: number deviceType: NetworkInterfaceType | null subnets: string[] diff --git a/sdk/base/lib/osBindings/MebiBytes.ts b/sdk/base/lib/osBindings/MebiBytes.ts new file mode 100644 index 000000000..c10760d23 --- /dev/null +++ b/sdk/base/lib/osBindings/MebiBytes.ts @@ -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 MebiBytes = number diff --git a/sdk/base/lib/osBindings/Metrics.ts b/sdk/base/lib/osBindings/Metrics.ts new file mode 100644 index 000000000..7a4be1648 --- /dev/null +++ b/sdk/base/lib/osBindings/Metrics.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MetricsCpu } from "./MetricsCpu" +import type { MetricsDisk } from "./MetricsDisk" +import type { MetricsGeneral } from "./MetricsGeneral" +import type { MetricsMemory } from "./MetricsMemory" + +export type Metrics = { + general: MetricsGeneral + memory: MetricsMemory + cpu: MetricsCpu + disk: MetricsDisk +} diff --git a/sdk/base/lib/osBindings/MetricsCpu.ts b/sdk/base/lib/osBindings/MetricsCpu.ts new file mode 100644 index 000000000..3576bd8c0 --- /dev/null +++ b/sdk/base/lib/osBindings/MetricsCpu.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Percentage } from "./Percentage" + +export type MetricsCpu = { + percentageUsed: Percentage + idle: Percentage + userSpace: Percentage + kernelSpace: Percentage + wait: Percentage +} diff --git a/sdk/base/lib/osBindings/MetricsDisk.ts b/sdk/base/lib/osBindings/MetricsDisk.ts new file mode 100644 index 000000000..2085b780d --- /dev/null +++ b/sdk/base/lib/osBindings/MetricsDisk.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { GigaBytes } from "./GigaBytes" +import type { Percentage } from "./Percentage" + +export type MetricsDisk = { + percentageUsed: Percentage + used: GigaBytes + available: GigaBytes + capacity: GigaBytes +} diff --git a/sdk/base/lib/osBindings/MetricsGeneral.ts b/sdk/base/lib/osBindings/MetricsGeneral.ts new file mode 100644 index 000000000..9376e12ee --- /dev/null +++ b/sdk/base/lib/osBindings/MetricsGeneral.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Celsius } from "./Celsius" + +export type MetricsGeneral = { temperature: Celsius | null } diff --git a/sdk/base/lib/osBindings/MetricsMemory.ts b/sdk/base/lib/osBindings/MetricsMemory.ts new file mode 100644 index 000000000..36c576757 --- /dev/null +++ b/sdk/base/lib/osBindings/MetricsMemory.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MebiBytes } from "./MebiBytes" +import type { Percentage } from "./Percentage" + +export type MetricsMemory = { + percentageUsed: Percentage + total: MebiBytes + available: MebiBytes + used: MebiBytes + zramTotal: MebiBytes + zramAvailable: MebiBytes + zramUsed: MebiBytes +} diff --git a/sdk/base/lib/osBindings/NetworkInfo.ts b/sdk/base/lib/osBindings/NetworkInfo.ts new file mode 100644 index 000000000..1933332e4 --- /dev/null +++ b/sdk/base/lib/osBindings/NetworkInfo.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AcmeProvider } from "./AcmeProvider" +import type { AcmeSettings } from "./AcmeSettings" +import type { Host } from "./Host" +import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" +import type { WifiInfo } from "./WifiInfo" + +export type NetworkInfo = { + wifi: WifiInfo + host: Host + networkInterfaces: { [key: string]: NetworkInterfaceInfo } + acme: { [key: AcmeProvider]: AcmeSettings } +} diff --git a/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts b/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts index 796046b93..324b3df78 100644 --- a/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts +++ b/sdk/base/lib/osBindings/NetworkInterfaceInfo.ts @@ -2,6 +2,7 @@ import type { IpInfo } from "./IpInfo" export type NetworkInterfaceInfo = { - public: boolean | null + inbound: boolean | null + outbound: boolean | null ipInfo: IpInfo | null } diff --git a/sdk/base/lib/osBindings/NetworkInterfaceSetPublicParams.ts b/sdk/base/lib/osBindings/NetworkInterfaceSetInboundParams.ts similarity index 63% rename from sdk/base/lib/osBindings/NetworkInterfaceSetPublicParams.ts rename to sdk/base/lib/osBindings/NetworkInterfaceSetInboundParams.ts index 516bfc817..30a5804bf 100644 --- a/sdk/base/lib/osBindings/NetworkInterfaceSetPublicParams.ts +++ b/sdk/base/lib/osBindings/NetworkInterfaceSetInboundParams.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type NetworkInterfaceSetPublicParams = { +export type NetworkInterfaceSetInboundParams = { interface: string - public: boolean | null + inbound: boolean | null } diff --git a/sdk/base/lib/osBindings/Percentage.ts b/sdk/base/lib/osBindings/Percentage.ts new file mode 100644 index 000000000..aff21db40 --- /dev/null +++ b/sdk/base/lib/osBindings/Percentage.ts @@ -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 Percentage = number diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts index 5779fa7d5..a8ca5adf3 100644 --- a/sdk/base/lib/osBindings/ServerInfo.ts +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -1,28 +1,21 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AcmeProvider } from "./AcmeProvider" -import type { AcmeSettings } from "./AcmeSettings" import type { Governor } from "./Governor" -import type { Host } from "./Host" import type { LshwDevice } from "./LshwDevice" -import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" +import type { NetworkInfo } from "./NetworkInfo" import type { ServerStatus } from "./ServerStatus" import type { SmtpValue } from "./SmtpValue" -import type { WifiInfo } from "./WifiInfo" export type ServerInfo = { arch: string platform: string id: string hostname: string - host: Host version: string packageVersionCompat: string postInitMigrationTodos: string[] lastBackup: string | null - networkInterfaces: { [key: string]: NetworkInterfaceInfo } - acme: { [key: AcmeProvider]: AcmeSettings } + network: NetworkInfo statusInfo: ServerStatus - wifi: WifiInfo unreadNotificationCount: number passwordHash: string pubkey: string diff --git a/sdk/base/lib/osBindings/UnsetInboundParams.ts b/sdk/base/lib/osBindings/UnsetInboundParams.ts new file mode 100644 index 000000000..df519e752 --- /dev/null +++ b/sdk/base/lib/osBindings/UnsetInboundParams.ts @@ -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 UnsetInboundParams = { interface: string } diff --git a/sdk/base/lib/osBindings/WifiInfo.ts b/sdk/base/lib/osBindings/WifiInfo.ts index c5103e7e2..ce9b07e9a 100644 --- a/sdk/base/lib/osBindings/WifiInfo.ts +++ b/sdk/base/lib/osBindings/WifiInfo.ts @@ -1,6 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type WifiInfo = { + enabled: boolean interface: string | null ssids: Array selected: string | null diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index e2ab33033..cbf4c8e0b 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -46,6 +46,7 @@ export { BlockDev } from "./BlockDev" export { BuildArg } from "./BuildArg" export { CallbackId } from "./CallbackId" export { Category } from "./Category" +export { Celsius } from "./Celsius" export { CheckDependenciesParam } from "./CheckDependenciesParam" export { CheckDependenciesResult } from "./CheckDependenciesResult" export { Cifs } from "./Cifs" @@ -93,6 +94,7 @@ export { GetSslKeyParams } from "./GetSslKeyParams" export { GetStatusParams } from "./GetStatusParams" export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" +export { GigaBytes } from "./GigaBytes" export { GitHash } from "./GitHash" export { Governor } from "./Governor" export { Guid } from "./Guid" @@ -125,14 +127,21 @@ export { LshwProcessor } from "./LshwProcessor" export { MainStatus } from "./MainStatus" export { Manifest } from "./Manifest" export { MaybeUtf8String } from "./MaybeUtf8String" +export { MebiBytes } from "./MebiBytes" export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +export { MetricsCpu } from "./MetricsCpu" +export { MetricsDisk } from "./MetricsDisk" +export { MetricsGeneral } from "./MetricsGeneral" +export { MetricsMemory } from "./MetricsMemory" +export { Metrics } from "./Metrics" export { MountParams } from "./MountParams" export { MountTarget } from "./MountTarget" export { NamedHealthCheckResult } from "./NamedHealthCheckResult" export { NamedProgress } from "./NamedProgress" export { NetInfo } from "./NetInfo" +export { NetworkInfo } from "./NetworkInfo" export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" -export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicParams" +export { NetworkInterfaceSetInboundParams } from "./NetworkInterfaceSetInboundParams" export { NetworkInterfaceType } from "./NetworkInterfaceType" export { OnionHostname } from "./OnionHostname" export { OsIndex } from "./OsIndex" @@ -149,6 +158,7 @@ export { PackageState } from "./PackageState" export { PackageVersionInfo } from "./PackageVersionInfo" export { PasswordType } from "./PasswordType" export { PathOrUrl } from "./PathOrUrl" +export { Percentage } from "./Percentage" export { ProcedureId } from "./ProcedureId" export { Progress } from "./Progress" export { Public } from "./Public" @@ -188,7 +198,7 @@ export { SignerInfo } from "./SignerInfo" export { SmtpValue } from "./SmtpValue" export { StartStop } from "./StartStop" export { TestSmtpParams } from "./TestSmtpParams" -export { UnsetPublicParams } from "./UnsetPublicParams" +export { UnsetInboundParams } from "./UnsetInboundParams" export { UpdatingState } from "./UpdatingState" export { VerifyCifsParams } from "./VerifyCifsParams" export { VersionSignerParams } from "./VersionSignerParams" diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 7abbc42e2..40777d9c0 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -73,7 +73,7 @@ import * as actions from "../../base/lib/actions" import { setupInit } from "./inits/setupInit" import * as fs from "node:fs/promises" -export const OSVersion = testTypeVersion("0.3.6-alpha.15") +export const OSVersion = testTypeVersion("0.4.0-alpha.0") // prettier-ignore type AnyNeverCond = diff --git a/web/package-lock.json b/web/package-lock.json index 1a9f50495..7dca1bc4c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.3.6-alpha.15", + "version": "0.4.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.3.6-alpha.15", + "version": "0.4.0-alpha.0", "license": "MIT", "dependencies": { "@angular/animations": "^17.3.1", diff --git a/web/package.json b/web/package.json index b4076d71a..0d0d76153 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.6-alpha.15", + "version": "0.4.0-alpha.0", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts index b035586a5..46a0074b0 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.component.ts @@ -96,7 +96,7 @@ import { map } from 'rxjs' }) export class InterfaceComponent { readonly acme$ = inject>(PatchDB) - .watch$('serverInfo', 'acme') + .watch$('serverInfo', 'network', 'acme') .pipe(map(acme => Object.keys(acme))) @Input() packageId?: string diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts index e94e62370..5aa45aebd 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts @@ -19,7 +19,6 @@ export const REMOVE: Partial> = { }, } -// @TODO 040 Aiden audit export function getAddresses( serviceInterface: T.ServiceInterface, host: T.Host, @@ -81,11 +80,17 @@ export function getAddresses( } else { const hostnameKind = h.hostname.kind - if (hostnameKind === 'domain') { + if (h.public) { clearnet.push({ - label: 'Domain', + label: + hostnameKind == 'domain' + ? 'Domain' + : `${h.networkInterfaceId} (${hostnameKind})`, url, - acme: host.domains[h.hostname.domain]?.acme, + acme: + hostnameKind == 'domain' + ? host.domains[h.hostname.domain]?.acme + : null, // @TODO Matt make sure this is handled correctly - looks like ACME settings aren't built yet anyway, but ACME settings aren't *available* for public IPs }) } else { local.push({ @@ -101,15 +106,19 @@ export function getAddresses( }) return { - clearnet, - local, - tor, + clearnet: clearnet.filter( + (value, index, self) => + index === self.findIndex(t => t.url === value.url), + ), + local: local.filter( + (value, index, self) => + index === self.findIndex(t => t.url === value.url), + ), + tor: tor.filter( + (value, index, self) => + index === self.findIndex(t => t.url === value.url), + ), } - - // @TODO Aiden what was going on here in 036? - // return mappedAddresses.filter( - // (value, index, self) => index === self.findIndex(t => t.url === value.url), - // ) } export type AddressDetails = { diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts index e6bcedc26..c9d878603 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/notifications.component.ts @@ -95,7 +95,7 @@ export default class NotificationsComponent { this.notifications.set( current.map(c => ({ ...c, - read: toUpdate.some(n => n.id === c.id) || c.read, + read: toUpdate.some(n => n.id === c.id) || c.seen, })), ) @@ -111,7 +111,7 @@ export default class NotificationsComponent { this.notifications.set( current.map(c => ({ ...c, - read: c.read && !toUpdate.some(n => n.id === c.id), + read: c.seen && !toUpdate.some(n => n.id === c.id), })), ) diff --git a/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts b/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts index 5ff33a1dc..521b66be1 100644 --- a/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/notifications/table.component.ts @@ -40,7 +40,7 @@ import { NotificationItemComponent } from './item.component' @for (notification of notifications; track $index) { { const providerUrls = Object.keys(acme) return providerUrls.map(url => { diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 75022a0e5..adea02c66 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -771,7 +771,7 @@ export namespace Mock { }, }, }, - read: false, + seen: false, }, { id: 2, @@ -782,7 +782,7 @@ export namespace Mock { title: 'SSH Key Added', message: 'A new SSH key was added. If you did not do this, shit is bad.', data: null, - read: false, + seen: false, }, { id: 3, @@ -793,7 +793,7 @@ export namespace Mock { title: 'SSH Key Removed', message: 'A SSH key was removed.', data: null, - read: false, + seen: false, }, { id: 4, @@ -811,7 +811,7 @@ export namespace Mock { ) .join(''), data: null, - read: false, + seen: false, }, { id: 5, @@ -822,7 +822,7 @@ export namespace Mock { title: 'Welcome to StartOS 0.3.6!', message: 'Click "View Details" to learn all about the new version', data: markdown, - read: false, + seen: false, }, ] diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 336768791..3f9b94100 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -81,7 +81,6 @@ export namespace RR { guid: string } - // @TODO 040 implement websocket export type FollowServerMetricsReq = {} // server.metrics.follow export type FollowServerMetricsRes = { guid: string @@ -136,17 +135,16 @@ export namespace RR { } // notification.list export type GetNotificationsRes = ServerNotification[] - // @TODO 040 all these notification endpoints need updating - export type DeleteNotificationReq = { ids: number[] } // notification.delete - export type DeleteNotificationRes = null + export type DeleteNotificationsReq = { ids: number[] } // notification.remove + export type DeleteNotificationsRes = null - export type MarkSeenNotificationReq = DeleteNotificationReq // notification.mark-seen + export type MarkSeenNotificationReq = DeleteNotificationsReq // notification.mark-seen export type MarkSeenNotificationRes = null export type MarkSeenAllNotificationsReq = { before: number } // notification.mark-seen-before export type MarkSeenAllNotificationsRes = null - export type MarkUnseenNotificationReq = DeleteNotificationReq // notification.mark-unseen + export type MarkUnseenNotificationReq = DeleteNotificationsReq // notification.mark-unseen export type MarkUnseenNotificationRes = null // wifi @@ -175,9 +173,11 @@ export namespace RR { } export type AddWifiRes = null - // @TODO 040 - export type EnableWifiReq = { enable: boolean } // wifi.enable - export type EnableWifiRes = null + export type EnabledWifiReq = { enable: boolean } // wifi.set-enabled + export type EnabledWifiRes = null + + export type SetWifiCountryReq = { country: string } // wifi.country.set + export type SetWifiCountryRes = null export type ConnectWifiReq = { ssid: string } // wifi.connect export type ConnectWifiRes = null @@ -523,8 +523,7 @@ export type ServerNotification = { title: string message: string data: NotificationData - // @TODO 040 - read: boolean + seen: boolean } export type NotificationLevel = 'success' | 'info' | 'warning' | 'error' @@ -532,10 +531,10 @@ export type NotificationLevel = 'success' | 'info' | 'warning' | 'error' export type NotificationData = T extends 0 ? null : T extends 1 - ? BackupReport - : T extends 2 - ? string - : any + ? BackupReport + : T extends 2 + ? string + : any export type BackupReport = { server: { diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 7cced9105..0cd79b9eb 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -177,12 +177,12 @@ export abstract class ApiService { ): Promise abstract markUnseenNotifications( - params: RR.DeleteNotificationReq, - ): Promise + params: RR.DeleteNotificationsReq, + ): Promise abstract deleteNotifications( - params: RR.DeleteNotificationReq, - ): Promise + params: RR.DeleteNotificationsReq, + ): Promise // ** proxies ** @@ -220,7 +220,11 @@ export abstract class ApiService { // wifi - abstract enableWifi(params: RR.EnableWifiReq): Promise + abstract enableWifi(params: RR.EnabledWifiReq): Promise + + abstract setWifiCountry( + params: RR.SetWifiCountryReq, + ): Promise abstract getWifi( params: RR.GetWifiReq, diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index bec6820bb..e3003a5da 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -245,7 +245,6 @@ export class LiveApiService extends ApiService { async followServerMetrics( params: RR.FollowServerMetricsReq, ): Promise { - // @TODO 040 implement .follow return this.rpcRequest({ method: 'server.metrics.follow', params }) } @@ -350,8 +349,8 @@ export class LiveApiService extends ApiService { } async deleteNotifications( - params: RR.DeleteNotificationReq, - ): Promise { + params: RR.DeleteNotificationsReq, + ): Promise { return this.rpcRequest({ method: 'notification.remove', params }) } @@ -422,7 +421,7 @@ export class LiveApiService extends ApiService { // wifi - async enableWifi(params: RR.EnableWifiReq): Promise { + async enableWifi(params: RR.EnabledWifiReq): Promise { return this.rpcRequest({ method: 'wifi.enable', params }) } @@ -433,6 +432,12 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'wifi.get', params, timeout }) } + async setWifiCountry( + params: RR.SetWifiCountryReq, + ): Promise { + return this.rpcRequest({ method: 'wifi.country.set', params }) + } + async addWifi(params: RR.AddWifiReq): Promise { return this.rpcRequest({ method: 'wifi.add', params }) } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 4496661f0..d6c9f8834 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -519,8 +519,8 @@ export class MockApiService extends ApiService { } async deleteNotifications( - params: RR.DeleteNotificationReq, - ): Promise { + params: RR.DeleteNotificationsReq, + ): Promise { await pauseFor(2000) return null } @@ -696,7 +696,7 @@ export class MockApiService extends ApiService { // wifi - async enableWifi(params: RR.EnableWifiReq): Promise { + async enableWifi(params: RR.EnabledWifiReq): Promise { await pauseFor(2000) const patch = [ { @@ -710,6 +710,13 @@ export class MockApiService extends ApiService { return null } + async setWifiCountry( + params: RR.SetWifiCountryReq, + ): Promise { + await pauseFor(2000) + return null + } + async getWifi(params: RR.GetWifiReq): Promise { await pauseFor(2000) return Mock.Wifi diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 052e52389..0a9285735 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -39,6 +39,11 @@ export const mockPatchData: DataModel = { selected: null, lastRegion: null, }, + acme: { + [knownACME[0].url]: { + contact: ['mailto:support@start9.com'], + }, + }, host: { bindings: { 80: { @@ -171,11 +176,6 @@ export const mockPatchData: DataModel = { }, }, }, - acme: { - [knownACME[0].url]: { - contact: ['mailto:support@start9.com'], - }, - }, unreadNotificationCount: 4, // password is asdfasdf passwordHash: @@ -191,109 +191,6 @@ export const mockPatchData: DataModel = { backupProgress: {}, }, hostname: 'random-words', - // @ts-ignore - host: { - bindings: { - 80: { - enabled: true, - net: { - assignedPort: null, - assignedSslPort: 443, - public: false, - }, - options: { - preferredExternalPort: 80, - addSsl: { - preferredExternalPort: 443, - alpn: { specified: ['http/1.1', 'h2'] }, - }, - secure: null, - }, - }, - }, - domains: {}, - onions: ['myveryownspecialtoraddress'], - hostnameInfo: { - 80: [ - { - kind: 'ip', - networkInterfaceId: 'eth0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'eth0', - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.1', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'ipv4', - value: '10.0.0.2', - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'eth0', - public: false, - hostname: { - kind: 'ipv6', - value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd', - scopeId: 2, - port: null, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'ipv6', - value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234', - scopeId: 3, - port: null, - sslPort: 443, - }, - }, - { - kind: 'onion', - hostname: { - value: 'myveryownspecialtoraddress.onion', - port: 80, - sslPort: 443, - }, - }, - ], - }, - }, pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', ntpSynced: false, diff --git a/web/projects/ui/src/app/services/notification.service.ts b/web/projects/ui/src/app/services/notification.service.ts index dcbda3158..e861f6209 100644 --- a/web/projects/ui/src/app/services/notification.service.ts +++ b/web/projects/ui/src/app/services/notification.service.ts @@ -25,7 +25,7 @@ export class NotificationService { ).pipe(shareReplay(1)) async markSeen(notifications: ServerNotifications) { - const ids = notifications.filter(n => !n.read).map(n => n.id) + const ids = notifications.filter(n => !n.seen).map(n => n.id) this.updateCount(-ids.length) @@ -43,7 +43,7 @@ export class NotificationService { } async markUnseen(notifications: ServerNotifications) { - const ids = notifications.filter(n => n.read).map(n => n.id) + const ids = notifications.filter(n => n.seen).map(n => n.id) this.updateCount(ids.length) @@ -53,7 +53,7 @@ export class NotificationService { } async remove(notifications: ServerNotifications): Promise { - this.updateCount(-notifications.filter(n => !n.read).length) + this.updateCount(-notifications.filter(n => !n.seen).length) this.api .deleteNotifications({ ids: notifications.map(n => n.id) }) diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index b150dd4a8..d8a469101 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -1,16 +1,6 @@ import { T } from '@start9labs/start-sdk' -export type DataModel = Omit & { - ui: UIData - // @TODO 040 - serverInfo: Omit< - T.Public['serverInfo'], - 'wifi' | 'networkInterfaces' | 'host' - > & { - network: NetworkInfo - } - packageData: Record -} +export type DataModel = T.Public & { ui: UIData; packageData: AllPackageData } export type UIData = { name: string | null @@ -37,20 +27,7 @@ export type UIStore = { name?: string } -export type NetworkInfo = { - wifi: T.WifiInfo & { enabled: boolean } - host: T.Host - networkInterfaces: { - [id: string]: { - inbound: boolean | null - outbound: boolean | null - ipInfo: - | (T.IpInfo & { - name: string - }) - | null - } - } +export type NetworkInfo = T.NetworkInfo & { // @TODO 041 // start9To: { // subdomain: string