Update/040 types (#2845)

* small type changes and clear todos

* handle notifications and metrics

* wip

* fixes

* migration

* dedup all urls

* better handling of clearnet ips

* add rfkill dependency

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2025-03-06 20:36:19 -07:00
committed by GitHub
parent ac392dcb96
commit e830fade06
63 changed files with 800 additions and 480 deletions

View File

@@ -43,6 +43,7 @@ podman
postgresql postgresql
psmisc psmisc
qemu-guest-agent qemu-guest-agent
rfkill
rsync rsync
samba-common-bin samba-common-bin
smartmontools smartmontools

2
core/Cargo.lock generated
View File

@@ -5952,7 +5952,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "start-os" name = "start-os"
version = "0.3.6-alpha.15" version = "0.4.0-alpha.0"
dependencies = [ dependencies = [
"aes 0.7.5", "aes 0.7.5",
"async-acme", "async-acme",

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os" name = "start-os"
readme = "README.md" readme = "README.md"
repository = "https://github.com/Start9Labs/start-os" 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" license = "MIT"
[lib] [lib]

View File

@@ -59,7 +59,13 @@ impl AccountInfo {
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?); let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let password = db.as_private().as_password().de()?; let password = db.as_private().as_password().de()?;
let key_store = db.as_private().as_key_store(); 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 let tor_keys = tor_addrs
.into_iter() .into_iter()
.map(|tor_addr| key_store.as_onion().get_key(&tor_addr)) .map(|tor_addr| key_store.as_onion().get_key(&tor_addr))
@@ -89,13 +95,17 @@ impl AccountInfo {
server_info server_info
.as_pubkey_mut() .as_pubkey_mut()
.ser(&self.ssh_key.public_key().to_openssh()?)?; .ser(&self.ssh_key.public_key().to_openssh()?)?;
server_info.as_host_mut().as_onions_mut().ser( server_info
&self .as_network_mut()
.tor_keys .as_host_mut()
.iter() .as_onions_mut()
.map(|tor_key| tor_key.public().get_onion_address()) .ser(
.collect(), &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_password_mut().ser(&self.password)?;
db.as_private_mut() db.as_private_mut()
.as_ssh_privkey_mut() .as_ssh_privkey_mut()

View File

@@ -1,6 +1,5 @@
use std::cmp::max; use std::cmp::max;
use std::ffi::OsString; use std::ffi::OsString;
use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -14,7 +13,6 @@ use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases; use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, RpcContext}; use crate::context::{DiagnosticContext, InitContext, RpcContext};
use crate::net::network_interface::SelfContainedNetworkInterfaceListener; use crate::net::network_interface::SelfContainedNetworkInterfaceListener;
use crate::net::utils::ipv6_is_local;
use crate::net::web_server::{Acceptor, UpgradableListener, WebServer}; use crate::net::web_server::{Acceptor, UpgradableListener, WebServer};
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::system::launch_metrics_task; use crate::system::launch_metrics_task;

View File

@@ -1,4 +1,3 @@
use std::backtrace;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::future::Future; use std::future::Future;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
@@ -41,7 +40,7 @@ use crate::service::effects::callbacks::ServiceCallbacks;
use crate::service::ServiceMap; use crate::service::ServiceMap;
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::util::lshw::LshwDevice; use crate::util::lshw::LshwDevice;
use crate::util::sync::SyncMutex; use crate::util::sync::{SyncMutex, Watch};
pub struct RpcContextSeed { pub struct RpcContextSeed {
is_closed: AtomicBool, is_closed: AtomicBool,
@@ -57,14 +56,14 @@ pub struct RpcContextSeed {
pub os_net_service: NetService, pub os_net_service: NetService,
pub s9pk_arch: Option<&'static str>, pub s9pk_arch: Option<&'static str>,
pub services: ServiceMap, pub services: ServiceMap,
pub metrics_cache: RwLock<Option<crate::system::Metrics>>, pub metrics_cache: Watch<Option<crate::system::Metrics>>,
pub shutdown: broadcast::Sender<Option<Shutdown>>, pub shutdown: broadcast::Sender<Option<Shutdown>>,
pub tor_socks: SocketAddr, pub tor_socks: SocketAddr,
pub lxc_manager: Arc<LxcManager>, pub lxc_manager: Arc<LxcManager>,
pub open_authed_continuations: OpenAuthedContinuations<Option<InternedString>>, pub open_authed_continuations: OpenAuthedContinuations<Option<InternedString>>,
pub rpc_continuations: RpcContinuations, pub rpc_continuations: RpcContinuations,
pub callbacks: ServiceCallbacks, pub callbacks: ServiceCallbacks,
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>, pub wifi_manager: Arc<RwLock<Option<WpaCli>>>,
pub current_secret: Arc<Jwk>, pub current_secret: Arc<Jwk>,
pub client: Client, pub client: Client,
pub start_time: Instant, pub start_time: Instant,
@@ -174,7 +173,7 @@ impl RpcContext {
tracing::info!("Initialized Net Controller"); tracing::info!("Initialized Net Controller");
let services = ServiceMap::default(); let services = ServiceMap::default();
let metrics_cache = RwLock::<Option<crate::system::Metrics>>::new(None); let metrics_cache = Watch::<Option<crate::system::Metrics>>::new(None);
let tor_proxy_url = format!("socks5h://{tor_proxy}"); let tor_proxy_url = format!("socks5h://{tor_proxy}");
let crons = SyncMutex::new(BTreeMap::new()); let crons = SyncMutex::new(BTreeMap::new());
@@ -245,9 +244,7 @@ impl RpcContext {
open_authed_continuations: OpenAuthedContinuations::new(), open_authed_continuations: OpenAuthedContinuations::new(),
rpc_continuations: RpcContinuations::new(), rpc_continuations: RpcContinuations::new(),
callbacks: Default::default(), callbacks: Default::default(),
wifi_manager: wifi_interface wifi_manager: Arc::new(RwLock::new(wifi_interface.clone().map(|i| WpaCli::init(i)))),
.clone()
.map(|i| Arc::new(RwLock::new(WpaCli::init(i)))),
current_secret: Arc::new( current_secret: Arc::new(
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| { Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
tracing::debug!("{:?}", e); tracing::debug!("{:?}", e);
@@ -488,7 +485,7 @@ impl Drop for RpcContext {
let count = Arc::strong_count(&self.0) - 1; let count = Arc::strong_count(&self.0) - 1;
tracing::info!("RpcContext dropped. {} left.", count); tracing::info!("RpcContext dropped. {} left.", count);
if count > 0 { if count > 0 {
tracing::debug!("{}", backtrace::Backtrace::force_capture()); tracing::debug!("{}", std::backtrace::Backtrace::force_capture());
tracing::debug!("{:?}", eyre!("")) tracing::debug!("{:?}", eyre!(""))
} }
} }

View File

@@ -48,44 +48,50 @@ impl Public {
id: account.server_id.clone(), id: account.server_id.clone(),
version: Current::default().semver(), version: Current::default().semver(),
hostname: account.hostname.no_dot_host_name(), 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, last_backup: None,
package_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(),
network_interfaces: BTreeMap::new(), network: NetworkInfo {
acme: BTreeMap::new(), 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 { status_info: ServerStatus {
backup_progress: None, backup_progress: None,
updated: false, updated: false,
@@ -93,7 +99,6 @@ impl Public {
shutting_down: false, shutting_down: false,
restarting: false, restarting: false,
}, },
wifi: WifiInfo::default(),
unread_notification_count: 0, unread_notification_count: 0,
password_hash: account.password.clone(), password_hash: account.password.clone(),
pubkey: ssh_key::PublicKey::from(&account.ssh_key) pubkey: ssh_key::PublicKey::from(&account.ssh_key)
@@ -145,7 +150,6 @@ pub struct ServerInfo {
pub id: String, pub id: String,
#[ts(type = "string")] #[ts(type = "string")]
pub hostname: InternedString, pub hostname: InternedString,
pub host: Host,
#[ts(type = "string")] #[ts(type = "string")]
pub version: Version, pub version: Version,
#[ts(type = "string")] #[ts(type = "string")]
@@ -154,14 +158,9 @@ pub struct ServerInfo {
pub post_init_migration_todos: BTreeSet<Version>, pub post_init_migration_todos: BTreeSet<Version>,
#[ts(type = "string | null")] #[ts(type = "string | null")]
pub last_backup: Option<DateTime<Utc>>, pub last_backup: Option<DateTime<Utc>>,
#[ts(as = "BTreeMap::<String, NetworkInterfaceInfo>")] pub network: NetworkInfo,
#[serde(default)]
pub network_interfaces: BTreeMap<InternedString, NetworkInterfaceInfo>,
#[serde(default)]
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
#[serde(default)] #[serde(default)]
pub status_info: ServerStatus, pub status_info: ServerStatus,
pub wifi: WifiInfo,
#[ts(type = "number")] #[ts(type = "number")]
pub unread_notification_count: u64, pub unread_notification_count: u64,
pub password_hash: String, pub password_hash: String,
@@ -178,17 +177,32 @@ pub struct ServerInfo {
pub devices: Vec<LshwDevice>, pub devices: Vec<LshwDevice>,
} }
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct NetworkInfo {
pub wifi: WifiInfo,
pub host: Host,
#[ts(as = "BTreeMap::<String, NetworkInterfaceInfo>")]
#[serde(default)]
pub network_interfaces: BTreeMap<InternedString, NetworkInterfaceInfo>,
#[serde(default)]
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct NetworkInterfaceInfo { pub struct NetworkInterfaceInfo {
pub public: Option<bool>, pub inbound: Option<bool>,
pub outbound: Option<bool>,
pub ip_info: Option<IpInfo>, pub ip_info: Option<IpInfo>,
} }
impl NetworkInterfaceInfo { impl NetworkInterfaceInfo {
pub fn public(&self) -> bool { pub fn inbound(&self) -> bool {
self.public.unwrap_or_else(|| { self.inbound.unwrap_or_else(|| {
!self.ip_info.as_ref().map_or(true, |ip_info| { !self.ip_info.as_ref().map_or(true, |ip_info| {
let ip4s = ip_info let ip4s = ip_info
.subnets .subnets
@@ -224,6 +238,8 @@ impl NetworkInterfaceInfo {
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct IpInfo { pub struct IpInfo {
#[ts(type = "string")]
pub name: InternedString,
pub scope_id: u32, pub scope_id: u32,
pub device_type: Option<NetworkInterfaceType>, pub device_type: Option<NetworkInterfaceType>,
#[ts(type = "string[]")] #[ts(type = "string[]")]
@@ -276,6 +292,7 @@ pub struct ServerStatus {
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)] #[ts(export)]
pub struct WifiInfo { pub struct WifiInfo {
pub enabled: bool,
pub interface: Option<String>, pub interface: Option<String>,
pub ssids: BTreeSet<String>, pub ssids: BTreeSet<String>,
pub selected: Option<String>, pub selected: Option<String>,

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::str::FromStr; use std::str::FromStr;
@@ -267,7 +267,7 @@ where
T::Key: FromStr + Ord + Clone, T::Key: FromStr + Ord + Clone,
Error: From<<T::Key as FromStr>::Err>, Error: From<<T::Key as FromStr>::Err>,
{ {
pub fn keys(&self) -> Result<Vec<T::Key>, Error> { pub fn keys(&self) -> Result<BTreeSet<T::Key>, Error> {
use serde::de::Error; use serde::de::Error;
match &self.value { match &self.value {
Value::Object(o) => o Value::Object(o) => o

View File

@@ -415,7 +415,11 @@ pub async fn init(
let wifi_interface = find_wifi_iface().await?; let wifi_interface = find_wifi_iface().await?;
let wifi = db let wifi = db
.mutate(|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.as_interface_mut().ser(&wifi_interface)?;
wifi.de() wifi.de()
}) })

View File

@@ -89,7 +89,7 @@ use crate::context::{
use crate::disk::fsck::RequiresReboot; use crate::disk::fsck::RequiresReboot;
use crate::net::net; use crate::net::net;
use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::util::serde::HandlerExtSerde; use crate::util::serde::{HandlerExtSerde, WithIoFormat};
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -242,10 +242,18 @@ pub fn server<C: Context>() -> ParentHandler<C> {
) )
.subcommand( .subcommand(
"metrics", "metrics",
from_fn_async(system::metrics) ParentHandler::<C, WithIoFormat<Empty>>::new()
.with_display_serializable() .root_handler(
.with_about("Display information about the server i.e. temperature, RAM, CPU, and disk usage") from_fn_async(system::metrics)
.with_call_remote::<CliContext>() .with_display_serializable()
.with_about("Display information about the server i.e. temperature, RAM, CPU, and disk usage")
.with_call_remote::<CliContext>()
)
.subcommand(
"follow",
from_fn_async(system::metrics_follow)
.no_cli()
)
) )
.subcommand( .subcommand(
"shutdown", "shutdown",

View File

@@ -255,7 +255,7 @@ pub async fn init(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
db.as_public_mut() db.as_public_mut()
.as_server_info_mut() .as_server_info_mut().as_network_mut()
.as_acme_mut() .as_acme_mut()
.insert(&provider, &AcmeSettings { contact }) .insert(&provider, &AcmeSettings { contact })
}) })
@@ -276,7 +276,7 @@ pub async fn remove(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
db.as_public_mut() db.as_public_mut()
.as_server_info_mut() .as_server_info_mut().as_network_mut()
.as_acme_mut() .as_acme_mut()
.remove(&provider) .remove(&provider)
}) })

View File

@@ -155,7 +155,7 @@ impl LanPortForwardController {
let mut interfaces = ip_info.peek_and_mark_seen(|ip_info| { let mut interfaces = ip_info.peek_and_mark_seen(|ip_info| {
ip_info ip_info
.iter() .iter()
.map(|(iface, info)| (iface.clone(), info.public())) .map(|(iface, info)| (iface.clone(), info.inbound()))
.collect() .collect()
}); });
let mut reply: Option<oneshot::Sender<Result<(), Error>>> = None; let mut reply: Option<oneshot::Sender<Result<(), Error>>> = None;
@@ -175,7 +175,7 @@ impl LanPortForwardController {
interfaces = ip_info.peek(|ip_info| { interfaces = ip_info.peek(|ip_info| {
ip_info ip_info
.iter() .iter()
.map(|(iface, info)| (iface.clone(), info.public())) .map(|(iface, info)| (iface.clone(), info.inbound()))
.collect() .collect()
}); });
} }

View File

@@ -186,7 +186,7 @@ pub async fn add_domain<Kind: HostApiKind>(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
if let Some(acme) = &acme { 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)); return Err(Error::new(eyre!("unknown acme provider {}, please run acme.init for this provider first", acme.0), ErrorKind::InvalidRequest));
} }
} }

View File

@@ -85,7 +85,11 @@ pub fn host_for<'a>(
host_id: &HostId, host_id: &HostId,
) -> Result<&'a mut Model<Host>, Error> { ) -> Result<&'a mut Model<Host>, Error> {
let Some(package_id) = package_id else { 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>( fn host_info<'a>(
db: &'a mut DatabaseModel, db: &'a mut DatabaseModel,
@@ -122,7 +126,7 @@ pub fn host_for<'a>(
} }
pub fn all_hosts(db: &DatabaseModel) -> impl Iterator<Item = Result<&Model<Host>, Error>> { pub fn all_hosts(db: &DatabaseModel) -> impl Iterator<Item = Result<&Model<Host>, Error>> {
[Ok(db.as_public().as_server_info().as_host())] [Ok(db.as_public().as_server_info().as_network().as_host())]
.into_iter() .into_iter()
.chain( .chain(
[db.as_public().as_package_data().as_entries()] [db.as_public().as_package_data().as_entries()]
@@ -255,7 +259,7 @@ pub async fn list_hosts(
ctx: RpcContext, ctx: RpcContext,
_: Empty, _: Empty,
package: PackageId, package: PackageId,
) -> Result<Vec<HostId>, Error> { ) -> Result<BTreeSet<HostId>, Error> {
ctx.db ctx.db
.peek() .peek()
.await .await

View File

@@ -188,7 +188,11 @@ impl NetServiceData {
let host = ctrl let host = ctrl
.db .db
.mutate(|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| { host.as_bindings_mut().mutate(|b| {
for (internal_port, info) in b { for (internal_port, info) in b {
if !except.contains(&BindId { if !except.contains(&BindId {
@@ -326,7 +330,7 @@ impl NetServiceData {
for (interface, public, ip_info) in for (interface, public, ip_info) in
net_ifaces.iter().filter_map(|(interface, info)| { net_ifaces.iter().filter_map(|(interface, info)| {
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
Some((interface, info.public(), ip_info)) Some((interface, info.inbound(), ip_info))
} else { } else {
None None
} }
@@ -612,6 +616,7 @@ impl NetServiceData {
.await .await
.as_public() .as_public()
.as_server_info() .as_server_info()
.as_network()
.as_host() .as_host()
.de()?, .de()?,
) )

View File

@@ -58,7 +58,7 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
info.ip_info.as_ref() info.ip_info.as_ref()
.and_then(|ip_info| ip_info.device_type) .and_then(|ip_info| ip_info.device_type)
.map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")), .map_or_else(|| "UNKNOWN".to_owned(), |ty| format!("{ty:?}")),
info.public(), info.inbound(),
info.ip_info.as_ref().map_or_else( info.ip_info.as_ref().map_or_else(
|| "<DISCONNECTED>".to_owned(), || "<DISCONNECTED>".to_owned(),
|ip_info| ip_info.subnets |ip_info| ip_info.subnets
@@ -86,18 +86,18 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
"set-public", "set-inbound",
from_fn_async(set_public) from_fn_async(set_inbound)
.with_metadata("sync_db", Value::Bool(true)) .with_metadata("sync_db", Value::Bool(true))
.no_display() .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::<CliContext>(), .with_call_remote::<CliContext>(),
).subcommand( ).subcommand(
"unset-public", "unset-inbound",
from_fn_async(unset_public) from_fn_async(unset_inbound)
.with_metadata("sync_db", Value::Bool(true)) .with_metadata("sync_db", Value::Bool(true))
.no_display() .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::<CliContext>(), .with_call_remote::<CliContext>(),
).subcommand("forget", ).subcommand("forget",
from_fn_async(forget_iface) from_fn_async(forget_iface)
@@ -116,36 +116,36 @@ async fn list_interfaces(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
struct NetworkInterfaceSetPublicParams { struct NetworkInterfaceSetInboundParams {
#[ts(type = "string")] #[ts(type = "string")]
interface: InternedString, interface: InternedString,
public: Option<bool>, inbound: Option<bool>,
} }
async fn set_public( async fn set_inbound(
ctx: RpcContext, ctx: RpcContext,
NetworkInterfaceSetPublicParams { interface, public }: NetworkInterfaceSetPublicParams, NetworkInterfaceSetInboundParams { interface, inbound }: NetworkInterfaceSetInboundParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.net_controller ctx.net_controller
.net_iface .net_iface
.set_public(&interface, Some(public.unwrap_or(true))) .set_inbound(&interface, Some(inbound.unwrap_or(true)))
.await .await
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)] #[ts(export)]
struct UnsetPublicParams { struct UnsetInboundParams {
#[ts(type = "string")] #[ts(type = "string")]
interface: InternedString, interface: InternedString,
} }
async fn unset_public( async fn unset_inbound(
ctx: RpcContext, ctx: RpcContext,
UnsetPublicParams { interface }: UnsetPublicParams, UnsetInboundParams { interface }: UnsetInboundParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.net_controller ctx.net_controller
.net_iface .net_iface
.set_public(&interface, None) .set_inbound(&interface, None)
.await .await
} }
@@ -193,6 +193,9 @@ mod active_connection {
default_service = "org.freedesktop.NetworkManager" default_service = "org.freedesktop.NetworkManager"
)] )]
pub trait ActiveConnection { pub trait ActiveConnection {
#[zbus(property)]
fn id(&self) -> Result<String, Error>;
#[zbus(property)] #[zbus(property)]
fn state_flags(&self) -> Result<u32, Error>; fn state_flags(&self) -> Result<u32, Error>;
@@ -511,6 +514,8 @@ async fn watch_ip(
_ => None, _ => None,
}; };
let name = InternedString::from(active_connection_proxy.id().await?);
let dhcp4_config = active_connection_proxy.dhcp4_config().await?; let dhcp4_config = active_connection_proxy.dhcp4_config().await?;
let ip4_proxy = let ip4_proxy =
Ip4ConfigProxy::new(&connection, ip4_config.clone()).await?; Ip4ConfigProxy::new(&connection, ip4_config.clone()).await?;
@@ -568,6 +573,7 @@ async fn watch_ip(
} }
}; };
Some(IpInfo { Some(IpInfo {
name: name.clone(),
scope_id, scope_id,
device_type, device_type,
subnets, subnets,
@@ -579,11 +585,14 @@ async fn watch_ip(
}; };
write_to.send_if_modified(|m| { 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( m.insert(
iface.clone(), iface.clone(),
NetworkInterfaceInfo { NetworkInterfaceInfo {
public, inbound,
outbound,
ip_info: ip_info.clone(), ip_info: ip_info.clone(),
}, },
) )
@@ -663,6 +672,7 @@ impl NetworkInterfaceController {
db.mutate(|db| { db.mutate(|db| {
db.as_public_mut() db.as_public_mut()
.as_server_info_mut() .as_server_info_mut()
.as_network_mut()
.as_network_interfaces_mut() .as_network_interfaces_mut()
.ser(info) .ser(info)
}) })
@@ -731,6 +741,7 @@ impl NetworkInterfaceController {
.await .await
.as_public() .as_public()
.as_server_info() .as_server_info()
.as_network()
.as_network_interfaces() .as_network_interfaces()
.de() .de()
{ {
@@ -820,7 +831,7 @@ impl NetworkInterfaceController {
Ok(listener) Ok(listener)
} }
pub async fn set_public( pub async fn set_inbound(
&self, &self,
interface: &InternedString, interface: &InternedString,
public: Option<bool>, public: Option<bool>,
@@ -828,7 +839,7 @@ impl NetworkInterfaceController {
let mut sub = self let mut sub = self
.db .db
.subscribe( .subscribe(
"/public/serverInfo/networkInterfaces" "/public/serverInfo/network/networkInterfaces"
.parse::<JsonPointer<_, _>>() .parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?, .with_kind(ErrorKind::Database)?,
) )
@@ -843,7 +854,7 @@ impl NetworkInterfaceController {
return false; return false;
} }
} }
.public, .inbound,
public, public,
); );
prev != public prev != public
@@ -861,7 +872,7 @@ impl NetworkInterfaceController {
let mut sub = self let mut sub = self
.db .db
.subscribe( .subscribe(
"/public/serverInfo/networkInterfaces" "/public/serverInfo/network/networkInterfaces"
.parse::<JsonPointer<_, _>>() .parse::<JsonPointer<_, _>>()
.with_kind(ErrorKind::Database)?, .with_kind(ErrorKind::Database)?,
) )
@@ -955,8 +966,10 @@ impl ListenerMap {
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut keep = BTreeSet::<SocketAddr>::new(); let mut keep = BTreeSet::<SocketAddr>::new();
for info in ip_info.values().chain([&NetworkInterfaceInfo { for info in ip_info.values().chain([&NetworkInterfaceInfo {
public: Some(false), inbound: Some(false),
outbound: Some(false),
ip_info: Some(IpInfo { ip_info: Some(IpInfo {
name: "lo".into(),
scope_id: 1, scope_id: 1,
device_type: None, device_type: None,
subnets: [ subnets: [
@@ -969,7 +982,7 @@ impl ListenerMap {
ntp_servers: Default::default(), ntp_servers: Default::default(),
}), }),
}]) { }]) {
if public || !info.public() { if public || !info.inbound() {
if let Some(ip_info) = &info.ip_info { if let Some(ip_info) = &info.ip_info {
for ipnet in &ip_info.subnets { for ipnet in &ip_info.subnets {
let addr = match ipnet.addr() { let addr = match ipnet.addr() {
@@ -988,7 +1001,7 @@ impl ListenerMap {
}; };
keep.insert(addr); keep.insert(addr);
if let Some((_, is_public, wan_ip)) = self.listeners.get_mut(&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); *wan_ip = info.ip_info.as_ref().and_then(|i| i.wan_ip);
continue; continue;
} }
@@ -1006,7 +1019,7 @@ impl ListenerMap {
.into(), .into(),
) )
.with_kind(ErrorKind::Network)?, .with_kind(ErrorKind::Network)?,
info.public(), info.inbound(),
info.ip_info.as_ref().and_then(|i| i.wan_ip), info.ip_info.as_ref().and_then(|i| i.wan_ip),
), ),
); );

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
@@ -179,7 +179,7 @@ pub async fn add_key(
Ok(key.public().get_onion_address()) Ok(key.public().get_onion_address())
} }
pub async fn list_keys(ctx: RpcContext) -> Result<Vec<OnionAddressV3>, Error> { pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddressV3>, Error> {
ctx.db ctx.db
.peek() .peek()
.await .await

View File

@@ -461,6 +461,7 @@ impl VHostServer {
target.acme.as_ref().and_then(|a| { target.acme.as_ref().and_then(|a| {
peek.as_public() peek.as_public()
.as_server_info() .as_server_info()
.as_network()
.as_acme() .as_acme()
.as_idx(a) .as_idx(a)
.map(|s| (domain, a, s)) .map(|s| (domain, a, s))

View File

@@ -24,21 +24,28 @@ use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::Invoke; use crate::util::Invoke;
use crate::{Error, ErrorKind}; use crate::{Error, ErrorKind};
type WifiManager = Arc<RwLock<WpaCli>>; type WifiManager = Arc<RwLock<Option<WpaCli>>>;
pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> { // pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> {
if let Some(wifi_manager) = ctx.wifi_manager.as_ref() { // if let Some(wifi_manager) = ctx.wifi_manager.as_ref() {
Ok(wifi_manager) // Ok(wifi_manager)
} else { // } else {
Err(Error::new( // Err(Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"), // color_eyre::eyre::eyre!("No WiFi interface available"),
ErrorKind::Wifi, // ErrorKind::Wifi,
)) // ))
} // }
} // }
pub fn wifi<C: Context>() -> ParentHandler<C> { pub fn wifi<C: Context>() -> ParentHandler<C> {
ParentHandler::new() ParentHandler::new()
.subcommand(
"set-enabled",
from_fn_async(set_enabled)
.no_display()
.with_about("Enable or disable wifi")
.with_call_remote::<CliContext>(),
)
.subcommand( .subcommand(
"add", "add",
from_fn_async(add) from_fn_async(add)
@@ -80,6 +87,48 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
) )
} }
#[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<C: Context>() -> ParentHandler<C> { pub fn available<C: Context>() -> ParentHandler<C> {
ParentHandler::new().subcommand( ParentHandler::new().subcommand(
"get", "get",
@@ -110,7 +159,7 @@ pub struct AddParams {
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Result<(), Error> { 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() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
color_eyre::eyre::eyre!("SSID may not have special characters"), 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, password: &Psk,
) -> Result<(), Error> { ) -> Result<(), Error> {
tracing::info!("Adding new WiFi network: '{}'", ssid.0); 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?; wpa_supplicant.add_network(db, ssid, password).await?;
drop(wpa_supplicant);
Ok(()) Ok(())
} }
if let Err(err) = add_procedure( if let Err(err) = add_procedure(
@@ -154,6 +208,7 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re
.mutate(|db| { .mutate(|db| {
db.as_public_mut() db.as_public_mut()
.as_server_info_mut() .as_server_info_mut()
.as_network_mut()
.as_wifi_mut() .as_wifi_mut()
.as_ssids_mut() .as_ssids_mut()
.mutate(|s| { .mutate(|s| {
@@ -173,7 +228,7 @@ pub struct SsidParams {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { 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() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
color_eyre::eyre::eyre!("SSID may not have special characters"), 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, wifi_manager: WifiManager,
ssid: &Ssid, ssid: &Ssid,
) -> Result<(), Error> { ) -> 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?; 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?; let connected = wpa_supplicant.select_network(db.clone(), ssid).await?;
if connected { if connected {
tracing::info!("Successfully connected to WiFi: '{}'", ssid.0); tracing::info!("Successfully connected to WiFi: '{}'", ssid.0);
@@ -218,7 +277,11 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result
ctx.db ctx.db
.mutate(|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| { wifi.as_ssids_mut().mutate(|s| {
s.insert(ssid.clone()); s.insert(ssid.clone());
Ok(()) Ok(())
@@ -231,17 +294,22 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { 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() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
color_eyre::eyre::eyre!("SSID may not have special characters"), color_eyre::eyre::eyre!("SSID may not have special characters"),
ErrorKind::Wifi, 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?; let current = wpa_supplicant.get_current_network().await?;
drop(wpa_supplicant);
let mut wpa_supplicant = wifi_manager.write().await;
let ssid = Ssid(ssid); let ssid = Ssid(ssid);
let is_current_being_removed = matches!(current, Some(current) if current == ssid); let is_current_being_removed = matches!(current, Some(current) if current == ssid);
let is_current_removed_and_no_hardwire = let is_current_removed_and_no_hardwire =
@@ -254,7 +322,11 @@ pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<
ctx.db ctx.db
.mutate(|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| { wifi.as_ssids_mut().mutate(|s| {
s.remove(&ssid.0); s.remove(&ssid.0);
Ok(()) Ok(())
@@ -379,8 +451,14 @@ fn display_wifi_list(params: WithIoFormat<Empty>, info: Vec<WifiListOut>) {
// #[command(display(display_wifi_info))] // #[command(display(display_wifi_info))]
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn get(ctx: RpcContext, _: Empty) -> Result<WifiListInfo, Error> { pub async fn get(ctx: RpcContext, _: Empty) -> Result<WifiListInfo, Error> {
let wifi_manager = wifi_manager(&ctx)?; let wifi_manager = ctx.wifi_manager.clone();
let wpa_supplicant = wifi_manager.read().await; 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!( let (list_networks, current_res, country_res, ethernet_res, signal_strengths) = tokio::join!(
wpa_supplicant.list_networks_low(), wpa_supplicant.list_networks_low(),
wpa_supplicant.get_current_network(), wpa_supplicant.get_current_network(),
@@ -427,8 +505,14 @@ pub async fn get(ctx: RpcContext, _: Empty) -> Result<WifiListInfo, Error> {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn get_available(ctx: RpcContext, _: Empty) -> Result<Vec<WifiListOut>, Error> { pub async fn get_available(ctx: RpcContext, _: Empty) -> Result<Vec<WifiListOut>, Error> {
let wifi_manager = wifi_manager(&ctx)?; let wifi_manager = ctx.wifi_manager.clone();
let wpa_supplicant = wifi_manager.read().await; 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!( let (wifi_list, network_list) = tokio::join!(
wpa_supplicant.list_wifi_low(), wpa_supplicant.list_wifi_low(),
wpa_supplicant.list_networks_low() wpa_supplicant.list_networks_low()
@@ -463,14 +547,20 @@ pub async fn set_country(
ctx: RpcContext, ctx: RpcContext,
SetCountryParams { country }: SetCountryParams, SetCountryParams { country }: SetCountryParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
let wifi_manager = wifi_manager(&ctx)?; let wifi_manager = ctx.wifi_manager.clone();
if !interface_connected(&ctx.ethernet_interface).await? { if !interface_connected(&ctx.ethernet_interface).await? {
return Err(Error::new( return Err(Error::new(
color_eyre::eyre::eyre!("Won't change country without hardwire connection"), color_eyre::eyre::eyre!("Won't change country without hardwire connection"),
crate::ErrorKind::Wifi, 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?; wpa_supplicant.set_country_low(country.alpha2()).await?;
for (network_id, _wifi_info) in wpa_supplicant.list_networks_low().await? { for (network_id, _wifi_info) in wpa_supplicant.list_networks_low().await? {
wpa_supplicant.remove_network_low(network_id).await?; wpa_supplicant.remove_network_low(network_id).await?;
@@ -734,6 +824,7 @@ impl WpaCli {
db.mutate(|d| { db.mutate(|d| {
d.as_public_mut() d.as_public_mut()
.as_server_info_mut() .as_server_info_mut()
.as_network_mut()
.as_wifi_mut() .as_wifi_mut()
.as_last_region_mut() .as_last_region_mut()
.ser(&new_country) .ser(&new_country)
@@ -909,13 +1000,21 @@ pub async fn synchronize_network_manager<P: AsRef<Path>>(
crate::disk::mount::util::bind(&persistent, "/etc/NetworkManager/system-connections", false) crate::disk::mount::util::bind(&persistent, "/etc/NetworkManager/system-connections", false)
.await?; .await?;
if !wifi.enabled {
Command::new("rfkill")
.arg("block")
.arg("all")
.invoke(ErrorKind::Wifi)
.await?;
}
Command::new("systemctl") Command::new("systemctl")
.arg("restart") .arg("restart")
.arg("NetworkManager") .arg("NetworkManager")
.invoke(ErrorKind::Wifi) .invoke(ErrorKind::Wifi)
.await?; .await?;
let Some(wifi_iface) = &wifi.interface else { let Some(wifi_iface) = wifi.interface.as_ref().filter(|_| wifi.enabled) else {
return Ok(()); return Ok(());
}; };

View File

@@ -6,6 +6,7 @@ use chrono::{DateTime, Utc};
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use helpers::const_true;
use imbl_value::InternedString; use imbl_value::InternedString;
use models::{FromStrParser, PackageId}; use models::{FromStrParser, PackageId};
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
@@ -15,7 +16,7 @@ use ts_rs::TS;
use crate::backup::BackupReport; use crate::backup::BackupReport;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::{Database, DatabaseModel}; use crate::db::model::DatabaseModel;
use crate::prelude::*; use crate::prelude::*;
use crate::util::serde::HandlerExtSerde; use crate::util::serde::HandlerExtSerde;
@@ -33,14 +34,35 @@ pub fn notification<C: Context>() -> ParentHandler<C> {
"remove", "remove",
from_fn_async(remove) from_fn_async(remove)
.no_display() .no_display()
.with_about("Delete notification for a given id") .with_about("Remove notification for given ids")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
"remove-before", "remove-before",
from_fn_async(remove_before) from_fn_async(remove_before)
.no_display() .no_display()
.with_about("Delete notifications preceding a given id") .with_about("Remove notifications preceding a given id")
.with_call_remote::<CliContext>(),
)
.subcommand(
"mark-seen",
from_fn_async(mark_seen)
.no_display()
.with_about("Mark given notifications as seen")
.with_call_remote::<CliContext>(),
)
.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::<CliContext>(),
)
.subcommand(
"mark-unseen",
from_fn_async(mark_unseen)
.no_display()
.with_about("Mark given notifications as unseen")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand( .subcommand(
@@ -55,7 +77,7 @@ pub fn notification<C: Context>() -> ParentHandler<C> {
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ListParams { pub struct ListNotificationParams {
#[ts(type = "number | null")] #[ts(type = "number | null")]
before: Option<u32>, before: Option<u32>,
#[ts(type = "number | null")] #[ts(type = "number | null")]
@@ -65,7 +87,7 @@ pub struct ListParams {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn list( pub async fn list(
ctx: RpcContext, ctx: RpcContext,
ListParams { before, limit }: ListParams, ListNotificationParams { before, limit }: ListNotificationParams,
) -> Result<Vec<NotificationWithId>, Error> { ) -> Result<Vec<NotificationWithId>, Error> {
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
@@ -121,38 +143,124 @@ pub async fn list(
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct DeleteParams { pub struct ModifyNotificationParams {
#[ts(type = "number")] #[ts(type = "number[]")]
id: u32, ids: Vec<u32>,
} }
pub async fn remove(ctx: RpcContext, DeleteParams { id }: DeleteParams) -> Result<(), Error> { pub async fn remove(
ctx: RpcContext,
ModifyNotificationParams { ids }: ModifyNotificationParams,
) -> Result<(), Error> {
ctx.db ctx.db
.mutate(|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(()) Ok(())
}) })
.await .await
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct DeleteBeforeParams { pub struct ModifyNotificationBeforeParams {
#[ts(type = "number")] #[ts(type = "number")]
before: u32, before: u32,
} }
pub async fn remove_before( pub async fn remove_before(
ctx: RpcContext, ctx: RpcContext,
DeleteBeforeParams { before }: DeleteBeforeParams, ModifyNotificationBeforeParams { before }: ModifyNotificationBeforeParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
for id in db.as_private().as_notifications().keys()? { let n = db.as_private_mut().as_notifications_mut();
if id < before { for id in n.keys()?.range(..before) {
db.as_private_mut().as_notifications_mut().remove(&id)?; 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(()) Ok(())
}) })
.await .await
@@ -253,8 +361,9 @@ impl Map for Notifications {
} }
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, HasModel)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct Notification { pub struct Notification {
pub package_id: Option<PackageId>, pub package_id: Option<PackageId>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
@@ -263,6 +372,8 @@ pub struct Notification {
pub title: String, pub title: String,
pub message: String, pub message: String,
pub data: Value, pub data: Value,
#[serde(default = "const_true")]
pub seen: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -323,6 +434,7 @@ pub fn notify<T: NotificationType>(
title, title,
message, message,
data, data,
seen: false,
}, },
) )
} }

View File

@@ -217,7 +217,7 @@ impl PackParams {
}) })
.map_err(Error::from) .map_err(Error::from)
.try_fold( .try_fold(
Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), Err(Error::new(eyre!("license not found"), ErrorKind::NotFound)),
|acc, x| async move { |acc, x| async move {
match acc { match acc {
Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)), Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)),

View File

@@ -88,7 +88,7 @@ pub async fn mount(
Ok(()) Ok(())
} }
pub async fn get_installed_packages(context: EffectContext) -> Result<Vec<PackageId>, Error> { pub async fn get_installed_packages(context: EffectContext) -> Result<BTreeSet<PackageId>, Error> {
context context
.deref()? .deref()?
.seed .seed

View File

@@ -1,19 +1,20 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use chrono::Utc; use chrono::Utc;
use clap::Parser; use clap::Parser;
use color_eyre::eyre::eyre; use color_eyre::eyre::eyre;
use futures::FutureExt; use futures::{FutureExt, TryStreamExt};
use imbl::vector; use imbl::vector;
use imbl_value::InternedString;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use rustls::RootCertStore; use rustls::RootCertStore;
use rustls_pki_types::CertificateDer; use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::broadcast::Receiver; use tokio::sync::broadcast::Receiver;
use tokio::sync::RwLock;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS; use ts_rs::TS;
@@ -21,11 +22,13 @@ use crate::context::{CliContext, RpcContext};
use crate::disk::util::{get_available, get_used}; use crate::disk::util::{get_available, get_used};
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT}; use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::RpcContinuations; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::shutdown::Shutdown; use crate::shutdown::Shutdown;
use crate::util::cpupower::{get_available_governors, set_governor, Governor}; use crate::util::cpupower::{get_available_governors, set_governor, Governor};
use crate::util::io::open_file; use crate::util::io::open_file;
use crate::util::net::WebSocketExt;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::sync::Watch;
use crate::util::Invoke; use crate::util::Invoke;
use crate::{MAIN_DATA, PACKAGE_DATA}; use crate::{MAIN_DATA, PACKAGE_DATA};
@@ -249,7 +252,7 @@ pub struct MetricLeaf<T> {
unit: Option<String>, unit: Option<String>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, TS)]
pub struct Celsius(f64); pub struct Celsius(f64);
impl fmt::Display for Celsius { impl fmt::Display for Celsius {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 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)?)) Ok(Celsius(s.value.parse().map_err(serde::de::Error::custom)?))
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, PartialOrd, TS)]
pub struct Percentage(f64); pub struct Percentage(f64);
impl Serialize for Percentage { impl Serialize for Percentage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -303,7 +306,7 @@ impl<'de> Deserialize<'de> for Percentage {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, TS)]
pub struct MebiBytes(pub f64); pub struct MebiBytes(pub f64);
impl Serialize for MebiBytes { impl Serialize for MebiBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -329,7 +332,7 @@ impl<'de> Deserialize<'de> for MebiBytes {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq, PartialOrd, TS)]
pub struct GigaBytes(f64); pub struct GigaBytes(f64);
impl Serialize for GigaBytes { impl Serialize for GigaBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -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")] #[serde(rename_all = "camelCase")]
pub struct MetricsGeneral { pub struct MetricsGeneral {
pub temperature: Option<Celsius>, pub temperature: Option<Celsius>,
} }
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MetricsMemory { pub struct MetricsMemory {
pub percentage_used: Percentage, pub percentage_used: Percentage,
@@ -371,7 +375,8 @@ pub struct MetricsMemory {
pub zram_available: MebiBytes, pub zram_available: MebiBytes,
pub zram_used: MebiBytes, pub zram_used: MebiBytes,
} }
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MetricsCpu { pub struct MetricsCpu {
percentage_used: Percentage, percentage_used: Percentage,
@@ -380,7 +385,8 @@ pub struct MetricsCpu {
kernel_space: Percentage, kernel_space: Percentage,
wait: Percentage, wait: Percentage,
} }
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MetricsDisk { pub struct MetricsDisk {
percentage_used: Percentage, percentage_used: Percentage,
@@ -388,8 +394,10 @@ pub struct MetricsDisk {
available: GigaBytes, available: GigaBytes,
capacity: GigaBytes, capacity: GigaBytes,
} }
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Metrics { pub struct Metrics {
general: MetricsGeneral, general: MetricsGeneral,
memory: MetricsMemory, memory: MetricsMemory,
@@ -398,19 +406,74 @@ pub struct Metrics {
} }
// #[command(display(display_serializable))] // #[command(display(display_serializable))]
pub async fn metrics(ctx: RpcContext, _: Empty) -> Result<Metrics, Error> { pub async fn metrics(ctx: RpcContext) -> Result<Metrics, Error> {
match ctx.metrics_cache.read().await.clone() { ctx.metrics_cache.read().or_not_found("No Metrics Found")
None => Err(Error { }
source: color_eyre::eyre::eyre!("No Metrics Found"),
kind: ErrorKind::NotFound, #[derive(Deserialize, Serialize, Clone, Debug, TS)]
revision: None, #[serde(rename_all = "camelCase")]
}), pub struct MetricsFollowResponse {
Some(metrics_val) => Ok(metrics_val), 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<InternedString>,
}
pub async fn metrics_follow(
ctx: RpcContext,
MetricsFollowParams { session }: MetricsFollowParams,
) -> Result<MetricsFollowResponse, Error> {
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<F: FnMut() -> Receiver<Option<Shutdown>>>( pub async fn launch_metrics_task<F: FnMut() -> Receiver<Option<Shutdown>>>(
cache: &RwLock<Option<Metrics>>, cache: &Watch<Option<Metrics>>,
mut mk_shutdown: F, mut mk_shutdown: F,
) { ) {
// fetch init temp // fetch init temp
@@ -475,31 +538,21 @@ pub async fn launch_metrics_task<F: FnMut() -> Receiver<Option<Shutdown>>>(
} }
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
} }
{
// lock for writing let should_launch_temp_task = init_temp.is_some();
let mut guard = cache.write().await;
// write cache.send(Some(Metrics {
*guard = Some(Metrics { general: MetricsGeneral {
general: MetricsGeneral { temperature: init_temp,
temperature: init_temp, },
}, memory: init_mem,
memory: init_mem, cpu: init_cpu,
cpu: init_cpu, disk: init_disk,
disk: init_disk, }));
})
}
let mut task_vec = Vec::new(); let mut task_vec = Vec::new();
// launch persistent temp task // launch persistent temp task
if cache if should_launch_temp_task {
.read()
.await
.as_ref()
.unwrap()
.general
.temperature
.is_some()
{
task_vec.push(launch_temp_task(cache, mk_shutdown()).boxed()); task_vec.push(launch_temp_task(cache, mk_shutdown()).boxed());
} }
// launch persistent cpu task // launch persistent cpu task
@@ -513,14 +566,15 @@ pub async fn launch_metrics_task<F: FnMut() -> Receiver<Option<Shutdown>>>(
} }
async fn launch_temp_task( async fn launch_temp_task(
cache: &RwLock<Option<Metrics>>, cache: &Watch<Option<Metrics>>,
mut shutdown: Receiver<Option<Shutdown>>, mut shutdown: Receiver<Option<Shutdown>>,
) { ) {
loop { loop {
match get_temp().await { match get_temp().await {
Ok(a) => { Ok(a) => {
let mut lock = cache.write().await; cache.send_if_modified(|c| {
(*lock).as_mut().unwrap().general.temperature = Some(a) c.as_mut().unwrap().general.temperature.replace(a) != Some(a)
});
} }
Err(e) => { Err(e) => {
tracing::error!("Could not get new temperature: {}", e); tracing::error!("Could not get new temperature: {}", e);
@@ -535,7 +589,7 @@ async fn launch_temp_task(
} }
async fn launch_cpu_task( async fn launch_cpu_task(
cache: &RwLock<Option<Metrics>>, cache: &Watch<Option<Metrics>>,
mut init: ProcStat, mut init: ProcStat,
mut shutdown: Receiver<Option<Shutdown>>, mut shutdown: Receiver<Option<Shutdown>>,
) { ) {
@@ -543,8 +597,7 @@ async fn launch_cpu_task(
// read /proc/stat, diff against previous metrics, compute cpu load // read /proc/stat, diff against previous metrics, compute cpu load
match get_cpu_info(&mut init).await { match get_cpu_info(&mut init).await {
Ok(info) => { Ok(info) => {
let mut lock = cache.write().await; cache.send_modify(|c| c.as_mut().unwrap().cpu = info);
(*lock).as_mut().unwrap().cpu = info;
} }
Err(e) => { Err(e) => {
tracing::error!("Could not get new CPU Metrics: {}", e); tracing::error!("Could not get new CPU Metrics: {}", e);
@@ -558,16 +611,12 @@ async fn launch_cpu_task(
} }
} }
async fn launch_mem_task( async fn launch_mem_task(cache: &Watch<Option<Metrics>>, mut shutdown: Receiver<Option<Shutdown>>) {
cache: &RwLock<Option<Metrics>>,
mut shutdown: Receiver<Option<Shutdown>>,
) {
loop { loop {
// read /proc/meminfo // read /proc/meminfo
match get_mem_info().await { match get_mem_info().await {
Ok(a) => { Ok(a) => {
let mut lock = cache.write().await; cache.send_modify(|c| c.as_mut().unwrap().memory = a);
(*lock).as_mut().unwrap().memory = a;
} }
Err(e) => { Err(e) => {
tracing::error!("Could not get new Memory Metrics: {}", e); tracing::error!("Could not get new Memory Metrics: {}", e);
@@ -581,15 +630,22 @@ async fn launch_mem_task(
} }
} }
async fn launch_disk_task( async fn launch_disk_task(
cache: &RwLock<Option<Metrics>>, cache: &Watch<Option<Metrics>>,
mut shutdown: Receiver<Option<Shutdown>>, mut shutdown: Receiver<Option<Shutdown>>,
) { ) {
loop { loop {
// run df and capture output // run df and capture output
match get_disk_info().await { match get_disk_info().await {
Ok(a) => { Ok(a) => {
let mut lock = cache.write().await; cache.send_if_modified(|c| {
(*lock).as_mut().unwrap().disk = a; let c = c.as_mut().unwrap();
if c.disk != a {
c.disk = a;
true
} else {
false
}
});
} }
Err(e) => { Err(e) => {
tracing::error!("Could not get new Disk Metrics: {}", e); tracing::error!("Could not get new Disk Metrics: {}", e);

View File

@@ -36,7 +36,9 @@ mod v0_3_6_alpha_13;
mod v0_3_6_alpha_14; mod v0_3_6_alpha_14;
mod v0_3_6_alpha_15; 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 { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -133,7 +135,8 @@ enum Version {
V0_3_6_alpha_12(Wrapper<v0_3_6_alpha_12::Version>), V0_3_6_alpha_12(Wrapper<v0_3_6_alpha_12::Version>),
V0_3_6_alpha_13(Wrapper<v0_3_6_alpha_13::Version>), V0_3_6_alpha_13(Wrapper<v0_3_6_alpha_13::Version>),
V0_3_6_alpha_14(Wrapper<v0_3_6_alpha_14::Version>), V0_3_6_alpha_14(Wrapper<v0_3_6_alpha_14::Version>),
V0_3_6_alpha_15(Wrapper<v0_3_6_alpha_15::Version>), // VERSION_BUMP V0_3_6_alpha_15(Wrapper<v0_3_6_alpha_15::Version>),
V0_4_0_alpha_0(Wrapper<v0_4_0_alpha_0::Version>), // VERSION_BUMP
Other(exver::Version), 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_12(v) => DynVersion(Box::new(v.0)),
Self::V0_3_6_alpha_13(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_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) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), 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_12(Wrapper(x)) => x.semver(),
Version::V0_3_6_alpha_13(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_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(), Version::Other(x) => x.clone(),
} }
} }

View File

@@ -6,7 +6,6 @@ use const_format::formatcp;
use ed25519_dalek::SigningKey; use ed25519_dalek::SigningKey;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::{json, InternedString}; use imbl_value::{json, InternedString};
use models::PackageId;
use openssl::pkey::PKey; use openssl::pkey::PKey;
use openssl::x509::X509; use openssl::x509::X509;
use sqlx::postgres::PgConnectOptions; use sqlx::postgres::PgConnectOptions;
@@ -25,12 +24,12 @@ use crate::disk::mount::util::unmount;
use crate::hostname::Hostname; use crate::hostname::Hostname;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::keys::KeyStore; use crate::net::keys::KeyStore;
use crate::notifications::{Notification, Notifications}; use crate::notifications::Notifications;
use crate::prelude::*; use crate::prelude::*;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::ssh::{SshKeys, SshPubKey}; use crate::ssh::{SshKeys, SshPubKey};
use crate::util::crypto::ed25519_expand_key; use crate::util::crypto::ed25519_expand_key;
use crate::util::serde::{Pem, PemEncoding}; use crate::util::serde::Pem;
use crate::util::Invoke; use crate::util::Invoke;
use crate::{DATA_DIR, PACKAGE_DATA}; use crate::{DATA_DIR, PACKAGE_DATA};

View File

@@ -1,7 +1,4 @@
use std::collections::BTreeMap;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_3_6_alpha_12, VersionT}; use super::{v0_3_6_alpha_12, VersionT};
@@ -30,7 +27,7 @@ impl VersionT for Version {
fn compat(self) -> &'static VersionRange { fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT &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(()) Ok(())
} }
fn down(self, _db: &mut Value) -> Result<(), Error> { fn down(self, _db: &mut Value) -> Result<(), Error> {

View File

@@ -1,7 +1,4 @@
use std::collections::BTreeMap;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_3_6_alpha_13, VersionT}; use super::{v0_3_6_alpha_13, VersionT};
@@ -30,7 +27,7 @@ impl VersionT for Version {
fn compat(self) -> &'static VersionRange { fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT &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(()) Ok(())
} }
fn down(self, _db: &mut Value) -> Result<(), Error> { fn down(self, _db: &mut Value) -> Result<(), Error> {

View File

@@ -1,7 +1,4 @@
use std::collections::BTreeMap;
use exver::{PreReleaseSegment, VersionRange}; use exver::{PreReleaseSegment, VersionRange};
use imbl_value::json;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_3_6_alpha_14, VersionT}; use super::{v0_3_6_alpha_14, VersionT};
@@ -30,7 +27,7 @@ impl VersionT for Version {
fn compat(self) -> &'static VersionRange { fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT &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(()) Ok(())
} }
fn down(self, _db: &mut Value) -> Result<(), Error> { fn down(self, _db: &mut Value) -> Result<(), Error> {

View File

@@ -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(())
}
}

View File

@@ -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<Self::PreUpRes, Error> {
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(())
}
}

View File

@@ -1,3 +1,3 @@
// 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.
export type UnsetPublicParams = { interface: string } export type Celsius = number

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 GigaBytes = number

View File

@@ -2,6 +2,7 @@
import type { NetworkInterfaceType } from "./NetworkInterfaceType" import type { NetworkInterfaceType } from "./NetworkInterfaceType"
export type IpInfo = { export type IpInfo = {
name: string
scopeId: number scopeId: number
deviceType: NetworkInterfaceType | null deviceType: NetworkInterfaceType | null
subnets: string[] subnets: 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 MebiBytes = number

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 }

View File

@@ -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
}

View File

@@ -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 }
}

View File

@@ -2,6 +2,7 @@
import type { IpInfo } from "./IpInfo" import type { IpInfo } from "./IpInfo"
export type NetworkInterfaceInfo = { export type NetworkInterfaceInfo = {
public: boolean | null inbound: boolean | null
outbound: boolean | null
ipInfo: IpInfo | null ipInfo: IpInfo | null
} }

View File

@@ -1,6 +1,6 @@
// 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.
export type NetworkInterfaceSetPublicParams = { export type NetworkInterfaceSetInboundParams = {
interface: string interface: string
public: boolean | null inbound: boolean | null
} }

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 Percentage = number

View File

@@ -1,28 +1,21 @@
// 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 { AcmeProvider } from "./AcmeProvider"
import type { AcmeSettings } from "./AcmeSettings"
import type { Governor } from "./Governor" import type { Governor } from "./Governor"
import type { Host } from "./Host"
import type { LshwDevice } from "./LshwDevice" import type { LshwDevice } from "./LshwDevice"
import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" import type { NetworkInfo } from "./NetworkInfo"
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"
export type ServerInfo = { export type ServerInfo = {
arch: string arch: string
platform: string platform: string
id: string id: string
hostname: string hostname: string
host: Host
version: string version: string
packageVersionCompat: string packageVersionCompat: string
postInitMigrationTodos: string[] postInitMigrationTodos: string[]
lastBackup: string | null lastBackup: string | null
networkInterfaces: { [key: string]: NetworkInterfaceInfo } network: NetworkInfo
acme: { [key: AcmeProvider]: AcmeSettings }
statusInfo: ServerStatus statusInfo: ServerStatus
wifi: WifiInfo
unreadNotificationCount: number unreadNotificationCount: number
passwordHash: string passwordHash: string
pubkey: string pubkey: 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 UnsetInboundParams = { interface: 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.
export type WifiInfo = { export type WifiInfo = {
enabled: boolean
interface: string | null interface: string | null
ssids: Array<string> ssids: Array<string>
selected: string | null selected: string | null

View File

@@ -46,6 +46,7 @@ export { BlockDev } from "./BlockDev"
export { BuildArg } from "./BuildArg" export { BuildArg } from "./BuildArg"
export { CallbackId } from "./CallbackId" export { CallbackId } from "./CallbackId"
export { Category } from "./Category" export { Category } from "./Category"
export { Celsius } from "./Celsius"
export { CheckDependenciesParam } from "./CheckDependenciesParam" export { CheckDependenciesParam } from "./CheckDependenciesParam"
export { CheckDependenciesResult } from "./CheckDependenciesResult" export { CheckDependenciesResult } from "./CheckDependenciesResult"
export { Cifs } from "./Cifs" export { Cifs } from "./Cifs"
@@ -93,6 +94,7 @@ export { GetSslKeyParams } from "./GetSslKeyParams"
export { GetStatusParams } from "./GetStatusParams" export { GetStatusParams } from "./GetStatusParams"
export { GetStoreParams } from "./GetStoreParams" export { GetStoreParams } from "./GetStoreParams"
export { GetSystemSmtpParams } from "./GetSystemSmtpParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams"
export { GigaBytes } from "./GigaBytes"
export { GitHash } from "./GitHash" export { GitHash } from "./GitHash"
export { Governor } from "./Governor" export { Governor } from "./Governor"
export { Guid } from "./Guid" export { Guid } from "./Guid"
@@ -125,14 +127,21 @@ 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"
export { MebiBytes } from "./MebiBytes"
export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" 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 { MountParams } from "./MountParams"
export { MountTarget } from "./MountTarget" export { MountTarget } from "./MountTarget"
export { NamedHealthCheckResult } from "./NamedHealthCheckResult" export { NamedHealthCheckResult } from "./NamedHealthCheckResult"
export { NamedProgress } from "./NamedProgress" export { NamedProgress } from "./NamedProgress"
export { NetInfo } from "./NetInfo" export { NetInfo } from "./NetInfo"
export { NetworkInfo } from "./NetworkInfo"
export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo" export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicParams" export { NetworkInterfaceSetInboundParams } from "./NetworkInterfaceSetInboundParams"
export { NetworkInterfaceType } from "./NetworkInterfaceType" export { NetworkInterfaceType } from "./NetworkInterfaceType"
export { OnionHostname } from "./OnionHostname" export { OnionHostname } from "./OnionHostname"
export { OsIndex } from "./OsIndex" export { OsIndex } from "./OsIndex"
@@ -149,6 +158,7 @@ export { PackageState } from "./PackageState"
export { PackageVersionInfo } from "./PackageVersionInfo" export { PackageVersionInfo } from "./PackageVersionInfo"
export { PasswordType } from "./PasswordType" export { PasswordType } from "./PasswordType"
export { PathOrUrl } from "./PathOrUrl" export { PathOrUrl } from "./PathOrUrl"
export { Percentage } from "./Percentage"
export { ProcedureId } from "./ProcedureId" export { ProcedureId } from "./ProcedureId"
export { Progress } from "./Progress" export { Progress } from "./Progress"
export { Public } from "./Public" export { Public } from "./Public"
@@ -188,7 +198,7 @@ export { SignerInfo } from "./SignerInfo"
export { SmtpValue } from "./SmtpValue" export { SmtpValue } from "./SmtpValue"
export { StartStop } from "./StartStop" export { StartStop } from "./StartStop"
export { TestSmtpParams } from "./TestSmtpParams" export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetPublicParams } from "./UnsetPublicParams" export { UnsetInboundParams } from "./UnsetInboundParams"
export { UpdatingState } from "./UpdatingState" export { UpdatingState } from "./UpdatingState"
export { VerifyCifsParams } from "./VerifyCifsParams" export { VerifyCifsParams } from "./VerifyCifsParams"
export { VersionSignerParams } from "./VersionSignerParams" export { VersionSignerParams } from "./VersionSignerParams"

View File

@@ -73,7 +73,7 @@ import * as actions from "../../base/lib/actions"
import { setupInit } from "./inits/setupInit" import { setupInit } from "./inits/setupInit"
import * as fs from "node:fs/promises" 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 // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.3.6-alpha.15", "version": "0.4.0-alpha.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "startos-ui", "name": "startos-ui",
"version": "0.3.6-alpha.15", "version": "0.4.0-alpha.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular/animations": "^17.3.1", "@angular/animations": "^17.3.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.3.6-alpha.15", "version": "0.4.0-alpha.0",
"author": "Start9 Labs, Inc", "author": "Start9 Labs, Inc",
"homepage": "https://start9.com/", "homepage": "https://start9.com/",
"license": "MIT", "license": "MIT",

View File

@@ -96,7 +96,7 @@ import { map } from 'rxjs'
}) })
export class InterfaceComponent { export class InterfaceComponent {
readonly acme$ = inject<PatchDB<DataModel>>(PatchDB) readonly acme$ = inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'acme') .watch$('serverInfo', 'network', 'acme')
.pipe(map(acme => Object.keys(acme))) .pipe(map(acme => Object.keys(acme)))
@Input() packageId?: string @Input() packageId?: string

View File

@@ -19,7 +19,6 @@ export const REMOVE: Partial<TuiDialogOptions<TuiConfirmData>> = {
}, },
} }
// @TODO 040 Aiden audit
export function getAddresses( export function getAddresses(
serviceInterface: T.ServiceInterface, serviceInterface: T.ServiceInterface,
host: T.Host, host: T.Host,
@@ -81,11 +80,17 @@ export function getAddresses(
} else { } else {
const hostnameKind = h.hostname.kind const hostnameKind = h.hostname.kind
if (hostnameKind === 'domain') { if (h.public) {
clearnet.push({ clearnet.push({
label: 'Domain', label:
hostnameKind == 'domain'
? 'Domain'
: `${h.networkInterfaceId} (${hostnameKind})`,
url, 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 { } else {
local.push({ local.push({
@@ -101,15 +106,19 @@ export function getAddresses(
}) })
return { return {
clearnet, clearnet: clearnet.filter(
local, (value, index, self) =>
tor, 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 = { export type AddressDetails = {

View File

@@ -95,7 +95,7 @@ export default class NotificationsComponent {
this.notifications.set( this.notifications.set(
current.map(c => ({ current.map(c => ({
...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( this.notifications.set(
current.map(c => ({ current.map(c => ({
...c, ...c,
read: c.read && !toUpdate.some(n => n.id === c.id), read: c.seen && !toUpdate.some(n => n.id === c.id),
})), })),
) )

View File

@@ -40,7 +40,7 @@ import { NotificationItemComponent } from './item.component'
@for (notification of notifications; track $index) { @for (notification of notifications; track $index) {
<tr <tr
[notificationItem]="notification" [notificationItem]="notification"
[style.font-weight]="notification.read ? 'normal' : 'bold'" [style.font-weight]="notification.seen ? 'normal' : 'bold'"
> >
<input <input
tuiCheckbox tuiCheckbox

View File

@@ -28,7 +28,7 @@ export class SettingsACMEComponent {
readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme' readonly docsUrl = 'https://docs.start9.com/0.3.6/user-manual/acme'
acme$ = this.patch.watch$('serverInfo', 'acme').pipe( acme$ = this.patch.watch$('serverInfo', 'network', 'acme').pipe(
map(acme => { map(acme => {
const providerUrls = Object.keys(acme) const providerUrls = Object.keys(acme)
return providerUrls.map(url => { return providerUrls.map(url => {

View File

@@ -771,7 +771,7 @@ export namespace Mock {
}, },
}, },
}, },
read: false, seen: false,
}, },
{ {
id: 2, id: 2,
@@ -782,7 +782,7 @@ export namespace Mock {
title: 'SSH Key Added', title: 'SSH Key Added',
message: 'A new SSH key was added. If you did not do this, shit is bad.', message: 'A new SSH key was added. If you did not do this, shit is bad.',
data: null, data: null,
read: false, seen: false,
}, },
{ {
id: 3, id: 3,
@@ -793,7 +793,7 @@ export namespace Mock {
title: 'SSH Key Removed', title: 'SSH Key Removed',
message: 'A SSH key was removed.', message: 'A SSH key was removed.',
data: null, data: null,
read: false, seen: false,
}, },
{ {
id: 4, id: 4,
@@ -811,7 +811,7 @@ export namespace Mock {
) )
.join(''), .join(''),
data: null, data: null,
read: false, seen: false,
}, },
{ {
id: 5, id: 5,
@@ -822,7 +822,7 @@ export namespace Mock {
title: 'Welcome to StartOS 0.3.6!', title: 'Welcome to StartOS 0.3.6!',
message: 'Click "View Details" to learn all about the new version', message: 'Click "View Details" to learn all about the new version',
data: markdown, data: markdown,
read: false, seen: false,
}, },
] ]

View File

@@ -81,7 +81,6 @@ export namespace RR {
guid: string guid: string
} }
// @TODO 040 implement websocket
export type FollowServerMetricsReq = {} // server.metrics.follow export type FollowServerMetricsReq = {} // server.metrics.follow
export type FollowServerMetricsRes = { export type FollowServerMetricsRes = {
guid: string guid: string
@@ -136,17 +135,16 @@ export namespace RR {
} // notification.list } // notification.list
export type GetNotificationsRes = ServerNotification<number>[] export type GetNotificationsRes = ServerNotification<number>[]
// @TODO 040 all these notification endpoints need updating export type DeleteNotificationsReq = { ids: number[] } // notification.remove
export type DeleteNotificationReq = { ids: number[] } // notification.delete export type DeleteNotificationsRes = null
export type DeleteNotificationRes = null
export type MarkSeenNotificationReq = DeleteNotificationReq // notification.mark-seen export type MarkSeenNotificationReq = DeleteNotificationsReq // notification.mark-seen
export type MarkSeenNotificationRes = null export type MarkSeenNotificationRes = null
export type MarkSeenAllNotificationsReq = { before: number } // notification.mark-seen-before export type MarkSeenAllNotificationsReq = { before: number } // notification.mark-seen-before
export type MarkSeenAllNotificationsRes = null export type MarkSeenAllNotificationsRes = null
export type MarkUnseenNotificationReq = DeleteNotificationReq // notification.mark-unseen export type MarkUnseenNotificationReq = DeleteNotificationsReq // notification.mark-unseen
export type MarkUnseenNotificationRes = null export type MarkUnseenNotificationRes = null
// wifi // wifi
@@ -175,9 +173,11 @@ export namespace RR {
} }
export type AddWifiRes = null export type AddWifiRes = null
// @TODO 040 export type EnabledWifiReq = { enable: boolean } // wifi.set-enabled
export type EnableWifiReq = { enable: boolean } // wifi.enable export type EnabledWifiRes = null
export type EnableWifiRes = null
export type SetWifiCountryReq = { country: string } // wifi.country.set
export type SetWifiCountryRes = null
export type ConnectWifiReq = { ssid: string } // wifi.connect export type ConnectWifiReq = { ssid: string } // wifi.connect
export type ConnectWifiRes = null export type ConnectWifiRes = null
@@ -523,8 +523,7 @@ export type ServerNotification<T extends number> = {
title: string title: string
message: string message: string
data: NotificationData<T> data: NotificationData<T>
// @TODO 040 seen: boolean
read: boolean
} }
export type NotificationLevel = 'success' | 'info' | 'warning' | 'error' export type NotificationLevel = 'success' | 'info' | 'warning' | 'error'
@@ -532,10 +531,10 @@ export type NotificationLevel = 'success' | 'info' | 'warning' | 'error'
export type NotificationData<T> = T extends 0 export type NotificationData<T> = T extends 0
? null ? null
: T extends 1 : T extends 1
? BackupReport ? BackupReport
: T extends 2 : T extends 2
? string ? string
: any : any
export type BackupReport = { export type BackupReport = {
server: { server: {

View File

@@ -177,12 +177,12 @@ export abstract class ApiService {
): Promise<RR.MarkSeenAllNotificationsRes> ): Promise<RR.MarkSeenAllNotificationsRes>
abstract markUnseenNotifications( abstract markUnseenNotifications(
params: RR.DeleteNotificationReq, params: RR.DeleteNotificationsReq,
): Promise<RR.DeleteNotificationRes> ): Promise<RR.DeleteNotificationsRes>
abstract deleteNotifications( abstract deleteNotifications(
params: RR.DeleteNotificationReq, params: RR.DeleteNotificationsReq,
): Promise<RR.DeleteNotificationRes> ): Promise<RR.DeleteNotificationsRes>
// ** proxies ** // ** proxies **
@@ -220,7 +220,11 @@ export abstract class ApiService {
// wifi // wifi
abstract enableWifi(params: RR.EnableWifiReq): Promise<RR.EnableWifiRes> abstract enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes>
abstract setWifiCountry(
params: RR.SetWifiCountryReq,
): Promise<RR.SetWifiCountryRes>
abstract getWifi( abstract getWifi(
params: RR.GetWifiReq, params: RR.GetWifiReq,

View File

@@ -245,7 +245,6 @@ export class LiveApiService extends ApiService {
async followServerMetrics( async followServerMetrics(
params: RR.FollowServerMetricsReq, params: RR.FollowServerMetricsReq,
): Promise<RR.FollowServerMetricsRes> { ): Promise<RR.FollowServerMetricsRes> {
// @TODO 040 implement .follow
return this.rpcRequest({ method: 'server.metrics.follow', params }) return this.rpcRequest({ method: 'server.metrics.follow', params })
} }
@@ -350,8 +349,8 @@ export class LiveApiService extends ApiService {
} }
async deleteNotifications( async deleteNotifications(
params: RR.DeleteNotificationReq, params: RR.DeleteNotificationsReq,
): Promise<RR.DeleteNotificationRes> { ): Promise<RR.DeleteNotificationsRes> {
return this.rpcRequest({ method: 'notification.remove', params }) return this.rpcRequest({ method: 'notification.remove', params })
} }
@@ -422,7 +421,7 @@ export class LiveApiService extends ApiService {
// wifi // wifi
async enableWifi(params: RR.EnableWifiReq): Promise<RR.EnableWifiRes> { async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
return this.rpcRequest({ method: 'wifi.enable', params }) return this.rpcRequest({ method: 'wifi.enable', params })
} }
@@ -433,6 +432,12 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'wifi.get', params, timeout }) return this.rpcRequest({ method: 'wifi.get', params, timeout })
} }
async setWifiCountry(
params: RR.SetWifiCountryReq,
): Promise<RR.SetWifiCountryRes> {
return this.rpcRequest({ method: 'wifi.country.set', params })
}
async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> { async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
return this.rpcRequest({ method: 'wifi.add', params }) return this.rpcRequest({ method: 'wifi.add', params })
} }

View File

@@ -519,8 +519,8 @@ export class MockApiService extends ApiService {
} }
async deleteNotifications( async deleteNotifications(
params: RR.DeleteNotificationReq, params: RR.DeleteNotificationsReq,
): Promise<RR.DeleteNotificationRes> { ): Promise<RR.DeleteNotificationsRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
@@ -696,7 +696,7 @@ export class MockApiService extends ApiService {
// wifi // wifi
async enableWifi(params: RR.EnableWifiReq): Promise<RR.EnableWifiRes> { async enableWifi(params: RR.EnabledWifiReq): Promise<RR.EnabledWifiRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ const patch = [
{ {
@@ -710,6 +710,13 @@ export class MockApiService extends ApiService {
return null return null
} }
async setWifiCountry(
params: RR.SetWifiCountryReq,
): Promise<RR.SetWifiCountryRes> {
await pauseFor(2000)
return null
}
async getWifi(params: RR.GetWifiReq): Promise<RR.GetWifiRes> { async getWifi(params: RR.GetWifiReq): Promise<RR.GetWifiRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.Wifi return Mock.Wifi

View File

@@ -39,6 +39,11 @@ export const mockPatchData: DataModel = {
selected: null, selected: null,
lastRegion: null, lastRegion: null,
}, },
acme: {
[knownACME[0].url]: {
contact: ['mailto:support@start9.com'],
},
},
host: { host: {
bindings: { bindings: {
80: { 80: {
@@ -171,11 +176,6 @@ export const mockPatchData: DataModel = {
}, },
}, },
}, },
acme: {
[knownACME[0].url]: {
contact: ['mailto:support@start9.com'],
},
},
unreadNotificationCount: 4, unreadNotificationCount: 4,
// password is asdfasdf // password is asdfasdf
passwordHash: passwordHash:
@@ -191,109 +191,6 @@ export const mockPatchData: DataModel = {
backupProgress: {}, backupProgress: {},
}, },
hostname: 'random-words', 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', pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
ntpSynced: false, ntpSynced: false,

View File

@@ -25,7 +25,7 @@ export class NotificationService {
).pipe(shareReplay(1)) ).pipe(shareReplay(1))
async markSeen(notifications: ServerNotifications) { 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) this.updateCount(-ids.length)
@@ -43,7 +43,7 @@ export class NotificationService {
} }
async markUnseen(notifications: ServerNotifications) { 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) this.updateCount(ids.length)
@@ -53,7 +53,7 @@ export class NotificationService {
} }
async remove(notifications: ServerNotifications): Promise<void> { async remove(notifications: ServerNotifications): Promise<void> {
this.updateCount(-notifications.filter(n => !n.read).length) this.updateCount(-notifications.filter(n => !n.seen).length)
this.api this.api
.deleteNotifications({ ids: notifications.map(n => n.id) }) .deleteNotifications({ ids: notifications.map(n => n.id) })

View File

@@ -1,16 +1,6 @@
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
export type DataModel = Omit<T.Public, 'serverInfo'> & { export type DataModel = T.Public & { ui: UIData; packageData: AllPackageData }
ui: UIData
// @TODO 040
serverInfo: Omit<
T.Public['serverInfo'],
'wifi' | 'networkInterfaces' | 'host'
> & {
network: NetworkInfo
}
packageData: Record<string, PackageDataEntry>
}
export type UIData = { export type UIData = {
name: string | null name: string | null
@@ -37,20 +27,7 @@ export type UIStore = {
name?: string name?: string
} }
export type NetworkInfo = { export type NetworkInfo = T.NetworkInfo & {
wifi: T.WifiInfo & { enabled: boolean }
host: T.Host
networkInterfaces: {
[id: string]: {
inbound: boolean | null
outbound: boolean | null
ipInfo:
| (T.IpInfo & {
name: string
})
| null
}
}
// @TODO 041 // @TODO 041
// start9To: { // start9To: {
// subdomain: string // subdomain: string