diff --git a/src/changelog.rs b/src/changelog.rs index f5f09a749..e3ef90c5b 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 [`crate::command::Command`] +/// 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..7b5c3a668 100644 --- a/src/child.rs +++ b/src/child.rs @@ -43,26 +43,26 @@ 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 +/// is created via the [`Command`](crate::command::Command) struct through [`Session::command`], 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). +/// [`Command`](crate::command::Command). /// -/// 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 +74,8 @@ macro_rules! delegate { /// # } /// ``` #[derive(Debug)] -pub struct RemoteChild<'s> { - session: &'s Session, +pub struct Child { + session: S, imp: RemoteChildImp, stdin: Option, @@ -83,9 +83,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 +102,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 +197,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..cf58f9db6 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,7 +1,7 @@ use crate::escape::escape; +use super::child::Child; use super::stdio::TryFromChildIo; -use super::RemoteChild; use super::Stdio; use super::{Error, Session}; @@ -112,17 +112,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 +134,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 +143,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 +155,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 +167,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 +178,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 +/// `OwnedCommand` 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, `OwnedCommand` 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 +208,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 Command { + session: S, imp: CommandImp, stdin_set: bool, @@ -216,8 +217,8 @@ pub struct Command<'s> { stderr_set: bool, } -impl<'s> Command<'s> { - pub(crate) fn new(session: &'s super::Session, imp: CommandImp) -> Self { +impl Command { + pub(crate) fn new(session: S, imp: CommandImp) -> Self { Self { session, imp, @@ -231,7 +232,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`](Command::raw_arg). /// /// Only one argument can be passed per use. So instead of: /// @@ -251,7 +253,7 @@ impl<'s> Command<'s> { /// ``` /// /// To pass multiple arguments see [`args`](Command::args). - pub fn arg>(&mut self, arg: S) -> &mut Self { + pub fn arg>(&mut self, arg: A) -> &mut Self { self.raw_arg(&*shell_escape::unix::escape(Cow::Borrowed(arg.as_ref()))) } @@ -263,7 +265,7 @@ impl<'s> Command<'s> { /// remote shell. /// /// To pass multiple unescaped arguments see [`raw_args`](Command::raw_args). - pub fn raw_arg>(&mut self, arg: S) -> &mut Self { + pub fn raw_arg>(&mut self, arg: A) -> &mut Self { delegate!(&mut self.imp, imp, { imp.raw_arg(arg.as_ref()); }); @@ -277,10 +279,10 @@ impl<'s> Command<'s> { /// use [`raw_args`](Command::raw_args). /// /// To pass a single argument see [`arg`](Command::arg). - pub fn args(&mut self, args: I) -> &mut Self + 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); @@ -296,10 +298,10 @@ impl<'s> Command<'s> { /// 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 + 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 +353,12 @@ impl<'s> Command<'s> { self.stderr_set = true; self } +} - async fn spawn_impl(&mut self) -> Result, Error> { - Ok(RemoteChild::new( - self.session, +impl Command { + 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 +375,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..582c8a350 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,11 +22,11 @@ //! 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 +//! status](Command::status). Unlike its `std` counterpart though, these methods on [`Command`](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`](Command::spawn) gives you a [`RemoteChild`] rather +//! 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 +167,16 @@ mod builder; pub use builder::{KnownHosts, SessionBuilder}; mod command; -pub use command::{Command, OverSsh}; +pub use command::{Command as OwningCommand, OverSsh}; +/// 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..655efc430 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,4 +1,7 @@ -use super::{Command, Error, ForwardType, KnownHosts, SessionBuilder, Socket}; +use super::{ + Error, ForwardType, KnownHosts, SessionBuilder, Socket, + command::Command, +}; #[cfg(feature = "process-mux")] use super::process_impl; @@ -119,8 +122,7 @@ impl Session { /// let tempdir = builder.launch_master(destination).await?; /// /// let session = Session::new_native_mux(tempdir); - /// - /// let mut child = session + /// /// let mut child = session /// .subsystem("sftp") /// .stdin(Stdio::piped()) /// .stdout(Stdio::piped()) @@ -243,14 +245,14 @@ impl Session { /// The returned `Command` 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<'_> { + pub fn command<'a, S: Into>>(&self, program: S) -> Command<&'_ Self> { self.raw_command(&*shell_escape::unix::escape(program.into())) } @@ -270,13 +272,85 @@ 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<'_> { + pub fn raw_command>(&self, program: S) -> Command<&'_ Self> { Command::new( self, delegate!(&self.0, imp, { imp.raw_command(program.as_ref()).into() }), ) } + /// Version of [`command`](Self::command) which stores an + /// `Arc` instead of a reference, making the resulting + /// [`Command`] 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<(), openssh::Error> { + /// + /// let session = Arc::new(Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?); + /// + /// let log = session.arc_command("less").arg("+F").arg("./some-log-file").spawn().await?; + /// tokio::spawn(async move { + /// // can move the child around + /// let stdout = log.stdout().take()?; + /// let buf = vec![0;100]; + /// while let Ok(n) = stdout.read(&mut buf).await? { + /// if n == 0 { + /// break; + /// } + /// println!("read {:?}", &buf[..n]); + /// } + /// Ok(()) + /// }) + /// # Ok(()) } + pub fn arc_command<'a, P: Into>>( + self: std::sync::Arc, + program: P, + ) -> Command> { + Self::to_command(self, program) + } + + /// 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, + ) -> Command> { + 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) -> Command + where + P: Into>, + S: AsRef + 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) -> Command + where + P: AsRef, + S: AsRef + Clone, + { + let session_impl = delegate!(&session.as_ref().0, imp, { + imp.raw_command(program.as_ref()).into() + }); + Command::new(session, session_impl) + } + /// Constructs a new [`Command`] for launching subsystem `program` on the remote /// host. /// @@ -327,7 +401,7 @@ impl Session { /// /// # Ok(()) } /// ``` - pub fn subsystem>(&self, program: S) -> Command<'_> { + pub fn subsystem>(&self, program: S) -> Command<&'_ Self> { Command::new( self, delegate!(&self.0, imp, { imp.subsystem(program.as_ref()).into() }), @@ -373,7 +447,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) -> Command<&'_ 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()); + } +}