Browse Source

CTest: Re-implement test process handling using libuv

Co-Author: Brad King <[email protected]>
Bryon Bean 7 years ago
parent
commit
b5e21d7d2e

+ 21 - 33
Source/CTest/cmCTestMultiProcessHandler.cxx

@@ -9,9 +9,12 @@
 #include "cmSystemTools.h"
 #include "cmWorkingDirectory.h"
 
+#include "cm_uv.h"
+
 #include "cmsys/FStream.hxx"
 #include "cmsys/String.hxx"
 #include "cmsys/SystemInformation.hxx"
+
 #include <algorithm>
 #include <chrono>
 #include <iomanip>
@@ -133,18 +136,16 @@ void cmCTestMultiProcessHandler::RunTests()
   if (this->HasCycles) {
     return;
   }
+#ifdef CMAKE_UV_SIGNAL_HACK
+  cmUVSignalHackRAII hackRAII;
+#endif
   this->TestHandler->SetMaxIndex(this->FindMaxIndex());
+
+  uv_loop_init(&this->Loop);
   this->StartNextTests();
-  while (!this->Tests.empty()) {
-    if (this->StopTimePassed) {
-      return;
-    }
-    this->CheckOutput();
-    this->StartNextTests();
-  }
-  // let all running tests finish
-  while (this->CheckOutput()) {
-  }
+  uv_run(&this->Loop, UV_RUN_DEFAULT);
+  uv_loop_close(&this->Loop);
+
   this->MarkFinished();
   this->UpdateCostData();
 }
@@ -168,7 +169,7 @@ bool cmCTestMultiProcessHandler::StartTestProcess(int test)
   this->EraseTest(test);
   this->RunningCount += GetProcessorsUsed(test);
 
-  cmCTestRunTest* testRun = new cmCTestRunTest(this->TestHandler);
+  cmCTestRunTest* testRun = new cmCTestRunTest(*this);
   if (this->CTest->GetRepeatUntilFail()) {
     testRun->SetRunUntilFailOn();
     testRun->SetNumberOfRuns(this->CTest->GetTestRepeat());
@@ -191,7 +192,6 @@ bool cmCTestMultiProcessHandler::StartTestProcess(int test)
   this->LockResources(test);
 
   if (testRun->StartTest(this->Total)) {
-    this->RunningTests.insert(testRun);
     return true;
   }
 
@@ -264,6 +264,11 @@ bool cmCTestMultiProcessHandler::StartTest(int test)
 void cmCTestMultiProcessHandler::StartNextTests()
 {
   size_t numToStart = 0;
+
+  if (this->Tests.empty()) {
+    return;
+  }
+
   if (this->RunningCount < this->ParallelLevel) {
     numToStart = this->ParallelLevel - this->RunningCount;
   }
@@ -396,25 +401,6 @@ void cmCTestMultiProcessHandler::StartNextTests()
   }
 }
 
-bool cmCTestMultiProcessHandler::CheckOutput()
-{
-  // no more output we are done
-  if (this->RunningTests.empty()) {
-    return false;
-  }
-  std::vector<cmCTestRunTest*> finished;
-  std::string out, err;
-  for (cmCTestRunTest* p : this->RunningTests) {
-    if (!p->CheckOutput()) {
-      finished.push_back(p);
-    }
-  }
-  for (cmCTestRunTest* p : finished) {
-    this->FinishTestProcess(p, true);
-  }
-  return true;
-}
-
 void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner,
                                                    bool started)
 {
@@ -429,7 +415,6 @@ void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner,
       this->Completed--; // remove the completed test because run again
       return;
     }
-    this->RunningTests.erase(runner);
   }
 
   if (testResult) {
@@ -449,6 +434,9 @@ void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner,
   this->RunningCount -= GetProcessorsUsed(test);
 
   delete runner;
+  if (started) {
+    this->StartNextTests();
+  }
 }
 
 void cmCTestMultiProcessHandler::UpdateCostData()
@@ -715,7 +703,7 @@ void cmCTestMultiProcessHandler::PrintTestList()
 
     cmWorkingDirectory workdir(p.Directory);
 
-    cmCTestRunTest testRun(this->TestHandler);
+    cmCTestRunTest testRun(*this);
     testRun.SetIndex(p.Index);
     testRun.SetTestProperties(&p);
     testRun.ComputeArguments(); // logs the command in verbose mode

+ 4 - 4
Source/CTest/cmCTestMultiProcessHandler.h

@@ -12,6 +12,8 @@
 #include <string>
 #include <vector>
 
+#include "cm_uv.h"
+
 class cmCTest;
 class cmCTestRunTest;
 
