Przeglądaj źródła

Merge topic 'ctest-libuv'

b5e21d7d CTest: Re-implement test process handling using libuv
fcebff75 cmProcess: Use explicit enum for process exit exception
3dd2edf4 cmProcess: Use explicit enum for process state
5238e6db cmProcess: Remove unused ReportStatus method
c13b68e6 cmCTestRunTest: Modernize constructor and destructor decls
4d6b0903 cmCTestRunTest: Drop unused members
05da65bc cmCTestMultiProcessHandler: Factor out duplicate test finish logic
dd945345 cmCTestMultiProcessHandler: Add helper to make libuv use SA_RESTART
...

Acked-by: Kitware Robot <[email protected]>
Merge-request: !1455
Brad King 8 lat temu
rodzic
commit
4cf08c96f8

+ 103 - 70
Source/CTest/cmCTestMultiProcessHandler.cxx

@@ -9,10 +9,14 @@
 #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>
 #include <list>
 #include <math.h>
@@ -21,6 +25,43 @@
 #include <stdlib.h>
 #include <utility>
 
+#if defined(CMAKE_USE_SYSTEM_LIBUV) && !defined(_WIN32) &&                    \
+  UV_VERSION_MAJOR == 1 && UV_VERSION_MINOR < 19
+#define CMAKE_UV_SIGNAL_HACK
+/*
+   libuv does not use SA_RESTART on its signal handler, but C++ streams
+   depend on it for reliable i/o operations.  This RAII helper convinces
+   libuv to install its handler, and then revises the handler to add the
+   SA_RESTART flag.  We use a distinct uv loop that never runs to avoid
+   ever really getting a callback.  libuv may fill the hack loop's signal
+   pipe and then stop writing, but that won't break any real loops.
+ */
+class cmUVSignalHackRAII
+{
+  uv_loop_t HackLoop;
+  cm::uv_signal_ptr HackSignal;
+  static void HackCB(uv_signal_t*, int) {}
+public:
+  cmUVSignalHackRAII()
+  {
+    uv_loop_init(&this->HackLoop);
+    this->HackSignal.init(this->HackLoop);
+    this->HackSignal.start(HackCB, SIGCHLD);
+    struct sigaction hack_sa;
+    sigaction(SIGCHLD, NULL, &hack_sa);
+    if (!(hack_sa.sa_flags & SA_RESTART)) {
+      hack_sa.sa_flags |= SA_RESTART;
+      sigaction(SIGCHLD, &hack_sa, NULL);
+    }
+  }
+  ~cmUVSignalHackRAII()
+  {
+    this->HackSignal.stop();
+    uv_loop_close(&this->HackLoop);
+  }
+};
+#endif
+
 class TestComparator
 {
 public:
@@ -95,24 +136,32 @@ 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();
 }
 
-void cmCTestMultiProcessHandler::StartTestProcess(int test)
+bool cmCTestMultiProcessHandler::StartTestProcess(int test)
 {
+  std::chrono::system_clock::time_point stop_time = this->CTest->GetStopTime();
+  if (stop_time != std::chrono::system_clock::time_point() &&
+      stop_time <= std::chrono::system_clock::now()) {
+    cmCTestLog(this->CTest, ERROR_MESSAGE, "The stop time has been passed. "
+                                           "Stopping all tests."
+                 << std::endl);
+    this->StopTimePassed = true;
+    return false;
+  }
+
   cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
                      "test " << test << "\n", this->Quiet);
   this->TestRunningMap[test] = true; // mark the test as running
