2018-07-25 03:34:35 -07:00
|
|
|
use std::env;
|
|
|
|
use std::io::{self, Read};
|
|
|
|
use std::net::{SocketAddr, TcpListener, TcpStream};
|
|
|
|
use std::path::{PathBuf, Path};
|
2018-07-27 13:32:09 -07:00
|
|
|
use std::process::{Child, Command, Stdio};
|
2018-07-25 03:34:35 -07:00
|
|
|
use std::thread;
|
|
|
|
use std::time::{Instant, Duration};
|
|
|
|
|
|
|
|
use curl::easy::Easy;
|
|
|
|
use failure::{ResultExt, Error};
|
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use serde_json;
|
|
|
|
|
|
|
|
use shell::Shell;
|
|
|
|
|
|
|
|
/// Execute a headless browser tests against a server running on `server`
|
|
|
|
/// address.
|
2018-07-26 18:49:20 -07:00
|
|
|
///
|
|
|
|
/// This function will take care of everything from spawning the WebDriver
|
|
|
|
/// binary, controlling it, running tests, scraping output, displaying output,
|
|
|
|
/// etc. It will return `Ok` if all tests finish successfully, and otherwise it
|
|
|
|
/// will return an error if some tests failed.
|
2018-07-25 03:34:35 -07:00
|
|
|
pub fn run(server: &SocketAddr, shell: &Shell) -> Result<(), Error> {
|
|
|
|
let (driver, args) = Driver::find()?;
|
|
|
|
println!("Running headless tests in {} with `{}`",
|
|
|
|
driver.browser(),
|
|
|
|
driver.path().display());
|
|
|
|
|
|
|
|
// Allow tests to run in parallel (in theory) by finding any open port
|
|
|
|
// available for our driver. We can't bind the port for the driver, but
|
|
|
|
// hopefully the OS gives this invocation unique ports across processes
|
|
|
|
let driver_addr = TcpListener::bind("127.0.0.1:0")?.local_addr()?;
|
|
|
|
|
|
|
|
// Spawn the driver binary, collecting its stdout/stderr in separate
|
|
|
|
// threads. We'll print this output later.
|
|
|
|
shell.status("Spawning Geckodriver...");
|
2018-07-27 13:32:09 -07:00
|
|
|
let mut cmd = Command::new(driver.path());
|
2018-07-25 03:34:35 -07:00
|
|
|
cmd.args(&args)
|
2018-07-27 13:32:09 -07:00
|
|
|
.arg(format!("--port={}", driver_addr.port().to_string()));
|
|
|
|
let mut child = BackgroundChild::spawn(driver.path(), &mut cmd, shell)?;
|
2018-07-25 03:34:35 -07:00
|
|
|
|
|
|
|
// Wait for the driver to come online and bind its port before we try to
|
|
|
|
// connect to it.
|
|
|
|
let start = Instant::now();
|
|
|
|
let max = Duration::new(5, 0);
|
|
|
|
let mut bound = false;
|
|
|
|
while start.elapsed() < max {
|
|
|
|
if TcpStream::connect(&driver_addr).is_ok() {
|
|
|
|
bound = true;
|
|
|
|
break
|
|
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
}
|
|
|
|
if !bound {
|
|
|
|
bail!("driver failed to bind port during startup")
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
let mut client = Client {
|
|
|
|
handle: Easy::new(),
|
2018-07-25 03:34:35 -07:00
|
|
|
driver_addr,
|
2018-07-27 13:32:09 -07:00
|
|
|
session: None,
|
2018-07-25 03:34:35 -07:00
|
|
|
};
|
|
|
|
shell.status("Starting new webdriver session...");
|
|
|
|
// Allocate a new session with the webdriver protocol, and once we've done
|
|
|
|
// so schedule the browser to get closed with a call to `close_window`.
|
|
|
|
let id = client.new_session(&driver)?;
|
2018-07-27 13:32:09 -07:00
|
|
|
client.session = Some(id.clone());
|
2018-07-25 03:34:35 -07:00
|
|
|
|
|
|
|
// Visit our local server to open up the page that runs tests, and then get
|
|
|
|
// some handles to objects on the page which we'll be scraping output from.
|
|
|
|
let url = format!("http://{}", server);
|
|
|
|
shell.status(&format!("Visiting {}...", url));
|
|
|
|
client.goto(&id, &url)?;
|
|
|
|
shell.status("Loading page elements...");
|
|
|
|
let output = client.element(&id, "#output")?;
|
|
|
|
let logs = client.element(&id, "#console_log")?;
|
|
|
|
let errors = client.element(&id, "#console_error")?;
|
|
|
|
|
|
|
|
// At this point we need to wait for the test to finish before we can take a
|
|
|
|
// look at what happened. There appears to be no great way to do this with
|
|
|
|
// the webdriver protocol today (in terms of synchronization), so for now we
|
|
|
|
// just go with a loop.
|
|
|
|
//
|
|
|
|
// We periodically check the page to see if the output contains a known
|
|
|
|
// string to only be printed when tests have finished running.
|
|
|
|
//
|
|
|
|
// TODO: harness failures aren't well handled here, they always force a
|
|
|
|
// timeout. These sorts of failures could be "you typo'd the path to a
|
|
|
|
// local script" which is pretty bad to time out for, we should detect
|
|
|
|
// this on the page and look for such output here, printing diagnostic
|
|
|
|
// information.
|
|
|
|
shell.status("Waiting for test to finish...");
|
|
|
|
let start = Instant::now();
|
|
|
|
let max = Duration::new(20, 0);
|
|
|
|
while start.elapsed() < max {
|
|
|
|
if client.text(&id, &output)?.contains("test result: ") {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
}
|
|
|
|
shell.clear();
|
|
|
|
|
|
|
|
// Tests have now finished or have timed out. At this point we need to print
|
|
|
|
// what happened on the console. Currently we just do this by scraping the
|
|
|
|
// output of various fields and printing them out, hopefully providing
|
|
|
|
// enough diagnostic info to see what went wrong (if anything).
|
|
|
|
let output = client.text(&id, &output)?;
|
|
|
|
let logs = client.text(&id, &logs)?;
|
|
|
|
let errors = client.text(&id, &errors)?;
|
|
|
|
|
|
|
|
if output.contains("test result: ") {
|
|
|
|
println!("{}", output);
|
|
|
|
|
|
|
|
// If the tests harness finished (either successfully or unsuccessfully)
|
|
|
|
// then in theory all the info needed to debug the failure is in its own
|
|
|
|
// output, so we shouldn't need the driver logs to get printed.
|
2018-07-27 13:32:09 -07:00
|
|
|
child.print_stdio_on_drop = false;
|
2018-07-25 03:34:35 -07:00
|
|
|
} else {
|
|
|
|
println!("failed to detect test as having been run");
|
|
|
|
if output.len() > 0 {
|
|
|
|
println!("output div contained:\n{}", tab(&output));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if logs.len() > 0 {
|
|
|
|
println!("console.log div contained:\n{}", tab(&logs));
|
|
|
|
}
|
|
|
|
if errors.len() > 0 {
|
|
|
|
println!("console.log div contained:\n{}", tab(&errors));
|
|
|
|
}
|
|
|
|
|
2018-07-26 18:49:20 -07:00
|
|
|
if !output.contains("test result: ok") {
|
|
|
|
bail!("some tests failed")
|
|
|
|
}
|
|
|
|
|
2018-07-25 03:34:35 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
enum Driver {
|
|
|
|
Gecko(PathBuf),
|
|
|
|
Safari(PathBuf),
|
|
|
|
Chrome(PathBuf),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Driver {
|
2018-07-26 18:49:20 -07:00
|
|
|
/// Attempts to find an appropriate WebDriver server binary to execute tests
|
|
|
|
/// with. Performs a number of heuristics to find one available, including:
|
|
|
|
///
|
|
|
|
/// * Env vars like `GECKODRIVER` point to the path to a binary to execute.
|
|
|
|
/// * Otherwise, `PATH` is searched for an appropriate binary.
|
|
|
|
///
|
|
|
|
/// In both cases a list of auxiliary arguments is also returned which is
|
|
|
|
/// configured through env vars like `GECKODRIVER_ARGS` to support extra
|
|
|
|
/// arguments to the driver's invocation.
|
2018-07-25 03:34:35 -07:00
|
|
|
fn find() -> Result<(Driver, Vec<String>), Error> {
|
|
|
|
let env_args = |name: &str| {
|
|
|
|
env::var(format!("{}_ARGS", name.to_uppercase()))
|
|
|
|
.unwrap_or_default()
|
|
|
|
.split_whitespace()
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
};
|
|
|
|
|
|
|
|
let drivers = [
|
|
|
|
("geckodriver", Driver::Gecko as fn(PathBuf) -> Driver),
|
|
|
|
("safaridriver", Driver::Safari as fn(PathBuf) -> Driver),
|
|
|
|
("chromedriver", Driver::Chrome as fn(PathBuf) -> Driver),
|
|
|
|
];
|
|
|
|
|
|
|
|
// First up, if env vars like GECKODRIVER are present, use those to
|
|
|
|
// allow forcing usage of a particular driver.
|
|
|
|
for (driver, ctor) in drivers.iter() {
|
|
|
|
let env = driver.to_uppercase();
|
|
|
|
let path = match env::var_os(&env) {
|
|
|
|
Some(path) => path,
|
|
|
|
None => continue,
|
|
|
|
};
|
|
|
|
return Ok((ctor(path.into()), env_args(driver)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Next, check PATH. If we can find any supported driver, use that by
|
|
|
|
// default.
|
|
|
|
for path in env::split_paths(&env::var_os("PATH").unwrap_or_default()) {
|
|
|
|
let found = drivers
|
|
|
|
.iter()
|
|
|
|
.find(|(name, _)| {
|
|
|
|
path.join(name)
|
|
|
|
.with_extension(env::consts::EXE_EXTENSION)
|
|
|
|
.exists()
|
|
|
|
});
|
|
|
|
let (name, ctor) = match found {
|
|
|
|
Some(p) => p,
|
|
|
|
None => continue,
|
|
|
|
};
|
|
|
|
return Ok((ctor(name.into()), env_args(name)))
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: download an appropriate driver? How to know which one to
|
|
|
|
// download?
|
|
|
|
|
2018-07-27 13:35:16 -07:00
|
|
|
bail!("\
|
|
|
|
failed to find a suitable WebDriver binary to drive headless testing; to
|
|
|
|
configure the location of the webdriver binary you can use environment
|
|
|
|
variables like `GECKODRIVER=/path/to/geckodriver` or make sure that the binary
|
|
|
|
is in `PATH`
|
|
|
|
|
|
|
|
This crate currently supports `geckodriver`, `chromedriver`, and `safaridriver`,
|
|
|
|
although more driver support may be added! You can download these at:
|
|
|
|
|
|
|
|
* geckodriver - https://github.com/mozilla/geckodriver/releases
|
|
|
|
* chromedriver - http://chromedriver.chromium.org/downloads
|
|
|
|
* safaridriver - should be preinstalled on OSX
|
|
|
|
|
|
|
|
If you're still having difficulty resolving this error, please feel free to open
|
|
|
|
an issue against rustwasm/wasm-bindgen!
|
|
|
|
")
|
2018-07-25 03:34:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn path(&self) -> &Path {
|
|
|
|
match self {
|
|
|
|
Driver::Gecko(path) => path,
|
|
|
|
Driver::Safari(path) => path,
|
|
|
|
Driver::Chrome(path) => path,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn browser(&self) -> &str {
|
|
|
|
match self {
|
|
|
|
Driver::Gecko(_) => "Firefox",
|
|
|
|
Driver::Safari(_) => "Safari",
|
|
|
|
Driver::Chrome(_) => "Chrome",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Client {
|
2018-07-27 13:32:09 -07:00
|
|
|
handle: Easy,
|
2018-07-25 03:34:35 -07:00
|
|
|
driver_addr: SocketAddr,
|
2018-07-27 13:32:09 -07:00
|
|
|
session: Option<String>,
|
2018-07-25 03:34:35 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
enum Method<'a> {
|
|
|
|
Get,
|
|
|
|
Post(&'a str),
|
|
|
|
Delete,
|
|
|
|
}
|
|
|
|
|
2018-07-26 18:49:20 -07:00
|
|
|
// Below here is a bunch of details of the WebDriver protocol implementation.
|
|
|
|
// I'm not too too familiar with them myself, but these seem to work! I mostly
|
|
|
|
// copied the `webdriver-client` crate when writing the below bindings.
|
|
|
|
|
2018-07-25 03:34:35 -07:00
|
|
|
impl Client {
|
2018-07-27 13:32:09 -07:00
|
|
|
fn new_session(&mut self, driver: &Driver) -> Result<String, Error> {
|
2018-07-25 03:34:35 -07:00
|
|
|
match driver {
|
|
|
|
Driver::Gecko(_) => {
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
value: ResponseValue,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct ResponseValue {
|
|
|
|
#[serde(rename = "sessionId")]
|
|
|
|
session_id: String,
|
|
|
|
}
|
|
|
|
let request = json!({
|
|
|
|
"capabilities": {
|
|
|
|
"alwaysMatch": {
|
|
|
|
"moz:firefoxOptions": {
|
|
|
|
"args": ["-headless"],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let x: Response = self.post("/session", &request)?;
|
|
|
|
Ok(x.value.session_id)
|
|
|
|
}
|
|
|
|
Driver::Safari(_) => {
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
#[serde(rename = "sessionId")]
|
|
|
|
session_id: String,
|
|
|
|
}
|
|
|
|
let request = json!({
|
|
|
|
"desiredCapabilities": {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let x: Response = self.post("/session", &request)?;
|
|
|
|
Ok(x.session_id)
|
|
|
|
}
|
|
|
|
Driver::Chrome(_) => {
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
#[serde(rename = "sessionId")]
|
|
|
|
session_id: String,
|
|
|
|
}
|
|
|
|
let request = json!({
|
|
|
|
"desiredCapabilities": {
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let x: Response = self.post("/session", &request)?;
|
|
|
|
Ok(x.session_id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn close_window(&mut self, id: &str) -> Result<(), Error> {
|
2018-07-25 03:34:35 -07:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
}
|
|
|
|
let x: Response = self.delete(&format!("/session/{}/window", id))?;
|
|
|
|
drop(x);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn goto(&mut self, id: &str, url: &str) -> Result<(), Error> {
|
2018-07-25 03:34:35 -07:00
|
|
|
#[derive(Serialize)]
|
|
|
|
struct Request {
|
|
|
|
url: String,
|
|
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
}
|
|
|
|
|
|
|
|
let request = Request {
|
|
|
|
url: url.to_string(),
|
|
|
|
};
|
|
|
|
let x: Response = self.post(&format!("/session/{}/url", id), &request)?;
|
|
|
|
drop(x);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn element(&mut self, id: &str, selector: &str) -> Result<String, Error> {
|
2018-07-25 03:34:35 -07:00
|
|
|
#[derive(Serialize)]
|
|
|
|
struct Request {
|
|
|
|
using: String,
|
|
|
|
value: String,
|
|
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
value: Reference,
|
|
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Reference {
|
|
|
|
#[serde(rename = "element-6066-11e4-a52e-4f735466cecf")]
|
|
|
|
gecko_reference: Option<String>,
|
|
|
|
#[serde(rename = "ELEMENT")]
|
|
|
|
safari_reference: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
let request = Request {
|
|
|
|
using: "css selector".to_string(),
|
|
|
|
value: selector.to_string(),
|
|
|
|
};
|
|
|
|
let x: Response = self.post(&format!("/session/{}/element", id), &request)?;
|
|
|
|
Ok(x.value.gecko_reference
|
|
|
|
.or(x.value.safari_reference)
|
|
|
|
.ok_or(format_err!("failed to find element reference in response"))?)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn text(&mut self, id: &str, element: &str) -> Result<String, Error> {
|
2018-07-25 03:34:35 -07:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Response {
|
|
|
|
value: String,
|
|
|
|
}
|
|
|
|
let x: Response = self.get(&format!("/session/{}/element/{}/text", id, element))?;
|
|
|
|
Ok(x.value)
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn get<U>(&mut self, path: &str) -> Result<U, Error>
|
2018-07-25 03:34:35 -07:00
|
|
|
where U: for<'a> Deserialize<'a>,
|
|
|
|
{
|
|
|
|
debug!("GET {}", path);
|
|
|
|
let result = self.doit(path, Method::Get)?;
|
|
|
|
Ok(serde_json::from_str(&result)?)
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn post<T, U>(&mut self, path: &str, data: &T) -> Result<U, Error>
|
2018-07-25 03:34:35 -07:00
|
|
|
where T: Serialize,
|
|
|
|
U: for<'a> Deserialize<'a>,
|
|
|
|
{
|
|
|
|
let input = serde_json::to_string(data)?;
|
|
|
|
debug!("POST {} {}", path, input);
|
|
|
|
let result = self.doit(path, Method::Post(&input))?;
|
|
|
|
Ok(serde_json::from_str(&result)?)
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn delete<U>(&mut self, path: &str) -> Result<U, Error>
|
2018-07-25 03:34:35 -07:00
|
|
|
where U: for<'a> Deserialize<'a>,
|
|
|
|
{
|
|
|
|
debug!("DELETE {}", path);
|
|
|
|
let result = self.doit(path, Method::Delete)?;
|
|
|
|
Ok(serde_json::from_str(&result)?)
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
fn doit(&mut self, path: &str, method: Method) -> Result<String, Error> {
|
2018-07-25 03:34:35 -07:00
|
|
|
let url = format!("http://{}{}", self.driver_addr, path);
|
2018-07-27 13:32:09 -07:00
|
|
|
self.handle.reset();
|
|
|
|
self.handle.url(&url)?;
|
2018-07-25 03:34:35 -07:00
|
|
|
match method {
|
|
|
|
Method::Post(data) => {
|
2018-07-27 13:32:09 -07:00
|
|
|
self.handle.post(true)?;
|
|
|
|
self.handle.post_fields_copy(data.as_bytes())?;
|
2018-07-25 03:34:35 -07:00
|
|
|
}
|
2018-07-27 13:32:09 -07:00
|
|
|
Method::Delete => self.handle.custom_request("DELETE")?,
|
|
|
|
Method::Get => self.handle.get(true)?,
|
2018-07-25 03:34:35 -07:00
|
|
|
}
|
|
|
|
let mut result = Vec::new();
|
|
|
|
{
|
2018-07-27 13:32:09 -07:00
|
|
|
let mut t = self.handle.transfer();
|
2018-07-25 03:34:35 -07:00
|
|
|
t.write_function(|buf| {
|
|
|
|
result.extend_from_slice(buf);
|
|
|
|
Ok(buf.len())
|
|
|
|
})?;
|
|
|
|
t.perform()?
|
|
|
|
}
|
|
|
|
let result = String::from_utf8_lossy(&result);
|
2018-07-27 13:32:09 -07:00
|
|
|
if self.handle.response_code()? != 200 {
|
|
|
|
bail!("non-200 response code: {}\n{}", self.handle.response_code()?, result);
|
2018-07-25 03:34:35 -07:00
|
|
|
}
|
|
|
|
debug!("got: {}", result);
|
|
|
|
Ok(result.into_owned())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
impl Drop for Client {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
let id = match &self.session {
|
|
|
|
Some(id) => id.clone(),
|
|
|
|
None => return,
|
|
|
|
};
|
|
|
|
if let Err(e) = self.close_window(&id) {
|
|
|
|
warn!("failed to close window {:?}", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-25 03:34:35 -07:00
|
|
|
fn read<R: Read>(r: &mut R) -> io::Result<Vec<u8>> {
|
|
|
|
let mut dst = Vec::new();
|
|
|
|
r.read_to_end(&mut dst)?;
|
|
|
|
Ok(dst)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn tab(s: &str) -> String {
|
|
|
|
let mut result = String::new();
|
|
|
|
for line in s.lines() {
|
|
|
|
result.push_str(" ");
|
|
|
|
result.push_str(line);
|
|
|
|
result.push_str("\n");
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
struct BackgroundChild<'a> {
|
|
|
|
child: Child,
|
|
|
|
stdout: Option<thread::JoinHandle<io::Result<Vec<u8>>>>,
|
|
|
|
stderr: Option<thread::JoinHandle<io::Result<Vec<u8>>>>,
|
|
|
|
shell: &'a Shell,
|
|
|
|
print_stdio_on_drop: bool,
|
|
|
|
}
|
2018-07-25 03:34:35 -07:00
|
|
|
|
2018-07-27 13:32:09 -07:00
|
|
|
impl<'a> BackgroundChild<'a> {
|
|
|
|
fn spawn(path: &Path, cmd: &mut Command, shell: &'a Shell)
|
|
|
|
-> Result<BackgroundChild<'a>, Error>
|
|
|
|
{
|
|
|
|
cmd
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
.stderr(Stdio::piped())
|
|
|
|
.stdin(Stdio::null());
|
|
|
|
let mut child = cmd.spawn()
|
|
|
|
.context(format!("failed to spawn {:?} binary", path))?;
|
|
|
|
let mut stdout = child.stdout.take().unwrap();
|
|
|
|
let mut stderr = child.stderr.take().unwrap();
|
|
|
|
let stdout = Some(thread::spawn(move || read(&mut stdout)));
|
|
|
|
let stderr = Some(thread::spawn(move || read(&mut stderr)));
|
|
|
|
Ok(BackgroundChild {
|
|
|
|
child,
|
|
|
|
stdout,
|
|
|
|
stderr,
|
|
|
|
shell,
|
|
|
|
print_stdio_on_drop: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> Drop for BackgroundChild<'a> {
|
2018-07-25 03:34:35 -07:00
|
|
|
fn drop(&mut self) {
|
2018-07-27 13:32:09 -07:00
|
|
|
self.child.kill().unwrap();
|
|
|
|
let status = self.child.wait().unwrap();
|
|
|
|
if !self.print_stdio_on_drop {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
self.shell.clear();
|
|
|
|
println!("driver status: {}", status);
|
|
|
|
|
|
|
|
let stdout = self.stdout.take().unwrap().join().unwrap().unwrap();
|
|
|
|
if stdout.len() > 0 {
|
|
|
|
println!("driver stdout:\n{}", tab(&String::from_utf8_lossy(&stdout)));
|
|
|
|
}
|
|
|
|
let stderr = self.stderr.take().unwrap().join().unwrap().unwrap();
|
|
|
|
if stderr.len() > 0 {
|
|
|
|
println!("driver stderr:\n{}", tab(&String::from_utf8_lossy(&stderr)));
|
|
|
|
}
|
2018-07-25 03:34:35 -07:00
|
|
|
}
|
|
|
|
}
|