diff --git a/CHANGELOG.md b/CHANGELOG.md index 0125fcd..d66c29c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 when executing `distant spawn` in order to run the command within a shell rather than directly +- `semver` crate to be used for version information in protocol and manager ### Changed - `distant_protocol::PROTOCOL_VERSION` now uses the crate's major, minor, and patch version at compile-time (parsed via `const-str` crate) to streamline 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` to contain a set of more + broad capabilities instead of every possible request type ### 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 do what `distant-local` or `distant-ssh2` do) +- Removed `Capability` and `Capabilities` from protocol and manager ## [0.20.0-alpha.12] diff --git a/Cargo.lock b/Cargo.lock index 3fd371b..64ab77c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -944,6 +944,7 @@ dependencies = [ "rand", "rmp", "rmp-serde", + "semver 1.0.17", "serde", "serde_bytes", "serde_json", @@ -964,6 +965,7 @@ dependencies = [ "regex", "rmp", "rmp-serde", + "semver 1.0.17", "serde", "serde_bytes", "serde_json", @@ -2776,6 +2778,9 @@ name = "semver" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" diff --git a/distant-local/src/api.rs b/distant-local/src/api.rs index fd589bc..a0560e0 100644 --- a/distant-local/src/api.rs +++ b/distant-local/src/api.rs @@ -3,10 +3,10 @@ use std::time::SystemTime; use std::{env, io}; use async_trait::async_trait; +use distant_core::protocol::semver; use distant_core::protocol::{ - Capabilities, ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, - Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, - Version, PROTOCOL_VERSION, + ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, Permissions, ProcessId, + PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, Version, PROTOCOL_VERSION, }; use distant_core::{DistantApi, DistantCtx}; use ignore::{DirEntry as WalkDirEntry, WalkBuilder}; @@ -635,10 +635,32 @@ impl DistantApi for Api { async fn version(&self, ctx: DistantCtx) -> io::Result { 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 { - server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), + server_version, protocol_version: PROTOCOL_VERSION, - capabilities: Capabilities::all(), + capabilities: Version::capabilities() + .iter() + .map(ToString::to_string) + .collect(), }) } } diff --git a/distant-net/Cargo.toml b/distant-net/Cargo.toml index cf2ed2b..040e745 100644 --- a/distant-net/Cargo.toml +++ b/distant-net/Cargo.toml @@ -28,6 +28,7 @@ rand = { version = "0.8.5", features = ["getrandom"] } rmp = "0.8.11" rmp-serde = "1.1.1" sha2 = "0.10.6" +semver = { version = "1.0.17", features = ["serde"] } serde = { version = "1.0.163", features = ["derive"] } serde_bytes = "0.11.9" serde_json = "1.0.96" diff --git a/distant-net/src/manager/client.rs b/distant-net/src/manager/client.rs index fe3d038..fe642b4 100644 --- a/distant-net/src/manager/client.rs +++ b/distant-net/src/manager/client.rs @@ -7,7 +7,7 @@ use log::*; use crate::client::Client; use crate::common::{ConnectionId, Destination, Map, Request}; use crate::manager::data::{ - ConnectionInfo, ConnectionList, ManagerCapabilities, ManagerRequest, ManagerResponse, + ConnectionInfo, ConnectionList, ManagerRequest, ManagerResponse, SemVer, }; mod channel; @@ -231,12 +231,12 @@ impl ManagerClient { RawChannel::spawn(connection_id, self).await } - /// Retrieves a list of supported capabilities - pub async fn capabilities(&mut self) -> io::Result { - trace!("capabilities()"); - let res = self.send(ManagerRequest::Capabilities).await?; + /// Retrieves the version of the manager. + pub async fn version(&mut self) -> io::Result { + trace!("version()"); + let res = self.send(ManagerRequest::Version).await?; match res.payload { - ManagerResponse::Capabilities { supported } => Ok(supported), + ManagerResponse::Version(version) => Ok(version), ManagerResponse::Error { description } => { Err(io::Error::new(io::ErrorKind::Other, description)) } diff --git a/distant-net/src/manager/data.rs b/distant-net/src/manager/data.rs index 3a9cc80..757c4bb 100644 --- a/distant-net/src/manager/data.rs +++ b/distant-net/src/manager/data.rs @@ -1,8 +1,6 @@ pub type ManagerChannelId = u32; pub type ManagerAuthenticationId = u32; - -mod capabilities; -pub use capabilities::*; +pub use semver::Version as SemVer; mod info; pub use info::*; diff --git a/distant-net/src/manager/data/capabilities.rs b/distant-net/src/manager/data/capabilities.rs deleted file mode 100644 index fdab311..0000000 --- a/distant-net/src/manager/data/capabilities.rs +++ /dev/null @@ -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); - -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) -> 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) -> bool { - self.0.insert(cap.into()) - } - - /// Removes the capability with the described kind, returning the capability - pub fn take(&mut self, kind: impl AsRef) -> Option { - 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) -> 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 { - let mut this = self.0.into_iter().collect::>(); - - 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 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 for ManagerCapabilities { - fn from_iter>(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::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 { - 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(&self, state: &mut H) { - self.kind.to_ascii_lowercase().hash(state); - } -} - -impl From 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(), - } - } -} diff --git a/distant-net/src/manager/data/request.rs b/distant-net/src/manager/data/request.rs index ebfc77c..c04cc0c 100644 --- a/distant-net/src/manager/data/request.rs +++ b/distant-net/src/manager/data/request.rs @@ -1,36 +1,17 @@ -use derive_more::IsVariant; use distant_auth::msg::AuthenticationResponse; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString}; use super::{ManagerAuthenticationId, ManagerChannelId}; use crate::common::{ConnectionId, Destination, Map, UntypedRequest}; #[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, EnumDiscriminants, 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"))] +#[derive(Clone, Debug, Serialize, Deserialize)] #[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, + /// Retrieve information about the manager's version. + Version, /// Launch a server using the manager - #[strum_discriminants(strum(message = "Supports launching a server on remote machines"))] Launch { // NOTE: Boxed per clippy's large_enum_variant warning destination: Box, @@ -40,7 +21,6 @@ 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, @@ -50,7 +30,6 @@ pub enum ManagerRequest { }, /// Submit some authentication message for the manager to use with an active connection - #[strum_discriminants(strum(message = "Supports authenticating with a remote server"))] Authenticate { /// Id of the authentication request that is being responded to id: ManagerAuthenticationId, @@ -60,16 +39,12 @@ pub enum ManagerRequest { }, /// Opens a channel for communication with an already-connected server - #[strum_discriminants(strum(message = "Supports opening a channel with a remote server"))] OpenChannel { /// Id of the connection id: ConnectionId, }, /// Sends data through channel - #[strum_discriminants(strum( - message = "Supports sending data through a channel with a remote server" - ))] Channel { /// Id of the channel id: ManagerChannelId, @@ -79,21 +54,17 @@ pub enum ManagerRequest { }, /// Closes an open channel - #[strum_discriminants(strum(message = "Supports closing a channel with a remote server"))] CloseChannel { /// Id of the channel to close id: ManagerChannelId, }, /// 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, } diff --git a/distant-net/src/manager/data/response.rs b/distant-net/src/manager/data/response.rs index 3ee84b1..0a52d23 100644 --- a/distant-net/src/manager/data/response.rs +++ b/distant-net/src/manager/data/response.rs @@ -1,9 +1,7 @@ use distant_auth::msg::Authentication; use serde::{Deserialize, Serialize}; -use super::{ - ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerCapabilities, ManagerChannelId, -}; +use super::{ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerChannelId, SemVer}; use crate::common::{ConnectionId, Destination, UntypedResponse}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -15,8 +13,8 @@ pub enum ManagerResponse { /// Indicates that some error occurred during a request Error { description: String }, - /// Response to retrieving information about the manager's capabilities - Capabilities { supported: ManagerCapabilities }, + /// Information about the manager's version. + Version(SemVer), /// Confirmation of a server being launched Launched { diff --git a/distant-net/src/manager/server.rs b/distant-net/src/manager/server.rs index c223ee4..0b8ca27 100644 --- a/distant-net/src/manager/server.rs +++ b/distant-net/src/manager/server.rs @@ -9,8 +9,8 @@ use tokio::sync::{oneshot, RwLock}; use crate::common::{ConnectionId, Destination, Map}; use crate::manager::{ - ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerCapabilities, ManagerChannelId, - ManagerRequest, ManagerResponse, + ConnectionInfo, ConnectionList, ManagerAuthenticationId, ManagerChannelId, ManagerRequest, + ManagerResponse, SemVer, }; use crate::server::{RequestCtx, Server, ServerHandler}; @@ -138,9 +138,11 @@ impl ManagerServer { Ok(id) } - /// Retrieves the list of supported capabilities for this manager - async fn capabilities(&self) -> io::Result { - Ok(ManagerCapabilities::all()) + /// Retrieves the manager's version. + async fn version(&self) -> io::Result { + 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` @@ -196,10 +198,10 @@ impl ServerHandler for ManagerServer { } = ctx; let response = match request.payload { - ManagerRequest::Capabilities {} => { - debug!("Looking up capabilities"); - match self.capabilities().await { - Ok(supported) => ManagerResponse::Capabilities { supported }, + ManagerRequest::Version {} => { + debug!("Looking up version"); + match self.version().await { + Ok(version) => ManagerResponse::Version(version), Err(x) => ManagerResponse::from(x), } } diff --git a/distant-protocol/Cargo.toml b/distant-protocol/Cargo.toml index e10d357..ab40428 100644 --- a/distant-protocol/Cargo.toml +++ b/distant-protocol/Cargo.toml @@ -20,6 +20,7 @@ bitflags = "2.3.1" 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"] } regex = "1.8.3" +semver = { version = "1.0.17", features = ["serde"] } serde = { version = "1.0.163", features = ["derive"] } serde_bytes = "0.11.9" strum = { version = "0.24.1", features = ["derive"] } diff --git a/distant-protocol/src/common.rs b/distant-protocol/src/common.rs index 8e891cd..24ee0dc 100644 --- a/distant-protocol/src/common.rs +++ b/distant-protocol/src/common.rs @@ -1,4 +1,3 @@ -mod capabilities; mod change; mod cmd; mod error; @@ -10,7 +9,6 @@ mod search; mod system; mod version; -pub use capabilities::*; pub use change::*; pub use cmd::*; pub use error::*; @@ -24,6 +22,3 @@ pub use version::*; /// Id for a remote process pub type ProcessId = u32; - -/// Version indicated by the tuple of (major, minor, patch). -pub type SemVer = (u8, u8, u8); diff --git a/distant-protocol/src/common/capabilities.rs b/distant-protocol/src/common/capabilities.rs deleted file mode 100644 index 7a9d7c1..0000000 --- a/distant-protocol/src/common/capabilities.rs +++ /dev/null @@ -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); - -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) -> 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) -> bool { - self.0.insert(cap.into()) - } - - /// Removes the capability with the described kind, returning the capability - pub fn take(&mut self, kind: impl AsRef) -> Option { - 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) -> 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 { - let mut this = self.0.into_iter().collect::>(); - - this.sort_unstable(); - - this - } -} - -impl AsRef> for Capabilities { - fn as_ref(&self) -> &HashSet { - &self.0 - } -} - -impl AsMut> for Capabilities { - fn as_mut(&mut self) -> &mut HashSet { - &mut self.0 - } -} - -impl Deref for Capabilities { - type Target = HashSet; - - 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 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 for Capabilities { - fn from_iter>(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::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 { - 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(&self, state: &mut H) { - self.kind.to_ascii_lowercase().hash(state); - } -} - -impl From 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::(), - ) - .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(), - } - ); - } - } -} diff --git a/distant-protocol/src/common/version.rs b/distant-protocol/src/common/version.rs index 92d79ca..7e7c95f 100644 --- a/distant-protocol/src/common/version.rs +++ b/distant-protocol/src/common/version.rs @@ -1,48 +1,80 @@ use serde::{Deserialize, Serialize}; -use crate::common::{Capabilities, SemVer}; +use crate::semver; /// Represents version information. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Version { - /// General version of server (arbitrary format) - pub server_version: String, + /// Server version. + pub server_version: semver::Version, - /// Protocol version - pub protocol_version: SemVer, + /// Protocol version. + pub protocol_version: semver::Version, - /// Capabilities of the server - pub capabilities: Capabilities, + /// Additional features available. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub capabilities: Vec, +} + +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)] mod tests { use super::*; - use crate::common::Capability; + use semver::Version as SemVer; #[test] fn should_be_able_to_serialize_to_json() { let version = Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }; let value = serde_json::to_value(version).unwrap(); assert_eq!( value, serde_json::json!({ - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": [{ - "kind": "some kind", - "description": "some description", - }] + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"] }) ); } @@ -50,26 +82,18 @@ mod tests { #[test] fn should_be_able_to_deserialize_from_json() { let value = serde_json::json!({ - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": [{ - "kind": "some kind", - "description": "some description", - }] + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"] }); let version: Version = serde_json::from_value(value).unwrap(); assert_eq!( version, Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], } ); } @@ -77,14 +101,9 @@ mod tests { #[test] fn should_be_able_to_serialize_to_msgpack() { let version = Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }; // 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 // enough times with minor changes that we need tests to verify. let buf = rmp_serde::encode::to_vec_named(&Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }) .unwrap(); @@ -116,14 +130,9 @@ mod tests { assert_eq!( version, Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], } ); } diff --git a/distant-protocol/src/lib.rs b/distant-protocol/src/lib.rs index 8f0907c..892cba1 100644 --- a/distant-protocol/src/lib.rs +++ b/distant-protocol/src/lib.rs @@ -14,14 +14,15 @@ pub use common::*; pub use msg::*; pub use request::*; 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 /// version will also be reflected in this constant that can be used to verify compatibility across /// the wire. -pub const PROTOCOL_VERSION: SemVer = ( - const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u8), - const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u8), - const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u8), +pub const PROTOCOL_VERSION: semver::Version = semver::Version::new( + const_str::parse!(env!("CARGO_PKG_VERSION_MAJOR"), u64), + const_str::parse!(env!("CARGO_PKG_VERSION_MINOR"), u64), + const_str::parse!(env!("CARGO_PKG_VERSION_PATCH"), u64), ); diff --git a/distant-protocol/src/request.rs b/distant-protocol/src/request.rs index 9d3509e..b725044 100644 --- a/distant-protocol/src/request.rs +++ b/distant-protocol/src/request.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use derive_more::IsVariant; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumDiscriminants, EnumIter, EnumMessage, EnumString}; use crate::common::{ ChangeKind, Cmd, Permissions, ProcessId, PtySize, SearchId, SearchQuery, SetPermissionsOptions, @@ -14,26 +13,10 @@ use crate::utils; pub type Environment = HashMap; /// Represents the payload of a request to be performed on the remote machine -#[derive(Clone, Debug, PartialEq, Eq, EnumDiscriminants, 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"))] +#[derive(Clone, Debug, PartialEq, Eq, IsVariant, Serialize, Deserialize)] #[serde(rename_all = "snake_case", deny_unknown_fields, tag = "type")] pub enum Request { /// Reads a file from the specified path on the remote machine - #[strum_discriminants(strum(message = "Supports reading binary file"))] FileRead { /// The path to the file on the remote machine path: PathBuf, @@ -41,7 +24,6 @@ pub enum Request { /// 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, @@ -49,7 +31,6 @@ pub enum Request { /// 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, @@ -61,7 +42,6 @@ pub enum Request { /// 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, @@ -71,7 +51,6 @@ pub enum Request { }, /// 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, @@ -82,7 +61,6 @@ pub enum Request { }, /// 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, @@ -92,7 +70,6 @@ pub enum Request { }, /// Reads a directory from the specified path on the remote machine - #[strum_discriminants(strum(message = "Supports reading directory"))] DirRead { /// The path to the directory on the remote machine path: PathBuf, @@ -126,7 +103,6 @@ pub enum Request { }, /// Creates a directory on the remote machine - #[strum_discriminants(strum(message = "Supports creating directory"))] DirCreate { /// The path to the directory on the remote machine path: PathBuf, @@ -137,7 +113,6 @@ pub enum Request { }, /// Removes a file or directory on the remote machine - #[strum_discriminants(strum(message = "Supports removing files, directories, and symlinks"))] Remove { /// The path to the file or directory on the remote machine path: PathBuf, @@ -149,7 +124,6 @@ pub enum Request { }, /// Copies a file or directory on the remote machine - #[strum_discriminants(strum(message = "Supports copying files, directories, and symlinks"))] Copy { /// The path to the file or directory on the remote machine src: PathBuf, @@ -159,7 +133,6 @@ pub enum Request { }, /// Moves/renames a file or directory on the remote machine - #[strum_discriminants(strum(message = "Supports renaming files, directories, and symlinks"))] Rename { /// The path to the file or directory on the remote machine src: PathBuf, @@ -169,7 +142,6 @@ pub enum Request { }, /// 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, @@ -189,23 +161,18 @@ pub enum Request { }, /// 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, @@ -222,9 +189,6 @@ pub enum Request { }, /// 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 { /// The path to the file, directory, or symlink on the remote machine path: PathBuf, @@ -238,23 +202,18 @@ pub enum Request { }, /// Searches filesystem using the provided query - #[strum_discriminants(strum(message = "Supports searching filesystem using queries"))] Search { /// Query to perform against the filesystem query: SearchQuery, }, /// Cancels an active search being run against the filesystem - #[strum_discriminants(strum( - message = "Supports canceling an active search against the filesystem" - ))] CancelSearch { /// Id of the search to cancel id: SearchId, }, /// Spawns a new process on the remote machine - #[strum_discriminants(strum(message = "Supports spawning a process"))] ProcSpawn { /// The full command to run including arguments cmd: Cmd, @@ -273,14 +232,12 @@ pub enum Request { }, /// Kills a process running on the remote machine - #[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, @@ -291,7 +248,6 @@ pub enum Request { }, /// 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, @@ -301,11 +257,9 @@ pub enum Request { }, /// Retrieve information about the server and the system it is on - #[strum_discriminants(strum(message = "Supports retrieving system information"))] SystemInfo {}, /// Retrieve information about the server's protocol version - #[strum_discriminants(strum(message = "Supports retrieving version"))] Version {}, } diff --git a/distant-protocol/src/response.rs b/distant-protocol/src/response.rs index c73812e..2ce3a54 100644 --- a/distant-protocol/src/response.rs +++ b/distant-protocol/src/response.rs @@ -2013,19 +2013,14 @@ mod tests { mod version { use super::*; - use crate::common::{Capabilities, Capability}; + use crate::semver::Version as SemVer; #[test] fn should_be_able_to_serialize_to_json() { let payload = Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: [Capability { - kind: String::from("some kind"), - description: String::from("some description"), - }] - .into_iter() - .collect(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }); let value = serde_json::to_value(payload).unwrap(); @@ -2033,12 +2028,9 @@ mod tests { value, serde_json::json!({ "type": "version", - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": [{ - "kind": "some kind", - "description": "some description", - }], + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"], }) ); } @@ -2047,18 +2039,18 @@ mod tests { fn should_be_able_to_deserialize_from_json() { let value = serde_json::json!({ "type": "version", - "server_version": "some version", - "protocol_version": [1, 2, 3], - "capabilities": Capabilities::all(), + "server_version": "123.456.789-rc+build", + "protocol_version": "1.2.3", + "capabilities": ["cap"], }); let payload: Response = serde_json::from_value(value).unwrap(); assert_eq!( payload, Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }) ); } @@ -2066,9 +2058,9 @@ mod tests { #[test] fn should_be_able_to_serialize_to_msgpack() { let payload = Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }); // 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 // enough times with minor changes that we need tests to verify. let buf = rmp_serde::encode::to_vec_named(&Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], })) .unwrap(); @@ -2095,9 +2087,9 @@ mod tests { assert_eq!( payload, Response::Version(Version { - server_version: String::from("some version"), - protocol_version: (1, 2, 3), - capabilities: Capabilities::all(), + server_version: "123.456.789-rc+build".parse().unwrap(), + protocol_version: SemVer::new(1, 2, 3), + capabilities: vec![String::from("cap")], }) ); } diff --git a/distant-ssh2/src/api.rs b/distant-ssh2/src/api.rs index 093388b..b75bcc9 100644 --- a/distant-ssh2/src/api.rs +++ b/distant-ssh2/src/api.rs @@ -7,9 +7,10 @@ use std::time::Duration; use async_compat::CompatExt; use async_once_cell::OnceCell; use async_trait::async_trait; +use distant_core::protocol::semver; use distant_core::protocol::{ - Capabilities, CapabilityKind, DirEntry, Environment, FileType, Metadata, Permissions, - ProcessId, PtySize, SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION, + DirEntry, Environment, FileType, Metadata, Permissions, ProcessId, PtySize, + SetPermissionsOptions, SystemInfo, UnixMetadata, Version, PROTOCOL_VERSION, }; use distant_core::{DistantApi, DistantCtx}; use log::*; @@ -936,18 +937,33 @@ impl DistantApi for SshDistantApi { async fn version(&self, ctx: DistantCtx) -> io::Result { debug!("[Conn {}] Querying capabilities", ctx.connection_id); - let mut capabilities = Capabilities::all(); - - // Searching is not supported by ssh implementation - // TODO: Could we have external search using ripgrep's JSON lines API? - capabilities.take(CapabilityKind::Search); - capabilities.take(CapabilityKind::CancelSearch); - - // Broken via wezterm-ssh, so not supported right now - capabilities.take(CapabilityKind::SetPermissions); + let capabilities = vec![ + Version::CAP_EXEC.to_string(), + Version::CAP_FS_IO.to_string(), + Version::CAP_SYS_INFO.to_string(), + ]; + + // 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 { - server_version: format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), + server_version, protocol_version: PROTOCOL_VERSION, capabilities, }) diff --git a/src/cli/commands/client.rs b/src/cli/commands/client.rs index 84981c8..a043d81 100644 --- a/src/cli/commands/client.rs +++ b/src/cli/commands/client.rs @@ -7,10 +7,10 @@ use std::time::Duration; use anyhow::Context; use distant_core::net::common::{ConnectionId, Host, Map, Request, Response}; use distant_core::net::manager::ManagerClient; +use distant_core::protocol::semver; use distant_core::protocol::{ - self, Capabilities, ChangeKind, ChangeKindSet, FileType, Permissions, SearchQuery, - SearchQueryContentsMatch, SearchQueryMatch, SearchQueryPathMatch, SetPermissionsOptions, - SystemInfo, + self, ChangeKind, ChangeKindSet, FileType, Permissions, SearchQuery, SearchQueryContentsMatch, + SearchQueryMatch, SearchQueryPathMatch, SetPermissionsOptions, SystemInfo, Version, }; use distant_core::{DistantChannel, DistantChannelExt, RemoteCommand, Searcher, Watcher}; use log::*; @@ -581,32 +581,51 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult { match format { 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!( - "Client: {} {} (Protocol {major}.{minor}.{patch})", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") + "Client: {client_version} (Protocol {})", + distant_core::protocol::PROTOCOL_VERSION ); - let (major, minor, patch) = version.protocol_version; println!( - "Server: {} (Protocol {major}.{minor}.{patch})", - version.server_version + "Server: {} (Protocol {})", + version.server_version, version.protocol_version ); // Build a complete set of capabilities to show which ones we support - let client_capabilities = Capabilities::all(); - let server_capabilities = version.capabilities; - let mut capabilities: Vec = client_capabilities - .union(server_capabilities.as_ref()) - .map(|cap| { - let kind = &cap.kind; - if client_capabilities.contains(kind) - && server_capabilities.contains(kind) - { - format!("+{kind}") + let mut capabilities: HashMap = Version::capabilities() + .iter() + .map(|cap| (cap.to_string(), 1)) + .collect(); + + for cap in version.capabilities { + *capabilities.entry(cap).or_default() += 1; + } + + let mut capabilities: Vec = capabilities + .into_iter() + .map(|(cap, cnt)| { + if cnt > 1 { + format!("+{cap}") } else { - format!("-{kind}") + format!("-{cap}") } }) .collect(); diff --git a/src/cli/commands/manager.rs b/src/cli/commands/manager.rs index 3eca61d..f65c9f0 100644 --- a/src/cli/commands/manager.rs +++ b/src/cli/commands/manager.rs @@ -228,41 +228,24 @@ async fn async_run(cmd: ManagerSubcommand) -> CliResult { Ok(()) } - ManagerSubcommand::Capabilities { format, network } => { + ManagerSubcommand::Version { format, network } => { debug!("Connecting to manager"); let mut client = connect_to_manager(format, network).await?; - debug!("Getting list of capabilities"); - let caps = client - .capabilities() - .await - .context("Failed to get list of capabilities")?; - debug!("Got capabilities: {caps:?}"); + debug!("Getting version"); + let version = client.version().await.context("Failed to get version")?; + debug!("Got version: {version}"); match format { Format::Json => { println!( "{}", - serde_json::to_string(&caps) - .context("Failed to format capabilities as json")? + serde_json::to_string(&version) + .context("Failed to format version as json")? ); } Format::Shell => { - #[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, - } - })) - ); + println!("{version}"); } } diff --git a/src/options.rs b/src/options.rs index d77821b..67f3fe9 100644 --- a/src/options.rs +++ b/src/options.rs @@ -171,7 +171,7 @@ impl Options { DistantSubcommand::Manager(cmd) => { update_logging!(manager); match cmd { - ManagerSubcommand::Capabilities { network, .. } => { + ManagerSubcommand::Version { network, .. } => { network.merge(config.manager.network); } ManagerSubcommand::Info { network, .. } => { @@ -1054,7 +1054,7 @@ pub enum ManagerSubcommand { }, /// Retrieve a list of capabilities that the manager supports - Capabilities { + Version { #[clap(short, long, default_value_t, value_enum)] format: Format, @@ -1111,7 +1111,7 @@ impl ManagerSubcommand { Self::Select { format, .. } => *format, Self::Service(_) => Format::Shell, Self::Listen { .. } => Format::Shell, - Self::Capabilities { format, .. } => *format, + Self::Version { format, .. } => *format, Self::Info { format, .. } => *format, Self::List { format, .. } => *format, Self::Kill { format, .. } => *format, @@ -3505,7 +3505,7 @@ mod tests { log_file: None, log_level: None, }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: None, @@ -3537,7 +3537,7 @@ mod tests { log_file: Some(PathBuf::from("config-log-file")), log_level: Some(LogLevel::Trace), }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: Some(PathBuf::from("config-unix-socket")), @@ -3556,7 +3556,7 @@ mod tests { log_file: Some(PathBuf::from("cli-log-file")), log_level: Some(LogLevel::Info), }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: Some(PathBuf::from("cli-unix-socket")), @@ -3588,7 +3588,7 @@ mod tests { log_file: Some(PathBuf::from("cli-log-file")), log_level: Some(LogLevel::Info), }, - command: DistantSubcommand::Manager(ManagerSubcommand::Capabilities { + command: DistantSubcommand::Manager(ManagerSubcommand::Version { format: Format::Json, network: NetworkSettings { unix_socket: Some(PathBuf::from("cli-unix-socket")), diff --git a/tests/cli/api/version.rs b/tests/cli/api/version.rs index 2dd9972..b930695 100644 --- a/tests/cli/api/version.rs +++ b/tests/cli/api/version.rs @@ -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 serde_json::json; use test_log::test; @@ -25,17 +26,17 @@ async fn should_support_json_capabilities(mut api_process: CtxCommand = res["payload"]["capabilities"] .as_array() - .expect("Field 'supported' was not an array") + .expect("Field 'capabilities' was not an array") .iter() .map(|value| { - serde_json::from_value::(value.clone()) - .expect("Could not read array value as capability") + serde_json::from_value::(value.clone()) + .expect("Could not read array value as string") }) .collect(); // NOTE: Our local server api should always support all capabilities since it is the reference // implementation for our api - assert_eq!(capabilities, Capabilities::all()); + assert_eq!(capabilities, Version::capabilities()); } diff --git a/tests/cli/client/version.rs b/tests/cli/client/version.rs index f822cf8..1cb0a0c 100644 --- a/tests/cli/client/version.rs +++ b/tests/cli/client/version.rs @@ -1,3 +1,4 @@ +use distant_core::protocol::semver; use distant_core::protocol::PROTOCOL_VERSION; use rstest::*; @@ -8,22 +9,40 @@ use crate::common::utils::TrimmedLinesMatchPredicate; #[test_log::test] fn should_output_capabilities(ctx: DistantManagerCtx) { // Because all of our crates have the same version, we can expect it to match - let package_name = "distant-local"; - let package_version = env!("CARGO_PKG_VERSION"); - let (major, minor, patch) = PROTOCOL_VERSION; + let version: semver::Version = env!("CARGO_PKG_VERSION").parse().unwrap(); + + // 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 + // and using 4 columns since we are not using a tty let expected = indoc::formatdoc! {" - Client: distant {package_version} (Protocol {major}.{minor}.{patch}) - Server: {package_name} {package_version} (Protocol {major}.{minor}.{patch}) + Client: {client_version} (Protocol {PROTOCOL_VERSION}) + Server: {server_version} (Protocol {PROTOCOL_VERSION}) Capabilities supported (+) or not (-): - +cancel_search +copy +dir_create +dir_read - +exists +file_append +file_append_text +file_read - +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 + +exec +fs_io +fs_perm +fs_search + +fs_watch +sys_info "}; ctx.cmd("version") diff --git a/tests/cli/manager/capabilities.rs b/tests/cli/manager/capabilities.rs deleted file mode 100644 index 2908222..0000000 --- a/tests/cli/manager/capabilities.rs +++ /dev/null @@ -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(""); -} diff --git a/tests/cli/manager/mod.rs b/tests/cli/manager/mod.rs index 98a3830..81c2f21 100644 --- a/tests/cli/manager/mod.rs +++ b/tests/cli/manager/mod.rs @@ -1 +1 @@ -mod capabilities; +mod version; diff --git a/tests/cli/manager/version.rs b/tests/cli/manager/version.rs new file mode 100644 index 0000000..8013609 --- /dev/null +++ b/tests/cli/manager/version.rs @@ -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(""); +}