@@ -120,7 +169,7 @@ void 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());
@@ -143,28 +192,11 @@ void cmCTestMultiProcessHandler::StartTestProcess(int test)
   this->LockResources(test);
 
   if (testRun->StartTest(this->Total)) {
-    this->RunningTests.insert(testRun);
-  } else if (testRun->IsStopTimePassed()) {
-    this->StopTimePassed = true;
-    delete testRun;
-    return;
-  } else {
-
-    for (auto& j : this->Tests) {
-      j.second.erase(test);
-    }
-
-    this->UnlockResources(test);
-    this->Completed++;
-    this->TestFinishMap[test] = true;
-    this->TestRunningMap[test] = false;
-    this->RunningCount -= GetProcessorsUsed(test);
-    testRun->EndTest(this->Completed, this->Total, false);
-    if (!this->Properties[test]->Disabled) {
-      this->Failed->push_back(this->Properties[test]->Name);
-    }
-    delete testRun;
+    return true;
   }
+
+  this->FinishTestProcess(testRun, false);
+  return false;
 }
 
 void cmCTestMultiProcessHandler::LockResources(int index)
@@ -222,8 +254,7 @@ bool cmCTestMultiProcessHandler::StartTest(int test)
 
   // if there are no depends left then run this test
   if (this->Tests[test].empty()) {
-    this->StartTestProcess(test);
-    return true;
+    return this->StartTestProcess(test);
   }
   // This test was not able to start because it is waiting
   // on depends to run
@@ -233,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;
   }
@@ -365,45 +401,42 @@ void cmCTestMultiProcessHandler::StartNextTests()
   }
 }
 
-bool cmCTestMultiProcessHandler::CheckOutput()
+void cmCTestMultiProcessHandler::FinishTestProcess(cmCTestRunTest* runner,
+                                                   bool started)
 {
-  // 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->Completed++;
-    int test = p->GetIndex();
+  this->Completed++;
+
+  int test = runner->GetIndex();
+  auto properties = runner->GetTestProperties();
 
-    bool testResult = p->EndTest(this->Completed, this->Total, true);
-    if (p->StartAgain()) {
+  bool testResult = runner->EndTest(this->Completed, this->Total, started);
+  if (started) {
+    if (runner->StartAgain()) {
       this->Completed--; // remove the completed test because run again
-      continue;
-    }
-    if (testResult) {
-      this->Passed->push_back(p->GetTestProperties()->Name);
-    } else {
-      this->Failed->push_back(p->GetTestProperties()->Name);
-    }
-    for (auto& t : this->Tests) {
-      t.second.erase(test);
+      return;
     }
-    this->TestFinishMap[test] = true;
-    this->TestRunningMap[test] = false;
-    this->RunningTests.erase(p);
-    this->WriteCheckpoint(test);
-    this->UnlockResources(test);
-    this->RunningCount -= GetProcessorsUsed(test);
-    delete p;
   }
-  return true;
+
+  if (testResult) {
+    this->Passed->push_back(properties->Name);
+  } else if (!properties->Disabled) {
+    this->Failed->push_back(properties->Name);
+  }
+
+  for (auto& t : this->Tests) {
+    t.second.erase(test);
+  }
+
+  this->TestFinishMap[test] = true;
+  this->TestRunningMap[test] = false;
+  this->WriteCheckpoint(test);
+  this->UnlockResources(test);
+  this->RunningCount -= GetProcessorsUsed(test);
+
+  delete runner;
+  if (started) {
+    this->StartNextTests();
+  }
 }
 
 void cmCTestMultiProcessHandler::UpdateCostData()
@@ -670,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

+ 7 - 5
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>
@@ -75,7 +78,7 @@ protected:
   // Start the next test or tests as many as are allowed by
   // ParallelLevel
   void StartNextTests();
-  void StartTestProcess(int test);
+  bool StartTestProcess(int test);
   bool StartTest(int test);
   // Mark the checkpoint for the given test
   void WriteCheckpoint(int index);
@@ -95,9 +98,8 @@ 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);
   // Check if we need to resume an interrupted test set
   void CheckResume();
@@ -130,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;

+ 66 - 135
Source/CTest/cmCTestRunTest.cxx

@@ -4,28 +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_curl.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 <time.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;
@@ -34,60 +33,37 @@ cmCTestRunTest::cmCTestRunTest(cmCTestTestHandler* handler)
   this->ProcessOutput.clear();
   this->CompressedOutput.clear();
   this->CompressionRatio = 2;