@@ -23,6 +25,7 @@ class cmCTestRunTest;
 class cmCTestMultiProcessHandler
 {
   friend class TestComparator;
+  friend class cmCTestRunTest;
 
 public:
   struct TestSet : public std::set<int>
@@ -95,9 +98,6 @@ protected:
   // Removes the checkpoint file
   void MarkFinished();
   void EraseTest(int index);
-  // Return true if there are still tests running
-  // check all running processes for output and exit case
-  bool CheckOutput();
   void FinishTestProcess(cmCTestRunTest* runner, bool started);
 
   void RemoveTest(int index);
@@ -132,7 +132,7 @@ protected:
   std::vector<cmCTestTestHandler::cmCTestTestResult>* TestResults;
   size_t ParallelLevel; // max number of process that can be run at once
   unsigned long TestLoad;
-  std::set<cmCTestRunTest*> RunningTests; // current running tests
+  uv_loop_t Loop;
   cmCTestTestHandler* TestHandler;
   cmCTest* CTest;
   bool HasCycles;

+ 41 - 54
Source/CTest/cmCTestRunTest.cxx

@@ -4,27 +4,27 @@
 
 #include "cmCTest.h"
 #include "cmCTestMemCheckHandler.h"
-#include "cmCTestTestHandler.h"
+#include "cmCTestMultiProcessHandler.h"
 #include "cmProcess.h"
 #include "cmSystemTools.h"
 #include "cmWorkingDirectory.h"
 
 #include "cm_zlib.h"
 #include "cmsys/Base64.h"
-#include "cmsys/Process.h"
 #include "cmsys/RegularExpression.hxx"
 #include <chrono>
+#include <cmAlgorithms.h>
 #include <iomanip>
 #include <ratio>
 #include <sstream>
 #include <stdio.h>
 #include <utility>
 
-cmCTestRunTest::cmCTestRunTest(cmCTestTestHandler* handler)
+cmCTestRunTest::cmCTestRunTest(cmCTestMultiProcessHandler& multiHandler)
+  : MultiTestHandler(multiHandler)
 {
-  this->CTest = handler->CTest;
-  this->TestHandler = handler;
-  this->TestProcess = nullptr;
+  this->CTest = multiHandler.CTest;
+  this->TestHandler = multiHandler.TestHandler;
   this->TestResult.ExecutionTime = std::chrono::duration<double>::zero();
   this->TestResult.ReturnValue = 0;
   this->TestResult.Status = cmCTestTestHandler::NOT_RUN;
@@ -38,50 +38,32 @@ cmCTestRunTest::cmCTestRunTest(cmCTestTestHandler* handler)
   this->RunAgain = false;     // default to not having to run again
 }
 
-bool cmCTestRunTest::CheckOutput()
+void cmCTestRunTest::CheckOutput(std::string const& line)
 {
-  // Read lines for up to 0.1 seconds of total time.
-  std::chrono::duration<double> timeout = std::chrono::milliseconds(100);
-  auto timeEnd = std::chrono::steady_clock::now() + timeout;
-  std::string line;
-  while ((timeout = timeEnd - std::chrono::steady_clock::now(),
-          timeout > std::chrono::seconds(0))) {
-    int p = this->TestProcess->GetNextOutputLine(line, timeout);
-    if (p == cmsysProcess_Pipe_None) {
-      // Process has terminated and all output read.
-      return false;
-    }
-    if (p == cmsysProcess_Pipe_STDOUT) {
-      // Store this line of output.
-      cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, this->GetIndex()
-                   << ": " << line << std::endl);
-      this->ProcessOutput += line;
-      this->ProcessOutput += "\n";
-
-      // Check for TIMEOUT_AFTER_MATCH property.
-      if (!this->TestProperties->TimeoutRegularExpressions.empty()) {
-        for (auto& reg : this->TestProperties->TimeoutRegularExpressions) {
-          if (reg.first.find(this->ProcessOutput.c_str())) {
-            cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, this->GetIndex()
-                         << ": "
-                         << "Test timeout changed to "
-                         << std::chrono::duration_cast<std::chrono::seconds>(
-                              this->TestProperties->AlternateTimeout)
-                              .count()
-                         << std::endl);
-            this->TestProcess->ResetStartTime();
-            this->TestProcess->ChangeTimeout(
-              this->TestProperties->AlternateTimeout);
-            this->TestProperties->TimeoutRegularExpressions.clear();
-            break;
-          }
-        }
+  cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, this->GetIndex()
+               << ": " << line << std::endl);
+  this->ProcessOutput += line;
+  this->ProcessOutput += "\n";
+
+  // Check for TIMEOUT_AFTER_MATCH property.
+  if (!this->TestProperties->TimeoutRegularExpressions.empty()) {
+    for (auto& reg : this->TestProperties->TimeoutRegularExpressions) {
+      if (reg.first.find(this->ProcessOutput.c_str())) {
+        cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, this->GetIndex()
+                     << ": "
+                     << "Test timeout changed to "
+                     << std::chrono::duration_cast<std::chrono::seconds>(
+                          this->TestProperties->AlternateTimeout)
+                          .count()
+                     << std::endl);
+        this->TestProcess->ResetStartTime();
+        this->TestProcess->ChangeTimeout(
+          this->TestProperties->AlternateTimeout);
+        this->TestProperties->TimeoutRegularExpressions.clear();
+        break;
       }
-    } else { // if(p == cmsysProcess_Pipe_Timeout)
-      break;
     }
   }
-  return true;
 }
 
 // Streamed compression of test output.  The compressed data
@@ -344,7 +326,7 @@ bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
   if (!this->NeedsToRerun()) {
     this->TestHandler->TestResults.push_back(this->TestResult);
   }
-  delete this->TestProcess;
+  this->TestProcess.reset();
   return passed || skipped;
 }
 
@@ -426,7 +408,7 @@ bool cmCTestRunTest::StartTest(size_t total)
     this->TestResult.TestCount = this->TestProperties->Index;
     this->TestResult.Name = this->TestProperties->Name;
     this->TestResult.Path = this->TestProperties->Directory;
-    this->TestProcess = new cmProcess;
+    this->TestProcess = cm::make_unique<cmProcess>(*this);
     this->TestResult.Output = "Disabled";
     this->TestResult.FullCommandLine.clear();
     return false;
