Initial commit -- got most things working, but tests for manager version are hanging

pull/219/head
Chip Senkbeil 11 months ago
parent 0efb5aee4c
commit 33f06652dd
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

@ -12,12 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for `--shell` with optional path to an explicit shell as an option - Support for `--shell` with optional path to an explicit shell as an option
when executing `distant spawn` in order to run the command within a shell when executing `distant spawn` in order to run the command within a shell
rather than directly rather than directly
- `semver` crate to be used for version information in protocol and manager
### Changed ### Changed
- `distant_protocol::PROTOCOL_VERSION` now uses the crate's major, minor, and - `distant_protocol::PROTOCOL_VERSION` now uses the crate's major, minor, and
patch version at compile-time (parsed via `const-str` crate) to streamline patch version at compile-time (parsed via `const-str` crate) to streamline
version handling between crate and protocol version handling between crate and protocol
- Protocol and manager now supply a version request instead of capabilities and
the capabilities of protocol are now a `Vec<String>` to contain a set of more
broad capabilities instead of every possible request type
### Fixed ### Fixed
@ -30,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Cmd::program` and `Cmd::arguments` functions as they were misleading (didn't - `Cmd::program` and `Cmd::arguments` functions as they were misleading (didn't
do what `distant-local` or `distant-ssh2` do) do what `distant-local` or `distant-ssh2` do)
- Removed `Capability` and `Capabilities` from protocol and manager
## [0.20.0-alpha.12] ## [0.20.0-alpha.12]

5
Cargo.lock generated

@ -944,6 +944,7 @@ dependencies = [
"rand", "rand",
"rmp", "rmp",
"rmp-serde", "rmp-serde",
"semver 1.0.17",
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
@ -964,6 +965,7 @@ dependencies = [
"regex", "regex",
"rmp", "rmp",
"rmp-serde", "rmp-serde",
"semver 1.0.17",
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
@ -2776,6 +2778,9 @@ name = "semver"
version = "1.0.17" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "semver-parser" name = "semver-parser"

@ -3,10 +3,10 @@ use std::time::SystemTime;
use std::{env, io}; use std::{env, io};
use async_trait::async_trait; use async_trait::async_trait;
use distant_core::protocol::semver;
use distant_core::protocol::{ use distant_core::protocol::{
Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, Permissions, ProcessId,
Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, Version, PROTOCOL_VERSION,
Version, PROTOCOL_VERSION,
}; };
use distant_core::{DistantApi, DistantCtx}; use distant_core::{DistantApi, DistantCtx};
use ignore::{DirEntry as WalkDirEntry, WalkBuilder}; use ignore::{DirEntry as WalkDirEntry, WalkBuilder};
@ -635,10 +635,32 @@ impl DistantApi for Api {
async fn version(&self, ctx: DistantCtx) -> io::Result<Version> { async fn version(&self, ctx: DistantCtx) -> io::Result<Version> {
debug!("[Conn {}] Querying version", ctx.connection_id); debug!("[Conn {}] Querying version", ctx.connection_id);
// Parse our server's version
let mut server_version: semver::Version = env!("CARGO_PKG_VERSION")
.parse()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
// Add the package name to the version information
if server_version.build.is_empty() {
server_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME"))
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
} else {
let raw_build_str = format!(
"{}.{}",
server_version.build.as_str(),
env!("CARGO_PKG_NAME")
);
server_version.build = semver::BuildMetadata::new(&raw_build_str)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
}
Ok(Version { Ok(Version {
server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), server_version,
protocol_version: PROTOCOL_VERSION, protocol_version: PROTOCOL_VERSION,
capabilities: Capabilities::all(), capabilities: Version::capabilities()
.iter()
.map(ToString::to_string)
.collect(),
}) })
} }
} }

@ -28,6 +28,7 @@ rand = { version = "0.8.5", features = ["getrandom"] }
rmp = "0.8.11" rmp = "0.8.11"
rmp-serde = "1.1.1" rmp-serde = "1.1.1"
sha2 = "0.10.6" sha2 = "0.10.6"
semver = { version = "1.0.17", features = ["serde"] }
serde = { version = "1.0.163", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] }
serde_bytes = "0.11.9" serde_bytes = "0.11.9"
serde_json = "1.0.96" serde_json = "1.0.96"

@ -7,7 +7,7 @@ use log::*;
use crate::client::Client; use crate::client::Client;
use crate::common::{ConnectionId, Destination, Map, Request}; use crate::common::{ConnectionId, Destination, Map, Request};
use crate::manager::data::{ use crate::manager::data::{
ConnectionInfo, ConnectionList, ManagerCapabilities, ManagerRequest, ManagerResponse, ConnectionInfo, ConnectionList, ManagerRequest, ManagerResponse, SemVer,
}; };
mod channel; mod channel;
@ -231,12 +231,12 @@ impl ManagerClient {
RawChannel::spawn(connection_id, self).await RawChannel::spawn(connection_id, self).await
} }
/// Retrieves a list of supported capabilities /// Retrieves the version of the manager.
pub async fn capabilities(&mut self) -> io::Result<ManagerCapabilities> { pub async fn version(&mut self) -> io::Result<SemVer> {
trace!("capabilities()"); trace!("version()");
let res = self.send(ManagerRequest::Capabilities).await?; let res = self.send(ManagerRequest::Version).await?;
match res.payload { match res.payload {
ManagerResponse::Capabilities { supported } => Ok(supported), ManagerResponse::Version(version) => Ok(version),
ManagerResponse::Error { description } => { ManagerResponse::Error { description } => {
Err(io::Error::new(io::ErrorKind::Other, description)) Err(io::Error::new(io::ErrorKind::Other, description))
} }

@ -1,8 +1,6 @@
pub type ManagerChannelId = u32; pub type ManagerChannelId = u32;
pub type ManagerAuthenticationId = u32; pub type ManagerAuthenticationId = u32;
pub use semver::Version as SemVer;
mod capabilities;
pub use capabilities::*;
mod info; mod info;
pub use info::*; pub use info::*;

@ -1,189 +0,0 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::ops::{BitAnd, BitOr, BitXor};
use std::str::FromStr;
use derive_more::{From, Into, IntoIterator};
use serde::{Deserialize, Serialize};
use strum::{EnumMessage, IntoEnumIterator};
use super::ManagerCapabilityKind;
/// Set of supported capabilities for a manager
#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
#[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)
}
/// Removes the capability with the described kind, returning true if it existed
pub fn remove(&mut self, kind: impl AsRef<str>) -> bool {
let cap = ManagerCapability {
kind: kind.as_ref().to_string(),
description: String::new(),
};
self.0.remove(&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
}
}
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)]
#[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(),
}
}
}

@ -1,36 +1,17 @@
use derive_more::IsVariant;
use distant_auth::msg::AuthenticationResponse; use distant_auth::msg::AuthenticationResponse;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString};
use super::{ManagerAuthenticationId, ManagerChannelId}; use super::{ManagerAuthenticationId, ManagerChannelId};
use crate::common::{ConnectionId, Destination, Map, UntypedRequest}; use crate::common::{ConnectionId, Destination, Map, UntypedRequest};
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, EnumDiscriminants, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[strum_discriminants(derive(
AsRefStr,
strum::Display,
EnumIter,
EnumMessage,
EnumString,
Hash,
PartialOrd,
Ord,
IsVariant,
Serialize,
Deserialize
))]
#[strum_discriminants(name(ManagerCapabilityKind))]
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")] #[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
pub enum ManagerRequest { pub enum ManagerRequest {
/// Retrieve information about the server's capabilities /// Retrieve information about the manager's version.
#[strum_discriminants(strum(message = "Supports retrieving capabilities"))] Version,
Capabilities,
/// Launch a server using the manager /// Launch a server using the manager
#[strum_discriminants(strum(message = "Supports launching a server on remote machines"))]
Launch { Launch {
// NOTE: Boxed per clippy's large_enum_variant warning // NOTE: Boxed per clippy's large_enum_variant warning
destination: Box<Destination>, destination: Box<Destination>,
@ -40,7 +21,6 @@ pub enum ManagerRequest {
}, },
/// Initiate a connection through the manager /// Initiate a connection through the manager
#[strum_discriminants(strum(message = "Supports connecting to remote servers"))]
Connect { Connect {
// NOTE: Boxed per clippy's large_enum_variant warning // NOTE: Boxed per clippy's large_enum_variant warning
destination: Box<Destination>, destination: Box<Destination>,
@ -50,7 +30,6 @@ pub enum ManagerRequest {
}, },
/// Submit some authentication message for the manager to use with an active connection /// Submit some authentication message for the manager to use with an active connection
#[strum_discriminants(strum(message = "Supports authenticating with a remote server"))]
Authenticate { Authenticate {
/// Id of the authentication request that is being responded to /// Id of the authentication request that is being responded to
id: ManagerAuthenticationId, id: ManagerAuthenticationId,
@ -60,16 +39,12 @@ pub enum ManagerRequest {
}, },
/// Opens a channel for communication with an already-connected server /// Opens a channel for communication with an already-connected server
#[strum_discriminants(strum(message = "Supports opening a channel with a remote server"))]
OpenChannel { OpenChannel {
/// Id of the connection /// Id of the connection
id: ConnectionId, id: ConnectionId,
}, },
/// Sends data through channel /// Sends data through channel
#[strum_discriminants(strum(
message = "Supports sending data through a channel with a remote server"
))]
Channel { Channel {
/// Id of the channel /// Id of the channel
id: ManagerChannelId, id: ManagerChannelId,
@ -79,21 +54,17 @@ pub enum ManagerRequest {
}, },
/// Closes an open channel /// Closes an open channel
#[strum_discriminants(strum(message = "Supports closing a channel with a remote server"))]
CloseChannel { CloseChannel {
/// Id of the channel to close /// Id of the channel to close
id: ManagerChannelId, id: ManagerChannelId,
}, },
/// Retrieve information about a specific connection /// Retrieve information about a specific connection
#[strum_discriminants(strum(message = "Supports retrieving connection-specific information"))]
Info { id: ConnectionId }, Info { id: ConnectionId },
/// Kill a specific connection /// Kill a specific connection
#[strum_discriminants(strum(message = "Supports killing a remote connection"))]
Kill { id: ConnectionId }, Kill { id: ConnectionId },
/// Retrieve list of connections being managed /// Retrieve list of connections being managed
#[strum_discriminants(strum(message = "Supports retrieving a list of managed connections"))]
List, List,
} }

@ -1,9 +1,7 @@
use distant_auth::msg::Authentication; use distant_auth::msg::Authentication;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ use super::{ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerChannelId, SemVer};
ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerCapabilities, ManagerChannelId,
};
use crate::common::{ConnectionId, Destination, UntypedResponse}; use crate::common::{ConnectionId, Destination, UntypedResponse};
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -15,8 +13,8 @@ pub enum ManagerResponse {
/// Indicates that some error occurred during a request /// Indicates that some error occurred during a request
Error { description: String }, Error { description: String },
/// Response to retrieving information about the manager's capabilities /// Information about the manager's version.
Capabilities { supported: ManagerCapabilities }, Version(SemVer),
/// Confirmation of a server being launched /// Confirmation of a server being launched
Launched { Launched {

@ -9,8 +9,8 @@ use tokio::sync::{oneshot, RwLock};
use crate::common::{ConnectionId, Destination, Map}; use crate::common::{ConnectionId, Destination, Map};
use crate::manager::{ use crate::manager::{
ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerCapabilities, ManagerChannelId, ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerChannelId, ManagerRequest,
ManagerRequest, ManagerResponse, ManagerResponse, SemVer,
}; };
use crate::server::{RequestCtx, Server, ServerHandler}; use crate::server::{RequestCtx, Server, ServerHandler};
@ -138,9 +138,11 @@ impl ManagerServer {
Ok(id) Ok(id)
} }
/// Retrieves the list of supported capabilities for this manager /// Retrieves the manager's version.
async fn capabilities(&self) -> io::Result<ManagerCapabilities> { async fn version(&self) -> io::Result<SemVer> {
Ok(ManagerCapabilities::all()) env!("CARGO_PKG_VERSION")
.parse()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))
} }
/// Retrieves information about the connection to the server with the specified `id` /// Retrieves information about the connection to the server with the specified `id`
@ -196,10 +198,10 @@ impl ServerHandler for ManagerServer {
} = ctx; } = ctx;
let response = match request.payload { let response = match request.payload {
ManagerRequest::Capabilities {} => { ManagerRequest::Version {} => {
debug!("Looking up capabilities"); debug!("Looking up version");
match self.capabilities().await { match self.version().await {
Ok(supported) => ManagerResponse::Capabilities { supported }, Ok(version) => ManagerResponse::Version(version),
Err(x) => ManagerResponse::from(x), Err(x) => ManagerResponse::from(x),
} }
} }

@ -20,6 +20,7 @@ bitflags = "2.3.1"
const-str = "0.5.6" const-str = "0.5.6"
derive_more = { version = "0.99.17", default-features = false, features = ["deref", "deref_mut", "display", "from", "error", "into", "into_iterator", "is_variant"] } derive_more = { version = "0.99.17", default-features = false, features = ["deref", "deref_mut", "display", "from", "error", "into", "into_iterator", "is_variant"] }
regex = "1.8.3" regex = "1.8.3"
semver = { version = "1.0.17", features = ["serde"] }
serde = { version = "1.0.163", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] }
serde_bytes = "0.11.9" serde_bytes = "0.11.9"
strum = { version = "0.24.1", features = ["derive"] } strum = { version = "0.24.1", features = ["derive"] }

@ -1,4 +1,3 @@
mod capabilities;
mod change; mod change;
mod cmd; mod cmd;
mod error; mod error;
@ -10,7 +9,6 @@ mod search;
mod system; mod system;
mod version; mod version;
pub use capabilities::*;
pub use change::*; pub use change::*;
pub use cmd::*; pub use cmd::*;
pub use error::*; pub use error::*;
@ -24,6 +22,3 @@ pub use version::*;
/// Id for a remote process /// Id for a remote process
pub type ProcessId = u32; pub type ProcessId = u32;
/// Version indicated by the tuple of (major, minor, patch).
pub type SemVer = (u8, u8, u8);

@ -1,380 +0,0 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::ops::{BitAnd, BitOr, BitXor, Deref, DerefMut};
use std::str::FromStr;
use derive_more::{From, Into, IntoIterator};
use serde::{Deserialize, Serialize};
use strum::{EnumMessage, IntoEnumIterator};
/// Represents the kinds of capabilities available.
pub use crate::request::RequestKind as CapabilityKind;
/// Set of supported capabilities for a server
#[derive(Clone, Debug, From, Into, PartialEq, Eq, IntoIterator, Serialize, Deserialize)]
#[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)
}
/// Removes the capability with the described kind, returning true if it existed
pub fn remove(&mut self, kind: impl AsRef<str>) -> bool {
let cap = Capability {
kind: kind.as_ref().to_string(),
description: String::new(),
};
self.0.remove(&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
}
}
impl AsRef<HashSet<Capability>> for Capabilities {
fn as_ref(&self) -> &HashSet<Capability> {
&self.0
}
}
impl AsMut<HashSet<Capability>> for Capabilities {
fn as_mut(&mut self) -> &mut HashSet<Capability> {
&mut self.0
}
}
impl Deref for Capabilities {
type Target = HashSet<Capability>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Capabilities {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
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)]
#[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(test)]
mod tests {
use super::*;
mod capabilities {
use super::*;
#[test]
fn should_be_able_to_serialize_to_json() {
let capabilities: Capabilities = [Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}]
.into_iter()
.collect();
let value = serde_json::to_value(capabilities).unwrap();
assert_eq!(
value,
serde_json::json!([
{
"kind": "some kind",
"description": "some description",
}
])
);
}
#[test]
fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!([
{
"kind": "some kind",
"description": "some description",
}
]);
let capabilities: Capabilities = serde_json::from_value(value).unwrap();
assert_eq!(
capabilities,
[Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}]
.into_iter()
.collect()
);
}
#[test]
fn should_be_able_to_serialize_to_msgpack() {
let capabilities: Capabilities = [Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}]
.into_iter()
.collect();
// NOTE: We don't actually check the output here because it's an implementation detail
// and could change as we change how serialization is done. This is merely to verify
// that we can serialize since there are times when serde fails to serialize at
// runtime.
let _ = rmp_serde::encode::to_vec_named(&capabilities).unwrap();
}
#[test]
fn should_be_able_to_deserialize_from_msgpack() {
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
// verify that we are not corrupting or preventing issues when serializing on a
// client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(
&[Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}]
.into_iter()
.collect::<Capabilities>(),
)
.unwrap();
let capabilities: Capabilities = rmp_serde::decode::from_slice(&buf).unwrap();
assert_eq!(
capabilities,
[Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}]
.into_iter()
.collect()
);
}
}
mod capability {
use super::*;
#[test]
fn should_be_able_to_serialize_to_json() {
let capability = Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
};
let value = serde_json::to_value(capability).unwrap();
assert_eq!(
value,
serde_json::json!({
"kind": "some kind",
"description": "some description",
})
);
}
#[test]
fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({
"kind": "some kind",
"description": "some description",
});
let capability: Capability = serde_json::from_value(value).unwrap();
assert_eq!(
capability,
Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}
);
}
#[test]
fn should_be_able_to_serialize_to_msgpack() {
let capability = Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
};
// NOTE: We don't actually check the output here because it's an implementation detail
// and could change as we change how serialization is done. This is merely to verify
// that we can serialize since there are times when serde fails to serialize at
// runtime.
let _ = rmp_serde::encode::to_vec_named(&capability).unwrap();
}
#[test]
fn should_be_able_to_deserialize_from_msgpack() {
// NOTE: It may seem odd that we are serializing just to deserialize, but this is to
// verify that we are not corrupting or causing issues when serializing on a
// client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
})
.unwrap();
let capability: Capability = rmp_serde::decode::from_slice(&buf).unwrap();
assert_eq!(
capability,
Capability {
kind: "some kind".to_string(),
description: "some description".to_string(),
}
);
}
}
}

@ -1,48 +1,80 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::common::{Capabilities, SemVer}; use crate::semver;
/// Represents version information. /// Represents version information.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Version { pub struct Version {
/// General version of server (arbitrary format) /// Server version.
pub server_version: String, pub server_version: semver::Version,
/// Protocol version /// Protocol version.
pub protocol_version: SemVer, pub protocol_version: semver::Version,
/// Capabilities of the server /// Additional features available.
pub capabilities: Capabilities, #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
}
impl Version {
/// Supports executing processes.
pub const CAP_EXEC: &'static str = "exec";
/// Supports reading and writing via filesystem IO.
pub const CAP_FS_IO: &'static str = "fs_io";
/// Supports modifying permissions of filesystem.
pub const CAP_FS_PERM: &'static str = "fs_perm";
/// Supports searching filesystem.
pub const CAP_FS_SEARCH: &'static str = "fs_search";
/// Supports watching filesystem for changes.
pub const CAP_FS_WATCH: &'static str = "fs_watch";
/// Supports TCP tunneling.
// pub const CAP_TCP_TUNNEL: &'static str = "tcp_tunnel";
/// Supports TCP reverse tunneling.
// pub const CAP_TCP_REV_TUNNEL: &'static str = "tcp_rev_tunnel";
/// Supports retrieving system information.
pub const CAP_SYS_INFO: &'static str = "sys_info";
pub const fn capabilities() -> &'static [&'static str] {
&[
Self::CAP_EXEC,
Self::CAP_FS_IO,
Self::CAP_FS_PERM,
Self::CAP_FS_SEARCH,
Self::CAP_FS_WATCH,
/* Self::CAP_TCP_TUNNEL,
Self::CAP_TCP_REV_TUNNEL, */
Self::CAP_SYS_INFO,
]
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::common::Capability; use semver::Version as SemVer;
#[test] #[test]
fn should_be_able_to_serialize_to_json() { fn should_be_able_to_serialize_to_json() {
let version = Version { let version = Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: [Capability { capabilities: vec![String::from("cap")],
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
}; };
let value = serde_json::to_value(version).unwrap(); let value = serde_json::to_value(version).unwrap();
assert_eq!( assert_eq!(
value, value,
serde_json::json!({ serde_json::json!({
"server_version": "some version", "server_version": "123.456.789-rc+build",
"protocol_version": [1, 2, 3], "protocol_version": "1.2.3",
"capabilities": [{ "capabilities": ["cap"]
"kind": "some kind",
"description": "some description",
}]
}) })
); );
} }
@ -50,26 +82,18 @@ mod tests {
#[test] #[test]
fn should_be_able_to_deserialize_from_json() { fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({ let value = serde_json::json!({
"server_version": "some version", "server_version": "123.456.789-rc+build",
"protocol_version": [1, 2, 3], "protocol_version": "1.2.3",
"capabilities": [{ "capabilities": ["cap"]
"kind": "some kind",
"description": "some description",
}]
}); });
let version: Version = serde_json::from_value(value).unwrap(); let version: Version = serde_json::from_value(value).unwrap();
assert_eq!( assert_eq!(
version, version,
Version { Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: [Capability { capabilities: vec![String::from("cap")],
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
} }
); );
} }
@ -77,14 +101,9 @@ mod tests {
#[test] #[test]
fn should_be_able_to_serialize_to_msgpack() { fn should_be_able_to_serialize_to_msgpack() {
let version = Version { let version = Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: [Capability { capabilities: vec![String::from("cap")],
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
}; };
// NOTE: We don't actually check the output here because it's an implementation detail // NOTE: We don't actually check the output here because it's an implementation detail
@ -101,14 +120,9 @@ mod tests {
// client/server and then trying to deserialize on the other side. This has happened // client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify. // enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Version { let buf = rmp_serde::encode::to_vec_named(&Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: [Capability { capabilities: vec![String::from("cap")],
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
}) })
.unwrap(); .unwrap();
@ -116,14 +130,9 @@ mod tests {
assert_eq!( assert_eq!(
version, version,
Version { Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: [Capability { capabilities: vec![String::from("cap")],
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
} }
); );
} }

@ -14,14 +14,15 @@ pub use common::*;
pub use msg::*; pub use msg::*;
pub use request::*; pub use request::*;
pub use response::*; pub use response::*;
pub use semver;
/// Protocol version indicated by the tuple of (major, minor, patch). /// Protocol version of major/minor/patch.
/// ///
/// This should match the version of this crate such that any significant change to the crate /// This should match the version of this crate such that any significant change to the crate
/// version will also be reflected in this constant that can be used to verify compatibility across /// version will also be reflected in this constant that can be used to verify compatibility across
/// the wire. /// the wire.
pub const PROTOCOL_VERSION: SemVer = ( pub const PROTOCOL_VERSION: semver::Version = semver::Version::new(
const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u8), const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64),
const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u8), const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u64),
const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u8), const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u64),
); );

@ -3,7 +3,6 @@ use std::path::PathBuf;
use derive_more::IsVariant; use derive_more::IsVariant;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString};
use crate::common::{ use crate::common::{
ChangeKind, Cmd, Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, ChangeKind, Cmd, Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions,
@ -14,26 +13,10 @@ use crate::utils;
pub type Environment = HashMap<String, String>; pub type Environment = HashMap<String, String>;
/// Represents the payload of a request to be performed on the remote machine /// Represents the payload of a request to be performed on the remote machine
#[derive(Clone, Debug, PartialEq, Eq, EnumDiscriminants, IsVariant, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, IsVariant, Serialize, Deserialize)]
#[strum_discriminants(derive(
AsRefStr,
strum::Display,
EnumIter,
EnumMessage,
EnumString,
Hash,
PartialOrd,
Ord,
IsVariant,
Serialize,
Deserialize
))]
#[strum_discriminants(name(RequestKind))]
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")] #[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")]
pub enum Request { pub enum Request {
/// Reads a file from the specified path on the remote machine /// Reads a file from the specified path on the remote machine
#[strum_discriminants(strum(message = "Supports reading binary file"))]
FileRead { FileRead {
/// The path to the file on the remote machine /// The path to the file on the remote machine
path: PathBuf, path: PathBuf,
@ -41,7 +24,6 @@ pub enum Request {
/// Reads a file from the specified path on the remote machine /// Reads a file from the specified path on the remote machine
/// and treats the contents as text /// and treats the contents as text
#[strum_discriminants(strum(message = "Supports reading text file"))]
FileReadText { FileReadText {
/// The path to the file on the remote machine /// The path to the file on the remote machine
path: PathBuf, path: PathBuf,
@ -49,7 +31,6 @@ pub enum Request {
/// Writes a file, creating it if it does not exist, and overwriting any existing content /// Writes a file, creating it if it does not exist, and overwriting any existing content
/// on the remote machine /// on the remote machine
#[strum_discriminants(strum(message = "Supports writing binary file"))]
FileWrite { FileWrite {
/// The path to the file on the remote machine /// The path to the file on the remote machine
path: PathBuf, path: PathBuf,
@ -61,7 +42,6 @@ pub enum Request {
/// Writes a file using text instead of bytes, creating it if it does not exist, /// Writes a file using text instead of bytes, creating it if it does not exist,
/// and overwriting any existing content on the remote machine /// and overwriting any existing content on the remote machine
#[strum_discriminants(strum(message = "Supports writing text file"))]
FileWriteText { FileWriteText {
/// The path to the file on the remote machine /// The path to the file on the remote machine
path: PathBuf, path: PathBuf,
@ -71,7 +51,6 @@ pub enum Request {
}, },
/// Appends to a file, creating it if it does not exist, on the remote machine /// 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 { FileAppend {
/// The path to the file on the remote machine /// The path to the file on the remote machine
path: PathBuf, path: PathBuf,
@ -82,7 +61,6 @@ pub enum Request {
}, },
/// Appends text to a file, creating it if it does not exist, on the remote machine /// 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 { FileAppendText {
/// The path to the file on the remote machine /// The path to the file on the remote machine
path: PathBuf, path: PathBuf,
@ -92,7 +70,6 @@ pub enum Request {
}, },
/// Reads a directory from the specified path on the remote machine /// Reads a directory from the specified path on the remote machine
#[strum_discriminants(strum(message = "Supports reading directory"))]
DirRead { DirRead {
/// The path to the directory on the remote machine /// The path to the directory on the remote machine
path: PathBuf, path: PathBuf,
@ -126,7 +103,6 @@ pub enum Request {
}, },
/// Creates a directory on the remote machine /// Creates a directory on the remote machine
#[strum_discriminants(strum(message = "Supports creating directory"))]
DirCreate { DirCreate {
/// The path to the directory on the remote machine /// The path to the directory on the remote machine
path: PathBuf, path: PathBuf,
@ -137,7 +113,6 @@ pub enum Request {
}, },
/// Removes a file or directory on the remote machine /// Removes a file or directory on the remote machine
#[strum_discriminants(strum(message = "Supports removing files, directories, and symlinks"))]
Remove { Remove {
/// The path to the file or directory on the remote machine /// The path to the file or directory on the remote machine
path: PathBuf, path: PathBuf,
@ -149,7 +124,6 @@ pub enum Request {
}, },
/// Copies a file or directory on the remote machine /// Copies a file or directory on the remote machine
#[strum_discriminants(strum(message = "Supports copying files, directories, and symlinks"))]
Copy { Copy {
/// The path to the file or directory on the remote machine /// The path to the file or directory on the remote machine
src: PathBuf, src: PathBuf,
@ -159,7 +133,6 @@ pub enum Request {
}, },
/// Moves/renames a file or directory on the remote machine /// Moves/renames a file or directory on the remote machine
#[strum_discriminants(strum(message = "Supports renaming files, directories, and symlinks"))]
Rename { Rename {
/// The path to the file or directory on the remote machine /// The path to the file or directory on the remote machine
src: PathBuf, src: PathBuf,
@ -169,7 +142,6 @@ pub enum Request {
}, },
/// Watches a path for changes /// Watches a path for changes
#[strum_discriminants(strum(message = "Supports watching filesystem for changes"))]
Watch { Watch {
/// The path to the file, directory, or symlink on the remote machine /// The path to the file, directory, or symlink on the remote machine
path: PathBuf, path: PathBuf,
@ -189,23 +161,18 @@ pub enum Request {
}, },
/// Unwatches a path for changes, meaning no additional changes will be reported /// Unwatches a path for changes, meaning no additional changes will be reported
#[strum_discriminants(strum(message = "Supports unwatching filesystem for changes"))]
Unwatch { Unwatch {
/// The path to the file, directory, or symlink on the remote machine /// The path to the file, directory, or symlink on the remote machine
path: PathBuf, path: PathBuf,
}, },
/// Checks whether the given path exists /// Checks whether the given path exists
#[strum_discriminants(strum(message = "Supports checking if a path exists"))]
Exists { Exists {
/// The path to the file or directory on the remote machine /// The path to the file or directory on the remote machine
path: PathBuf, path: PathBuf,
}, },
/// Retrieves filesystem metadata for the specified path on the remote machine /// 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 { Metadata {
/// The path to the file, directory, or symlink on the remote machine /// The path to the file, directory, or symlink on the remote machine
path: PathBuf, path: PathBuf,
@ -222,9 +189,6 @@ pub enum Request {
}, },
/// Sets permissions on a file, directory, or symlink on the remote machine /// Sets permissions on a file, directory, or symlink on the remote machine
#[strum_discriminants(strum(
message = "Supports setting permissions on a file, directory, or symlink"
))]
SetPermissions { SetPermissions {
/// The path to the file, directory, or symlink on the remote machine /// The path to the file, directory, or symlink on the remote machine
path: PathBuf, path: PathBuf,
@ -238,23 +202,18 @@ pub enum Request {
}, },
/// Searches filesystem using the provided query /// Searches filesystem using the provided query
#[strum_discriminants(strum(message = "Supports searching filesystem using queries"))]
Search { Search {
/// Query to perform against the filesystem /// Query to perform against the filesystem
query: SearchQuery, query: SearchQuery,
}, },
/// Cancels an active search being run against the filesystem /// Cancels an active search being run against the filesystem
#[strum_discriminants(strum(
message = "Supports canceling an active search against the filesystem"
))]
CancelSearch { CancelSearch {
/// Id of the search to cancel /// Id of the search to cancel
id: SearchId, id: SearchId,
}, },
/// Spawns a new process on the remote machine /// Spawns a new process on the remote machine
#[strum_discriminants(strum(message = "Supports spawning a process"))]
ProcSpawn { ProcSpawn {
/// The full command to run including arguments /// The full command to run including arguments
cmd: Cmd, cmd: Cmd,
@ -273,14 +232,12 @@ pub enum Request {
}, },
/// Kills a process running on the remote machine /// Kills a process running on the remote machine
#[strum_discriminants(strum(message = "Supports killing a spawned process"))]
ProcKill { ProcKill {
/// Id of the actively-running process /// Id of the actively-running process
id: ProcessId, id: ProcessId,
}, },
/// Sends additional data to stdin of running process /// Sends additional data to stdin of running process
#[strum_discriminants(strum(message = "Supports sending stdin to a spawned process"))]
ProcStdin { ProcStdin {
/// Id of the actively-running process to send stdin data /// Id of the actively-running process to send stdin data
id: ProcessId, id: ProcessId,
@ -291,7 +248,6 @@ pub enum Request {
}, },
/// Resize pty of remote process /// Resize pty of remote process
#[strum_discriminants(strum(message = "Supports resizing the pty of a spawned process"))]
ProcResizePty { ProcResizePty {
/// Id of the actively-running process whose pty to resize /// Id of the actively-running process whose pty to resize
id: ProcessId, id: ProcessId,
@ -301,11 +257,9 @@ pub enum Request {
}, },
/// Retrieve information about the server and the system it is on /// Retrieve information about the server and the system it is on
#[strum_discriminants(strum(message = "Supports retrieving system information"))]
SystemInfo {}, SystemInfo {},
/// Retrieve information about the server's protocol version /// Retrieve information about the server's protocol version
#[strum_discriminants(strum(message = "Supports retrieving version"))]
Version {}, Version {},
} }

@ -2013,19 +2013,14 @@ mod tests {
mod version { mod version {
use super::*; use super::*;
use crate::common::{Capabilities, Capability}; use crate::semver::Version as SemVer;
#[test] #[test]
fn should_be_able_to_serialize_to_json() { fn should_be_able_to_serialize_to_json() {
let payload = Response::Version(Version { let payload = Response::Version(Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: [Capability { capabilities: vec![String::from("cap")],
kind: String::from("some kind"),
description: String::from("some description"),
}]
.into_iter()
.collect(),
}); });
let value = serde_json::to_value(payload).unwrap(); let value = serde_json::to_value(payload).unwrap();
@ -2033,12 +2028,9 @@ mod tests {
value, value,
serde_json::json!({ serde_json::json!({
"type": "version", "type": "version",
"server_version": "some version", "server_version": "123.456.789-rc+build",
"protocol_version": [1, 2, 3], "protocol_version": "1.2.3",
"capabilities": [{ "capabilities": ["cap"],
"kind": "some kind",
"description": "some description",
}],
}) })
); );
} }
@ -2047,18 +2039,18 @@ mod tests {
fn should_be_able_to_deserialize_from_json() { fn should_be_able_to_deserialize_from_json() {
let value = serde_json::json!({ let value = serde_json::json!({
"type": "version", "type": "version",
"server_version": "some version", "server_version": "123.456.789-rc+build",
"protocol_version": [1, 2, 3], "protocol_version": "1.2.3",
"capabilities": Capabilities::all(), "capabilities": ["cap"],
}); });
let payload: Response = serde_json::from_value(value).unwrap(); let payload: Response = serde_json::from_value(value).unwrap();
assert_eq!( assert_eq!(
payload, payload,
Response::Version(Version { Response::Version(Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: Capabilities::all(), capabilities: vec![String::from("cap")],
}) })
); );
} }
@ -2066,9 +2058,9 @@ mod tests {
#[test] #[test]
fn should_be_able_to_serialize_to_msgpack() { fn should_be_able_to_serialize_to_msgpack() {
let payload = Response::Version(Version { let payload = Response::Version(Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: Capabilities::all(), capabilities: vec![String::from("cap")],
}); });
// NOTE: We don't actually check the errput here because it's an implementation detail // NOTE: We don't actually check the errput here because it's an implementation detail
@ -2085,9 +2077,9 @@ mod tests {
// client/server and then trying to deserialize on the other side. This has happened // client/server and then trying to deserialize on the other side. This has happened
// enough times with minor changes that we need tests to verify. // enough times with minor changes that we need tests to verify.
let buf = rmp_serde::encode::to_vec_named(&Response::Version(Version { let buf = rmp_serde::encode::to_vec_named(&Response::Version(Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: Capabilities::all(), capabilities: vec![String::from("cap")],
})) }))
.unwrap(); .unwrap();
@ -2095,9 +2087,9 @@ mod tests {
assert_eq!( assert_eq!(
payload, payload,
Response::Version(Version { Response::Version(Version {
server_version: String::from("some version"), server_version: "123.456.789-rc+build".parse().unwrap(),
protocol_version: (1, 2, 3), protocol_version: SemVer::new(1, 2, 3),
capabilities: Capabilities::all(), capabilities: vec![String::from("cap")],
}) })
); );
} }

@ -7,9 +7,10 @@ use std::time::Duration;
use async_compat::CompatExt; use async_compat::CompatExt;
use async_once_cell::OnceCell; use async_once_cell::OnceCell;
use async_trait::async_trait; use async_trait::async_trait;
use distant_core::protocol::semver;
use distant_core::protocol::{ use distant_core::protocol::{
Capabilities, CapabilityKind, DirEntry, Environment, FileType, Metadata, Permissions, DirEntry, Environment, FileType, Metadata, Permissions, ProcessId, PtySize,
ProcessId, PtySize, SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION, SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION,
}; };
use distant_core::{DistantApi, DistantCtx}; use distant_core::{DistantApi, DistantCtx};
use log::*; use log::*;
@ -936,18 +937,33 @@ impl DistantApi for SshDistantApi {
async fn version(&self, ctx: DistantCtx) -> io::Result<Version> { async fn version(&self, ctx: DistantCtx) -> io::Result<Version> {
debug!("[Conn {}] Querying capabilities", ctx.connection_id); debug!("[Conn {}] Querying capabilities", ctx.connection_id);
let mut capabilities = Capabilities::all(); let capabilities = vec![
Version::CAP_EXEC.to_string(),
// Searching is not supported by ssh implementation Version::CAP_FS_IO.to_string(),
// TODO: Could we have external search using ripgrep's JSON lines API? Version::CAP_SYS_INFO.to_string(),
capabilities.take(CapabilityKind::Search); ];
capabilities.take(CapabilityKind::CancelSearch);
// Parse our server's version
// Broken via wezterm-ssh, so not supported right now let mut server_version: semver::Version = env!("CARGO_PKG_VERSION")
capabilities.take(CapabilityKind::SetPermissions); .parse()
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
// Add the package name to the version information
if server_version.build.is_empty() {
server_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME"))
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
} else {
let raw_build_str = format!(
"{}.{}",
server_version.build.as_str(),
env!("CARGO_PKG_NAME")
);
server_version.build = semver::BuildMetadata::new(&raw_build_str)
.map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
}
Ok(Version { Ok(Version {
server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), server_version,
protocol_version: PROTOCOL_VERSION, protocol_version: PROTOCOL_VERSION,
capabilities, capabilities,
}) })

@ -7,10 +7,10 @@ use std::time::Duration;
use anyhow::Context; use anyhow::Context;
use distant_core::net::common::{ConnectionId, Host, Map, Request, Response}; use distant_core::net::common::{ConnectionId, Host, Map, Request, Response};
use distant_core::net::manager::ManagerClient; use distant_core::net::manager::ManagerClient;
use distant_core::protocol::semver;
use distant_core::protocol::{ use distant_core::protocol::{
self, Capabilities, ChangeKind, ChangeKindSet, FileType, Permissions, SearchQuery, self, ChangeKind, ChangeKindSet, FileType, Permissions, SearchQuery, SearchQueryContentsMatch,
SearchQueryContentsMatch, SearchQueryMatch, SearchQueryPathMatch, SetPermissionsOptions, SearchQueryMatch, SearchQueryPathMatch, SetPermissionsOptions, SystemInfo, Version,
SystemInfo,
}; };
use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher}; use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher};
use log::*; use log::*;
@ -581,32 +581,51 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
match format { match format {
Format::Shell => { Format::Shell => {
let (major, minor, patch) = distant_core::protocol::PROTOCOL_VERSION; let mut client_version: semver::Version = env!("CARGO_PKG_VERSION")
.parse()
.context("Failed to parse client version")?;
// Add the package name to the version information
if client_version.build.is_empty() {
client_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME"))
.context("Failed to define client build metadata")?;
} else {
let raw_build_str = format!(
"{}.{}",
client_version.build.as_str(),
env!("CARGO_PKG_NAME")
);
client_version.build = semver::BuildMetadata::new(&raw_build_str)
.context("Failed to define client build metadata")?;
}
println!( println!(
"Client: {} {} (Protocol {major}.{minor}.{patch})", "Client: {client_version} (Protocol {})",
env!("CARGO_PKG_NAME"), distant_core::protocol::PROTOCOL_VERSION
env!("CARGO_PKG_VERSION")
); );
let (major, minor, patch) = version.protocol_version;
println!( println!(
"Server: {} (Protocol {major}.{minor}.{patch})", "Server: {} (Protocol {})",
version.server_version version.server_version, version.protocol_version
); );
// Build a complete set of capabilities to show which ones we support // Build a complete set of capabilities to show which ones we support
let client_capabilities = Capabilities::all(); let mut capabilities: HashMap<String, u8> = Version::capabilities()
let server_capabilities = version.capabilities; .iter()
let mut capabilities: Vec<String> = client_capabilities .map(|cap| (cap.to_string(), 1))
.union(server_capabilities.as_ref()) .collect();
.map(|cap| {
let kind = &cap.kind; for cap in version.capabilities {
if client_capabilities.contains(kind) *capabilities.entry(cap).or_default() += 1;
&& server_capabilities.contains(kind) }
{
format!("+{kind}") let mut capabilities: Vec<String> = capabilities
.into_iter()
.map(|(cap, cnt)| {
if cnt > 1 {
format!("+{cap}")
} else { } else {
format!("-{kind}") format!("-{cap}")
} }
}) })
.collect(); .collect();

@ -228,41 +228,24 @@ async fn async_run(cmd: ManagerSubcommand) -> CliResult {
Ok(()) Ok(())
} }
ManagerSubcommand::Capabilities { format, network } => { ManagerSubcommand::Version { format, network } => {
debug!("Connecting to manager"); debug!("Connecting to manager");
let mut client = connect_to_manager(format, network).await?; let mut client = connect_to_manager(format, network).await?;
debug!("Getting list of capabilities"); debug!("Getting version");
let caps = client let version = client.version().await.context("Failed to get version")?;
.capabilities() debug!("Got version: {version}");
.await
.context("Failed to get list of capabilities")?;
debug!("Got capabilities: {caps:?}");
match format { match format {
Format::Json => { Format::Json => {
println!( println!(
"{}", "{}",
serde_json::to_string(&caps) serde_json::to_string(&version)
.context("Failed to format capabilities as json")? .context("Failed to format version as json")?
); );
} }
Format::Shell => { Format::Shell => {
#[derive(Tabled)] println!("{version}");
struct CapabilityRow {
kind: String,
description: String,
}
println!(
"{}",
Table::new(caps.into_sorted_vec().into_iter().map(|cap| {
CapabilityRow {
kind: cap.kind,
description: cap.description,
}
}))
);
} }
} }

@ -171,7 +171,7 @@ impl Options {
DistantSubcommand::Manager(cmd) => { DistantSubcommand::Manager(cmd) => {
update_logging!(manager); update_logging!(manager);
match cmd { match cmd {
ManagerSubcommand::Capabilities { network, .. } => { ManagerSubcommand::Version { network, .. } => {
network.merge(config.manager.network); network.merge(config.manager.network);
} }
ManagerSubcommand::Info { network, .. } => { ManagerSubcommand::Info { network, .. } => {
@ -1054,7 +1054,7 @@ pub enum ManagerSubcommand {
}, },
/// Retrieve a list of capabilities that the manager supports /// Retrieve a list of capabilities that the manager supports
Capabilities { Version {
#[clap(short, long, default_value_t, value_enum)] #[clap(short, long, default_value_t, value_enum)]
format: Format, format: Format,
@ -1111,7 +1111,7 @@ impl ManagerSubcommand {
Self::Select { format, .. } => *format, Self::Select { format, .. } => *format,
Self::Service(_) => Format::Shell, Self::Service(_) => Format::Shell,
Self::Listen { .. } => Format::Shell, Self::Listen { .. } => Format::Shell,
Self::Capabilities { format, .. } => *format, Self::Version { format, .. } => *format,
Self::Info { format, .. } => *format, Self::Info { format, .. } => *format,
Self::List { format, .. } => *format, Self::List { format, .. } => *format,
Self::Kill { format, .. } => *format, Self::Kill { format, .. } => *format,
@ -3505,7 +3505,7 @@ mod tests {
log_file: None, log_file: None,
log_level: None, log_level: None,
}, },
command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { command: DistantSubcommand::Manager(ManagerSubcommand::Version {
format: Format::Json, format: Format::Json,
network: NetworkSettings { network: NetworkSettings {
unix_socket: None, unix_socket: None,
@ -3537,7 +3537,7 @@ mod tests {
log_file: Some(PathBuf::from("config-log-file")), log_file: Some(PathBuf::from("config-log-file")),
log_level: Some(LogLevel::Trace), log_level: Some(LogLevel::Trace),
}, },
command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { command: DistantSubcommand::Manager(ManagerSubcommand::Version {
format: Format::Json, format: Format::Json,
network: NetworkSettings { network: NetworkSettings {
unix_socket: Some(PathBuf::from("config-unix-socket")), unix_socket: Some(PathBuf::from("config-unix-socket")),
@ -3556,7 +3556,7 @@ mod tests {
log_file: Some(PathBuf::from("cli-log-file")), log_file: Some(PathBuf::from("cli-log-file")),
log_level: Some(LogLevel::Info), log_level: Some(LogLevel::Info),
}, },
command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { command: DistantSubcommand::Manager(ManagerSubcommand::Version {
format: Format::Json, format: Format::Json,
network: NetworkSettings { network: NetworkSettings {
unix_socket: Some(PathBuf::from("cli-unix-socket")), unix_socket: Some(PathBuf::from("cli-unix-socket")),
@ -3588,7 +3588,7 @@ mod tests {
log_file: Some(PathBuf::from("cli-log-file")), log_file: Some(PathBuf::from("cli-log-file")),
log_level: Some(LogLevel::Info), log_level: Some(LogLevel::Info),
}, },
command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { command: DistantSubcommand::Manager(ManagerSubcommand::Version {
format: Format::Json, format: Format::Json,
network: NetworkSettings { network: NetworkSettings {
unix_socket: Some(PathBuf::from("cli-unix-socket")), unix_socket: Some(PathBuf::from("cli-unix-socket")),

@ -1,4 +1,5 @@
use distant_core::protocol::{Capabilities, Capability, SemVer, PROTOCOL_VERSION}; use distant_core::protocol::semver::Version as SemVer;
use distant_core::protocol::{Version, PROTOCOL_VERSION};
use rstest::*; use rstest::*;
use serde_json::json; use serde_json::json;
use test_log::test; use test_log::test;
@ -25,17 +26,17 @@ async fn should_support_json_capabilities(mut api_process: CtxCommand<ApiProcess
serde_json::from_value(res["payload"]["protocol_version"].clone()).unwrap(); serde_json::from_value(res["payload"]["protocol_version"].clone()).unwrap();
assert_eq!(protocol_version, PROTOCOL_VERSION); assert_eq!(protocol_version, PROTOCOL_VERSION);
let capabilities: Capabilities = res["payload"]["capabilities"] let capabilities: Vec<String> = res["payload"]["capabilities"]
.as_array() .as_array()
.expect("Field 'supported' was not an array") .expect("Field 'capabilities' was not an array")
.iter() .iter()
.map(|value| { .map(|value| {
serde_json::from_value::<Capability>(value.clone()) serde_json::from_value::<String>(value.clone())
.expect("Could not read array value as capability") .expect("Could not read array value as string")
}) })
.collect(); .collect();
// NOTE: Our local server api should always support all capabilities since it is the reference // NOTE: Our local server api should always support all capabilities since it is the reference
// implementation for our api // implementation for our api
assert_eq!(capabilities, Capabilities::all()); assert_eq!(capabilities, Version::capabilities());
} }

@ -1,3 +1,4 @@
use distant_core::protocol::semver;
use distant_core::protocol::PROTOCOL_VERSION; use distant_core::protocol::PROTOCOL_VERSION;
use rstest::*; use rstest::*;
@ -8,22 +9,40 @@ use crate::common::utils::TrimmedLinesMatchPredicate;
#[test_log::test] #[test_log::test]
fn should_output_capabilities(ctx: DistantManagerCtx) { fn should_output_capabilities(ctx: DistantManagerCtx) {
// Because all of our crates have the same version, we can expect it to match // Because all of our crates have the same version, we can expect it to match
let package_name = "distant-local"; let version: semver::Version = env!("CARGO_PKG_VERSION").parse().unwrap();
let package_version = env!("CARGO_PKG_VERSION");
let (major, minor, patch) = PROTOCOL_VERSION; // Add the package name to the client version information
let client_version = if version.build.is_empty() {
let mut version = version.clone();
version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME")).unwrap();
version
} else {
let mut version = version.clone();
let raw_build_str = format!("{}.{}", version.build.as_str(), env!("CARGO_PKG_NAME"));
version.build = semver::BuildMetadata::new(&raw_build_str).unwrap();
version
};
// Add the distant-local to the server version information
let server_version = if version.build.is_empty() {
let mut version = version;
version.build = semver::BuildMetadata::new("distant-local").unwrap();
version
} else {
let raw_build_str = format!("{}.{}", version.build.as_str(), "distant-local");
let mut version = version;
version.build = semver::BuildMetadata::new(&raw_build_str).unwrap();
version
};
// Since our client and server are built the same, all capabilities should be listed with + // Since our client and server are built the same, all capabilities should be listed with +
// and using 4 columns since we are not using a tty // and using 4 columns since we are not using a tty
let expected = indoc::formatdoc! {" let expected = indoc::formatdoc! {"
Client: distant {package_version} (Protocol {major}.{minor}.{patch}) Client: {client_version} (Protocol {PROTOCOL_VERSION})
Server: {package_name} {package_version} (Protocol {major}.{minor}.{patch}) Server: {server_version} (Protocol {PROTOCOL_VERSION})
Capabilities supported (+) or not (-): Capabilities supported (+) or not (-):
+cancel_search +copy +dir_create +dir_read +exec +fs_io +fs_perm +fs_search
+exists +file_append +file_append_text +file_read +fs_watch +sys_info
+file_read_text +file_write +file_write_text +metadata
+proc_kill +proc_resize_pty +proc_spawn +proc_stdin
+remove +rename +search +set_permissions
+system_info +unwatch +version +watch
"}; "};
ctx.cmd("version") ctx.cmd("version")

@ -1,41 +0,0 @@
use indoc::indoc;
use rstest::*;
use crate::common::fixtures::*;
const EXPECTED_TABLE: &str = indoc! {"
+---------------+--------------------------------------------------------------+
| kind | description |
+---------------+--------------------------------------------------------------+
| authenticate | Supports authenticating with a remote server |
+---------------+--------------------------------------------------------------+
| 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 a server on remote machines |
+---------------+--------------------------------------------------------------+
| list | Supports retrieving a list of managed connections |
+---------------+--------------------------------------------------------------+
| open_channel | Supports opening a channel with a remote server |
+---------------+--------------------------------------------------------------+
"};
#[rstest]
#[test_log::test]
fn should_output_capabilities(ctx: DistantManagerCtx) {
// distant action capabilities
ctx.new_assert_cmd(vec!["manager", "capabilities"])
.assert()
.success()
.stdout(EXPECTED_TABLE)
.stderr("");
}

@ -1 +1 @@
mod capabilities; mod version;

@ -0,0 +1,25 @@
use rstest::*;
use crate::common::fixtures::*;
#[rstest]
#[test_log::test]
fn should_output_version(ctx: DistantManagerCtx) {
// distant action capabilities
ctx.new_assert_cmd(vec!["manager", "version"])
.assert()
.success()
.stdout("WRONG")
.stderr("");
}
#[rstest]
#[test_log::test]
fn should_support_output_version_as_json(ctx: DistantManagerCtx) {
// distant action capabilities
ctx.new_assert_cmd(vec!["manager", "version", "--format", "json"])
.assert()
.success()
.stdout("WRONG")
.stderr("");
}
Loading…
Cancel
Save