Fix shell/spawn args and re-add capabilities

pull/172/head
Chip Senkbeil 1 year ago
parent 75396834d2
commit f6ce46df12
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

@ -40,6 +40,60 @@ async fn read_cache(path: &Path) -> Cache {
async fn async_run(cmd: ClientSubcommand) -> CliResult {
match cmd {
ClientSubcommand::Capabilities {
cache,
connection,
format,
network,
} => {
debug!("Connecting to manager");
let mut client = connect_to_manager(format, network).await?;
let mut cache = read_cache(&cache).await;
let connection_id =
use_or_lookup_connection_id(&mut cache, connection, &mut client).await?;
debug!("Opening raw channel to connection {}", connection_id);
let channel = client
.open_raw_channel(connection_id)
.await
.with_context(|| {
format!("Failed to open raw channel to connection {connection_id}")
})?;
debug!("Retrieving capabilities");
let capabilities = channel
.into_client()
.into_channel()
.capabilities()
.await
.with_context(|| {
format!("Failed to retrieve capabilities using connection {connection_id}")
})?;
match format {
Format::Shell => {
#[derive(Tabled)]
struct EntryRow {
kind: String,
description: String,
}
let table = Table::new(capabilities.into_sorted_vec().into_iter().map(|cap| {
EntryRow {
kind: cap.kind,
description: cap.description,
}
}))
.with(Style::ascii())
.with(Modify::new(Rows::new(..)).with(Alignment::left()))
.to_string();
println!("{table}");
}
Format::Json => println!("{}", serde_json::to_string(&capabilities).unwrap()),
}
}
ClientSubcommand::Connect {
cache,
destination,
@ -316,8 +370,8 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
}
ClientSubcommand::Shell {
cache,
connection,
cmd,
connection,
current_dir,
environment,
network,
@ -340,7 +394,7 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
// Convert cmd into string
let cmd = cmd.map(Into::into);
let cmd = cmd.map(|cmd| cmd.join(" "));
debug!(
"Spawning shell (environment = {:?}): {}",
@ -378,6 +432,9 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
.await
.with_context(|| format!("Failed to open channel to connection {connection_id}"))?;
// Convert cmd into string
let cmd = cmd.join(" ");
if lsp {
debug!(
"Spawning LSP server (pty = {}, cwd = {:?}): {}",
@ -387,9 +444,6 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
.spawn(cmd, current_dir, pty, MAX_PIPE_CHUNK_SIZE)
.await?;
} else if pty {
// Convert cmd into string
let cmd = String::from(cmd);
debug!(
"Spawning pty process (environment = {:?}, cwd = {:?}): {}",
environment, current_dir, cmd
@ -406,7 +460,7 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
.environment(environment)
.current_dir(current_dir)
.pty(None)
.spawn(channel.into_client().into_channel(), cmd.as_str())
.spawn(channel.into_client().into_channel(), &cmd)
.await
.with_context(|| format!("Failed to spawn {cmd}"))?;
@ -599,6 +653,9 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
);
let results = channel
.send(DistantMsg::Batch(vec![
DistantRequestData::FileRead {
path: path.to_path_buf(),
},
DistantRequestData::DirRead {
path: path.to_path_buf(),
depth,
@ -606,9 +663,6 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
canonicalize,
include_root,
},
DistantRequestData::FileRead {
path: path.to_path_buf(),
},
]))
.await
.with_context(|| {
@ -647,12 +701,14 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
out.write_all(&data)
.context("Failed to write directory contents to stdout")?;
out.flush().context("Failed to flush stdout")?;
return Ok(());
}
DistantResponseData::Blob { data } => {
let mut out = std::io::stdout();
out.write_all(&data)
.context("Failed to write file contents to stdout")?;
out.flush().context("Failed to flush stdout")?;
return Ok(());
}
DistantResponseData::Error(x) => errors.push(x),
_ => continue,
@ -792,7 +848,14 @@ async fn async_run(cmd: ClientSubcommand) -> CliResult {
data,
}) => {
let data = match data {
Some(x) => x,
Some(x) => match x.into_string() {
Ok(x) => x.into_bytes(),
Err(_) => {
return Err(CliError::from(anyhow::anyhow!(
"Non-unicode input is disallowed!"
)));
}
},
None => {
debug!("No data provided, reading from stdin");
use std::io::Read;

@ -93,6 +93,9 @@ impl Options {
DistantSubcommand::Client(cmd) => {
update_logging!(client);
match cmd {
ClientSubcommand::Capabilities { network, .. } => {
network.merge(config.client.network);
}
ClientSubcommand::Connect {
network, options, ..
} => {
@ -229,6 +232,28 @@ pub enum DistantSubcommand {
/// Subcommands for `distant client`.
#[derive(Debug, PartialEq, Subcommand, IsVariant)]
pub enum ClientSubcommand {
/// Retrieves capabilities of the remote server
Capabilities {
/// Location to store cached data
#[clap(
long,
value_hint = ValueHint::FilePath,
value_parser,
default_value = CACHE_FILE_PATH_STR.as_str()
)]
cache: PathBuf,
/// Specify a connection being managed
#[clap(long)]
connection: Option<ConnectionId>,
#[clap(flatten)]
network: NetworkSettings,
#[clap(short, long, default_value_t, value_enum)]
format: Format,
},
/// Requests that active manager connects to the server at the specified destination
Connect {
/// Location to store cached data
@ -366,7 +391,8 @@ pub enum ClientSubcommand {
environment: Environment,
/// Optional command to run instead of $SHELL
cmd: Option<Cmd>,
#[clap(name = "CMD", last = true)]
cmd: Option<Vec<String>>,
},
/// Spawn a process on the remote machine
@ -405,7 +431,8 @@ pub enum ClientSubcommand {
environment: Environment,
/// Command to run
cmd: Cmd,
#[clap(name = "CMD", num_args = 1.., last = true)]
cmd: Vec<String>,
},
SystemInfo {
@ -430,6 +457,7 @@ pub enum ClientSubcommand {
impl ClientSubcommand {
pub fn cache_path(&self) -> &Path {
match self {
Self::Capabilities { cache, .. } => cache.as_path(),
Self::Connect { cache, .. } => cache.as_path(),
Self::FileSystem(fs) => fs.cache_path(),
Self::Launch { cache, .. } => cache.as_path(),
@ -442,6 +470,7 @@ impl ClientSubcommand {
pub fn network_settings(&self) -> &NetworkSettings {
match self {
Self::Capabilities { network, .. } => network,
Self::Connect { network, .. } => network,
Self::FileSystem(fs) => fs.network_settings(),
Self::Launch { network, .. } => network,
@ -677,7 +706,7 @@ pub enum ClientFileSystemSubcommand {
path: PathBuf,
/// Data for server-side writing of content. If not provided, will read from stdin.
data: Option<Vec<u8>>,
data: Option<OsString>,
},
}
@ -973,6 +1002,122 @@ mod tests {
use distant_core::net::map;
use std::time::Duration;
#[test]
fn distant_capabilities_should_support_merging_with_config() {
let mut options = Options {
config_path: None,
logging: LoggingSettings {
log_file: None,
log_level: None,
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
unix_socket: None,
windows_pipe: None,
},
format: Format::Json,
}),
};
options.merge(Config {
client: ClientConfig {
logging: LoggingSettings {
log_file: Some(PathBuf::from("config-log-file")),
log_level: Some(LogLevel::Trace),
},
network: NetworkSettings {
unix_socket: Some(PathBuf::from("config-unix-socket")),
windows_pipe: Some(String::from("config-windows-pipe")),
},
connect: ClientConnectConfig {
options: map!("hello" -> "world"),
},
..Default::default()
},
..Default::default()
});
assert_eq!(
options,
Options {
config_path: None,
logging: LoggingSettings {
log_file: Some(PathBuf::from("config-log-file")),
log_level: Some(LogLevel::Trace),
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
unix_socket: Some(PathBuf::from("config-unix-socket")),
windows_pipe: Some(String::from("config-windows-pipe")),
},
format: Format::Json,
}),
}
);
}
#[test]
fn distant_capabilities_should_prioritize_explicit_cli_options_when_merging() {
let mut options = Options {
config_path: None,
logging: LoggingSettings {
log_file: Some(PathBuf::from("cli-log-file")),
log_level: Some(LogLevel::Info),
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
unix_socket: Some(PathBuf::from("cli-unix-socket")),
windows_pipe: Some(String::from("cli-windows-pipe")),
},
format: Format::Json,
}),
};
options.merge(Config {
client: ClientConfig {
logging: LoggingSettings {
log_file: Some(PathBuf::from("config-log-file")),
log_level: Some(LogLevel::Trace),
},
network: NetworkSettings {
unix_socket: Some(PathBuf::from("config-unix-socket")),
windows_pipe: Some(String::from("config-windows-pipe")),
},
connect: ClientConnectConfig {
options: map!("hello" -> "world", "config" -> "value"),
},
..Default::default()
},
..Default::default()
});
assert_eq!(
options,
Options {
config_path: None,
logging: LoggingSettings {
log_file: Some(PathBuf::from("cli-log-file")),
log_level: Some(LogLevel::Info),
},
command: DistantSubcommand::Client(ClientSubcommand::Capabilities {
cache: PathBuf::new(),
connection: None,
network: NetworkSettings {
unix_socket: Some(PathBuf::from("cli-unix-socket")),
windows_pipe: Some(String::from("cli-windows-pipe")),
},
format: Format::Json,
}),
}
);
}
#[test]
fn distant_connect_should_support_merging_with_config() {
let mut options = Options {
@ -1496,7 +1641,7 @@ mod tests {
environment: map!(),
lsp: true,
pty: true,
cmd: Cmd::from("cmd"),
cmd: vec![String::from("cmd")],
}),
};
@ -1534,7 +1679,7 @@ mod tests {
environment: map!(),
lsp: true,
pty: true,
cmd: Cmd::from("cmd"),
cmd: vec![String::from("cmd")],
}),
}
);
@ -1559,7 +1704,7 @@ mod tests {
environment: map!(),
lsp: true,
pty: true,
cmd: Cmd::from("cmd"),
cmd: vec![String::from("cmd")],
}),
};
@ -1597,7 +1742,7 @@ mod tests {
environment: map!(),
lsp: true,
pty: true,
cmd: Cmd::from("cmd"),
cmd: vec![String::from("cmd")],
}),
}
);

@ -1,56 +1,90 @@
use clap::error::{Error, ErrorKind};
use clap::{Arg, ArgAction, ArgMatches, Args, Command, FromArgMatches};
use derive_more::{Display, From, Into};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
use clap::Args;
use std::fmt;
use std::str::FromStr;
/// Represents some command with arguments to execute
#[derive(Clone, Debug, Display, From, Into, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cmd(String);
/// Represents some command with arguments to execute.
///
/// NOTE: Must be derived with `#[clap(flatten)]` to properly take effect.
#[derive(Args, Clone, Debug, PartialEq, Eq)]
pub struct Cmd {
/// The command to execute.
#[clap(name = "CMD")]
cmd: String,
impl Cmd {
/// Creates a new command from the given `cmd`
pub fn new(cmd: impl Into<String>) -> Self {
Self(cmd.into())
}
/// Returns reference to the program portion of the command
pub fn program(&self) -> &str {
match self.0.split_once(' ') {
Some((program, _)) => program.trim(),
None => self.0.trim(),
}
}
/// Arguments to provide to the command.
#[clap(name = "ARGS")]
args: Vec<String>,
}
/// Returns reference to the arguments portion of the command
pub fn arguments(&self) -> &str {
match self.0.split_once(' ') {
Some((_, arguments)) => arguments.trim(),
None => "",
impl Cmd {
/// Creates a new command from the given `cmd`.
pub fn new<C, I, A>(cmd: C, args: I) -> Self
where
C: Into<String>,
I: Iterator<Item = A>,
A: Into<String>,
{
Self {
cmd: cmd.into(),
args: args.map(Into::into).collect(),
}
}
}
impl Deref for Cmd {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
impl From<Cmd> for String {
fn from(cmd: Cmd) -> Self {
cmd.to_string()
}
}
impl DerefMut for Cmd {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
impl fmt::Display for Cmd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.cmd)?;
for arg in self.args.iter() {
write!(f, " {arg}")?;
}
Ok(())
}
}
impl<'a> From<&'a str> for Cmd {
/// Parses `s` into [`Cmd`], or panics if unable to parse.
fn from(s: &'a str) -> Self {
Self(s.to_string())
s.parse().expect("Failed to parse into cmd")
}
}
impl FromStr for Cmd {
type Err = Box<dyn std::error::Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let tokens = if cfg!(unix) {
shell_words::split(s)?
} else if cfg!(windows) {
winsplit::split(s)
} else {
unreachable!(
"FromStr<Cmd>: Unsupported operating system outside Unix and Windows families!"
);
};
// If we get nothing, then we want an empty command
if tokens.is_empty() {
return Ok(Self {
cmd: String::new(),
args: Vec::new(),
});
}
let mut it = tokens.into_iter();
Ok(Self {
cmd: it.next().unwrap(),
args: it.collect(),
})
}
}
/*
impl FromArgMatches for Cmd {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
let mut matches = matches.clone();
@ -85,6 +119,7 @@ impl Args for Cmd {
Arg::new("cmd")
.required(true)
.value_name("CMD")
.help("")
.action(ArgAction::Set),
)
.trailing_var_arg(true)
@ -96,6 +131,16 @@ impl Args for Cmd {
)
}
fn augment_args_for_update(cmd: Command) -> Command {
cmd
Self::augment_args(cmd)
}
}
} */
/* #[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_cmd() {
Cmd::augment_args(Command::new("distant")).debug_assert();
}
} */

@ -1,4 +1,4 @@
use crate::cli::{fixtures::*, utils::FAILURE_LINE};
use crate::cli::fixtures::*;
use assert_fs::prelude::*;
use predicates::prelude::*;
use rstest::*;
@ -71,7 +71,7 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
src.assert(predicate::path::missing());
dst.assert(predicate::path::missing());

@ -1,4 +1,4 @@
use crate::cli::{fixtures::*, utils::FAILURE_LINE};
use crate::cli::fixtures::*;
use assert_fs::prelude::*;
use predicates::prelude::*;
use rstest::*;
@ -51,7 +51,7 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
dir.assert(predicate::path::missing());
}

@ -1,8 +1,6 @@
use crate::cli::{
fixtures::*,
utils::{regex_pred, FAILURE_LINE},
};
use crate::cli::{fixtures::*, utils::regex_pred};
use assert_fs::prelude::*;
use predicates::prelude::*;
use rstest::*;
const FILE_CONTENTS: &str = r#"
@ -133,5 +131,5 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
}

@ -1,8 +1,6 @@
use crate::cli::{
fixtures::*,
utils::{regex_pred, FAILURE_LINE},
};
use crate::cli::{fixtures::*, utils::regex_pred};
use assert_fs::prelude::*;
use predicates::prelude::*;
use rstest::*;
use std::path::Path;
@ -221,5 +219,5 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
}

@ -1,12 +1,14 @@
use crate::cli::{fixtures::*, utils::FAILURE_LINE};
use crate::cli::fixtures::*;
use assert_fs::prelude::*;
use indoc::indoc;
use predicates::prelude::*;
use rstest::*;
const FILE_CONTENTS: &str = r#"
some text
on multiple lines
that is a file's contents
"#;
const FILE_CONTENTS: &str = indoc! {r#"
some text
on multiple lines
that is a file's contents
"#};
#[rstest]
#[test_log::test]
@ -20,7 +22,7 @@ fn should_print_out_file_contents(ctx: DistantManagerCtx) {
.args([file.to_str().unwrap()])
.assert()
.success()
.stdout(format!("{}\n", FILE_CONTENTS))
.stdout(FILE_CONTENTS)
.stderr("");
}
@ -36,5 +38,5 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
}

@ -1,4 +1,4 @@
use crate::cli::{fixtures::*, utils::FAILURE_LINE};
use crate::cli::fixtures::*;
use assert_fs::prelude::*;
use predicates::prelude::*;
use rstest::*;
@ -78,7 +78,7 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("Directory not empty"));
dir.assert(predicate::path::exists());
dir.assert(predicate::path::is_dir());

@ -1,4 +1,4 @@
use crate::cli::{fixtures::*, utils::FAILURE_LINE};
use crate::cli::fixtures::*;
use assert_fs::prelude::*;
use predicates::prelude::*;
use rstest::*;
@ -74,7 +74,7 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
src.assert(predicate::path::missing());
dst.assert(predicate::path::missing());

@ -1,17 +1,19 @@
use crate::cli::{fixtures::*, utils::FAILURE_LINE};
use crate::cli::fixtures::*;
use assert_fs::prelude::*;
use indoc::indoc;
use predicates::prelude::*;
use rstest::*;
const FILE_CONTENTS: &str = r#"
some text
on multiple lines
that is a file's contents
"#;
const FILE_CONTENTS: &str = indoc! {r#"
some text
on multiple lines
that is a file's contents
"#};
const APPENDED_FILE_CONTENTS: &str = r#"
even more
file contents
"#;
const APPENDED_FILE_CONTENTS: &str = indoc! {r#"
even more
file contents
"#};
#[rstest]
#[test_log::test]
@ -40,11 +42,12 @@ fn should_support_writing_stdin_to_file(ctx: DistantManagerCtx) {
fn should_support_appending_stdin_to_file(ctx: DistantManagerCtx) {
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str(FILE_CONTENTS).unwrap();
// distant action file-write {path} -- {contents}
ctx.new_assert_cmd(["fs", "write"])
.args(["--append", file.to_str().unwrap()])
.write_stdin(FILE_CONTENTS)
.write_stdin(APPENDED_FILE_CONTENTS)
.assert()
.success()
.stdout("")
@ -65,7 +68,8 @@ fn should_support_writing_argument_to_file(ctx: DistantManagerCtx) {
// distant action file-write {path} -- {contents}
ctx.new_assert_cmd(["fs", "write"])
.args([file.to_str().unwrap(), "--", FILE_CONTENTS])
.args([file.to_str().unwrap(), "--"])
.arg(FILE_CONTENTS)
.assert()
.success()
.stdout("")
@ -83,10 +87,12 @@ fn should_support_writing_argument_to_file(ctx: DistantManagerCtx) {
fn should_support_appending_argument_to_file(ctx: DistantManagerCtx) {
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file");
file.write_str(FILE_CONTENTS).unwrap();
// distant action file-write {path} -- {contents}
ctx.new_assert_cmd(["fs", "write"])
.args(["--append", file.to_str().unwrap(), "--", FILE_CONTENTS])
.args(["--append", file.to_str().unwrap(), "--"])
.arg(APPENDED_FILE_CONTENTS)
.assert()
.success()
.stdout("")
@ -107,11 +113,12 @@ fn yield_an_error_when_fails(ctx: DistantManagerCtx) {
// distant action file-write {path} -- {contents}
ctx.new_assert_cmd(["fs", "write"])
.args([file.to_str().unwrap(), "--", FILE_CONTENTS])
.args([file.to_str().unwrap(), "--"])
.arg(FILE_CONTENTS)
.assert()
.code(1)
.stdout("")
.stderr(FAILURE_LINE.clone());
.stderr(predicate::str::contains("No such file or directory"));
// Because we're talking to a local server, we can verify locally
file.assert(predicates::path::missing());

@ -16,7 +16,7 @@ fn should_output_system_info(ctx: DistantManagerCtx) {
"Cwd: {:?}\n",
"Path Sep: {:?}\n",
"Username: {:?}\n",
"Shell: {:?}\n",
"Shell: {:?}",
),
env::consts::FAMILY.to_string(),
env::consts::OS.to_string(),

@ -1,13 +1,8 @@
use once_cell::sync::Lazy;
use predicates::prelude::*;
mod reader;
pub use reader::ThreadedReader;
/// Predicate that checks for a single line that is a failure
pub static FAILURE_LINE: Lazy<predicates::str::RegexPredicate> =
Lazy::new(|| regex_pred(r"^.*\n$"));
/// Produces a regex predicate using the given string
pub fn regex_pred(s: &str) -> predicates::str::RegexPredicate {
predicate::str::is_match(s).unwrap()

Loading…
Cancel
Save