execute_capture_output -> RunWithManagedStdio

RunWithManagedStdio is a more general function that can be applied to
any cvd::Command, which allows using the full set of configuration
options that cvd::Command supports.

The function supports writing a string to the subprocess stdin, reading
the subprocess stdout to a string, and/or reading the subprocess stderr
to a string.

Test: Run launch_cvd --help, fetch_cvd
Bug: 141889929
Change-Id: I2845add9d22c21e6853651f1ea93f011e6c434fe
diff --git a/common/libs/utils/archive.cpp b/common/libs/utils/archive.cpp
index ace5ab9..a22db4f 100644
--- a/common/libs/utils/archive.cpp
+++ b/common/libs/utils/archive.cpp
@@ -33,10 +33,12 @@
 }
 
 std::vector<std::string> Archive::Contents() {
-  std::string bsdtar_output;
-  auto bsdtar_ret =
-      cvd::execute_capture_output({"/usr/bin/bsdtar", "-tf", file},
-                                  &bsdtar_output);
+  cvd::Command bsdtar_cmd("/usr/bin/bsdtar");
+  bsdtar_cmd.AddParameter("-tf");
+  bsdtar_cmd.AddParameter(file);
+  std::string bsdtar_input, bsdtar_output;
+  auto bsdtar_ret = cvd::RunWithManagedStdio(std::move(bsdtar_cmd), &bsdtar_input,
+                                             &bsdtar_output, nullptr);
   if (bsdtar_ret != 0) {
     LOG(ERROR) << "`bsdtar -tf \"" << file << "\"` returned " << bsdtar_ret;
   }
diff --git a/common/libs/utils/subprocess.cpp b/common/libs/utils/subprocess.cpp
index 4685eeb..d3ebf1d 100644
--- a/common/libs/utils/subprocess.cpp
+++ b/common/libs/utils/subprocess.cpp
@@ -25,6 +25,7 @@
 
 #include <map>
 #include <set>
+#include <thread>
 
 #include <glog/logging.h>
 
@@ -306,6 +307,123 @@
   return StartHelper(with_control_socket, true);
 }
 
+// A class that waits for threads to exit in its destructor.
+class ThreadJoiner {
+std::vector<std::thread*> threads_;
+public:
+  ThreadJoiner(const std::vector<std::thread*> threads) : threads_(threads) {}
+  ~ThreadJoiner() {
+    for (auto& thread : threads_) {
+      if (thread->joinable()) {
+        thread->join();
+      }
+    }
+  }
+};
+
+int RunWithManagedStdio(cvd::Command&& cmd_tmp, const std::string* stdin,
+                        std::string* stdout, std::string* stderr) {
+  /*
+   * The order of these declarations is necessary for safety. If the function
+   * returns at any point, the cvd::Command will be destroyed first, closing all
+   * of its references to SharedFDs. This will cause the thread internals to fail
+   * their reads or writes. The ThreadJoiner then waits for the threads to
+   * complete, as running the destructor of an active std::thread crashes the
+   * program.
+   *
+   * C++ scoping rules dictate that objects are descoped in reverse order to
+   * construction, so this behavior is predictable.
+   */
+  std::thread stdin_thread, stdout_thread, stderr_thread;
+  ThreadJoiner thread_joiner({&stdin_thread, &stdout_thread, &stderr_thread});
+  cvd::Command cmd = std::move(cmd_tmp);
+  bool io_error = false;
+  if (stdin != nullptr) {
+    cvd::SharedFD pipe_read, pipe_write;
+    if (!cvd::SharedFD::Pipe(&pipe_read, &pipe_write)) {
+      LOG(ERROR) << "Could not create a pipe to write the stdin of \""
+                << cmd.GetShortName() << "\"";
+      return -1;
+    }
+    if (!cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdIn, pipe_read)) {
+      LOG(ERROR) << "Could not set stdout of \"" << cmd.GetShortName()
+                << "\", was already set.";
+      return -1;
+    }
+    stdin_thread = std::thread([pipe_write, stdin, &io_error]() {
+      int written = cvd::WriteAll(pipe_write, *stdin);
+      if (written < 0) {
+        io_error = true;
+        LOG(ERROR) << "Error in writing stdin to process";
+      }
+    });
+  }
+  if (stdout != nullptr) {
+    cvd::SharedFD pipe_read, pipe_write;
+    if (!cvd::SharedFD::Pipe(&pipe_read, &pipe_write)) {
+      LOG(ERROR) << "Could not create a pipe to read the stdout of \""
+                << cmd.GetShortName() << "\"";
+      return -1;
+    }
+    if (!cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdOut, pipe_write)) {
+      LOG(ERROR) << "Could not set stdout of \"" << cmd.GetShortName()
+                << "\", was already set.";
+      return -1;
+    }
+    stdout_thread = std::thread([pipe_read, stdout, &io_error]() {
+      int read = cvd::ReadAll(pipe_read, stdout);
+      if (read < 0) {
+        io_error = true;
+        LOG(ERROR) << "Error in reading stdout from process";
+      }
+    });
+  }
+  if (stderr != nullptr) {
+    cvd::SharedFD pipe_read, pipe_write;
+    if (!cvd::SharedFD::Pipe(&pipe_read, &pipe_write)) {
+      LOG(ERROR) << "Could not create a pipe to read the stderr of \""
+                << cmd.GetShortName() << "\"";
+      return -1;
+    }
+    if (!cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdErr, pipe_write)) {
+      LOG(ERROR) << "Could not set stderr of \"" << cmd.GetShortName()
+                << "\", was already set.";
+      return -1;
+    }
+    stderr_thread = std::thread([pipe_read, stderr, &io_error]() {
+      int read = cvd::ReadAll(pipe_read, stderr);
+      if (read < 0) {
+        io_error = true;
+        LOG(ERROR) << "Error in reading stderr from process";
+      }
+    });
+  }
+
+  auto subprocess = cmd.Start();
+  if (!subprocess.Started()) {
+    return -1;
+  }
+  {
+    // Force the destructor to run by moving it into a smaller scope.
+    // This is necessary to close the write end of the pipe.
+    cvd::Command forceDelete = std::move(cmd);
+  }
+  int wstatus;
+  subprocess.Wait(&wstatus, 0);
+  if (WIFSIGNALED(wstatus)) {
+    LOG(ERROR) << "Command was interrupted by a signal: " << WTERMSIG(wstatus);
+    return -1;
+  }
+  {
+    auto join_threads = std::move(thread_joiner);
+  }
+  if (io_error) {
+    LOG(ERROR) << "IO error communicating with " << cmd.GetShortName();
+    return -1;
+  }
+  return WEXITSTATUS(wstatus);
+}
+
 int execute(const std::vector<std::string>& command,
             const std::vector<std::string>& env) {
   Command cmd(command[0]);
@@ -331,33 +449,4 @@
   return subprocess.Wait();
 }
 