-  this->StopTimePassed = false;
   this->NumberOfRunsLeft = 1; // default to 1 run of the test
   this->RunUntilFail = false; // default to run the test once
   this->RunAgain = false;     // default to not having to run again
 }
 
-cmCTestRunTest::~cmCTestRunTest()
+void cmCTestRunTest::CheckOutput(std::string const& line)
 {
-}
-
-bool cmCTestRunTest::CheckOutput()
-{
-  // 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
@@ -160,8 +136,8 @@ bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
   this->WriteLogOutputTop(completed, total);
   std::string reason;
   bool passed = true;
-  int res =
-    started ? this->TestProcess->GetProcessStatus() : cmsysProcess_State_Error;
+  cmProcess::State res =
+    started ? this->TestProcess->GetProcessStatus() : cmProcess::State::Error;
   int retVal = this->TestProcess->GetExitValue();
   bool forceFail = false;
   bool skipped = false;
@@ -200,7 +176,7 @@ bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
       }
     }
   }
-  if (res == cmsysProcess_State_Exited) {
+  if (res == cmProcess::State::Exited) {
     bool success = !forceFail &&
       (retVal == 0 ||
        !this->TestProperties->RequiredRegularExpressions.empty());
@@ -221,29 +197,29 @@ bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
       cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Failed  " << reason);
       outputTestErrorsToConsole = this->CTest->OutputTestOutputOnTestFailure;
     }
-  } else if (res == cmsysProcess_State_Expired) {
+  } else if (res == cmProcess::State::Expired) {
     cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Timeout ");
     this->TestResult.Status = cmCTestTestHandler::TIMEOUT;
     outputTestErrorsToConsole = this->CTest->OutputTestOutputOnTestFailure;
-  } else if (res == cmsysProcess_State_Exception) {
+  } else if (res == cmProcess::State::Exception) {
     outputTestErrorsToConsole = this->CTest->OutputTestOutputOnTestFailure;
     cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Exception: ");
     this->TestResult.ExceptionStatus =
       this->TestProcess->GetExitExceptionString();
     switch (this->TestProcess->GetExitException()) {
-      case cmsysProcess_Exception_Fault:
+      case cmProcess::Exception::Fault:
         cmCTestLog(this->CTest, HANDLER_OUTPUT, "SegFault");
         this->TestResult.Status = cmCTestTestHandler::SEGFAULT;
         break;
-      case cmsysProcess_Exception_Illegal:
+      case cmProcess::Exception::Illegal:
         cmCTestLog(this->CTest, HANDLER_OUTPUT, "Illegal");
         this->TestResult.Status = cmCTestTestHandler::ILLEGAL;
         break;
-      case cmsysProcess_Exception_Interrupt:
+      case cmProcess::Exception::Interrupt:
         cmCTestLog(this->CTest, HANDLER_OUTPUT, "Interrupt");
         this->TestResult.Status = cmCTestTestHandler::INTERRUPT;
         break;
-      case cmsysProcess_Exception_Numerical:
+      case cmProcess::Exception::Numerical:
         cmCTestLog(this->CTest, HANDLER_OUTPUT, "Numerical");
         this->TestResult.Status = cmCTestTestHandler::NUMERICAL;
         break;
@@ -254,7 +230,7 @@ bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
     }
   } else if ("Disabled" == this->TestResult.CompletionStatus) {
     cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Not Run (Disabled) ");
-  } else // cmsysProcess_State_Error
+  } else // cmProcess::State::Error
   {
     cmCTestLog(this->CTest, HANDLER_OUTPUT, "***Not Run ");
   }
@@ -350,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;
 }
 
@@ -432,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;
@@ -453,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;
@@ -470,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.";
@@ -493,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,
@@ -509,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,
@@ -522,11 +498,22 @@ bool cmCTestRunTest::StartTest(size_t total)
   }
   this->StartTime = this->CTest->CurrentTime();
 
