use std::collections::HashMap;
use std::str::FromStr;
use auv_api_proto::auv::api::daemon::v1 as proto;
use futures_util::future::join_all;
use crate::client::Client;
use crate::error::{ClientError, ClientErrorKind};
use crate::profile::{self, ConfiguredDevice, ProfileStore};
use crate::resource::{DeviceId, DeviceSelector};
use crate::{AuvContext, ContextError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DevicePlatform {
Unspecified,
Linux,
Macos,
Windows,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Device {
pub id: DeviceId,
pub name: String,
pub platform: DevicePlatform,
pub local: bool,
pub labels: HashMap<String, String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeviceAvailability {
Online,
Offline,
Unauthorized,
Invalid,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfiguredDeviceObservation {
pub profile: ConfiguredDevice,
pub availability: DeviceAvailability,
pub remote: Option<Device>,
}
impl ConfiguredDeviceObservation {
pub fn matches(&self, selector: &DeviceSelector) -> bool {
self.profile.device_id().parse::<DeviceId>().is_ok_and(|id| selector.matches(&id, self.profile.device_name()))
}
}
impl Device {
pub fn validate_selection(&self, selected: Option<&Device>) -> Result<(), DeviceError> {
if let Some(selected) = selected
&& selected.id != self.id
{
return Err(DeviceError::SelectionConflict {
actual: self.id.to_string(),
selected: selected.id.to_string(),
});
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum DeviceError {
#[error(transparent)]
Client(#[from] ClientError),
#[error(transparent)]
Identity(#[from] crate::resource::IdentityError),
#[error("Device response omitted its canonical ID")]
MissingIdentity,
#[error("Device selector matched no Device")]
NotFound,
#[error("Device selector is ambiguous; candidate IDs: {candidate_ids}")]
Ambiguous {
candidate_ids: String,
},
#[error("Device {actual:?} conflicts with selected Device {selected:?}")]
SelectionConflict {
actual: String,
selected: String,
},
#[error(transparent)]
Profile(#[from] profile::ProfileError),
}
#[derive(Clone, Debug)]
pub struct Devices {
client: Client,
}
impl Devices {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
pub async fn list(&self) -> Result<Vec<Device>, DeviceError> {
self
.client
.grpc_client()
.devices()
.list_devices()
.await
.map_err(|status| ClientError::from_status("ListDevices", status))?
.into_iter()
.map(Device::try_from)
.collect()
}
pub async fn get(&self, selector: &DeviceSelector) -> Result<Device, DeviceError> {
let devices = self.list().await?;
let matches = devices.iter().filter(|device| selector.matches(&device.id, &device.name)).collect::<Vec<_>>();
match matches.as_slice() {
[] => Err(DeviceError::NotFound),
[device] => Ok((*device).clone()),
_ => Err(DeviceError::Ambiguous {
candidate_ids: matches.iter().map(|device| device.id.to_string()).collect::<Vec<_>>().join(", "),
}),
}
}
pub async fn observe_configured(store: &ProfileStore) -> Result<Vec<ConfiguredDeviceObservation>, DeviceError> {
let configured = match store.list_devices() {
Ok(configured) => configured,
Err(profile::ProfileError::Open { source, .. }) if source.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(error) => return Err(error.into()),
};
Ok(join_all(configured.into_iter().map(|profile| observe_profile(store, profile))).await)
}
}
async fn observe_profile(store: &ProfileStore, profile: ConfiguredDevice) -> ConfiguredDeviceObservation {
let context = AuvContext {
config_profile: Some(profile.config_profile().to_string()),
..AuvContext::default()
};
match Client::from_context_with_profiles(context, store).await {
Ok(client) => match client.devices().list().await {
Ok(devices) => {
let remote = devices.into_iter().find(|device| device.id.as_str() == profile.device_id());
ConfiguredDeviceObservation {
availability: if remote.is_some() {
DeviceAvailability::Online
} else {
DeviceAvailability::Invalid
},
profile,
remote,
}
}
Err(error) => ConfiguredDeviceObservation {
availability: availability_from_device_error(&error),
profile,
remote: None,
},
},
Err(error) => ConfiguredDeviceObservation {
availability: availability_from_context_error(&error),
profile,
remote: None,
},
}
}
fn availability_from_device_error(error: &DeviceError) -> DeviceAvailability {
match error {
DeviceError::Client(error) if error.kind() == ClientErrorKind::Unauthorized => DeviceAvailability::Unauthorized,
DeviceError::Client(error) if error.kind() == ClientErrorKind::Unavailable => DeviceAvailability::Offline,
DeviceError::Identity(_) | DeviceError::MissingIdentity => DeviceAvailability::Invalid,
_ => DeviceAvailability::Error,
}
}
fn availability_from_context_error(error: &ContextError) -> DeviceAvailability {
match error {
ContextError::Connect(_) | ContextError::PairedConnect(_) => DeviceAvailability::Offline,
ContextError::RemoteDeviceList(error) if error.kind() == ClientErrorKind::Unauthorized => DeviceAvailability::Unauthorized,
ContextError::RemoteDeviceList(error) if error.kind() == ClientErrorKind::Unavailable => DeviceAvailability::Offline,
ContextError::Profile(_) | ContextError::ProfileEndpointMismatch { .. } | ContextError::CanonicalDeviceMissing(_) => {
DeviceAvailability::Invalid
}
_ => DeviceAvailability::Error,
}
}
impl TryFrom<proto::Device> for Device {
type Error = DeviceError;
fn try_from(device: proto::Device) -> Result<Self, Self::Error> {
let id = device.r#ref.ok_or(DeviceError::MissingIdentity)?.device_id;
let platform = match proto::DevicePlatform::try_from(device.platform).unwrap_or(proto::DevicePlatform::Unspecified) {
proto::DevicePlatform::Unspecified => DevicePlatform::Unspecified,
proto::DevicePlatform::Linux => DevicePlatform::Linux,
proto::DevicePlatform::Macos => DevicePlatform::Macos,
proto::DevicePlatform::Windows => DevicePlatform::Windows,
};
Ok(Self {
id: DeviceId::from_str(&id)?,
name: device.name,
platform,
local: device.local,
labels: device.labels,
})
}
}