@@ -447,7 +429,7 @@ bool cmCTestRunTest::StartTest(size_t total)
   // its arguments are irrelevant. This matters for the case where a fixture
   // dependency might be creating the executable we want to run.
   if (!this->FailedDependencies.empty()) {
-    this->TestProcess = new cmProcess;
+    this->TestProcess = cm::make_unique<cmProcess>(*this);
     std::string msg = "Failed test dependencies:";
     for (std::string const& failedDep : this->FailedDependencies) {
       msg += " " + failedDep;
@@ -464,7 +446,7 @@ bool cmCTestRunTest::StartTest(size_t total)
   this->ComputeArguments();
   std::vector<std::string>& args = this->TestProperties->Args;
   if (args.size() >= 2 && args[1] == "NOT_AVAILABLE") {
-    this->TestProcess = new cmProcess;
+    this->TestProcess = cm::make_unique<cmProcess>(*this);
     std::string msg;
     if (this->CTest->GetConfigType().empty()) {
       msg = "Test not available without configuration.";
@@ -487,7 +469,7 @@ bool cmCTestRunTest::StartTest(size_t total)
   for (std::string const& file : this->TestProperties->RequiredFiles) {
     if (!cmSystemTools::FileExists(file.c_str())) {
       // Required file was not found
-      this->TestProcess = new cmProcess;
+      this->TestProcess = cm::make_unique<cmProcess>(*this);
       *this->TestHandler->LogFile << "Unable to find required file: " << file
                                   << std::endl;
       cmCTestLog(this->CTest, ERROR_MESSAGE,
@@ -503,7 +485,7 @@ bool cmCTestRunTest::StartTest(size_t total)
   if (this->ActualCommand.empty()) {
     // if the command was not found create a TestResult object
     // that has that information
-    this->TestProcess = new cmProcess;
+    this->TestProcess = cm::make_unique<cmProcess>(*this);
     *this->TestHandler->LogFile << "Unable to find executable: " << args[1]
                                 << std::endl;
     cmCTestLog(this->CTest, ERROR_MESSAGE,
@@ -612,7 +594,7 @@ bool cmCTestRunTest::ForkProcess(std::chrono::duration<double> testTimeOut,
                                  bool explicitTimeout,
                                  std::vector<std::string>* environment)
 {
-  this->TestProcess = new cmProcess;
+  this->TestProcess = cm::make_unique<cmProcess>(*this);
   this->TestProcess->SetId(this->Index);
   this->TestProcess->SetWorkingDirectory(
     this->TestProperties->Directory.c_str());
@@ -664,7 +646,7 @@ bool cmCTestRunTest::ForkProcess(std::chrono::duration<double> testTimeOut,
     cmSystemTools::AppendEnv(*environment);
   }
 
-  return this->TestProcess->StartProcess();
+  return this->TestProcess->StartProcess(this->MultiTestHandler.Loop);
 }
 
 void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total)
@@ -738,3 +720,8 @@ void cmCTestRunTest::WriteLogOutputTop(size_t completed, size_t total)
   cmCTestLog(this->CTest, DEBUG, "Testing " << this->TestProperties->Name
                                             << " ... ");
 }
+
+void cmCTestRunTest::FinalizeTest()
+{
+  this->MultiTestHandler.FinishTestProcess(this, true);
+}

+ 10 - 4
Source/CTest/cmCTestRunTest.h

@@ -12,9 +12,10 @@
 #include <vector>
 
 #include "cmCTestTestHandler.h"
+#include "cmProcess.h" // IWYU pragma: keep (for unique_ptr)
 
 class cmCTest;
-class cmProcess;
+class cmCTestMultiProcessHandler;
 
 /** \class cmRunTest
  * \brief represents a single test to be run
@@ -24,7 +25,7 @@ class cmProcess;
 class cmCTestRunTest
 {
 public:
-  explicit cmCTestRunTest(cmCTestTestHandler* handler);
+  explicit cmCTestRunTest(cmCTestMultiProcessHandler& multiHandler);
 
   ~cmCTestRunTest() = default;
 
@@ -57,7 +58,7 @@ public:
   }
 
   // Read and store output.  Returns true if it must be called again.
-  bool CheckOutput();
+  void CheckOutput(std::string const& line);
 
   // Compresses the output, writing to CompressedOutput
   void CompressOutput();
@@ -73,6 +74,10 @@ public:
 
   bool StartAgain();
 
+  cmCTest* GetCTest() const { return this->CTest; }
+
+  void FinalizeTest();
+
 private:
   bool NeedsToRerun();
   void DartProcessing();
@@ -88,12 +93,13 @@ private:
   // Pointer back to the "parent"; the handler that invoked this test run
   cmCTestTestHandler* TestHandler;
   cmCTest* CTest;
-  cmProcess* TestProcess;
+  std::unique_ptr<cmProcess> TestProcess;
   std::string ProcessOutput;
   std::string CompressedOutput;
   double CompressionRatio;
   // The test results
   cmCTestTestHandler::cmCTestTestResult TestResult;
+  cmCTestMultiProcessHandler& MultiTestHandler;
   int Index;
   std::set<std::string> FailedDependencies;
   std::string StartTime;

+ 583 - 86
Source/CTest/cmProcess.cxx

@@ -2,11 +2,65 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmProcess.h"
 
+#include "cmCTest.h"
+#include "cmCTestRunTest.h"
+#include "cmCTestTestHandler.h"
 #include "cmProcessOutput.h"
+#include "cmsys/Process.h"
 
-cmProcess::cmProcess()
+#include <algorithm>
+#include <fcntl.h>
+#include <iostream>
+#include <signal.h>
+#include <stdint.h>
+#include <string>
+#if !defined(_WIN32)
+#include <unistd.h>
+#endif
+
+#if defined(_WIN32) && !defined(__CYGWIN__)
+#include <io.h>
+
+static int cmProcessGetPipes(int* fds)
+{
+  SECURITY_ATTRIBUTES attr;
+  HANDLE readh, writeh;
+  attr.nLength = sizeof(attr);
+  attr.lpSecurityDescriptor = nullptr;
+  attr.bInheritHandle = FALSE;
+  if (!CreatePipe(&readh, &writeh, &attr, 0))
+    return uv_translate_sys_error(GetLastError());
+  fds[0] = _open_osfhandle((intptr_t)readh, 0);
+  fds[1] = _open_osfhandle((intptr_t)writeh, 0);
+  if (fds[0] == -1 || fds[1] == -1) {
+    CloseHandle(readh);
+    CloseHandle(writeh);
+    return uv_translate_sys_error(GetLastError());
+  }
+  return 0;
+}
+#else
+#include <errno.h>
+
+static int cmProcessGetPipes(int* fds)
+{
+  if (pipe(fds) == -1) {
+    return uv_translate_sys_error(errno);
+  }
+
+  if (fcntl(fds[0], F_SETFD, FD_CLOEXEC) == -1 ||
+      fcntl(fds[1], F_SETFD, FD_CLOEXEC) == -1) {
+    close(fds[0]);
+    close(fds[1]);
+    return uv_translate_sys_error(errno);
+  }
+  return 0;
+}
+#endif
+
+cmProcess::cmProcess(cmCTestRunTest& runner)
+  : Runner(runner)
 {
-  this->Process = nullptr;
   this->Timeout = std::chrono::duration<double>::zero();
   this->TotalTime = std::chrono::duration<double>::zero();
   this->ExitValue = 0;
@@ -16,8 +70,8 @@ cmProcess::cmProcess()
 
 cmProcess::~cmProcess()
 {
-  cmsysProcess_Delete(this->Process);
 }
+
 void cmProcess::SetCommand(const char* command)
 {
   this->Command = command;
@@ -28,8 +82,9 @@ void cmProcess::SetCommandArguments(std::vector<std::string> const& args)
   this->Arguments = args;
 }
 
-bool cmProcess::StartProcess()
+bool cmProcess::StartProcess(uv_loop_t& loop)
 {
+  this->ProcessState = cmProcess::State::Error;
   if (this->Command.empty()) {
     return false;
   }
@@ -42,17 +97,83 @@ bool cmProcess::StartProcess()
     this->ProcessArgs.push_back(arg.c_str());
   }
   this->ProcessArgs.push_back(nullptr); // null terminate the list
-  this->Process = cmsysProcess_New();
-  cmsysProcess_SetCommand(this->Process, &*this->ProcessArgs.begin());
-  if (!this->WorkingDirectory.empty()) {
-    cmsysProcess_SetWorkingDirectory(this->Process,
-                                     this->WorkingDirectory.c_str());
+
+  cm::uv_timer_ptr timer;
+  int status = timer.init(loop, this);
+  if (status != 0) {
+    cmCTestLog(this->Runner.GetCTest(), ERROR_MESSAGE,
+               "Error initializing timer: " << uv_strerror(status)
+                                            << std::endl);
+    return false;
+  }
+
+  cm::uv_pipe_ptr pipe_writer;
+  cm::uv_pipe_ptr pipe_reader;
+
+  pipe_writer.init(loop, 0);
+  pipe_reader.init(loop, 0, this);
+
+  int fds[2] = { -1, -1 };
+  status = cmProcessGetPipes(fds);
+  if (status != 0) {
+    cmCTestLog(this->Runner.GetCTest(), ERROR_MESSAGE,
+               "Error initializing pipe: " << uv_strerror(status)
+                                           << std::endl);
+    return false;
+  }
+
+  uv_pipe_open(pipe_reader, fds[0]);
+  uv_pipe_open(pipe_writer, fds[1]);
+
+  uv_stdio_container_t stdio[3];
+  stdio[0].flags = UV_IGNORE;
+  stdio[1].flags = UV_INHERIT_STREAM;
+  stdio[1].data.stream = pipe_writer;
+  stdio[2] = stdio[1];
+
+  uv_process_options_t options = uv_process_options_t();
+  options.file = this->Command.data();
+  options.args = const_cast<char**>(this->ProcessArgs.data());
+  options.stdio_count = 3; // in, out and err
+  options.exit_cb = &cmProcess::OnExitCB;
+  options.stdio = stdio;
+
+  status =
+    uv_read_start(pipe_reader, &cmProcess::OnAllocateCB, &cmProcess::OnReadCB);
+
+  if (status != 0) {
+    cmCTestLog(this->Runner.GetCTest(), ERROR_MESSAGE,
+               "Error starting read events: " << uv_strerror(status)
+                                              << std::endl);
+    return false;
+  }
+
+  status = this->Process.spawn(loop, options, this);
+  if (status != 0) {
+    cmCTestLog(this->Runner.GetCTest(), ERROR_MESSAGE, "Process not started\n "
+                 << this->Command << "\n[" << uv_strerror(status) << "]\n");
+    return false;
+  }
+
+  this->PipeReader = std::move(pipe_reader);
+  this->Timer = std::move(timer);
+
+  this->StartTimer();
+
+  this->ProcessState = cmProcess::State::Executing;
+  return true;
+}
+
+void cmProcess::StartTimer()
+{
+  auto properties = this->Runner.GetTestProperties();
+  auto msec =
+    std::chrono::duration_cast<std::chrono::milliseconds>(this->Timeout);
+
+  if (msec != std::chrono::milliseconds(0) || !properties->ExplicitTimeout) {
+    this->Timer.start(&cmProcess::OnTimeoutCB,
+                      static_cast<uint64_t>(msec.count()), 0);
   }
-  cmsysProcess_SetTimeout(this->Process, this->Timeout.count());
-  cmsysProcess_SetOption(this->Process, cmsysProcess_Option_MergeOutput, 1);
-  cmsysProcess_Execute(this->Process);
-  return (cmsysProcess_GetState(this->Process) ==
-          cmsysProcess_State_Executing);
 }
 
 bool cmProcess::Buffer::GetLine(std::string& line)
@@ -99,51 +220,121 @@ bool cmProcess::Buffer::GetLast(std::string& line)
   return false;
 }
 
-int cmProcess::GetNextOutputLine(std::string& line,
-                                 std::chrono::duration<double> timeout)
+void cmProcess::OnReadCB(uv_stream_t* stream, ssize_t nread,
+                         const uv_buf_t* buf)
 {
-  cmProcessOutput processOutput(cmProcessOutput::UTF8);
-  std::string strdata;
-  double waitTimeout = timeout.count();
-  for (;;) {
-    // Look for lines already buffered.
-    if (this->Output.GetLine(line)) {
-      return cmsysProcess_Pipe_STDOUT;
-    }
+  auto self = static_cast<cmProcess*>(stream->data);
+  self->OnRead(nread, buf);
+}
 
-    // Check for more data from the process.
-    char* data;
-    int length;
-    int p =
-      cmsysProcess_WaitForData(this->Process, &data, &length, &waitTimeout);
-    if (p == cmsysProcess_Pipe_Timeout) {
-      return cmsysProcess_Pipe_Timeout;
-    }
-    if (p == cmsysProcess_Pipe_STDOUT) {
-      processOutput.DecodeText(data, length, strdata);
-      this->Output.insert(this->Output.end(), strdata.begin(), strdata.end());
-    } else { // p == cmsysProcess_Pipe_None
-      // The process will provide no more data.
-      break;
+void cmProcess::OnRead(ssize_t nread, const uv_buf_t* buf)
+{
+  std::string line;
+  if (nread > 0) {
+    std::string strdata;
+    cmProcessOutput processOutput(cmProcessOutput::UTF8,
+                                  static_cast<unsigned int>(buf->len));
+    processOutput.DecodeText(buf->base, static_cast<size_t>(nread), strdata);
+    this->Output.insert(this->Output.end(), strdata.begin(), strdata.end());
+
+    while (this->Output.GetLine(line)) {
+      this->Runner.CheckOutput(line);
+      line.clear();
     }
+
+    return;
   }
-  processOutput.DecodeText(std::string(), strdata);
-  if (!strdata.empty()) {
-    this->Output.insert(this->Output.end(), strdata.begin(), strdata.end());
+
+  // The process will provide no more data.
+  if (nread != UV_EOF) {
+    auto error = static_cast<int>(nread);
+    cmCTestLog(this->Runner.GetCTest(), ERROR_MESSAGE,
+               "Error reading stream: " << uv_strerror(error) << std::endl);
   }
 
   // Look for partial last lines.
   if (this->Output.GetLast(line)) {
-    return cmsysProcess_Pipe_STDOUT;
+    this->Runner.CheckOutput(line);
+  }
+
+  this->ReadHandleClosed = true;
+  if (this->ProcessHandleClosed) {
+    uv_timer_stop(this->Timer);
+    this->Runner.FinalizeTest();
+  }
+}
+
+void cmProcess::OnAllocateCB(uv_handle_t* handle, size_t suggested_size,
+                             uv_buf_t* buf)
+{
+  auto self = static_cast<cmProcess*>(handle->data);
+  self->OnAllocate(suggested_size, buf);
+}
+
+void cmProcess::OnAllocate(size_t suggested_size, uv_buf_t* buf)
+{
+  if (this->Buf.size() < suggested_size) {
+    this->Buf.resize(suggested_size);
+  }
+
+  *buf =
+    uv_buf_init(this->Buf.data(), static_cast<unsigned int>(this->Buf.size()));
+}
+
+void cmProcess::OnTimeoutCB(uv_timer_t* timer)
+{
+  auto self = static_cast<cmProcess*>(timer->data);
+  self->OnTimeout();
+}
+
+void cmProcess::OnTimeout()
+{
+  if (this->ProcessState != cmProcess::State::Executing) {
+    return;
+  }
+  this->ProcessState = cmProcess::State::Expired;
+  bool const was_still_reading = !this->ReadHandleClosed;
+  if (!this->ReadHandleClosed) {
+    this->ReadHandleClosed = true;
+    this->PipeReader.reset();
   }
+  if (!this->ProcessHandleClosed) {
+    // Kill the child and let our on-exit handler finish the test.
+    cmsysProcess_KillPID(static_cast<unsigned long>(this->Process->pid));
+  } else if (was_still_reading) {
+    // Our on-exit handler already ran but did not finish the test
+    // because we were still reading output.  We've just dropped
+    // our read handler, so we need to finish the test now.
+    this->Runner.FinalizeTest();
+  }
+}
 
-  // No more data.  Wait for process exit.
-  if (!cmsysProcess_WaitForExit(this->Process, &waitTimeout)) {
-    return cmsysProcess_Pipe_Timeout;
+void cmProcess::OnExitCB(uv_process_t* process, int64_t exit_status,
+                         int term_signal)
+{
+  auto self = static_cast<cmProcess*>(process->data);
+  self->OnExit(exit_status, term_signal);
+}
+
+void cmProcess::OnExit(int64_t exit_status, int term_signal)
+{
+  if (this->ProcessState != cmProcess::State::Expired) {
+    if (
+#if defined(_WIN32)
+      ((DWORD)exit_status & 0xF0000000) == 0xC0000000
+#else
+      term_signal != 0
+#endif
+      ) {
+      this->ProcessState = cmProcess::State::Exception;
+    } else {
+      this->ProcessState = cmProcess::State::Exited;
+    }
   }
 
   // Record exit information.
-  this->ExitValue = cmsysProcess_GetExitValue(this->Process);
+  this->ExitValue = static_cast<int>(exit_status);
+  this->Signal = term_signal;
   this->TotalTime = std::chrono::steady_clock::now() - this->StartTime;
   // Because of a processor clock scew the runtime may become slightly
   // negative. If someone changed the system clock while the process was
@@ -152,67 +343,373 @@ int cmProcess::GetNextOutputLine(std::string& line,
   if (this->TotalTime <= std::chrono::duration<double>::zero()) {
     this->TotalTime = std::chrono::duration<double>::zero();
   }
-  //  std::cerr << "Time to run: " << this->TotalTime << "\n";
-  return cmsysProcess_Pipe_None;
+
+  this->ProcessHandleClosed = true;
+  if (this->ReadHandleClosed) {
+    uv_timer_stop(this->Timer);
+    this->Runner.FinalizeTest();
+  }
 }
 
 cmProcess::State cmProcess::GetProcessStatus()
 {
-  if (this->Process) {
-    switch (cmsysProcess_GetState(this->Process)) {
-      case cmsysProcess_State_Starting:
-        return State::Starting;
-      case cmsysProcess_State_Error:
-        return State::Error;
-      case cmsysProcess_State_Exception:
-        return State::Exception;
-      case cmsysProcess_State_Executing:
-        return State::Executing;
-      case cmsysProcess_State_Expired:
-        return State::Expired;
-      case cmsysProcess_State_Killed:
-        return State::Killed;
-      case cmsysProcess_State_Disowned:
-        return State::Disowned;
-      default: // case cmsysProcess_State_Exited:
-        break;
-    }
-  }
-  return State::Exited;
+  return this->ProcessState;
 }
 
 void cmProcess::ChangeTimeout(std::chrono::duration<double> t)
 {
   this->Timeout = t;
-  cmsysProcess_SetTimeout(this->Process, this->Timeout.count());
+  this->StartTimer();
 }
 
 void cmProcess::ResetStartTime()
 {
-  cmsysProcess_ResetStartTime(this->Process);
   this->StartTime = std::chrono::steady_clock::now();
 }
 
 cmProcess::Exception cmProcess::GetExitException()
 {
-  switch (cmsysProcess_GetExitException(this->Process)) {
-    case cmsysProcess_Exception_None:
-      return Exception::None;
-    case cmsysProcess_Exception_Fault:
-      return Exception::Fault;
-    case cmsysProcess_Exception_Illegal:
-      return Exception::Illegal;
-    case cmsysProcess_Exception_Interrupt:
-      return Exception::Interrupt;
-    case cmsysProcess_Exception_Numerical:
-      return Exception::Numerical;
-    default: // case cmsysProcess_Exception_Other:
-      break;
+  auto exception = Exception::None;
+#if defined(_WIN32) && !defined(__CYGWIN__)
+  auto exit_code = (DWORD) this->ExitValue;
+  if ((exit_code & 0xF0000000) != 0xC0000000) {
+    return exception;
+  }
+
+  if (exit_code) {
+    switch (exit_code) {
+      case STATUS_DATATYPE_MISALIGNMENT:
+      case STATUS_ACCESS_VIOLATION:
+      case STATUS_IN_PAGE_ERROR:
+      case STATUS_INVALID_HANDLE:
+      case STATUS_NONCONTINUABLE_EXCEPTION:
+      case STATUS_INVALID_DISPOSITION:
+      case STATUS_ARRAY_BOUNDS_EXCEEDED:
+      case STATUS_STACK_OVERFLOW:
+        exception = Exception::Fault;
+        break;
+      case STATUS_FLOAT_DENORMAL_OPERAND:
+      case STATUS_FLOAT_DIVIDE_BY_ZERO:
+      case STATUS_FLOAT_INEXACT_RESULT:
+      case STATUS_FLOAT_INVALID_OPERATION:
+      case STATUS_FLOAT_OVERFLOW:
+      case STATUS_FLOAT_STACK_CHECK:
+      case STATUS_FLOAT_UNDERFLOW:
+#ifdef STATUS_FLOAT_MULTIPLE_FAULTS
+      case STATUS_FLOAT_MULTIPLE_FAULTS:
+#endif
+#ifdef STATUS_FLOAT_MULTIPLE_TRAPS
+      case STATUS_FLOAT_MULTIPLE_TRAPS:
+#endif
+      case STATUS_INTEGER_DIVIDE_BY_ZERO:
+      case STATUS_INTEGER_OVERFLOW:
+        exception = Exception::Numerical;
+        break;
+      case STATUS_CONTROL_C_EXIT:
+        exception = Exception::Interrupt;
+        break;
+      case STATUS_ILLEGAL_INSTRUCTION:
+      case STATUS_PRIVILEGED_INSTRUCTION:
+        exception = Exception::Illegal;
+        break;
+      default:
+        exception = Exception::Other;
+    }
   }
-  return Exception::Other;
+#else
+  if (this->Signal) {
+    switch (this->Signal) {
+      case SIGSEGV:
+        exception = Exception::Fault;
+        break;
+      case SIGFPE:
+        exception = Exception::Numerical;
+        break;
+      case SIGINT:
+        exception = Exception::Interrupt;
+        break;
+      case SIGILL:
+        exception = Exception::Illegal;
+        break;
+      default:
+        exception = Exception::Other;
+    }
+  }
+#endif
+  return exception;
 }
 
 std::string cmProcess::GetExitExceptionString()
 {
-  return cmsysProcess_GetExceptionString(this->Process);
+  std::string exception_str;
+#if defined(_WIN32)
+  switch (this->ExitValue) {
+    case STATUS_CONTROL_C_EXIT:
+      exception_str = "User interrupt";
+      break;
+    case STATUS_FLOAT_DENORMAL_OPERAND:
+      exception_str = "Floating-point exception (denormal operand)";
+      break;
+    case STATUS_FLOAT_DIVIDE_BY_ZERO:
+      exception_str = "Divide-by-zero";
+      break;
+    case STATUS_FLOAT_INEXACT_RESULT:
+      exception_str = "Floating-point exception (inexact result)";
+      break;
+    case STATUS_FLOAT_INVALID_OPERATION:
+      exception_str = "Invalid floating-point operation";
+      break;
+    case STATUS_FLOAT_OVERFLOW:
+      exception_str = "Floating-point overflow";
+      break;
+    case STATUS_FLOAT_STACK_CHECK:
+      exception_str = "Floating-point stack check failed";
+      break;
+    case STATUS_FLOAT_UNDERFLOW:
+      exception_str = "Floating-point underflow";
+      break;
+#ifdef STATUS_FLOAT_MULTIPLE_FAULTS
+    case STATUS_FLOAT_MULTIPLE_FAULTS:
+      exception_str = "Floating-point exception (multiple faults)";
+      break;
+#endif
+#ifdef STATUS_FLOAT_MULTIPLE_TRAPS
+    case STATUS_FLOAT_MULTIPLE_TRAPS:
+      exception_str = "Floating-point exception (multiple traps)";
+      break;
+#endif
+    case STATUS_INTEGER_DIVIDE_BY_ZERO:
+      exception_str = "Integer divide-by-zero";
+      break;
+    case STATUS_INTEGER_OVERFLOW:
+      exception_str = "Integer overflow";
+      break;
+
+    case STATUS_DATATYPE_MISALIGNMENT:
+      exception_str = "Datatype misalignment";
+      break;
+    case STATUS_ACCESS_VIOLATION:
+      exception_str = "Access violation";
+      break;
+    case STATUS_IN_PAGE_ERROR:
+      exception_str = "In-page error";
+      break;
+    case STATUS_INVALID_HANDLE:
+      exception_str = "Invalid handle";
+      break;
+    case STATUS_NONCONTINUABLE_EXCEPTION:
+      exception_str = "Noncontinuable exception";
+      break;
+    case STATUS_INVALID_DISPOSITION:
+      exception_str = "Invalid disposition";
+      break;
+    case STATUS_ARRAY_BOUNDS_EXCEEDED:
+      exception_str = "Array bounds exceeded";
+      break;
+    case STATUS_STACK_OVERFLOW:
+      exception_str = "Stack overflow";
+      break;
+
+    case STATUS_ILLEGAL_INSTRUCTION:
+      exception_str = "Illegal instruction";
+      break;
+    case STATUS_PRIVILEGED_INSTRUCTION:
+      exception_str = "Privileged instruction";
+      break;
+    case STATUS_NO_MEMORY:
+    default:
+      char buf[1024];
+      _snprintf(buf, 1024, "Exit code 0x%x\n", this->ExitValue);
+      exception_str.assign(buf);
+  }
+#else
+  switch (this->Signal) {
+#ifdef SIGSEGV
+    case SIGSEGV:
+      exception_str = "Segmentation fault";
+      break;
+#endif
+#ifdef SIGBUS
+#if !defined(SIGSEGV) || SIGBUS != SIGSEGV
+    case SIGBUS:
+      exception_str = "Bus error";
+      break;
+#endif
+#endif
+#ifdef SIGFPE
+    case SIGFPE:
+      exception_str = "Floating-point exception";
+      break;
+#endif
+#ifdef SIGILL
+    case SIGILL:
+      exception_str = "Illegal instruction";
+      break;
+#endif
+#ifdef SIGINT
+    case SIGINT:
+      exception_str = "User interrupt";
+      break;
+#endif
+#ifdef SIGABRT
+    case SIGABRT:
+      exception_str = "Child aborted";
+      break;
+#endif
+#ifdef SIGKILL
+    case SIGKILL:
+      exception_str = "Child killed";
+      break;
+#endif
+#ifdef SIGTERM
+    case SIGTERM:
+      exception_str = "Child terminated";
+      break;
+#endif
+#ifdef SIGHUP
+    case SIGHUP:
+      exception_str = "SIGHUP";
+      break;
+#endif
+#ifdef SIGQUIT
+    case SIGQUIT:
+      exception_str = "SIGQUIT";
+      break;
+#endif
+#ifdef SIGTRAP
+    case SIGTRAP:
+      exception_str = "SIGTRAP";
+      break;
+#endif
+#ifdef SIGIOT
+#if !defined(SIGABRT) || SIGIOT != SIGABRT
+    case SIGIOT:
+      exception_str = "SIGIOT";
+      break;
+#endif
+#endif
+#ifdef SIGUSR1
+    case SIGUSR1:
+      exception_str = "SIGUSR1";
+      break;
+#endif
+#ifdef SIGUSR2
+    case SIGUSR2:
+      exception_str = "SIGUSR2";
+      break;
+#endif
+#ifdef SIGPIPE
+    case SIGPIPE:
+      exception_str = "SIGPIPE";
+      break;
+#endif
+#ifdef SIGALRM
+    case SIGALRM:
+      exception_str = "SIGALRM";
+      break;
+#endif
+#ifdef SIGSTKFLT
+    case SIGSTKFLT:
+      exception_str = "SIGSTKFLT";
+      break;
+#endif
+#ifdef SIGCHLD
+    case SIGCHLD:
+      exception_str = "SIGCHLD";
+      break;
+#elif defined(SIGCLD)
+    case SIGCLD:
+      exception_str = "SIGCLD";
+      break;
+#endif
+#ifdef SIGCONT
+    case SIGCONT:
+      exception_str = "SIGCONT";
+      break;
+#endif
+#ifdef SIGSTOP
+    case SIGSTOP:
+      exception_str = "SIGSTOP";
+      break;
+#endif
+#ifdef SIGTSTP
+    case SIGTSTP:
+      exception_str = "SIGTSTP";
+      break;
+#endif
+#ifdef SIGTTIN
+    case SIGTTIN:
+      exception_str = "SIGTTIN";
+      break;
+#endif
+#ifdef SIGTTOU
+    case SIGTTOU:
+      exception_str = "SIGTTOU";
+      break;
+#endif
+#ifdef SIGURG
+    case SIGURG:
+      exception_str = "SIGURG";
+      break;
+#endif
+#ifdef SIGXCPU
+    case SIGXCPU:
+      exception_str = "SIGXCPU";
+      break;
+#endif
+#ifdef SIGXFSZ
+    case SIGXFSZ:
+      exception_str = "SIGXFSZ";
+      break;
+#endif
+#ifdef SIGVTALRM
+    case SIGVTALRM:
+      exception_str = "SIGVTALRM";
+      break;
+#endif
+#ifdef SIGPROF
+    case SIGPROF:
+      exception_str = "SIGPROF";
+      break;
+#endif
+#ifdef SIGWINCH
+    case SIGWINCH:
+      exception_str = "SIGWINCH";
+      break;
+#endif
+#ifdef SIGPOLL
+    case SIGPOLL:
+      exception_str = "SIGPOLL";
+      break;
+#endif
+#ifdef SIGIO
+#if !defined(SIGPOLL) || SIGIO != SIGPOLL
+    case SIGIO:
+      exception_str = "SIGIO";
+      break;
+#endif
+#endif
+#ifdef SIGPWR
+    case SIGPWR:
+      exception_str = "SIGPWR";
+      break;
+#endif
+#ifdef SIGSYS
+    case SIGSYS:
+      exception_str = "SIGSYS";
+      break;
+#endif
+#ifdef SIGUNUSED
+#if !defined(SIGSYS) || SIGUNUSED != SIGSYS
+    case SIGUNUSED:
+      exception_str = "SIGUNUSED";
+      break;
+#endif
+#endif
+    default:
+      exception_str = "Signal ";
+      exception_str += std::to_string(this->Signal);
+  }
+#endif
+  return exception_str;
 }

+ 36 - 14
Source/CTest/cmProcess.h

@@ -5,11 +5,17 @@
 
 #include "cmConfigure.h" // IWYU pragma: keep
 
-#include "cmsys/Process.h"
+#include "cmUVHandlePtr.h"
+#include "cm_uv.h"
+
 #include <chrono>
+#include <stddef.h>
 #include <string>
+#include <sys/types.h>
 #include <vector>
 
+class cmCTestRunTest;
+
 /** \class cmProcess
  * \brief run a process with c++
  *
@@ -18,7 +24,7 @@
 class cmProcess
 {
 public:
-  cmProcess();
+  explicit cmProcess(cmCTestRunTest& runner);
   ~cmProcess();
   const char* GetCommand() { return this->Command.c_str(); }
   void SetCommand(const char* command);
@@ -28,7 +34,7 @@ public:
   void ChangeTimeout(std::chrono::duration<double> t);
   void ResetStartTime();
   // Return true if the process starts
-  bool StartProcess();
+  bool StartProcess(uv_loop_t& loop);
 
   enum class State
   {
@@ -61,21 +67,37 @@ public:
   Exception GetExitException();
   std::string GetExitExceptionString();
 
-  /**
-   * Read one line of output but block for no more than timeout.
-   * Returns:
-   *   cmsysProcess_Pipe_None    = Process terminated and all output read
-   *   cmsysProcess_Pipe_STDOUT  = Line came from stdout or stderr
-   *   cmsysProcess_Pipe_Timeout = Timeout expired while waiting
-   */
-  int GetNextOutputLine(std::string& line,
-                        std::chrono::duration<double> timeout);
-
 private:
   std::chrono::duration<double> Timeout;
   std::chrono::steady_clock::time_point StartTime;
   std::chrono::duration<double> TotalTime;
-  cmsysProcess* Process;
+  bool ReadHandleClosed = false;
+  bool ProcessHandleClosed = false;
+
+  cm::uv_process_ptr Process;
+  cm::uv_pipe_ptr PipeReader;
+  cm::uv_timer_ptr Timer;
+  std::vector<char> Buf;
+
+  cmCTestRunTest& Runner;
+  int Signal = 0;
+  cmProcess::State ProcessState = cmProcess::State::Starting;
+
+  static void OnExitCB(uv_process_t* process, int64_t exit_status,
+                       int term_signal);
+  static void OnTimeoutCB(uv_timer_t* timer);
+  static void OnReadCB(uv_stream_t* stream, ssize_t nread,
+                       const uv_buf_t* buf);
+  static void OnAllocateCB(uv_handle_t* handle, size_t suggested_size,
+                           uv_buf_t* buf);
+
+  void OnExit(int64_t exit_status, int term_signal);
+  void OnTimeout();
+  void OnRead(ssize_t nread, const uv_buf_t* buf);
+  void OnAllocate(size_t suggested_size, uv_buf_t* buf);
+
+  void StartTimer();
+
   class Buffer : public std::vector<char>
   {
     // Half-open index range of partial line already scanned.