-  auto timeout = this->ResolveTimeout();
+  auto timeout = this->TestProperties->Timeout;
 
-  if (this->StopTimePassed) {
-    return false;
+  std::chrono::system_clock::time_point stop_time = this->CTest->GetStopTime();
+  if (stop_time != std::chrono::system_clock::time_point()) {
+    std::chrono::duration<double> stop_timeout =
+      (stop_time - std::chrono::system_clock::now()) % std::chrono::hours(24);
+
+    if (stop_timeout <= std::chrono::duration<double>::zero()) {
+      stop_timeout = std::chrono::duration<double>::zero();
+    }
+    if (timeout == std::chrono::duration<double>::zero() ||
+        stop_timeout < timeout) {
+      timeout = stop_timeout;
+    }
   }
+
   return this->ForkProcess(timeout, this->TestProperties->ExplicitTimeout,
                            &this->TestProperties->Environment);
 }
@@ -603,72 +590,11 @@ void cmCTestRunTest::DartProcessing()
   }
 }
 
-std::chrono::duration<double> cmCTestRunTest::ResolveTimeout()
-{
-  auto timeout = this->TestProperties->Timeout;
-
-  if (this->CTest->GetStopTime().empty()) {
-    return timeout;
-  }
-  struct tm* lctime;
-  time_t current_time = time(nullptr);
-  lctime = gmtime(&current_time);
-  int gm_hour = lctime->tm_hour;
-  time_t gm_time = mktime(lctime);
-  lctime = localtime(&current_time);
-  int local_hour = lctime->tm_hour;
-
-  int tzone_offset = local_hour - gm_hour;
-  if (gm_time > current_time && gm_hour < local_hour) {
-    // this means gm_time is on the next day
-    tzone_offset -= 24;
-  } else if (gm_time < current_time && gm_hour > local_hour) {
-    // this means gm_time is on the previous day
-    tzone_offset += 24;
-  }
-
-  tzone_offset *= 100;
-  char buf[1024];
-  // add todays year day and month to the time in str because
-  // curl_getdate no longer assumes the day is today
-  sprintf(buf, "%d%02d%02d %s %+05i", lctime->tm_year + 1900,
-          lctime->tm_mon + 1, lctime->tm_mday,
-          this->CTest->GetStopTime().c_str(), tzone_offset);
-
-  time_t stop_time_t = curl_getdate(buf, &current_time);
-  if (stop_time_t == -1) {
-    return timeout;
-  }
-
-  auto stop_time = std::chrono::system_clock::from_time_t(stop_time_t);
-
-  // the stop time refers to the next day
-  if (this->CTest->NextDayStopTime) {
-    stop_time += std::chrono::hours(24);
-  }
-  auto stop_timeout =
-    (stop_time - std::chrono::system_clock::from_time_t(current_time)) %
-    std::chrono::hours(24);
-  this->CTest->LastStopTimeout = stop_timeout;
-
-  if (stop_timeout <= std::chrono::duration<double>::zero() ||
-      stop_timeout > this->CTest->LastStopTimeout) {
-    cmCTestLog(this->CTest, ERROR_MESSAGE, "The stop time has been passed. "
-                                           "Stopping all tests."
-                 << std::endl);
-    this->StopTimePassed = true;
-    return std::chrono::duration<double>::zero();
-  }
-  return timeout == std::chrono::duration<double>::zero()
-    ? stop_timeout
-    : (timeout < stop_timeout ? timeout : stop_timeout);
-}
-
 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());
@@ -720,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)
@@ -794,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);
+}

