diff --git a/src/changelog.rs b/src/changelog.rs index f5f09a749..af1d39b66 100644 --- a/src/changelog.rs +++ b/src/changelog.rs @@ -5,7 +5,20 @@ use crate::*; /// /// ## Added /// - Add new fn `SessionBuilder::ssh_auth_sock` +/// - Add new fns [`Session::arc_command`], [`Session::arc_raw_command`], +/// [`Session::to_command`], and [`Session::to_raw_command`] to support +/// session-owning commands +/// - Add generic [`crate::OwningCommand`], to support session-owning +/// commands. +/// - Add [`crate::child::Child`] as a generic version of [`RemoteChild`] +/// to support session-owning commands /// ## Changed +/// - Change [`RemoteChild`] to be an alias to [`crate::child::Child`] +/// owning a session references. +/// - Change [`Command`] to be an alias to [`OwningCommand`] owning a +/// session reference. +/// - Change [`OverSsh::over_ssh`] to be generic and support owned +/// sessions. /// ## Removed #[doc(hidden)] pub mod unreleased {} diff --git a/src/child.rs b/src/child.rs index efc1b7512..7f19e3321 100644 --- a/src/child.rs +++ b/src/child.rs @@ -1,4 +1,4 @@ -use super::{ChildStderr, ChildStdin, ChildStdout, Error, Session}; +use super::{ChildStderr, ChildStdin, ChildStdout, Error}; use std::io; use std::process::{ExitStatus, Output}; @@ -42,27 +42,30 @@ macro_rules! delegate { /// Representation of a running or exited remote child process. /// -/// This structure is used to represent and manage remote child processes. A remote child process -/// is created via the [`Command`](crate::Command) struct through [`Session::command`], which -/// configures the spawning process and can itself be constructed using a builder-style interface. +/// This structure is used to represent and manage remote child +/// processes. A remote child process is created via the +/// [`OwningCommand`](crate::OwningCommand) struct through +/// [`Session::command`](crate::Session::command) or one of its +/// variants, which configures the spawning process and can itself be +/// constructed using a builder-style interface. /// -/// Calling [`wait`](RemoteChild::wait) (or other functions that wrap around it) will make the +/// Calling [`wait`](Child::wait) (or other functions that wrap around it) will make the /// parent process wait until the child has actually exited before continuing. /// -/// Unlike [`std::process::Child`], `RemoteChild` *does* implement [`Drop`], and will terminate the +/// Unlike [`std::process::Child`], `Child` *does* implement [`Drop`], and will terminate the /// local `ssh` process corresponding to the remote process when it goes out of scope. Note that /// this does _not_ terminate the remote process. If you want to do that, you will need to kill it /// yourself by executing a remote command like `pkill` to kill it on the remote side. /// -/// As a result, `RemoteChild` cannot expose `stdin`, `stdout`, and `stderr` as fields for +/// As a result, `Child` cannot expose `stdin`, `stdout`, and `stderr` as fields for /// split-borrows like [`std::process::Child`] does. Instead, it exposes -/// [`stdin`](RemoteChild::stdin), [`stdout`](RemoteChild::stdout), -/// and [`stderr`](RemoteChild::stderr) as methods. Callers can call `.take()` to get the same +/// [`stdin`](Child::stdin), [`stdout`](Child::stdout), +/// and [`stderr`](Child::stderr) as methods. Callers can call `.take()` to get the same /// effect as a split borrow and use multiple streams concurrently. Note that for the streams to be /// available,`Stdio::piped()` should be passed to the corresponding method on -/// [`Command`](crate::Command). +/// [`OwningCommand`](crate::OwningCommand). /// -/// NOTE that once `RemoteChild` is dropped, any data written to `stdin` will not be sent to the +/// NOTE that once `Child` is dropped, any data written to `stdin` will not be sent to the /// remote process and `stdout` and `stderr` will yield EOF immediately. /// /// ```rust,no_run @@ -74,8 +77,8 @@ macro_rules! delegate { /// # } /// ``` #[derive(Debug)] -pub struct RemoteChild<'s> { - session: &'s Session, +pub struct Child { + session: S, imp: RemoteChildImp, stdin: Option, @@ -83,9 +86,9 @@ pub struct RemoteChild<'s> { stderr: Option, } -impl<'s> RemoteChild<'s> { +impl Child { pub(crate) fn new( - session: &'s Session, + session: S, (imp, stdin, stdout, stderr): ( RemoteChildImp, Option, @@ -102,11 +105,6 @@ impl<'s> RemoteChild<'s> { } } - /// Access the SSH session that this remote process was spawned from. - pub fn session(&self) -> &'s Session { - self.session - } - /// Disconnect from this given remote child process. /// /// Note that disconnecting does _not_ kill the remote process, it merely kills the local @@ -202,3 +200,10 @@ impl<'s> RemoteChild<'s> { &mut self.stderr } } + +impl Child { + /// Access the SSH session that this remote process was spawned from. + pub fn session(&self) -> S { + self.session.clone() + } +} diff --git a/src/command.rs b/src/command.rs index bc821a74f..4369dc3f0 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,12 +1,13 @@ use crate::escape::escape; +use super::child::Child; use super::stdio::TryFromChildIo; -use super::RemoteChild; use super::Stdio; use super::{Error, Session}; use std::borrow::Cow; use std::ffi::OsStr; +use std::ops::Deref; use std::process; #[derive(Debug)] @@ -112,17 +113,17 @@ pub trait OverSsh { /// } /// /// ``` - fn over_ssh<'session>( + fn over_ssh + Clone>( &self, - session: &'session Session, - ) -> Result, crate::Error>; + session: S, + ) -> Result, crate::Error>; } impl OverSsh for std::process::Command { - fn over_ssh<'session>( + fn over_ssh + Clone>( &self, - session: &'session Session, - ) -> Result, crate::Error> { + session: S, + ) -> Result, crate::Error> { // I'd really like `!self.get_envs().is_empty()` here, but that's // behind a `exact_size_is_empty` feature flag. if self.get_envs().len() > 0 { @@ -134,7 +135,7 @@ impl OverSsh for std::process::Command { } let program_escaped: Cow<'_, OsStr> = escape(self.get_program()); - let mut command = session.raw_command(program_escaped); + let mut command = Session::to_raw_command(session, program_escaped); let args = self.get_args().map(escape); command.raw_args(args); @@ -143,10 +144,10 @@ impl OverSsh for std::process::Command { } impl OverSsh for tokio::process::Command { - fn over_ssh<'session>( + fn over_ssh + Clone>( &self, - session: &'session Session, - ) -> Result, crate::Error> { + session: S, + ) -> Result, crate::Error> { self.as_std().over_ssh(session) } } @@ -155,10 +156,10 @@ impl OverSsh for &S where S: OverSsh, { - fn over_ssh<'session>( + fn over_ssh + Clone>( &self, - session: &'session Session, - ) -> Result, crate::Error> { + session: U, + ) -> Result, crate::Error> { ::over_ssh(self, session) } } @@ -167,10 +168,10 @@ impl OverSsh for &mut S where S: OverSsh, { - fn over_ssh<'session>( + fn over_ssh + Clone>( &self, - session: &'session Session, - ) -> Result, crate::Error> { + session: U, + ) -> Result, crate::Error> { ::over_ssh(self, session) } } @@ -178,18 +179,19 @@ where /// A remote process builder, providing fine-grained control over how a new remote process should /// be spawned. /// -/// A default configuration can be generated using [`Session::command(program)`](Session::command), -/// where `program` gives a path to the program to be executed. Additional builder methods allow -/// the configuration to be changed (for example, by adding arguments) prior to spawning. The -/// interface is almost identical to that of [`std::process::Command`]. +/// A default configuration can be generated using [`Session::command(program)`](Session::command) +/// or [`Session::arc_command(program)`](Session::arc_command), where `program` gives a path to +/// the program to be executed. Additional builder methods allow the configuration to be changed +/// (for example, by adding arguments) prior to spawning. The interface is almost identical to +/// that of [`std::process::Command`]. /// -/// `Command` can be reused to spawn multiple remote processes. The builder methods change the -/// command without needing to immediately spawn the process. Similarly, you can call builder +/// `OwningCommand` can be reused to spawn multiple remote processes. The builder methods change +/// the command without needing to immediately spawn the process. Similarly, you can call builder /// methods after spawning a process and then spawn a new process with the modified settings. /// /// # Environment variables and current working directory. /// -/// You'll notice that unlike its `std` counterpart, `Command` does not have any methods for +/// You'll notice that unlike its `std` counterpart, `OwningCommand` does not have any methods for /// setting environment variables or the current working directory for the remote command. This is /// because the SSH protocol does not support this (at least not in its standard configuration). /// For more details on this, see the `ENVIRONMENT` section of [`ssh(1)`]. To work around this, @@ -207,8 +209,8 @@ where /// [`ssh(1)`]: https://linux.die.net/man/1/ssh /// [`env(1)`]: https://linux.die.net/man/1/env #[derive(Debug)] -pub struct Command<'s> { - session: &'s Session, +pub struct OwningCommand { + session: S, imp: CommandImp, stdin_set: bool, @@ -216,8 +218,8 @@ pub struct Command<'s> { stderr_set: bool, } -impl<'s> Command<'s> { - pub(crate) fn new(session: &'s super::Session, imp: CommandImp) -> Self { +impl OwningCommand { + pub(crate) fn new(session: S, imp: CommandImp) -> Self { Self { session, imp, @@ -231,7 +233,8 @@ impl<'s> Command<'s> { /// Adds an argument to pass to the remote program. /// /// Before it is passed to the remote host, `arg` is escaped so that special characters aren't - /// evaluated by the remote shell. If you do not want this behavior, use [`raw_arg`](Command::raw_arg). + /// evaluated by the remote shell. If you do not want this behavior, use + /// [`raw_arg`](Self::raw_arg). /// /// Only one argument can be passed per use. So instead of: /// @@ -250,20 +253,20 @@ impl<'s> Command<'s> { /// # ; } /// ``` /// - /// To pass multiple arguments see [`args`](Command::args). - pub fn arg>(&mut self, arg: S) -> &mut Self { + /// To pass multiple arguments see [`args`](Self::args). + pub fn arg>(&mut self, arg: A) -> &mut Self { self.raw_arg(&*shell_escape::unix::escape(Cow::Borrowed(arg.as_ref()))) } /// Adds an argument to pass to the remote program. /// - /// Unlike [`arg`](Command::arg), this method does not shell-escape `arg`. The argument is passed as written + /// Unlike [`arg`](Self::arg), this method does not shell-escape `arg`. The argument is passed as written /// to `ssh`, which will pass it again as an argument to the remote shell. Since the remote /// shell may do argument parsing, characters such as spaces and `*` may be interpreted by the /// remote shell. /// - /// To pass multiple unescaped arguments see [`raw_args`](Command::raw_args). - pub fn raw_arg>(&mut self, arg: S) -> &mut Self { + /// To pass multiple unescaped arguments see [`raw_args`](Self::raw_args). + pub fn raw_arg>(&mut self, arg: A) -> &mut Self { delegate!(&mut self.imp, imp, { imp.raw_arg(arg.as_ref()); }); @@ -274,13 +277,13 @@ impl<'s> Command<'s> { /// /// Before they are passed to the remote host, each argument in `args` is escaped so that /// special characters aren't evaluated by the remote shell. If you do not want this behavior, - /// use [`raw_args`](Command::raw_args). + /// use [`raw_args`](Self::raw_args). /// - /// To pass a single argument see [`arg`](Command::arg). - pub fn args(&mut self, args: I) -> &mut Self + /// To pass a single argument see [`arg`](Self::arg). + pub fn args(&mut self, args: I) -> &mut Self where - I: IntoIterator, - S: AsRef, + I: IntoIterator, + A: AsRef, { for arg in args { self.arg(arg); @@ -290,16 +293,16 @@ impl<'s> Command<'s> { /// Adds multiple arguments to pass to the remote program. /// - /// Unlike [`args`](Command::args), this method does not shell-escape `args`. The arguments are passed as + /// Unlike [`args`](Self::args), this method does not shell-escape `args`. The arguments are passed as /// written to `ssh`, which will pass them again as arguments to the remote shell. However, /// since the remote shell may do argument parsing, characters such as spaces and `*` may be /// interpreted by the remote shell. /// - /// To pass a single argument see [`raw_arg`](Command::raw_arg). - pub fn raw_args(&mut self, args: I) -> &mut Self + /// To pass a single argument see [`raw_arg`](Self::raw_arg). + pub fn raw_args(&mut self, args: I) -> &mut Self where - I: IntoIterator, - S: AsRef, + I: IntoIterator, + A: AsRef, { for arg in args { self.raw_arg(arg); @@ -351,10 +354,12 @@ impl<'s> Command<'s> { self.stderr_set = true; self } +} - async fn spawn_impl(&mut self) -> Result, Error> { - Ok(RemoteChild::new( - self.session, +impl OwningCommand { + async fn spawn_impl(&mut self) -> Result, Error> { + Ok(Child::new( + self.session.clone(), delegate!(&mut self.imp, imp, { let (imp, stdin, stdout, stderr) = imp.spawn().await?; ( @@ -371,7 +376,7 @@ impl<'s> Command<'s> { /// instead. /// /// By default, stdin, stdout and stderr are inherited. - pub async fn spawn(&mut self) -> Result, Error> { + pub async fn spawn(&mut self) -> Result, Error> { if !self.stdin_set { self.stdin(Stdio::inherit()); } diff --git a/src/lib.rs b/src/lib.rs index 7444fced0..196fcc76f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,15 +18,17 @@ //! Note that the maximum number of multiplexed remote commands is 10 by default. This value can be //! increased by changing the `MaxSessions` setting in [`sshd_config`]. //! -//! Much like with [`std::process::Command`], you have multiple options when it comes to launching -//! a remote command. You can [spawn](Command::spawn) the remote command, which just gives you a -//! handle to the running process, you can run the command and wait for its -//! [output](Command::output), or you can run it and just extract its [exit -//! status](Command::status). Unlike its `std` counterpart though, these methods on [`Command`] can -//! fail even if the remote command executed successfully, since there is a fallible network -//! separating you from it. -//! -//! Also unlike its `std` counterpart, [`spawn`](Command::spawn) gives you a [`RemoteChild`] rather +//! Much like with [`std::process::Command`], you have multiple +//! options when it comes to launching a remote command. You can +//! [spawn](Command::spawn) the remote command, which just gives you a +//! handle to the running process, you can run the command and wait +//! for its [output](Command::output), or you can run it and just +//! extract its [exit status](Command::status). Unlike its `std` +//! counterpart though, these methods on [`OwningCommand`] can fail +//! even if the remote command executed successfully, since there is a +//! fallible network separating you from it. +//! +//! Also unlike its `std` counterpart, [`spawn`](OwningCommand::spawn) gives you a [`Child`] rather //! than a [`std::process::Child`]. Behind the scenes, a remote child is really just a process //! handle to the _local_ `ssh` instance corresponding to the spawned remote command. The behavior //! of the methods of [`RemoteChild`] therefore match the behavior of `ssh`, rather than that of @@ -167,12 +169,16 @@ mod builder; pub use builder::{KnownHosts, SessionBuilder}; mod command; -pub use command::{Command, OverSsh}; +pub use command::{OverSsh, OwningCommand}; +/// Convenience [`OwningCommand`] alias when working with a session reference. +pub type Command<'s> = OwningCommand<&'s Session>; mod escape; mod child; -pub use child::RemoteChild; +pub use child::Child; +/// Convenience [`Child`] alias when working with a session reference. +pub type RemoteChild<'a> = Child<&'a Session>; mod error; pub use error::Error; diff --git a/src/session.rs b/src/session.rs index d023cc8bc..719e9808b 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,4 +1,4 @@ -use super::{Command, Error, ForwardType, KnownHosts, SessionBuilder, Socket}; +use super::{Error, ForwardType, KnownHosts, OwningCommand, SessionBuilder, Socket}; #[cfg(feature = "process-mux")] use super::process_impl; @@ -8,6 +8,7 @@ use super::native_mux_impl; use std::borrow::Cow; use std::ffi::OsStr; +use std::ops::Deref; use std::path::Path; use tempfile::TempDir; @@ -119,7 +120,6 @@ impl Session { /// let tempdir = builder.launch_master(destination).await?; /// /// let session = Session::new_native_mux(tempdir); - /// /// let mut child = session /// .subsystem("sftp") /// .stdin(Stdio::piped()) @@ -233,34 +233,34 @@ impl Session { delegate!(&self.0, imp, { imp.ctl() }) } - /// Constructs a new [`Command`] for launching the program at path `program` on the remote + /// Constructs a new [`OwningCommand`] for launching the program at path `program` on the remote /// host. /// /// Before it is passed to the remote host, `program` is escaped so that special characters /// aren't evaluated by the remote shell. If you do not want this behavior, use /// [`raw_command`](Session::raw_command). /// - /// The returned `Command` is a builder, with the following default configuration: + /// The returned `OwningCommand` is a builder, with the following default configuration: /// /// * No arguments to the program - /// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for + /// * Empty stdin and discard stdout/stderr for `spawn` or `status`, but create output pipes for /// `output` /// /// Builder methods are provided to change these defaults and otherwise configure the process. /// /// If `program` is not an absolute path, the `PATH` will be searched in an OS-defined way on /// the host. - pub fn command<'a, S: Into>>(&self, program: S) -> Command<'_> { - self.raw_command(&*shell_escape::unix::escape(program.into())) + pub fn command<'a, S: Into>>(&self, program: S) -> OwningCommand<&'_ Self> { + Self::to_command(self, program) } - /// Constructs a new [`Command`] for launching the program at path `program` on the remote + /// Constructs a new [`OwningCommand`] for launching the program at path `program` on the remote /// host. /// /// Unlike [`command`](Session::command), this method does not shell-escape `program`, so it may be evaluated in /// unforeseen ways by the remote shell. /// - /// The returned `Command` is a builder, with the following default configuration: + /// The returned `OwningCommand` is a builder, with the following default configuration: /// /// * No arguments to the program /// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for @@ -270,20 +270,91 @@ impl Session { /// /// If `program` is not an absolute path, the `PATH` will be searched in an OS-defined way on /// the host. - pub fn raw_command>(&self, program: S) -> Command<'_> { - Command::new( - self, - delegate!(&self.0, imp, { imp.raw_command(program.as_ref()).into() }), - ) + pub fn raw_command>(&self, program: S) -> OwningCommand<&'_ Self> { + Self::to_raw_command(self, program) + } + + /// Version of [`command`](Self::command) which stores an + /// `Arc` instead of a reference, making the resulting + /// [`OwningCommand`] independent from the source [`Session`] and + /// simplifying lifetime management and concurrent usage: + /// + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use tokio::io::AsyncReadExt; + /// # use openssh::{Session, KnownHosts}; + /// # #[cfg(feature = "native-mux")] + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// + /// let session = Arc::new(Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?); + /// + /// let mut log = session.arc_command("less").arg("+F").arg("./some-log-file").spawn().await?; + /// # let t: tokio::task::JoinHandle> = + /// tokio::spawn(async move { + /// // can move the child around + /// let mut stdout = log.stdout().take().unwrap(); + /// let mut buf = vec![0;100]; + /// loop { + /// let n = stdout.read(&mut buf).await?; + /// if n == 0 { + /// return Ok(()) + /// } + /// println!("read {:?}", &buf[..n]); + /// } + /// }); + /// # t.await??; + /// # Ok(()) } + pub fn arc_command<'a, P: Into>>( + self: std::sync::Arc, + program: P, + ) -> OwningCommand> { + Self::to_command(self, program) } - /// Constructs a new [`Command`] for launching subsystem `program` on the remote + /// Version of [`raw_command`](Self::raw_command) which stores an + /// `Arc`, similar to [`arc_command`](Self::arc_command). + pub fn arc_raw_command>( + self: std::sync::Arc, + program: P, + ) -> OwningCommand> { + Self::to_raw_command(self, program) + } + + /// Version of [`command`](Self::command) which stores an + /// arbitrary shared-ownership smart pointer to a [`Session`], + /// more generic but less convenient than + /// [`arc_command`](Self::arc_command). + pub fn to_command<'a, S, P>(session: S, program: P) -> OwningCommand + where + P: Into>, + S: Deref + Clone, + { + Self::to_raw_command(session, &*shell_escape::unix::escape(program.into())) + } + + /// Version of [`raw_command`](Self::raw_command) which stores an + /// arbitrary shared-ownership smart pointer to a [`Session`], + /// more generic but less convenient than + /// [`arc_raw_command`](Self::arc_raw_command). + pub fn to_raw_command(session: S, program: P) -> OwningCommand + where + P: AsRef, + S: Deref + Clone, + { + let session_impl = delegate!(&session.0, imp, { + imp.raw_command(program.as_ref()).into() + }); + OwningCommand::new(session, session_impl) + } + + /// Constructs a new [`OwningCommand`] for launching subsystem `program` on the remote /// host. /// /// Unlike [`command`](Session::command), this method does not shell-escape `program`, so it may be evaluated in /// unforeseen ways by the remote shell. /// - /// The returned `Command` is a builder, with the following default configuration: + /// The returned `OwningCommand` is a builder, with the following default configuration: /// /// * No arguments to the program /// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for @@ -327,14 +398,25 @@ impl Session { /// /// # Ok(()) } /// ``` - pub fn subsystem>(&self, program: S) -> Command<'_> { - Command::new( - self, - delegate!(&self.0, imp, { imp.subsystem(program.as_ref()).into() }), - ) + pub fn subsystem>(&self, program: S) -> OwningCommand<&'_ Self> { + Self::to_subsystem(self, program) + } + + /// Version of [`subsystem`](Self::subsystem) which stores an + /// arbitrary shared-ownership pointer to a session making the + /// resulting [`OwningCommand`] independent from the source + /// [`Session`] and simplifying lifetime management and concurrent + /// usage: + pub fn to_subsystem(session: S, program: P) -> OwningCommand + where + P: AsRef, + S: Deref + Clone, + { + let session_impl = delegate!(&session.0, imp, { imp.subsystem(program.as_ref()).into() }); + OwningCommand::new(session, session_impl) } - /// Constructs a new [`Command`] that runs the provided shell command on the remote host. + /// Constructs a new [`OwningCommand`] that runs the provided shell command on the remote host. /// /// The provided command is passed as a single, escaped argument to `sh -c`, and from that /// point forward the behavior is up to `sh`. Since this executes a shell command, keep in mind @@ -342,7 +424,7 @@ impl Session { /// splitting, variable expansion, and other funkyness. I _highly_ recommend you read /// [this article] if you observe strange things. /// - /// While the returned `Command` is a builder, like for [`command`](Session::command), you should not add + /// While the returned `OwningCommand` is a builder, like for [`command`](Session::command), you should not add /// additional arguments to it, since the arguments are already passed within the shell /// command. /// @@ -373,7 +455,7 @@ impl Session { /// [POSIX compliant]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xcu_chap02.html /// [this article]: https://mywiki.wooledge.org/Arguments /// [`shell-escape`]: https://crates.io/crates/shell-escape - pub fn shell>(&self, command: S) -> Command<'_> { + pub fn shell>(&self, command: S) -> OwningCommand<&'_ Self> { let mut cmd = self.command("sh"); cmd.arg("-c").arg(command.as_ref()); cmd diff --git a/tests/openssh.rs b/tests/openssh.rs index 3d3549c5f..72fc0dfd3 100644 --- a/tests/openssh.rs +++ b/tests/openssh.rs @@ -1057,3 +1057,73 @@ async fn test_read_large_file_bug() { assert_eq!(stdout.len(), bs * count); } } + +#[tokio::test] +#[cfg_attr(not(ci), ignore)] +async fn arc_client() { + for session in connects().await { + let session = std::sync::Arc::new(session); + let mut child = session + .clone() + .arc_command("cat") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .await + .unwrap(); + + drop(session); + + // write something to standard in and send EOF + let mut stdin = child.stdin().take().unwrap(); + stdin.write_all(b"hello world").await.unwrap(); + drop(stdin); + + // cat should print it back on stdout + let mut stdout = child.stdout().take().unwrap(); + let mut out = String::new(); + stdout.read_to_string(&mut out).await.unwrap(); + assert_eq!(out, "hello world"); + drop(stdout); + + // cat should now have terminated + let status = child.wait().await.unwrap(); + + // ... successfully + assert!(status.success()); + } +} + +#[tokio::test] +#[cfg_attr(not(ci), ignore)] +async fn generic_client() { + for session in connects().await { + let session = std::rc::Rc::new(session); + let mut child = Session::to_command(session.clone(), "cat") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .await + .unwrap(); + + drop(session); + + // write something to standard in and send EOF + let mut stdin = child.stdin().take().unwrap(); + stdin.write_all(b"hello world").await.unwrap(); + drop(stdin); + + // cat should print it back on stdout + let mut stdout = child.stdout().take().unwrap(); + let mut out = String::new(); + stdout.read_to_string(&mut out).await.unwrap(); + assert_eq!(out, "hello world"); + drop(stdout); + + // cat should now have terminated + let status = child.wait().await.unwrap(); + + // ... successfully + assert!(status.success()); + } +}