-int execute_capture_output(const std::vector<std::string>& command,
-                           std::string* output) {
-  Command cmd(command[0]);
-  for (size_t i = 1; i < command.size(); ++i) {
-    cmd.AddParameter(command[i]);
-  }
-  cvd::SharedFD pipe_read, pipe_write;
-  cvd::SharedFD::Pipe(&pipe_read, &pipe_write);
-  cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdOut, pipe_write);
-  cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdErr, pipe_write);
-
-  auto subprocess = cmd.Start();
-  if (!subprocess.Started()) {
-    return -1;
-  }
-  {
-    pipe_write->Close();
-    // Force the destructor to run by moving it into a smaller scope.
-    // This is necessary to close the write end of the pipe.
-    cvd::Command forceDelete = std::move(cmd);
-  }
-
-  int read = cvd::ReadAll(pipe_read, output);
-  if (read < 0) {
-    LOG(FATAL) << "Could not read from pipe in execute_capture_output";
-  }
-  return subprocess.Wait();
-}
-
 }  // namespace cvd
diff --git a/common/libs/utils/subprocess.h b/common/libs/utils/subprocess.h
index 06539fd..44976f9 100644
--- a/common/libs/utils/subprocess.h
+++ b/common/libs/utils/subprocess.h
@@ -187,6 +187,21 @@
   bool verbose_;
 };
 
+/*
+ * Consumes a cvd::Command and runs it, optionally managing the stdio channels.
+ *
+ * If `stdin` is set, the subprocess stdin will be pipe providing its contents.
+ * If `stdout` is set, the subprocess stdout will be captured and saved to it.
+ * If `stderr` is set, the subprocess stderr will be captured and saved to it.
+ *
+ * If `command` exits normally, the lower 8 bits of the return code will be
+ * returned in a value between 0 and 255.
+ * If some setup fails, `command` fails to start, or `command` exits due to a
+ * signal, the return value will be negative.
+ */
+int RunWithManagedStdio(cvd::Command&& command, const std::string* stdin,
+                        std::string* stdout, std::string* stderr);
+
 // Convenience wrapper around Command and Subprocess class, allows to easily
 // execute a command and wait for it to complete. The version without the env
 // parameter starts the command with the same environment as the parent. Returns
@@ -195,8 +210,4 @@
             const std::vector<std::string>& env);
 int execute(const std::vector<std::string>& command);
 
-// Like execute, but captures stdout and stderr and returns it in "output".
-int execute_capture_output(const std::vector<std::string>& command,
-                           std::string* output);
-
 }  // namespace cvd
diff --git a/host/commands/fetcher/install_zip.cc b/host/commands/fetcher/install_zip.cc
index c862fcb..bd6294f 100644
--- a/host/commands/fetcher/install_zip.cc
+++ b/host/commands/fetcher/install_zip.cc
@@ -47,9 +47,11 @@
     }
     std::string extracted_file = target_directory + "/" + file;
 
