#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiskLevel {
Ok,
Low,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiskWarning {
pub level: DiskLevel,
pub free: u64,
pub total: u64,
pub threshold: u64,
}
#[derive(Debug, Clone, PartialEq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComputerResources {
pub id: String,
pub name: String,
pub online: bool,
pub sampled_at_ms: u64,
pub cpu_count: u32,
pub cpu_percent: f64,
pub load_average_1m: f64,
pub memory_total: u64,
pub memory_used: u64,
pub disk_total: u64,
pub disk_free: u64,
pub spaces_count: u32,
pub spaces_running: u32,
pub spaces_cpu_allocated: u32,
pub spaces_memory_allocated: u64,
pub spaces_memory_used: u64,
pub spaces_disk_bytes: u64,
pub spaces_disk_exact: bool,
pub warning: DiskWarning,
}
#[derive(Debug, Clone, Copy, PartialEq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GuestUsage {
pub cpu_count: u32,
pub cpu_percent: f64,
pub load_average_1m: f64,
pub memory_used: u64,
pub memory_total: u64,
pub memory_limited: bool,
pub disk_used: u64,
pub disk_total: u64,
pub disk_limited: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiskOnHost {
pub bytes: Option<u64>,
pub allocated: u64,
pub shares_space: bool,
}
#[derive(Debug, Clone, PartialEq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpaceResources {
pub id: String,
pub power_state: String,
pub cpu_allocated: u32,
pub memory_allocated: u64,
pub disk_allocated: u64,
pub usage: Option<GuestUsage>,
pub disk_on_host: Option<DiskOnHost>,
}
#[derive(Debug, Clone, PartialEq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesSnapshot {
pub sequence: u64,
pub sampled_at_ms: u64,
pub this_mac: ComputerResources,
pub spaces: Vec<SpaceResources>,
pub computers: Vec<ComputerResources>,
pub warning: DiskWarning,
}
#[derive(Debug, Clone, PartialEq, uniffi::Record, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUpdate {
pub sequence: u64,
pub sampled_at_ms: u64,
pub this_mac: Option<ComputerResources>,
pub spaces: Vec<SpaceResources>,
pub computers: Vec<ComputerResources>,
pub removed: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, uniffi::Enum)]
pub enum ResourcesEvent {
Snapshot {
snapshot: ResourcesSnapshot,
},
Update {
update: ResourcesUpdate,
},
LowDisk {
warning: DiskWarning,
},
}
#[uniffi::export(with_foreign)]
pub trait ResourcesListener: Send + Sync {
fn on_event(&self, event: ResourcesEvent);
}
#[derive(uniffi::Object)]
pub struct ResourcesWatch {
control: tokio::sync::watch::Sender<Control>,
task: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
}
#[derive(Clone, Copy)]
struct Control {
visible: bool,
stopped: bool,
}
#[uniffi::export]
impl ResourcesWatch {
pub fn set_visible(&self, visible: bool) {
self.control.send_modify(|c| c.visible = visible);
}
pub fn stop(&self) {
self.control.send_modify(|c| c.stopped = true);
if let Some(t) = self.task.lock().expect("watch task").take() {
t.abort();
}
}
}
impl Drop for ResourcesWatch {
fn drop(&mut self) {
self.stop();
}
}
fn record<T: serde::de::DeserializeOwned>(v: &impl serde::Serialize) -> Result<T> {
serde_json::to_value(v)
.and_then(serde_json::from_value)
.map_err(|e| CuaError::Internal(e.to_string()))
}
pub(crate) fn snapshot_of(r: &cua_spaces::resources::Resources) -> Result<ResourcesSnapshot> {
record(r)
}
fn event_of(e: &cua_spaces::resources::ResourcesEvent) -> Result<ResourcesEvent> {
use cua_spaces::resources::ResourcesEvent as E;
Ok(match e {
E::Snapshot(s) => ResourcesEvent::Snapshot {
snapshot: record(s)?,
},
E::Update(u) => ResourcesEvent::Update { update: record(u)? },
E::LowDisk(w) => ResourcesEvent::LowDisk {
warning: record(w)?,
},
})
}
const REOPEN_AFTER: Duration = Duration::from_secs(5);
#[uniffi::export]
impl Spaces {
pub async fn resources(&self, visible: bool) -> Result<ResourcesSnapshot> {
let host = self.host.clone();
run(async move {
let r = match &host {
Host::Embedded(s) => s.resources(visible).await?,
Host::Daemon(d) => d
.spaces()
.get_resources(dpb::GetResourcesRequest { visible })
.await
.map_err(daemon_err)?
.into_inner()
.snapshot
.map(cua_spaces::resources::Resources::from)
.ok_or_else(|| CuaError::Internal("the daemon sent no snapshot".into()))?,
};
snapshot_of(&r)
})
.await
}
pub fn watch_resources(
&self,
visible: bool,
listener: Arc<dyn ResourcesListener>,
) -> Arc<ResourcesWatch> {
let (control, rx) = tokio::sync::watch::channel(Control {
visible,
stopped: false,
});
let host = self.host.clone();
let task = super::runtime().spawn(async move {
match host {
Host::Embedded(s) => embedded_watch(s, rx, listener).await,
Host::Daemon(d) => daemon_watch(d, rx, listener).await,
}
});
Arc::new(ResourcesWatch {
control,
task: std::sync::Mutex::new(Some(task)),
})
}
}
async fn embedded_watch(
spaces: cua_spaces::Spaces,
mut control: tokio::sync::watch::Receiver<Control>,
listener: Arc<dyn ResourcesListener>,
) {
let mut watch = spaces.watch_resources(control.borrow().visible);
loop {
tokio::select! {
event = watch.next() => {
let Some(event) = event else { return };
if let Ok(e) = event_of(&event) {
listener.on_event(e);
}
}
changed = control.changed() => {
if changed.is_err() || control.borrow().stopped {
return;
}
watch.set_visible(control.borrow().visible);
}
}
}
}
async fn daemon_watch(
daemon: cua_daemon::client::DaemonClient,
mut control: tokio::sync::watch::Receiver<Control>,
listener: Arc<dyn ResourcesListener>,
) {
use dpb::watch_resources_response::Event;
loop {
let visible = control.borrow().visible;
let opened = daemon
.spaces()
.watch_resources(dpb::WatchResourcesRequest { visible })
.await;
let mut stream = match opened {
Ok(s) => s.into_inner(),
Err(_) => {
tokio::select! {
_ = tokio::time::sleep(REOPEN_AFTER) => continue,
changed = control.changed() => {
if changed.is_err() || control.borrow().stopped {
return;
}
continue;
}
}
}
};
loop {
tokio::select! {
msg = stream.message() => match msg {
Ok(Some(m)) => {
let event = match m.event {
Some(Event::Snapshot(s)) => {
event_of(&cua_spaces::resources::ResourcesEvent::Snapshot(s.into()))
}
Some(Event::Update(u)) => {
event_of(&cua_spaces::resources::ResourcesEvent::Update(u.into()))
}
Some(Event::LowDisk(w)) => {
event_of(&cua_spaces::resources::ResourcesEvent::LowDisk(w.into()))
}
None => continue,
};
if let Ok(e) = event {
listener.on_event(e);
}
}
Ok(None) | Err(_) => break,
},
changed = control.changed() => {
if changed.is_err() || control.borrow().stopped {
return;
}
if control.borrow().visible != visible {
break;
}
}
}
}
if control.borrow().visible == visible {
tokio::time::sleep(REOPEN_AFTER).await;
}
}
}
#[cfg(test)]
mod resources_tests {
use super::*;
use cua_spaces::resources as core;
fn core_snapshot() -> core::Resources {
let warning = core::DiskWarning {
level: core::DiskLevel::Low,
free: 90 << 30,
total: 1 << 40,
threshold: 100 << 30,
};
core::Resources {
sequence: 4,
sampled_at_ms: 99,
this_mac: core::ComputerResources {
id: "this-mac".into(),
online: true,
cpu_count: 12,
cpu_percent: 38.5,
memory_total: 64 << 30,
memory_used: 41 << 30,
disk_total: 1 << 40,
disk_free: 90 << 30,
spaces_count: 1,
warning,
..Default::default()
},
spaces: vec![core::SpaceResources {
id: "local:a".into(),
power_state: "running".into(),
cpu_allocated: 4,
memory_allocated: 8 << 30,
disk_allocated: 80 << 30,
usage: Some(core::GuestUsage {
cpu_count: 4,
cpu_percent: 22.0,
memory_limited: true,
..Default::default()
}),
disk_on_host: Some(core::DiskOnHost {
bytes: None,
allocated: 31 << 30,
shares_space: true,
}),
}],
computers: vec![],
warning,
}
}
#[test]
fn the_sdk_records_carry_every_field_of_the_snapshot() {
let r = core_snapshot();
let s = snapshot_of(&r).unwrap();
assert_eq!(s.sequence, 4);
assert_eq!(s.this_mac.cpu_count, 12);
assert_eq!(s.this_mac.warning.level, DiskLevel::Low);
assert_eq!(s.warning.threshold, 100 << 30);
let space = &s.spaces[0];
assert_eq!(space.id, "local:a");
assert_eq!((space.cpu_allocated, space.memory_allocated), (4, 8 << 30));
assert_eq!(space.usage.unwrap().cpu_percent, 22.0);
let d = space.disk_on_host.unwrap();
assert_eq!(
(d.bytes, d.allocated, d.shares_space),
(None, 31 << 30, true)
);
let back = serde_json::to_value(&r).unwrap();
assert_eq!(back["thisMac"]["loadAverage1m"], 0.0);
}
#[test]
fn events_become_records() {
let r = core_snapshot();
match event_of(&core::ResourcesEvent::Snapshot(r.clone())).unwrap() {
ResourcesEvent::Snapshot { snapshot } => assert_eq!(snapshot.sequence, 4),
other => panic!("{other:?}"),
}
let update = core::ResourcesUpdate {
sequence: 5,
sampled_at_ms: 100,
this_mac: None,
spaces: vec![r.spaces[0].clone()],
computers: vec![],
removed: vec!["local:gone".into()],
};
match event_of(&core::ResourcesEvent::Update(update)).unwrap() {
ResourcesEvent::Update { update } => {
assert_eq!(update.sequence, 5);
assert!(update.this_mac.is_none());
assert_eq!(update.spaces.len(), 1);
assert_eq!(update.removed, ["local:gone"]);
}
other => panic!("{other:?}"),
}
match event_of(&core::ResourcesEvent::LowDisk(r.warning)).unwrap() {
ResourcesEvent::LowDisk { warning } => {
assert_eq!(warning.level, DiskLevel::Low);
assert_eq!(warning.free, 90 << 30);
}
other => panic!("{other:?}"),
}
}
}