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
psmisc
qemu-guest-agent
rfkill
rsync
samba-common-bin
smartmontools

2
core/Cargo.lock generated
View File

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

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.3.6-alpha.15" # VERSION_BUMP
version = "0.4.0-alpha.0" # VERSION_BUMP
license = "MIT"
[lib]

View File

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

View File

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

View File

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

View File

@@ -48,44 +48,50 @@ impl Public {
id: account.server_id.clone(),
version: Current::default().semver(),
hostname: account.hostname.no_dot_host_name(),
host: Host {
bindings: [(
80,
BindInfo {
enabled: false,
options: BindOptions {
preferred_external_port: 80,
add_ssl: Some(AddSslOptions {
preferred_external_port: 443,
alpn: Some(AlpnInfo::Specified(vec![
MaybeUtf8String("http/1.1".into()),
MaybeUtf8String("h2".into()),
])),
}),
secure: None,
},
net: NetInfo {
assigned_port: None,
assigned_ssl_port: Some(443),
public: false,
},
},
)]
.into_iter()
.collect(),
onions: account
.tor_keys
.iter()
.map(|k| k.public().get_onion_address())
.collect(),
domains: BTreeMap::new(),
hostname_info: BTreeMap::new(),
},
last_backup: None,
package_version_compat: Current::default().compat().clone(),
post_init_migration_todos: BTreeSet::new(),
network_interfaces: BTreeMap::new(),
acme: BTreeMap::new(),
network: NetworkInfo {
host: Host {
bindings: [(
80,
BindInfo {
enabled: false,
options: BindOptions {
preferred_external_port: 80,
add_ssl: Some(AddSslOptions {
preferred_external_port: 443,
alpn: Some(AlpnInfo::Specified(vec![
MaybeUtf8String("http/1.1".into()),
MaybeUtf8String("h2".into()),
])),
}),
secure: None,
},
net: NetInfo {
assigned_port: None,
assigned_ssl_port: Some(443),
public: false,
},
},
)]
.into_iter()
.collect(),
onions: account
.tor_keys
.iter()
.map(|k| k.public().get_onion_address())
.collect(),
domains: BTreeMap::new(),
hostname_info: BTreeMap::new(),
},
wifi: WifiInfo {
enabled: true,
..Default::default()
},
network_interfaces: BTreeMap::new(),
acme: BTreeMap::new(),
},
status_info: ServerStatus {
backup_progress: None,
updated: false,
@@ -93,7 +99,6 @@ impl Public {
shutting_down: false,
restarting: false,
},
wifi: WifiInfo::default(),
unread_notification_count: 0,
password_hash: account.password.clone(),
pubkey: ssh_key::PublicKey::from(&account.ssh_key)
@@ -145,7 +150,6 @@ pub struct ServerInfo {
pub id: String,
#[ts(type = "string")]
pub hostname: InternedString,
pub host: Host,
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
@@ -154,14 +158,9 @@ pub struct ServerInfo {
pub post_init_migration_todos: BTreeSet<Version>,
#[ts(type = "string | null")]
pub last_backup: Option<DateTime<Utc>>,
#[ts(as = "BTreeMap::<String, NetworkInterfaceInfo>")]
#[serde(default)]
pub network_interfaces: BTreeMap<InternedString, NetworkInterfaceInfo>,
#[serde(default)]
pub acme: BTreeMap<AcmeProvider, AcmeSettings>,
pub network: NetworkInfo,
#[serde(default)]
pub status_info: ServerStatus,
pub wifi: WifiInfo,
#[ts(type = "number")]
pub unread_notification_count: u64,
pub password_hash: String,
@@ -178,17 +177,32 @@ pub struct ServerInfo {
pub devices: Vec<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)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct NetworkInterfaceInfo {
pub public: Option<bool>,
pub inbound: Option<bool>,
pub outbound: Option<bool>,
pub ip_info: Option<IpInfo>,
}
impl NetworkInterfaceInfo {
pub fn public(&self) -> bool {
self.public.unwrap_or_else(|| {
pub fn inbound(&self) -> bool {
self.inbound.unwrap_or_else(|| {
!self.ip_info.as_ref().map_or(true, |ip_info| {
let ip4s = ip_info
.subnets
@@ -224,6 +238,8 @@ impl NetworkInterfaceInfo {
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct IpInfo {
#[ts(type = "string")]
pub name: InternedString,
pub scope_id: u32,
pub device_type: Option<NetworkInterfaceType>,
#[ts(type = "string[]")]
@@ -276,6 +292,7 @@ pub struct ServerStatus {
#[model = "Model<Self>"]
#[ts(export)]
pub struct WifiInfo {
pub enabled: bool,
pub interface: Option<String>,
pub ssids: BTreeSet<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::str::FromStr;
@@ -267,7 +267,7 @@ where
T::Key: FromStr + Ord + Clone,
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;
match &self.value {
Value::Object(o) => o

View File

@@ -415,7 +415,11 @@ pub async fn init(
let wifi_interface = find_wifi_iface().await?;
let wifi = db
.mutate(|db| {
let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut();
let wifi = db
.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_wifi_mut();
wifi.as_interface_mut().ser(&wifi_interface)?;
wifi.de()
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -88,7 +88,7 @@ pub async fn mount(
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
.deref()?
.seed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
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"
export type IpInfo = {
name: string
scopeId: number
deviceType: NetworkInterfaceType | null
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"
export type NetworkInterfaceInfo = {
public: boolean | null
inbound: boolean | null
outbound: boolean | 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.
export type NetworkInterfaceSetPublicParams = {
export type NetworkInterfaceSetInboundParams = {
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.
import type { AcmeProvider } from "./AcmeProvider"
import type { AcmeSettings } from "./AcmeSettings"
import type { Governor } from "./Governor"
import type { Host } from "./Host"
import type { LshwDevice } from "./LshwDevice"
import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
import type { NetworkInfo } from "./NetworkInfo"
import type { ServerStatus } from "./ServerStatus"
import type { SmtpValue } from "./SmtpValue"
import type { WifiInfo } from "./WifiInfo"
export type ServerInfo = {
arch: string
platform: string
id: string
hostname: string
host: Host
version: string
packageVersionCompat: string
postInitMigrationTodos: string[]
lastBackup: string | null
networkInterfaces: { [key: string]: NetworkInterfaceInfo }
acme: { [key: AcmeProvider]: AcmeSettings }
network: NetworkInfo
statusInfo: ServerStatus
wifi: WifiInfo
unreadNotificationCount: number
passwordHash: string
pubkey: string

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.
export type WifiInfo = {
enabled: boolean
interface: string | null
ssids: Array<string>
selected: string | null

View File

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

View File

@@ -73,7 +73,7 @@ import * as actions from "../../base/lib/actions"
import { setupInit } from "./inits/setupInit"
import * as fs from "node:fs/promises"
export const OSVersion = testTypeVersion("0.3.6-alpha.15")
export const OSVersion = testTypeVersion("0.4.0-alpha.0")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =

4
web/package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -19,7 +19,6 @@ export const REMOVE: Partial<TuiDialogOptions<TuiConfirmData>> = {
},
}
// @TODO 040 Aiden audit
export function getAddresses(
serviceInterface: T.ServiceInterface,
host: T.Host,
@@ -81,11 +80,17 @@ export function getAddresses(
} else {
const hostnameKind = h.hostname.kind
if (hostnameKind === 'domain') {
if (h.public) {
clearnet.push({
label: 'Domain',
label:
hostnameKind == 'domain'
? 'Domain'
: `${h.networkInterfaceId} (${hostnameKind})`,
url,
acme: host.domains[h.hostname.domain]?.acme,
acme:
hostnameKind == 'domain'
? host.domains[h.hostname.domain]?.acme
: null, // @TODO Matt make sure this is handled correctly - looks like ACME settings aren't built yet anyway, but ACME settings aren't *available* for public IPs
})
} else {
local.push({
@@ -101,15 +106,19 @@ export function getAddresses(
})
return {
clearnet,
local,
tor,
clearnet: clearnet.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
local: local.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
tor: tor.filter(
(value, index, self) =>
index === self.findIndex(t => t.url === value.url),
),
}
// @TODO Aiden what was going on here in 036?
// return mappedAddresses.filter(
// (value, index, self) => index === self.findIndex(t => t.url === value.url),
// )
}
export type AddressDetails = {

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ export class SettingsACMEComponent {
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 => {
const providerUrls = Object.keys(acme)
return providerUrls.map(url => {

View File

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

View File

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

View File

@@ -177,12 +177,12 @@ export abstract class ApiService {
): Promise<RR.MarkSeenAllNotificationsRes>
abstract markUnseenNotifications(
params: RR.DeleteNotificationReq,
): Promise<RR.DeleteNotificationRes>
params: RR.DeleteNotificationsReq,
): Promise<RR.DeleteNotificationsRes>
abstract deleteNotifications(
params: RR.DeleteNotificationReq,
): Promise<RR.DeleteNotificationRes>
params: RR.DeleteNotificationsReq,
): Promise<RR.DeleteNotificationsRes>
// ** proxies **
@@ -220,7 +220,11 @@ export abstract class ApiService {
// 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(
params: RR.GetWifiReq,

View File

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

View File

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

View File

@@ -39,6 +39,11 @@ export const mockPatchData: DataModel = {
selected: null,
lastRegion: null,
},
acme: {
[knownACME[0].url]: {
contact: ['mailto:support@start9.com'],
},
},
host: {
bindings: {
80: {
@@ -171,11 +176,6 @@ export const mockPatchData: DataModel = {
},
},
},
acme: {
[knownACME[0].url]: {
contact: ['mailto:support@start9.com'],
},
},
unreadNotificationCount: 4,
// password is asdfasdf
passwordHash:
@@ -191,109 +191,6 @@ export const mockPatchData: DataModel = {
backupProgress: {},
},
hostname: 'random-words',
// @ts-ignore
host: {
bindings: {
80: {
enabled: true,
net: {
assignedPort: null,
assignedSslPort: 443,
public: false,
},
options: {
preferredExternalPort: 80,
addSsl: {
preferredExternalPort: 443,
alpn: { specified: ['http/1.1', 'h2'] },
},
secure: null,
},
},
},
domains: {},
onions: ['myveryownspecialtoraddress'],
hostnameInfo: {
80: [
{
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'wlan0',
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.1',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'wlan0',
public: false,
hostname: {
kind: 'ipv4',
value: '10.0.0.2',
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'eth0',
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:72cd',
scopeId: 2,
port: null,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'wlan0',
public: false,
hostname: {
kind: 'ipv6',
value: 'fe80::cd00:0000:0cde:1257:0000:211e:1234',
scopeId: 3,
port: null,
sslPort: 443,
},
},
{
kind: 'onion',
hostname: {
value: 'myveryownspecialtoraddress.onion',
port: 80,
sslPort: 443,
},
},
],
},
},
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
caFingerprint: 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
ntpSynced: false,

View File

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

View File

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