-    std::string file_output;
-    auto file_ret = cvd::execute_capture_output(
-      {"/usr/bin/file", extracted_file}, &file_output);
+    std::string file_input, file_output;
+    cvd::Command file_cmd("/usr/bin/file");
+    file_cmd.AddParameter(extracted_file);
+    auto file_ret = cvd::RunWithManagedStdio(std::move(file_cmd), &file_input,
+                                             &file_output, nullptr);
     if (file_ret != 0) {
       LOG(ERROR) << "Unable to run file on " << file << ", returned" << file_ret;
       extraction_success = false;
diff --git a/host/commands/launch/flag_forwarder.cc b/host/commands/launch/flag_forwarder.cc
index d310833..9a8e6a2 100644
--- a/host/commands/launch/flag_forwarder.cc
+++ b/host/commands/launch/flag_forwarder.cc
@@ -143,49 +143,6 @@
   return output;
 }
 
-std::string CaptureSubprocessStdout(cvd::Command cmd) {
-  // TODO(b/141889929): Unify this with execute_capture_output.
-  cmd.SetVerbose(false);
-  auto dev_null = cvd::SharedFD::Open("/dev/null", O_RDONLY);
-  if (!dev_null->IsOpen()) {
-    LOG(ERROR) << "Failed to open /dev/null: " << dev_null->StrError();
-    return "";
-  }
-  cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdIn, dev_null);
-  cvd::SharedFD pipe_read, pipe_write;
-  cvd::SharedFD::Pipe(&pipe_read, &pipe_write);
-  cmd.RedirectStdIO(cvd::Subprocess::StdIOChannel::kStdOut, pipe_write);
-  auto subprocess = cmd.Start();
-  if (!subprocess.Started()) {
-    LOG(ERROR) << "Could not start subprocess";
-    return "";
-  }
-  {
-    pipe_write->Close();
-    // Force the destructor to run by moving it into a smaller scope.
-    // This is necessary to close the write end of the pipe.
-    cvd::Command forceDelete = std::move(cmd);
-  }
-  std::string output;
-  int read = cvd::ReadAll(pipe_read, &output);
-  if (read < 0) {
-    LOG(ERROR) << "Could not read from pipe in CaptureSubprocessStdout";
-    return "";
-  }
-  int status;
-  subprocess.Wait(&status, 0);
-  if (!WIFEXITED(status)) {
-    LOG(ERROR) << "Subprocess exited with abnormal conditions.";
-    return "";
-  }
-  if (WEXITSTATUS(status) != 1) {
-    LOG(ERROR) << "Subprocess exited with status " << WEXITSTATUS(status)
-               << ", expected 1";
-    return "";
-  }
-  return output;
-}
-
 /**
  * Creates a dynamic flag
  */
@@ -278,8 +235,16 @@
 
   for (const auto& subprocess : subprocesses_) {
     cvd::Command cmd(subprocess);
+    cmd.SetVerbose(false);
     cmd.AddParameter("--helpxml");
-    std::string helpxml_output = CaptureSubprocessStdout(std::move(cmd));
+    std::string helpxml_input, helpxml_output, helpxml_error;
+    int helpxml_ret = cvd::RunWithManagedStdio(std::move(cmd), &helpxml_input,
+                                               &helpxml_output, &helpxml_error);
+    if (helpxml_ret != 1) {
+      LOG(FATAL) << subprocess << " --helpxml returned unexpected response "
+                 << helpxml_ret << ". Stderr was " << helpxml_error;
+      return;
+    }
 
     auto subprocess_flags = FlagsForSubprocess(helpxml_output);
     for (const auto& flag : subprocess_flags) {
@@ -307,6 +272,7 @@
 
   for (const auto& subprocess : subprocesses_) {
     cvd::Command cmd(subprocess);
+    cmd.SetVerbose(false);
     std::vector<std::string> invocation = {subprocess};
     for (const auto& flag : ArgvForSubprocess(subprocess)) {
       cmd.AddParameter(flag);
@@ -322,7 +288,14 @@
     cmd.AddParameter("--noversion");
     // Ensure this is set on by putting it at the end.
     cmd.AddParameter("--helpxml");
-    std::string helpxml_output = CaptureSubprocessStdout(std::move(cmd));
+    std::string helpxml_input, helpxml_output, helpxml_error;
+    int helpxml_ret = cvd::RunWithManagedStdio(std::move(cmd), &helpxml_input,
+                                               &helpxml_output, &helpxml_error);
+    if (helpxml_ret != 1) {
+      LOG(FATAL) << subprocess << " --helpxml returned unexpected response "
+                 << helpxml_ret << ". Stderr was " << helpxml_error;
+      return;
+    }
 
     auto subprocess_flags = FlagsForSubprocess(helpxml_output);
     for (const auto& flag : subprocess_flags) {