+ 12 - 18
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,8 +25,9 @@ class cmProcess;
 class cmCTestRunTest
 {
 public:
-  cmCTestRunTest(cmCTestTestHandler* handler);
-  ~cmCTestRunTest();
+  explicit cmCTestRunTest(cmCTestMultiProcessHandler& multiHandler);
+
+  ~cmCTestRunTest() = default;
 
   void SetNumberOfRuns(int n) { this->NumberOfRunsLeft = n; }
   void SetRunUntilFailOn() { this->RunUntilFail = true; }
@@ -50,15 +52,13 @@ public:
 
   std::string GetProcessOutput() { return this->ProcessOutput; }
 
-  bool IsStopTimePassed() { return this->StopTimePassed; }
-
   cmCTestTestHandler::cmCTestTestResult GetTestResults()
   {
     return this->TestResult;
   }
 
   // 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();
@@ -74,12 +74,14 @@ public:
 
   bool StartAgain();
 
+  cmCTest* GetCTest() const { return this->CTest; }
+
+  void FinalizeTest();
+
 private:
   bool NeedsToRerun();
   void DartProcessing();
   void ExeNotFound(std::string exe);
-  // Figures out a final timeout which is min(STOP_TIME, NOW+TIMEOUT)
-  std::chrono::duration<double> ResolveTimeout();
   bool ForkProcess(std::chrono::duration<double> testTimeOut,
                    bool explicitTimeout,
                    std::vector<std::string>* environment);
@@ -91,26 +93,18 @@ private:
   // Pointer back to the "parent"; the handler that invoked this test run
   cmCTestTestHandler* TestHandler;
   cmCTest* CTest;
-  cmProcess* TestProcess;
-  // If the executable to run is ctest, don't create a new process;
-  // just instantiate a new cmTest.  (Can be disabled for a single test
-  // if this option is set to false.)
-  // bool OptimizeForCTest;
-
-  bool UsePrefixCommand;
-  std::string PrefixCommand;
-
+  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;
   std::string ActualCommand;
   std::vector<std::string> Arguments;
-  bool StopTimePassed;
   bool RunUntilFail;
   int NumberOfRunsLeft;
   bool RunAgain;

+ 588 - 120
Source/CTest/cmProcess.cxx

@@ -2,12 +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"
+
+#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>
 
-cmProcess::cmProcess()
+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;
@@ -17,8 +70,8 @@ cmProcess::cmProcess()
 
 cmProcess::~cmProcess()
 {
-  cmsysProcess_Delete(this->Process);
 }
+
 void cmProcess::SetCommand(const char* command)
 {
   this->Command = command;
@@ -29,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;
   }
@@ -43,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)
@@ -100,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
@@ -153,95 +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;
-}
-
-// return the process status
-int cmProcess::GetProcessStatus()
-{
-  if (!this->Process) {
-    return cmsysProcess_State_Exited;
-  }
-  return cmsysProcess_GetState(this->Process);
-}
-
-int cmProcess::ReportStatus()
-{
-  int result = 1;
-  switch (cmsysProcess_GetState(this->Process)) {
-    case cmsysProcess_State_Starting: {
-      std::cerr << "cmProcess: Never started " << this->Command
-                << " process.\n";
-    } break;
-    case cmsysProcess_State_Error: {
-      std::cerr << "cmProcess: Error executing " << this->Command
-                << " process: " << cmsysProcess_GetErrorString(this->Process)
-                << "\n";
-    } break;
-    case cmsysProcess_State_Exception: {
-      std::cerr << "cmProcess: " << this->Command
-                << " process exited with an exception: ";
-      switch (cmsysProcess_GetExitException(this->Process)) {
-        case cmsysProcess_Exception_None: {
-          std::cerr << "None";
-        } break;
-        case cmsysProcess_Exception_Fault: {
-          std::cerr << "Segmentation fault";
-        } break;
-        case cmsysProcess_Exception_Illegal: {
-          std::cerr << "Illegal instruction";
-        } break;
-        case cmsysProcess_Exception_Interrupt: {
-          std::cerr << "Interrupted by user";
-        } break;
-        case cmsysProcess_Exception_Numerical: {
-          std::cerr << "Numerical exception";
-        } break;
-        case cmsysProcess_Exception_Other: {
-          std::cerr << "Unknown";
-        } break;
-      }
-      std::cerr << "\n";
-    } break;
-    case cmsysProcess_State_Executing: {
-      std::cerr << "cmProcess: Never terminated " << this->Command
-                << " process.\n";
-    } break;
-    case cmsysProcess_State_Exited: {
-      result = cmsysProcess_GetExitValue(this->Process);
-      std::cerr << "cmProcess: " << this->Command
-                << " process exited with code " << result << "\n";
-    } break;
-    case cmsysProcess_State_Expired: {
-      std::cerr << "cmProcess: killed " << this->Command
-                << " process due to timeout.\n";
-    } break;
-    case cmsysProcess_State_Killed: {
-      std::cerr << "cmProcess: killed " << this->Command << " process.\n";
-    } break;
-  }
-  return result;
+
+  this->ProcessHandleClosed = true;
+  if (this->ReadHandleClosed) {
+    uv_timer_stop(this->Timer);
+    this->Runner.FinalizeTest();
+  }
+}
+
+cmProcess::State cmProcess::GetProcessStatus()
+{
+  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();
 }
 
