Add a subprocess API to use output callbacks.

This is to be used by the Centipede adaptor.

Also fix a minor issue of counting the unavailablity of the same file descriptor redundantly, which is incorrect.

PiperOrigin-RevId: 763130600
diff --git a/fuzztest/BUILD b/fuzztest/BUILD
index df77213..d7a6eeb 100644
--- a/fuzztest/BUILD
+++ b/fuzztest/BUILD
@@ -784,8 +784,10 @@
     deps = [
         ":logging",
         "@abseil-cpp//absl/container:flat_hash_map",
+        "@abseil-cpp//absl/functional:function_ref",
         "@abseil-cpp//absl/strings",
         "@abseil-cpp//absl/time",
+        "@abseil-cpp//absl/types:span",
     ],
 )
 
diff --git a/fuzztest/CMakeLists.txt b/fuzztest/CMakeLists.txt
index 64655c6..5de10c0 100644
--- a/fuzztest/CMakeLists.txt
+++ b/fuzztest/CMakeLists.txt
@@ -756,7 +756,10 @@
   DEPS
     fuzztest::logging
     absl::flat_hash_map
+    absl::function_ref
+    absl::span
     absl::strings
+    absl::string_view
     absl::time
 )
 
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index a06abaf..273afa5 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -332,12 +332,12 @@
 int RunCentipede(const Environment& env,
                  const std::optional<std::string>& centipede_command) {
   if (centipede_command.has_value()) {
-    std::string cmdline = *centipede_command;
+    std::string cmdline = "exec 2>&1 ";
+    absl::StrAppend(&cmdline, *centipede_command);
     for (const auto& flag : env.CreateFlags()) {
       absl::StrAppend(&cmdline, " ");
       absl::StrAppend(&cmdline, ShellEscape(flag));
     }
-    absl::StrAppend(&cmdline, " 2>&1");
     absl::FPrintF(GetStderr(), "[.] Running Centipede command %s\n", cmdline);
     FILE* pipe = popen(cmdline.c_str(), "r");
     FUZZTEST_INTERNAL_CHECK(pipe != nullptr, "popen failed with errno %d",
diff --git a/fuzztest/internal/subprocess.cc b/fuzztest/internal/subprocess.cc
index 959bcef..a6951a8 100644
--- a/fuzztest/internal/subprocess.cc
+++ b/fuzztest/internal/subprocess.cc
@@ -30,12 +30,16 @@
 
 #include <future>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "absl/container/flat_hash_map.h"
+#include "absl/functional/function_ref.h"
 #include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
 #include "absl/time/clock.h"
 #include "absl/time/time.h"
+#include "absl/types/span.h"
 #include "./fuzztest/internal/logging.h"
 
 namespace fuzztest::internal {
@@ -58,8 +62,10 @@
 // Helper class for running commands in a subprocess.
 class SubProcess {
  public:
-  RunResults Run(
-      const std::vector<std::string>& command_line,
+  TerminationStatus Run(
+      absl::Span<const std::string> command_line,
+      absl::FunctionRef<void(absl::string_view)> on_stdout_output,
+      absl::FunctionRef<void(absl::string_view)> on_stderr_output,
       const absl::flat_hash_map<std::string, std::string>& environment,
       absl::Duration timeout);
 
@@ -70,11 +76,14 @@
   posix_spawn_file_actions_t CreateChildFileActions();
   void StartWatchdog(absl::Duration timeout);
   pid_t StartChild(
-      const std::vector<std::string>& command_line,
+      absl::Span<const std::string> command_line,
       const absl::flat_hash_map<std::string, std::string>& environment);
-  void ReadChildOutput(std::string* stdout_output, std::string* stderr_output);
+  void ReadChildOutput(
+      absl::FunctionRef<void(absl::string_view)> on_stdout_output,
+      absl::FunctionRef<void(absl::string_view)> on_stderr_output);
 
-  // Pipe file descriptors pairs. Index 0 is for stdout, index 1 is for stderr.
+  // Pipe file descriptors pairs. Index 0 is for stdout, index 1 is for
+  // stderr.
   static constexpr int kStdOutIdx = 0;
   static constexpr int kStdErrIdx = 1;
   int parent_pipe_[2];
@@ -147,7 +156,7 @@
 
 // Do fork() and exec() in one step, using posix_spawnp().
 pid_t SubProcess::StartChild(
-    const std::vector<std::string>& command_line,
+    absl::Span<const std::string> command_line,
     const absl::flat_hash_map<std::string, std::string>& environment) {
   posix_spawn_file_actions_t actions = CreateChildFileActions();
 
@@ -155,7 +164,7 @@
   size_t argc = command_line.size();
   std::vector<char*> argv(argc + 1);
   for (int i = 0; i < argc; i++) {
-    argv[i] = strdup(command_line[i].c_str());
+    argv[i] = strndup(command_line[i].data(), command_line[i].size());
   }
   argv[argc] = nullptr;
 
@@ -189,17 +198,16 @@
   return ((e == EINTR) || (e == EAGAIN) || (e == EWOULDBLOCK));
 }
 
-void SubProcess::ReadChildOutput(std::string* stdout_output,
-                                 std::string* stderr_output) {
+void SubProcess::ReadChildOutput(
+    absl::FunctionRef<void(absl::string_view)> on_stdout_output,
+    absl::FunctionRef<void(absl::string_view)> on_stderr_output) {
   // Set up poll()-ing the pipes.
   constexpr int fd_count = 2;
   struct pollfd pfd[fd_count];
-  std::string* out_str[fd_count];
   for (int channel : {kStdOutIdx, kStdErrIdx}) {
     pfd[channel].fd = parent_pipe_[channel];
     pfd[channel].events = POLLIN;
     pfd[channel].revents = 0;
-    out_str[channel] = channel == kStdOutIdx ? stdout_output : stderr_output;
   }
 
   // Loop reading stdout and stderr from the child process.
@@ -213,14 +221,22 @@
       FUZZTEST_INTERNAL_CHECK(false, "Impossible timeout: ", strerror(errno));
     } else if (ret > 0) {
       for (int channel : {kStdOutIdx, kStdErrIdx}) {
+        // According to the poll() spec, use -1 for ignored entries.
+        if (pfd[channel].fd == -1) {
+          continue;
+        }
         if ((pfd[channel].revents & (POLLIN | POLLHUP)) != 0) {
           ssize_t n = read(pfd[channel].fd, buf, sizeof(buf));
           if (n > 0) {
-            out_str[channel]->append(buf, n);
+            auto on_output =
+                channel == kStdOutIdx ? on_stdout_output : on_stderr_output;
+            on_output({buf, static_cast<size_t>(n)});
           } else if ((n == 0) || !ShouldRetry(errno)) {
+            pfd[channel].fd = -1;
             fd_remain--;
           }
         } else if ((pfd[channel].revents & (POLLERR | POLLNVAL)) != 0) {
+          pfd[channel].fd = -1;
           fd_remain--;
         }
       }
@@ -274,8 +290,10 @@
 
 }  // anonymous namespace
 
-RunResults SubProcess::Run(
-    const std::vector<std::string>& command_line,
+TerminationStatus SubProcess::Run(
+    absl::Span<const std::string> command_line,
+    absl::FunctionRef<void(absl::string_view)> on_stdout_output,
+    absl::FunctionRef<void(absl::string_view)> on_stderr_output,
     const absl::flat_hash_map<std::string, std::string>& environment,
     absl::Duration timeout) {
   CreatePipes();
@@ -283,17 +301,18 @@
   CloseChildPipes();
   std::future<int> status =
       std::async(std::launch::async, &WaitWithTimeout, child_pid, timeout);
-  std::string stdout_output, stderr_output;
-  ReadChildOutput(&stdout_output, &stderr_output);
+  ReadChildOutput(on_stdout_output, on_stderr_output);
   CloseParentPipes();
-  return {TerminationStatus(status.get()), stdout_output, stderr_output};
+  return TerminationStatus(status.get());
 }
 
 #endif  // !defined(_MSC_VER) && !(defined(__ANDROID_MIN_SDK_VERSION__) &&
         // __ANDROID_MIN_SDK_VERSION__ < 28)
 
-RunResults RunCommand(
-    const std::vector<std::string>& command_line,
+TerminationStatus RunCommandWithOutputCallbacks(
+    absl::Span<const std::string> command_line,
+    absl::FunctionRef<void(absl::string_view)> on_stdout_output,
+    absl::FunctionRef<void(absl::string_view)> on_stderr_output,
     const absl::flat_hash_map<std::string, std::string>& environment,
     absl::Duration timeout) {
 #if defined(_MSC_VER)
@@ -305,8 +324,23 @@
       "Subprocess library not implemented on older Android NDK versions yet");
 #else
   SubProcess proc;
-  return proc.Run(command_line, environment, timeout);
+  return proc.Run(command_line, on_stdout_output, on_stderr_output, environment,
+                  timeout);
 #endif
 }
 
+RunResults RunCommand(
+    absl::Span<const std::string> command_line,
+    const absl::flat_hash_map<std::string, std::string>& environment,
+    absl::Duration timeout) {
+  std::string stdout;
+  std::string stderr;
+  auto status = RunCommandWithOutputCallbacks(
+      command_line,
+      [&stdout](absl::string_view output) { stdout.append(output); },
+      [&stderr](absl::string_view output) { stderr.append(output); },
+      environment, timeout);
+  return {std::move(status), std::move(stdout), std::move(stderr)};
+}
+
 }  // namespace fuzztest::internal
diff --git a/fuzztest/internal/subprocess.h b/fuzztest/internal/subprocess.h
index e3f080d..8d70020 100644
--- a/fuzztest/internal/subprocess.h
+++ b/fuzztest/internal/subprocess.h
@@ -21,7 +21,10 @@
 #include <vector>
 
 #include "absl/container/flat_hash_map.h"
+#include "absl/functional/function_ref.h"
+#include "absl/strings/string_view.h"
 #include "absl/time/time.h"
+#include "absl/types/span.h"
 
 namespace fuzztest::internal {
 
@@ -93,11 +96,24 @@
   std::string stderr_output;
 };
 
-// Runs `command_line` in a subprocess. Environment variables can be set via
+// Runs `command_line` in a subprocess and passes through its stdout/stderr to
+// `on_stdout_output` and `on_stderr_output` callbacks. Environment variables
+// can be set via `environment`. If optional `timeout` is provided, the process
+// is terminated after the given timeout. The timeout will be rounded up to
+// seconds.
+TerminationStatus RunCommandWithOutputCallbacks(
+    absl::Span<const std::string> command_line,
+    absl::FunctionRef<void(absl::string_view)> on_stdout_output,
+    absl::FunctionRef<void(absl::string_view)> on_stderr_output,
+    const absl::flat_hash_map<std::string, std::string>& environment = {},
+    absl::Duration timeout = absl::InfiniteDuration());
+
+// Runs `command_line` in a subprocess and returns the run results that captures
+// the stdout/stderr as strings. Environment variables can be set via
 // `environment`. If optional `timeout` is provided, the process is terminated
 // after the given timeout. The timeout will be rounded up to seconds.
 RunResults RunCommand(
-    const std::vector<std::string>& command_line,
+    absl::Span<const std::string> command_line,
     const absl::flat_hash_map<std::string, std::string>& environment = {},
     absl::Duration timeout = absl::InfiniteDuration());
 
diff --git a/fuzztest/internal/subprocess_test.cc b/fuzztest/internal/subprocess_test.cc
index bfb3c3a..844c0bc 100644
--- a/fuzztest/internal/subprocess_test.cc
+++ b/fuzztest/internal/subprocess_test.cc
@@ -57,6 +57,16 @@
   EXPECT_THAT(std_err, HasSubstr("command not found"));
 }
 
+TEST(SubProcessTest, StdErrIsCapturedIfStdOutIsClosedEarly) {
+  auto [status, std_out, std_err] = RunCommand(
+      {"bash", "-c",
+       "exec >&- bash -c 'sleep 1; echo some stderr output >&2; exit 0'"});
+  EXPECT_TRUE(status.Exited());
+  EXPECT_EQ(status, ExitCode(0));
+  EXPECT_EQ(std_out, "");
+  EXPECT_THAT(std_err, HasSubstr("some stderr output"));
+}
+
 TEST(SubProcessTest, CrashesWithWrongArguments) {
   EXPECT_DEATH(RunCommand({"not-a-binary"}), "Cannot spawn child process");
 }