1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
//! Source: <https://github.com/paulgb/interactive_process>
//!
//! `interactive_process` is a lightweight wrapper on [std::process] that provides
//! an interface for running a process and relaying messages to/from it as
//! newline-delimited strings over `stdin`/`stdout`.
//!
//! This behavior is provided through the [InteractiveProcess] struct, which
//! is constructed with a [std::process::Command] and a callback which is called
//! with a [std::io::Result]-wrapped [String] for each string received from the
//! child process. Upon construction, [InteractiveProcess] begins executing the
//! passed command and starts the event loop. Whilst the process is running, you
//! can send
use std::io::{BufRead, BufReader, Result, Write};
use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
use std::thread;
const ASCII_NEWLINE: u8 = 10;
/// Wraps a [Child] object in an interface for doing newline-dellimited string IO
/// with a child process.
///
/// Calling `send` sends a string to the process's `stdin`. A newline delimiter
/// is automatically appended. If newline characters are present in the provided
/// string, they will _not_ be escaped.
///
/// Each newline-separated string sent by the child process over `stdout` results
/// a call to the provided `line_callback` function. The line is wrapped in a
/// [std::io::Result]; it will be in the `Err` state if the line is not valid
/// UTF-8.
///
/// A callback may optionally be provided (via `new_with_exit_callback`) which is
/// invoked when the child's `stdout` stream is closed.
pub struct InteractiveProcess {
child: Child,
stdin: ChildStdin,
}
impl InteractiveProcess {
/// Attempt to start a process for the provided [Command], capturing the
/// standard in and out streams for later use. The provided callback is
/// called for every newline-terminated string written to `stdout` by the
/// process.
pub fn new<T>(command: Command, line_callback: T) -> Result<Self>
where
T: FnMut(Result<String>) + Send + 'static,
{
Self::new_with_exit_callback::<T, T, _>(command, line_callback, None, || ())
}
/// Attempt to start a process for the provided [Command], capturing the
/// standard in and out/err streams for later use. The provided stdout callback is
/// called for every newline-terminated string written to `stdout` by the
/// process. The provided stderr callback is
/// called for every newline-terminated string written to `stderr` by the
/// process.
pub fn new_with_stderr<T, E>(
command: Command,
stdout_callback: T,
stderr_callback: E,
) -> Result<Self>
where
T: FnMut(Result<String>) + Send + 'static,
E: FnMut(Result<String>) + Send + 'static,
{
Self::new_with_exit_callback(command, stdout_callback, Some(stderr_callback), || ())
}
/// Constructor with the same semantics as `new`, except that an additional
/// no-argument closure is provided which is called when the client exits.
pub fn new_with_exit_callback<T, E, S>(
mut command: Command,
mut stdout_callback: T,
stderr_callback: Option<E>,
exit_callback: S,
) -> Result<Self>
where
T: FnMut(Result<String>) + Send + 'static,
E: FnMut(Result<String>) + Send + 'static,
S: Fn() + Send + 'static,
{
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child
.stdout
.take()
.expect("Accessing stdout should never fail after passing Stdio::piped().");
let stderr = if stderr_callback.is_some() {
Some(
child
.stderr
.take()
.expect("Accessing stdout should never fail after passing Stdio::piped()."),
)
} else {
None
};
let stdin = child
.stdin
.take()
.expect("Accessing stdin should never fail after passing Stdio::piped().");
thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
stdout_callback(line);
}
exit_callback();
});
if let Some(mut stderr_callback) = stderr_callback {
let stderr = stderr.expect("Previously set so should not fail");
thread::spawn(move || {
for line in BufReader::new(stderr).lines() {
stderr_callback(line);
}
});
}
Ok(InteractiveProcess { stdin, child })
}
/// Send a string to the client process's `stdin` stream. A newline will be
/// appended to the string.
pub fn send(&mut self, data: &str) -> std::io::Result<()> {
self.stdin.write_all(data.as_bytes())?;
self.stdin.write_all(&[ASCII_NEWLINE])
}
/// Send a string to the client process's `stdin` stream, without appending a
/// newline.
pub fn send_unterminated(&mut self, data: &str) -> std::io::Result<()> {
self.stdin.write_all(data.as_bytes())
}
/// Consume this `InteractiveProcess` and return its child. This closes the
/// process's stdin stream, which usually kills the process. If it doesn't,
/// you can use the returned `Child` object to kill it:
///
/// proc = InteractiveProces::new(...);
/// proc.take().kill().unwrap();
pub fn close(self) -> Child {
self.child
}
/// Block the current thread on the process exiting, and return the exit code when
/// it does. This does _not_ send a signal to kill the child, so it only makes
/// sense when the child process is self-terminating.
pub fn wait(mut self) -> std::io::Result<ExitStatus> {
self.child.wait()
}
}