-int cmProcess::GetExitException()
+cmProcess::Exception cmProcess::GetExitException()
 {
-  return cmsysProcess_GetExitException(this->Process);
+  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;
+    }
+  }
+#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;
 }

+ 61 - 18
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,33 +34,70 @@ 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
+  {
+    Starting,
+    Error,
+    Exception,
+    Executing,
+    Exited,
+    Expired,
+    Killed,
+    Disowned
+  };
 
-  // return the process status
-  int GetProcessStatus();
-  // Report the status of the program
-  int ReportStatus();
+  State GetProcessStatus();
   int GetId() { return this->Id; }
   void SetId(int id) { this->Id = id; }
   int GetExitValue() { return this->ExitValue; }
   std::chrono::duration<double> GetTotalTime() { return this->TotalTime; }
-  int GetExitException();
+
+  enum class Exception
+  {
+    None,
+    Fault,
+    Illegal,
+    Interrupt,
+    Numerical,
+    Other
+  };
+
+  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.

+ 34 - 38
Source/cmCTest.cxx

@@ -279,11 +279,8 @@ cmCTest::cmCTest()
   this->InteractiveDebugMode = true;
   this->TimeOut = std::chrono::duration<double>::zero();
   this->GlobalTimeout = std::chrono::duration<double>::zero();
-  this->LastStopTimeout = std::chrono::hours(24);
   this->CompressXMLFiles = false;
   this->ScheduleType.clear();
-  this->StopTime.clear();
-  this->NextDayStopTime = false;
   this->OutputLogFile = nullptr;
   this->OutputLogFileLastTag = -1;
   this->SuppressUpdatingCTestConfiguration = false;
@@ -2269,10 +2266,41 @@ void cmCTest::SetNotesFiles(const char* notes)
   this->NotesFiles = notes;
 }
 
-void cmCTest::SetStopTime(std::string const& time)
+void cmCTest::SetStopTime(std::string const& time_str)
 {
-  this->StopTime = time;
-  this->DetermineNextDayStop();
+
+  struct tm* lctime;
+  time_t current_time = time(nullptr);
+  lctime = gmtime(&current_time);
+  int gm_hour = lctime->tm_hour;
+  time_t gm_time = mktime(lctime);
+  lctime = localtime(&current_time);
+  int local_hour = lctime->tm_hour;
+
+  int tzone_offset = local_hour - gm_hour;
+  if (gm_time > current_time && gm_hour < local_hour) {
+    // this means gm_time is on the next day
+    tzone_offset -= 24;
+  } else if (gm_time < current_time && gm_hour > local_hour) {
+    // this means gm_time is on the previous day
+    tzone_offset += 24;
+  }
+
+  tzone_offset *= 100;
+  char buf[1024];
+  sprintf(buf, "%d%02d%02d %s %+05i", lctime->tm_year + 1900,
+          lctime->tm_mon + 1, lctime->tm_mday, time_str.c_str(), tzone_offset);
+
+  time_t stop_time = curl_getdate(buf, &current_time);
+  if (stop_time == -1) {
+    this->StopTime = std::chrono::system_clock::time_point();
+    return;
+  }
+  this->StopTime = std::chrono::system_clock::from_time_t(stop_time);
+
+  if (stop_time < current_time) {
+    this->StopTime += std::chrono::hours(24);
+  }
 }
 
 int cmCTest::ReadCustomConfigurationFileTree(const char* dir, cmMakefile* mf)
