Add capabilities support to server and manager

pull/137/head
Chip Senkbeil 2 years ago
parent c19df9f538
commit 53fd8d0c4f
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `SystemInfo` via ssh backend now detects and reports username and shell
- `SystemInfo` via ssh backend now reports os when windows detected
- `Capabilities` request/response for server and manager that report back the
capabilities (and descriptions) supported by the server or manager
### Changed

@ -1,5 +1,8 @@
use crate::{
data::{ChangeKind, DirEntry, Environment, Error, Metadata, ProcessId, PtySize, SystemInfo},
data::{
Capabilities, ChangeKind, DirEntry, Environment, Error, Metadata, ProcessId, PtySize,
SystemInfo,
},
ConnectionId, DistantMsg, DistantRequestData, DistantResponseData,
};
use async_trait::async_trait;
@ -70,6 +73,14 @@ pub trait DistantApi {
#[allow(unused_variables)]
async fn on_accept(&self, local_data: &mut Self::LocalData) {}
/// Retrieves information about the server's capabilities.
///
/// *Override this, otherwise it will return "unsupported" as an error.*
#[allow(unused_variables)]
async fn capabilities(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Capabilities> {
unsupported("capabilities")
}
/// Reads bytes from a file.
///
/// * `path` - the path to the file
@ -488,6 +499,12 @@ where
D: Send + Sync,
{
match request {
DistantRequestData::Capabilities {} => server
.api
.capabilities(ctx)
.await
.map(|supported| DistantResponseData::Capabilities { supported })
.unwrap_or_else(DistantResponseData::from),
DistantRequestData::FileRead { path } => server
.api
.read_file(ctx, path)

@ -1,7 +1,7 @@
use crate::{
data::{
ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, ProcessId, PtySize,
SystemInfo,
Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata,
ProcessId, PtySize, SystemInfo,
},
DistantApi, DistantCtx,
};
@ -54,6 +54,12 @@ impl DistantApi for LocalDistantApi {
local_data.watcher_channel = self.state.watcher.clone_channel();
}
async fn capabilities(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Capabilities> {
debug!("[Conn {}] Querying capabilities", ctx.connection_id);
Ok(Capabilities::all())
}
async fn read_file(
&self,
ctx: DistantCtx<Self::LocalData>,

@ -1,11 +1,14 @@
use derive_more::{From, IsVariant};
use serde::{Deserialize, Serialize};
use std::{io, path::PathBuf};
use strum::AsRefStr;
use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString};
#[cfg(feature = "clap")]
use strum::VariantNames;
mod capabilities;
pub use capabilities::*;
mod change;
pub use change::*;
@ -138,14 +141,37 @@ impl<T: schemars::JsonSchema> DistantMsg<T> {
}
/// Represents the payload of a request to be performed on the remote machine
#[derive(Clone, Debug, PartialEq, Eq, IsVariant, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, EnumDiscriminants, IsVariant, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
#[strum_discriminants(derive(
strum::Display,
EnumIter,
EnumMessage,
EnumString,
Hash,
PartialOrd,
Ord,
IsVariant,
Serialize,
Deserialize
))]
#[cfg_attr(
feature = "schemars",
strum_discriminants(derive(schemars::JsonSchema))
)]
#[strum_discriminants(name(CapabilityKind))]
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
#[cfg_attr(feature = "clap", clap(rename_all = "kebab-case"))]
pub enum DistantRequestData {
/// Retrieve information about the server's capabilities
#[strum_discriminants(strum(message = "Supports retrieving capabilities"))]
Capabilities {},
/// Reads a file from the specified path on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["cat"]))]
#[strum_discriminants(strum(message = "Supports reading binary file"))]
FileRead {
/// The path to the file on the remote machine
path: PathBuf,
@ -153,6 +179,7 @@ pub enum DistantRequestData {
/// Reads a file from the specified path on the remote machine
/// and treats the contents as text
#[strum_discriminants(strum(message = "Supports reading text file"))]
FileReadText {
/// The path to the file on the remote machine
path: PathBuf,
@ -160,6 +187,7 @@ pub enum DistantRequestData {
/// Writes a file, creating it if it does not exist, and overwriting any existing content
/// on the remote machine
#[strum_discriminants(strum(message = "Supports writing binary file"))]
FileWrite {
/// The path to the file on the remote machine
path: PathBuf,
@ -171,6 +199,7 @@ pub enum DistantRequestData {
/// Writes a file using text instead of bytes, creating it if it does not exist,
/// and overwriting any existing content on the remote machine
#[strum_discriminants(strum(message = "Supports writing text file"))]
FileWriteText {
/// The path to the file on the remote machine
path: PathBuf,
@ -180,6 +209,7 @@ pub enum DistantRequestData {
},
/// Appends to a file, creating it if it does not exist, on the remote machine
#[strum_discriminants(strum(message = "Supports appending to binary file"))]
FileAppend {
/// The path to the file on the remote machine
path: PathBuf,
@ -190,6 +220,7 @@ pub enum DistantRequestData {
},
/// Appends text to a file, creating it if it does not exist, on the remote machine
#[strum_discriminants(strum(message = "Supports appending to text file"))]
FileAppendText {
/// The path to the file on the remote machine
path: PathBuf,
@ -200,6 +231,7 @@ pub enum DistantRequestData {
/// Reads a directory from the specified path on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["ls"]))]
#[strum_discriminants(strum(message = "Supports reading directory"))]
DirRead {
/// The path to the directory on the remote machine
path: PathBuf,
@ -238,6 +270,7 @@ pub enum DistantRequestData {
/// Creates a directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["mkdir"]))]
#[strum_discriminants(strum(message = "Supports creating directory"))]
DirCreate {
/// The path to the directory on the remote machine
path: PathBuf,
@ -250,6 +283,7 @@ pub enum DistantRequestData {
/// Removes a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["rm"]))]
#[strum_discriminants(strum(message = "Supports removing files, directories, and symlinks"))]
Remove {
/// The path to the file or directory on the remote machine
path: PathBuf,
@ -263,6 +297,7 @@ pub enum DistantRequestData {
/// Copies a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["cp"]))]
#[strum_discriminants(strum(message = "Supports copying files, directories, and symlinks"))]
Copy {
/// The path to the file or directory on the remote machine
src: PathBuf,
@ -273,6 +308,7 @@ pub enum DistantRequestData {
/// Moves/renames a file or directory on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["mv"]))]
#[strum_discriminants(strum(message = "Supports renaming files, directories, and symlinks"))]
Rename {
/// The path to the file or directory on the remote machine
src: PathBuf,
@ -282,6 +318,7 @@ pub enum DistantRequestData {
},
/// Watches a path for changes
#[strum_discriminants(strum(message = "Supports watching filesystem for changes"))]
Watch {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
@ -310,18 +347,23 @@ pub enum DistantRequestData {
},
/// Unwatches a path for changes, meaning no additional changes will be reported
#[strum_discriminants(strum(message = "Supports unwatching filesystem for changes"))]
Unwatch {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
},
/// Checks whether the given path exists
#[strum_discriminants(strum(message = "Supports checking if a path exists"))]
Exists {
/// The path to the file or directory on the remote machine
path: PathBuf,
},
/// Retrieves filesystem metadata for the specified path on the remote machine
#[strum_discriminants(strum(
message = "Supports retrieving metadata about a file, directory, or symlink"
))]
Metadata {
/// The path to the file, directory, or symlink on the remote machine
path: PathBuf,
@ -341,6 +383,7 @@ pub enum DistantRequestData {
/// Spawns a new process on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["spawn", "run"]))]
#[strum_discriminants(strum(message = "Supports spawning a process"))]
ProcSpawn {
/// The full command to run including arguments
#[cfg_attr(feature = "clap", clap(flatten))]
@ -370,12 +413,14 @@ pub enum DistantRequestData {
/// Kills a process running on the remote machine
#[cfg_attr(feature = "clap", clap(visible_aliases = &["kill"]))]
#[strum_discriminants(strum(message = "Supports killing a spawned process"))]
ProcKill {
/// Id of the actively-running process
id: ProcessId,
},
/// Sends additional data to stdin of running process
#[strum_discriminants(strum(message = "Supports sending stdin to a spawned process"))]
ProcStdin {
/// Id of the actively-running process to send stdin data
id: ProcessId,
@ -387,6 +432,7 @@ pub enum DistantRequestData {
},
/// Resize pty of remote process
#[strum_discriminants(strum(message = "Supports resizing the pty of a spawned process"))]
ProcResizePty {
/// Id of the actively-running process whose pty to resize
id: ProcessId,
@ -396,6 +442,7 @@ pub enum DistantRequestData {
},
/// Retrieve information about the server and the system it is on
#[strum_discriminants(strum(message = "Supports retrieving system information"))]
SystemInfo {},
}
@ -494,6 +541,9 @@ pub enum DistantResponseData {
/// Response to retrieving information about the server and the system it is on
SystemInfo(SystemInfo),
/// Response to retrieving information about the server's capabilities
Capabilities { supported: Capabilities },
}
#[cfg(feature = "schemars")]

@ -0,0 +1,198 @@
use super::CapabilityKind;
use derive_more::{From, Into, IntoIterator};
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
collections::HashSet,
hash::{Hash, Hasher},
ops::{BitAnd, BitOr, BitXor},
str::FromStr,
};
use strum::{EnumMessage, IntoEnumIterator};
/// Set of supported capabilities for a server
#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct Capabilities(#[into_iterator(owned, ref)] HashSet<Capability>);
impl Capabilities {
/// Return set of capabilities encompassing all possible capabilities
pub fn all() -> Self {
Self(CapabilityKind::iter().map(Capability::from).collect())
}
/// Return empty set of capabilities
pub fn none() -> Self {
Self(HashSet::new())
}
/// Returns true if the capability with described kind is included
pub fn contains(&self, kind: impl AsRef<str>) -> bool {
let cap = Capability {
kind: kind.as_ref().to_string(),
description: String::new(),
};
self.0.contains(&cap)
}
/// Adds the specified capability to the set of capabilities
///
/// * If the set did not have this capability, returns `true`
/// * If the set did have this capability, returns `false`
pub fn insert(&mut self, cap: impl Into<Capability>) -> bool {
self.0.insert(cap.into())
}
/// Removes the capability with the described kind, returning the capability
pub fn take(&mut self, kind: impl AsRef<str>) -> Option<Capability> {
let cap = Capability {
kind: kind.as_ref().to_string(),
description: String::new(),
};
self.0.take(&cap)
}
/// Converts into vec of capabilities sorted by kind
pub fn into_sorted_vec(self) -> Vec<Capability> {
let mut this = self.0.into_iter().collect::<Vec<_>>();
this.sort_unstable();
this
}
}
#[cfg(feature = "schemars")]
impl Capabilities {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(Capabilities)
}
}
impl BitAnd for &Capabilities {
type Output = Capabilities;
fn bitand(self, rhs: Self) -> Self::Output {
Capabilities(self.0.bitand(&rhs.0))
}
}
impl BitOr for &Capabilities {
type Output = Capabilities;
fn bitor(self, rhs: Self) -> Self::Output {
Capabilities(self.0.bitor(&rhs.0))
}
}
impl BitOr<Capability> for &Capabilities {
type Output = Capabilities;
fn bitor(self, rhs: Capability) -> Self::Output {
let mut other = Capabilities::none();
other.0.insert(rhs);
self.bitor(&other)
}
}
impl BitXor for &Capabilities {
type Output = Capabilities;
fn bitxor(self, rhs: Self) -> Self::Output {
Capabilities(self.0.bitxor(&rhs.0))
}
}
impl FromIterator<Capability> for Capabilities {
fn from_iter<I: IntoIterator<Item = Capability>>(iter: I) -> Self {
let mut this = Capabilities::none();
for capability in iter {
this.0.insert(capability);
}
this
}
}
/// Capability tied to a server. A capability is equivalent based on its kind and not description.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct Capability {
/// Label describing the kind of capability
pub kind: String,
/// Information about the capability
pub description: String,
}
impl Capability {
/// Will convert the [`Capability`]'s `kind` into a known [`CapabilityKind`] if possible,
/// returning None if the capability is unknown
pub fn to_capability_kind(&self) -> Option<CapabilityKind> {
CapabilityKind::from_str(&self.kind).ok()
}
/// Returns true if the described capability is unknown
pub fn is_unknown(&self) -> bool {
self.to_capability_kind().is_none()
}
}
impl PartialEq for Capability {
fn eq(&self, other: &Self) -> bool {
self.kind.eq_ignore_ascii_case(&other.kind)
}
}
impl Eq for Capability {}
impl PartialOrd for Capability {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Capability {
fn cmp(&self, other: &Self) -> Ordering {
self.kind
.to_ascii_lowercase()
.cmp(&other.kind.to_ascii_lowercase())
}
}
impl Hash for Capability {
fn hash<H: Hasher>(&self, state: &mut H) {
self.kind.to_ascii_lowercase().hash(state);
}
}
impl From<CapabilityKind> for Capability {
/// Creates a new capability using the kind's default message
fn from(kind: CapabilityKind) -> Self {
Self {
kind: kind.to_string(),
description: kind
.get_message()
.map(ToString::to_string)
.unwrap_or_default(),
}
}
}
#[cfg(feature = "schemars")]
impl Capability {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(Capability)
}
}
#[cfg(feature = "schemars")]
impl CapabilityKind {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(CapabilityKind)
}
}

@ -1,5 +1,6 @@
use super::data::{
ConnectionId, ConnectionInfo, ConnectionList, Destination, ManagerRequest, ManagerResponse,
ConnectionId, ConnectionInfo, ConnectionList, Destination, ManagerCapabilities, ManagerRequest,
ManagerResponse,
};
use crate::{
DistantChannel, DistantClient, DistantMsg, DistantRequestData, DistantResponseData, Map,
@ -301,6 +302,20 @@ impl DistantManagerClient {
})
}
/// Retrieves a list of supported capabilities
pub async fn capabilities(&mut self) -> io::Result<ManagerCapabilities> {
trace!("capabilities()");
let res = self.client.send(ManagerRequest::Capabilities).await?;
match res.payload {
ManagerResponse::Capabilities { supported } => Ok(supported),
ManagerResponse::Error(x) => Err(x.into()),
x => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Got unexpected response: {:?}", x),
)),
}
}
/// Retrieves information about a specific connection
pub async fn info(&mut self, id: ConnectionId) -> io::Result<ConnectionInfo> {
trace!("info({})", id);

@ -1,3 +1,6 @@
mod capabilities;
pub use capabilities::*;
mod destination;
pub use destination::*;

@ -0,0 +1,203 @@
use super::ManagerCapabilityKind;
use derive_more::{From, Into, IntoIterator};
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
collections::HashSet,
hash::{Hash, Hasher},
ops::{BitAnd, BitOr, BitXor},
str::FromStr,
};
use strum::{EnumMessage, IntoEnumIterator};
/// Set of supported capabilities for a manager
#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct ManagerCapabilities(#[into_iterator(owned, ref)] HashSet<ManagerCapability>);
impl ManagerCapabilities {
/// Return set of capabilities encompassing all possible capabilities
pub fn all() -> Self {
Self(
ManagerCapabilityKind::iter()
.map(ManagerCapability::from)
.collect(),
)
}
/// Return empty set of capabilities
pub fn none() -> Self {
Self(HashSet::new())
}
/// Returns true if the capability with described kind is included
pub fn contains(&self, kind: impl AsRef<str>) -> bool {
let cap = ManagerCapability {
kind: kind.as_ref().to_string(),
description: String::new(),
};
self.0.contains(&cap)
}
/// Adds the specified capability to the set of capabilities
///
/// * If the set did not have this capability, returns `true`
/// * If the set did have this capability, returns `false`
pub fn insert(&mut self, cap: impl Into<ManagerCapability>) -> bool {
self.0.insert(cap.into())
}
/// Removes the capability with the described kind, returning the capability
pub fn take(&mut self, kind: impl AsRef<str>) -> Option<ManagerCapability> {
let cap = ManagerCapability {
kind: kind.as_ref().to_string(),
description: String::new(),
};
self.0.take(&cap)
}
/// Converts into vec of capabilities sorted by kind
pub fn into_sorted_vec(self) -> Vec<ManagerCapability> {
let mut this = self.0.into_iter().collect::<Vec<_>>();
this.sort_unstable();
this
}
}
#[cfg(feature = "schemars")]
impl ManagerCapabilities {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(ManagerCapabilities)
}
}
impl BitAnd for &ManagerCapabilities {
type Output = ManagerCapabilities;
fn bitand(self, rhs: Self) -> Self::Output {
ManagerCapabilities(self.0.bitand(&rhs.0))
}
}
impl BitOr for &ManagerCapabilities {
type Output = ManagerCapabilities;
fn bitor(self, rhs: Self) -> Self::Output {
ManagerCapabilities(self.0.bitor(&rhs.0))
}
}
impl BitOr<ManagerCapability> for &ManagerCapabilities {
type Output = ManagerCapabilities;
fn bitor(self, rhs: ManagerCapability) -> Self::Output {
let mut other = ManagerCapabilities::none();
other.0.insert(rhs);
self.bitor(&other)
}
}
impl BitXor for &ManagerCapabilities {
type Output = ManagerCapabilities;
fn bitxor(self, rhs: Self) -> Self::Output {
ManagerCapabilities(self.0.bitxor(&rhs.0))
}
}
impl FromIterator<ManagerCapability> for ManagerCapabilities {
fn from_iter<I: IntoIterator<Item = ManagerCapability>>(iter: I) -> Self {
let mut this = ManagerCapabilities::none();
for capability in iter {
this.0.insert(capability);
}
this
}
}
/// ManagerCapability tied to a manager. A capability is equivalent based on its kind and not
/// description.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct ManagerCapability {
/// Label describing the kind of capability
pub kind: String,
/// Information about the capability
pub description: String,
}
impl ManagerCapability {
/// Will convert the [`ManagerCapability`]'s `kind` into a known [`ManagerCapabilityKind`] if
/// possible, returning None if the capability is unknown
pub fn to_capability_kind(&self) -> Option<ManagerCapabilityKind> {
ManagerCapabilityKind::from_str(&self.kind).ok()
}
/// Returns true if the described capability is unknown
pub fn is_unknown(&self) -> bool {
self.to_capability_kind().is_none()
}
}
impl PartialEq for ManagerCapability {
fn eq(&self, other: &Self) -> bool {
self.kind.eq_ignore_ascii_case(&other.kind)
}
}
impl Eq for ManagerCapability {}
impl PartialOrd for ManagerCapability {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ManagerCapability {
fn cmp(&self, other: &Self) -> Ordering {
self.kind
.to_ascii_lowercase()
.cmp(&other.kind.to_ascii_lowercase())
}
}
impl Hash for ManagerCapability {
fn hash<H: Hasher>(&self, state: &mut H) {
self.kind.to_ascii_lowercase().hash(state);
}
}
impl From<ManagerCapabilityKind> for ManagerCapability {
/// Creates a new capability using the kind's default message
fn from(kind: ManagerCapabilityKind) -> Self {
Self {
kind: kind.to_string(),
description: kind
.get_message()
.map(ToString::to_string)
.unwrap_or_default(),
}
}
}
#[cfg(feature = "schemars")]
impl ManagerCapability {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(ManagerCapability)
}
}
#[cfg(feature = "schemars")]
impl ManagerCapabilityKind {
pub fn root_schema() -> schemars::schema::RootSchema {
schemars::schema_for!(ManagerCapabilityKind)
}
}

@ -1,13 +1,38 @@
use super::{ChannelId, ConnectionId, Destination};
use crate::{DistantMsg, DistantRequestData, Map};
use derive_more::IsVariant;
use distant_net::Request;
use serde::{Deserialize, Serialize};
use strum::{EnumDiscriminants, EnumIter, EnumMessage, EnumString};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, EnumDiscriminants, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::Subcommand))]
#[strum_discriminants(derive(
strum::Display,
EnumIter,
EnumMessage,
EnumString,
Hash,
PartialOrd,
Ord,
IsVariant,
Serialize,
Deserialize
))]
#[cfg_attr(
feature = "schemars",
strum_discriminants(derive(schemars::JsonSchema))
)]
#[strum_discriminants(name(ManagerCapabilityKind))]
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
pub enum ManagerRequest {
/// Retrieve information about the server's capabilities
#[strum_discriminants(strum(message = "Supports retrieving capabilities"))]
Capabilities,
/// Launch a server using the manager
#[strum_discriminants(strum(message = "Supports launching distant on remote servers"))]
Launch {
// NOTE: Boxed per clippy's large_enum_variant warning
destination: Box<Destination>,
@ -18,6 +43,7 @@ pub enum ManagerRequest {
},
/// Initiate a connection through the manager
#[strum_discriminants(strum(message = "Supports connecting to remote servers"))]
Connect {
// NOTE: Boxed per clippy's large_enum_variant warning
destination: Box<Destination>,
@ -29,6 +55,7 @@ pub enum ManagerRequest {
/// Opens a channel for communication with a server
#[cfg_attr(feature = "clap", clap(skip))]
#[strum_discriminants(strum(message = "Supports opening a channel with a remote server"))]
OpenChannel {
/// Id of the connection
id: ConnectionId,
@ -36,6 +63,9 @@ pub enum ManagerRequest {
/// Sends data through channel
#[cfg_attr(feature = "clap", clap(skip))]
#[strum_discriminants(strum(
message = "Supports sending data through a channel with a remote server"
))]
Channel {
/// Id of the channel
id: ChannelId,
@ -47,21 +77,26 @@ pub enum ManagerRequest {
/// Closes an open channel
#[cfg_attr(feature = "clap", clap(skip))]
#[strum_discriminants(strum(message = "Supports closing a channel with a remote server"))]
CloseChannel {
/// Id of the channel to close
id: ChannelId,
},
/// Retrieve information about a specific connection
#[strum_discriminants(strum(message = "Supports retrieving connection-specific information"))]
Info { id: ConnectionId },
/// Kill a specific connection
#[strum_discriminants(strum(message = "Supports killing a remote connection"))]
Kill { id: ConnectionId },
/// Retrieve list of connections being managed
#[strum_discriminants(strum(message = "Supports retrieving a list of managed connections"))]
List,
/// Signals the manager to shutdown
#[strum_discriminants(strum(message = "Supports being shut down on demand"))]
Shutdown,
}

@ -1,4 +1,4 @@
use crate::{data::Error, ConnectionInfo, ConnectionList, Destination};
use crate::{data::Error, ConnectionInfo, ConnectionList, Destination, ManagerCapabilities};
use crate::{ChannelId, ConnectionId, DistantMsg, DistantResponseData};
use distant_net::Response;
use serde::{Deserialize, Serialize};
@ -15,6 +15,9 @@ pub enum ManagerResponse {
/// Indicates that some error occurred during a request
Error(Error),
/// Response to retrieving information about the manager's capabilities
Capabilities { supported: ManagerCapabilities },
/// Confirmation of a distant server being launched
Launched {
/// Updated location of the spawned server

@ -1,6 +1,6 @@
use crate::{
ChannelId, ConnectionId, ConnectionInfo, ConnectionList, Destination, ManagerRequest,
ManagerResponse, Map,
ChannelId, ConnectionId, ConnectionInfo, ConnectionList, Destination, ManagerCapabilities,
ManagerRequest, ManagerResponse, Map,
};
use async_trait::async_trait;
use distant_net::{
@ -217,6 +217,11 @@ impl DistantManager {
Ok(id)
}
/// Retrieves the list of supported capabilities for this manager
async fn capabilities(&self) -> io::Result<ManagerCapabilities> {
Ok(ManagerCapabilities::all())
}
/// Retrieves information about the connection to the server with the specified `id`
async fn info(&self, id: ConnectionId) -> io::Result<ConnectionInfo> {
match self.connections.read().await.get(&id) {
@ -297,6 +302,10 @@ impl Server for DistantManager {
} = ctx;
let response = match request.payload {
ManagerRequest::Capabilities {} => match self.capabilities().await {
Ok(supported) => ManagerResponse::Capabilities { supported },
Err(x) => ManagerResponse::Error(x.into()),
},
ManagerRequest::Launch {
destination,
options,

@ -7,7 +7,8 @@ use async_once_cell::OnceCell;
use async_trait::async_trait;
use distant_core::{
data::{
DirEntry, Environment, FileType, Metadata, ProcessId, PtySize, SystemInfo, UnixMetadata,
Capabilities, DirEntry, Environment, FileType, Metadata, ProcessId, PtySize, SystemInfo,
UnixMetadata,
},
DistantApi, DistantCtx,
};
@ -78,6 +79,12 @@ impl DistantApi for SshDistantApi {
local_data.global_processes = Arc::downgrade(&self.processes);
}
async fn capabilities(&self, ctx: DistantCtx<Self::LocalData>) -> io::Result<Capabilities> {
debug!("[Conn {}] Querying capabilities", ctx.connection_id);
Ok(Capabilities::all())
}
async fn read_file(
&self,
ctx: DistantCtx<Self::LocalData>,

@ -318,5 +318,21 @@ fn format_shell(data: DistantResponseData) -> Output {
)
.into_bytes(),
),
DistantResponseData::Capabilities { supported } => {
#[derive(Tabled)]
struct EntryRow {
kind: String,
description: String,
}
let table = Table::new(supported.into_sorted_vec().into_iter().map(|cap| EntryRow {
kind: cap.kind,
description: cap.description,
}))
.with(Style::ascii())
.with(Modify::new(Rows::new(..)).with(Alignment::left()));
Output::Stdout(table.to_string().into_bytes())
}
}
}

@ -49,6 +49,12 @@ pub enum ManagerSubcommand {
network: NetworkConfig,
},
/// Retrieve a list of capabilities that the manager supports
Capabilities {
#[clap(flatten)]
network: NetworkConfig,
},
/// Retrieve information about a specific connection
Info {
id: ConnectionId,
@ -326,6 +332,35 @@ impl ManagerSubcommand {
Ok(())
}
Self::Capabilities { network } => {
let network = network.merge(config.network);
debug!("Getting list of capabilities");
let caps = Client::new(network)
.connect()
.await
.context("Failed to connect to manager")?
.capabilities()
.await
.context("Failed to get list of capabilities")?;
#[derive(Tabled)]
struct CapabilityRow {
kind: String,
description: String,
}
println!(
"{}",
Table::new(caps.into_sorted_vec().into_iter().map(|cap| {
CapabilityRow {
kind: cap.kind,
description: cap.description,
}
}))
);
Ok(())
}
Self::Info { network, id } => {
let network = network.merge(config.network);
debug!("Getting info about connection {}", id);

@ -0,0 +1,63 @@
use crate::cli::fixtures::*;
use assert_cmd::Command;
use indoc::indoc;
use rstest::*;
const EXPECTED_TABLE: &str = indoc! {"
+------------------+------------------------------------------------------------------+
| kind | description |
+------------------+------------------------------------------------------------------+
| capabilities | Supports retrieving capabilities |
+------------------+------------------------------------------------------------------+
| copy | Supports copying files, directories, and symlinks |
+------------------+------------------------------------------------------------------+
| dir_create | Supports creating directory |
+------------------+------------------------------------------------------------------+
| dir_read | Supports reading directory |
+------------------+------------------------------------------------------------------+
| exists | Supports checking if a path exists |
+------------------+------------------------------------------------------------------+
| file_append | Supports appending to binary file |
+------------------+------------------------------------------------------------------+
| file_append_text | Supports appending to text file |
+------------------+------------------------------------------------------------------+
| file_read | Supports reading binary file |
+------------------+------------------------------------------------------------------+
| file_read_text | Supports reading text file |
+------------------+------------------------------------------------------------------+
| file_write | Supports writing binary file |
+------------------+------------------------------------------------------------------+
| file_write_text | Supports writing text file |
+------------------+------------------------------------------------------------------+
| metadata | Supports retrieving metadata about a file, directory, or symlink |
+------------------+------------------------------------------------------------------+
| proc_kill | Supports killing a spawned process |
+------------------+------------------------------------------------------------------+
| proc_resize_pty | Supports resizing the pty of a spawned process |
+------------------+------------------------------------------------------------------+
| proc_spawn | Supports spawning a process |
+------------------+------------------------------------------------------------------+
| proc_stdin | Supports sending stdin to a spawned process |
+------------------+------------------------------------------------------------------+
| remove | Supports removing files, directories, and symlinks |
+------------------+------------------------------------------------------------------+
| rename | Supports renaming files, directories, and symlinks |
+------------------+------------------------------------------------------------------+
| system_info | Supports retrieving system information |
+------------------+------------------------------------------------------------------+
| unwatch | Supports unwatching filesystem for changes |
+------------------+------------------------------------------------------------------+
| watch | Supports watching filesystem for changes |
+------------------+------------------------------------------------------------------+
"};
#[rstest]
fn should_output_capabilities(mut action_cmd: CtxCommand<Command>) {
// distant action capabilities
action_cmd
.arg("capabilities")
.assert()
.success()
.stdout(EXPECTED_TABLE)
.stderr("");
}

@ -1,3 +1,4 @@
mod capabilities;
mod copy;
mod dir_create;
mod dir_read;

@ -0,0 +1,39 @@
use crate::cli::fixtures::*;
use indoc::indoc;
use rstest::*;
const EXPECTED_TABLE: &str = indoc! {"
+---------------+--------------------------------------------------------------+
| kind | description |
+---------------+--------------------------------------------------------------+
| capabilities | Supports retrieving capabilities |
+---------------+--------------------------------------------------------------+
| channel | Supports sending data through a channel with a remote server |
+---------------+--------------------------------------------------------------+
| close_channel | Supports closing a channel with a remote server |
+---------------+--------------------------------------------------------------+
| connect | Supports connecting to remote servers |
+---------------+--------------------------------------------------------------+
| info | Supports retrieving connection-specific information |
+---------------+--------------------------------------------------------------+
| kill | Supports killing a remote connection |
+---------------+--------------------------------------------------------------+
| launch | Supports launching distant on remote servers |
+---------------+--------------------------------------------------------------+
| list | Supports retrieving a list of managed connections |
+---------------+--------------------------------------------------------------+
| open_channel | Supports opening a channel with a remote server |
+---------------+--------------------------------------------------------------+
| shutdown | Supports being shut down on demand |
+---------------+--------------------------------------------------------------+
"};
#[rstest]
fn should_output_capabilities(ctx: DistantManagerCtx) {
// distant action capabilities
ctx.new_assert_cmd(vec!["manager", "capabilities"])
.assert()
.success()
.stdout(format!("{EXPECTED_TABLE}\n"))
.stderr("");
}

@ -0,0 +1 @@
mod capabilities;

@ -1,5 +1,6 @@
mod action;
mod fixtures;
mod manager;
mod repl;
mod scripts;
mod utils;

@ -0,0 +1,33 @@
use crate::cli::fixtures::*;
use distant_core::data::{Capabilities, Capability};
use rstest::*;
use serde_json::json;
#[rstest]
#[tokio::test]
async fn should_support_json_capabilities(mut json_repl: CtxCommand<Repl>) {
let id = rand::random::<u64>().to_string();
let req = json!({
"id": id,
"payload": { "type": "capabilities" },
});
let res = json_repl.write_and_read_json(req).await.unwrap().unwrap();
assert_eq!(res["origin_id"], id);
assert_eq!(res["payload"]["type"], "capabilities");
let supported: Capabilities = res["payload"]["supported"]
.as_array()
.expect("Field 'supported' was not an array")
.iter()
.map(|value| {
serde_json::from_value::<Capability>(value.clone())
.expect("Could not read array value as capability")
})
.collect();
// NOTE: Our local server api should always support all capabilities since it is the reference
// implementation for our api
assert_eq!(supported, Capabilities::all());
}

@ -1,3 +1,4 @@
mod capabilities;
mod copy;
mod dir_create;
mod dir_read;

Loading…
Cancel
Save