diff options
-rw-r--r-- | Source/CTest/cmCTestMultiProcessHandler.cxx | 173 | ||||
-rw-r--r-- | Source/CTest/cmCTestMultiProcessHandler.h | 12 | ||||
-rw-r--r-- | Source/CTest/cmCTestRunTest.cxx | 201 | ||||
-rw-r--r-- | Source/CTest/cmCTestRunTest.h | 30 | ||||
-rw-r--r-- | Source/CTest/cmProcess.cxx | 708 | ||||
-rw-r--r-- | Source/CTest/cmProcess.h | 79 | ||||
-rw-r--r-- | Source/cmCTest.cxx | 72 | ||||
-rw-r--r-- | Source/cmCTest.h | 12 | ||||
-rw-r--r-- | Utilities/cmlibuv/src/unix/signal.c | 7 |
9 files changed, 882 insertions, 412 deletions
diff --git a/Source/CTest/cmCTestMultiProcessHandler.cxx b/Source/CTest/cmCTestMultiProcessHandler.cxx index ae07feb..53c47a2 100644 --- a/Source/CTest/cmCTestMultiProcessHandler.cxx +++ b/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 diff --git a/Source/CTest/cmCTestMultiProcessHandler.h b/Source/CTest/cmCTestMultiProcessHandler.h index dccc2c8..7837ff9 100644 --- a/Source/CTest/cmCTestMultiProcessHandler.h +++ b/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; diff --git a/Source/CTest/cmCTestRunTest.cxx b/Source/CTest/cmCTestRunTest.cxx index dbdefae..baf894e 100644 --- a/Source/CTest/cmCTestRunTest.cxx +++ b/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(¤t_time); - int gm_hour = lctime->tm_hour; - time_t gm_time = mktime(lctime); - lctime = localtime(¤t_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, ¤t_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); +} diff --git a/Source/CTest/cmCTestRunTest.h b/Source/CTest/cmCTestRunTest.h index cd380ca..fbc202f 100644 --- a/Source/CTest/cmCTestRunTest.h +++ b/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; diff --git a/Source/CTest/cmProcess.cxx b/Source/CTest/cmProcess.cxx index 0db66c3..c8806a7 100644 --- a/Source/CTest/cmProcess.cxx +++ b/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; } diff --git a/Source/CTest/cmProcess.h b/Source/CTest/cmProcess.h index f3b0bd7..9250896 100644 --- a/Source/CTest/cmProcess.h +++ b/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. diff --git a/Source/cmCTest.cxx b/Source/cmCTest.cxx index 26e1dcb..fd7c5e8 100644 --- a/Source/cmCTest.cxx +++ b/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(¤t_time); + int gm_hour = lctime->tm_hour; + time_t gm_time = mktime(lctime); + lctime = localtime(¤t_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, ¤t_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(¤t_time); - int gm_hour = lctime->tm_hour; - time_t gm_time = mktime(lctime); - lctime = localtime(¤t_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, ¤t_time); - - if (stop_time < current_time) { - this->NextDayStopTime = true; - } -} - void cmCTest::SetCTestConfiguration(const char* name, const char* value, bool suppress) { diff --git a/Source/cmCTest.h b/Source/cmCTest.h index 23d71cb..61487f1 100644 --- a/Source/cmCTest.h +++ b/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; diff --git a/Utilities/cmlibuv/src/unix/signal.c b/Utilities/cmlibuv/src/unix/signal.c index cb09ead..3759778 100644 --- a/Utilities/cmlibuv/src/unix/signal.c +++ b/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)) |