@@ -2430,38 +2458,6 @@ void cmCTest::EmptyCTestConfiguration()
   this->CTestConfiguration.clear();
 }
 
-void cmCTest::DetermineNextDayStop()
-{
-  struct tm* lctime;
-  time_t current_time = time(nullptr);
-  lctime = gmtime(&current_time);
-  int gm_hour = lctime->tm_hour;
-  time_t gm_time = mktime(lctime);
-  lctime = localtime(&current_time);
-  int local_hour = lctime->tm_hour;
-
-  int tzone_offset = local_hour - gm_hour;
-  if (gm_time > current_time && gm_hour < local_hour) {
-    // this means gm_time is on the next day
-    tzone_offset -= 24;
-  } else if (gm_time < current_time && gm_hour > local_hour) {
-    // this means gm_time is on the previous day
-    tzone_offset += 24;
-  }
-
-  tzone_offset *= 100;
-  char buf[1024];
-  sprintf(buf, "%d%02d%02d %s %+05i", lctime->tm_year + 1900,
-          lctime->tm_mon + 1, lctime->tm_mday, this->StopTime.c_str(),
-          tzone_offset);
-
-  time_t stop_time = curl_getdate(buf, &current_time);
-
-  if (stop_time < current_time) {
-    this->NextDayStopTime = true;
-  }
-}
-
 void cmCTest::SetCTestConfiguration(const char* name, const char* value,
                                     bool suppress)
 {

+ 5 - 7
Source/cmCTest.h

@@ -226,7 +226,10 @@ public:
   bool ShouldCompressTestOutput();
   bool CompressString(std::string& str);
 
-  std::string GetStopTime() { return this->StopTime; }
+  std::chrono::system_clock::time_point GetStopTime()
+  {
+    return this->StopTime;
+  }
   void SetStopTime(std::string const& time);
 
   /** Used for parallel ctest job scheduling */
@@ -464,8 +467,7 @@ private:
   bool RepeatUntilFail;
   std::string ConfigType;
   std::string ScheduleType;
-  std::string StopTime;
-  bool NextDayStopTime;
+  std::chrono::system_clock::time_point StopTime;
   bool Verbose;
   bool ExtraVerbose;
   bool ProduceXML;
@@ -481,8 +483,6 @@ private:
 
   int GenerateNotesFile(const char* files);
 
-  void DetermineNextDayStop();
-
   // these are helper classes
   typedef std::map<std::string, cmCTestGenericHandler*> t_TestingHandlers;
   t_TestingHandlers TestingHandlers;
@@ -512,8 +512,6 @@ private:
 
   std::chrono::duration<double> GlobalTimeout;
 
-  std::chrono::duration<double> LastStopTimeout;
-
   int MaxTestNameWidth;
 
   int ParallelLevel;

+ 6 - 1
Utilities/cmlibuv/src/unix/signal.c

@@ -28,6 +28,9 @@
 #include <string.h>
 #include <unistd.h>
 
+#ifndef SA_RESTART
+# define SA_RESTART 0
+#endif
 
 typedef struct {
   uv_signal_t* handle;
@@ -216,7 +219,9 @@ static int uv__signal_register_handler(int signum, int oneshot) {
   if (sigfillset(&sa.sa_mask))
     abort();
   sa.sa_handler = uv__signal_handler;
-  sa.sa_flags = oneshot ? SA_RESETHAND : 0;
+  sa.sa_flags = SA_RESTART;
+  if (oneshot)
+    sa.sa_flags |= SA_RESETHAND;
 
   /* XXX save old action so we can restore it later on? */
   if (sigaction(signum, &sa, NULL))