CMake/Source/CTest/cmProcess.cxx
Brad King d1976cd1f2 CTest: Fix timeout when grandchild keeps pipes open
When a test's process creates its own child and exits, the grandchild
may keep pipes open.  Fix CTest logic to correctly timeout if the
grandchild does not exit and close the pipes before the timeout expires.
This was broken by commit b5e21d7d2e (CTest: Re-implement test process
handling using libuv, 2017-12-10, v3.11.0-rc1~117^2) which added an
unnecessary condition to the timeout handling.

Fixes: #20116
2020-01-14 10:29:05 -05:00

702 lines
17 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmProcess.h"
#include <csignal>
#include <iostream>
#include <string>
#include <cmext/algorithm>
#include "cmsys/Process.h"
#include "cmCTest.h"
#include "cmCTestRunTest.h"
#include "cmCTestTestHandler.h"
#include "cmGetPipes.h"
#include "cmStringAlgorithms.h"
#if defined(_WIN32)
# include "cm_kwiml.h"
#endif
#include <utility>
#define CM_PROCESS_BUF_SIZE 65536
cmProcess::cmProcess(cmCTestRunTest& runner)
: Runner(runner)
, Conv(cmProcessOutput::UTF8, CM_PROCESS_BUF_SIZE)
{
this->Timeout = cmDuration::zero();
this->TotalTime = cmDuration::zero();
this->ExitValue = 0;
this->Id = 0;
this->StartTime = std::chrono::steady_clock::time_point();
}
cmProcess::~cmProcess() = default;
void cmProcess::SetCommand(std::string const& command)
{
this->Command = command;
}
void cmProcess::SetCommandArguments(std::vector<std::string> const& args)
{
this->Arguments = args;
}
void cmProcess::SetWorkingDirectory(std::string const& dir)
{
this->WorkingDirectory = dir;
}
bool cmProcess::StartProcess(uv_loop_t& loop, std::vector<size_t>* affinity)
{
this->ProcessState = cmProcess::State::Error;
if (this->Command.empty()) {
return false;
}
this->StartTime = std::chrono::steady_clock::now();
this->ProcessArgs.clear();
// put the command as arg0
this->ProcessArgs.push_back(this->Command.c_str());
// now put the command arguments in
for (std::string const& arg : this->Arguments) {
this->ProcessArgs.push_back(arg.c_str());
}
this->ProcessArgs.push_back(nullptr); // null terminate the list
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 = cmGetPipes(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_INHERIT_FD;
stdio[0].data.fd = 0;
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;
#if !defined(CMAKE_USE_SYSTEM_LIBUV)
std::vector<char> cpumask;
if (affinity && !affinity->empty()) {
cpumask.resize(static_cast<size_t>(uv_cpumask_size()), 0);
for (auto p : *affinity) {
cpumask[p] = 1;
}
options.cpumask = cpumask.data();
options.cpumask_size = cpumask.size();
} else {
options.cpumask = nullptr;
options.cpumask_size = 0;
}
#else
static_cast<void>(affinity);
#endif
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);
}
}
bool cmProcess::Buffer::GetLine(std::string& line)
{
// Scan for the next newline.
for (size_type sz = this->size(); this->Last != sz; ++this->Last) {
if ((*this)[this->Last] == '\n' || (*this)[this->Last] == '\0') {
// Extract the range first..last as a line.
const char* text = this->data() + this->First;
size_type length = this->Last - this->First;
while (length && text[length - 1] == '\r') {
length--;
}
line.assign(text, length);
// Start a new range for the next line.
++this->Last;
this->First = Last;
// Return the line extracted.
return true;
}
}
// Available data have been exhausted without a newline.
if (this->First != 0) {
// Move the partial line to the beginning of the buffer.
this->erase(this->begin(), this->begin() + this->First);
this->First = 0;
this->Last = this->size();
}
return false;
}
bool cmProcess::Buffer::GetLast(std::string& line)
{
// Return the partial last line, if any.
if (!this->empty()) {
line.assign(this->data(), this->size());
this->First = this->Last = 0;
this->clear();
return true;
}
return false;
}
void cmProcess::OnReadCB(uv_stream_t* stream, ssize_t nread,
const uv_buf_t* buf)
{
auto self = static_cast<cmProcess*>(stream->data);
self->OnRead(nread, buf);
}
void cmProcess::OnRead(ssize_t nread, const uv_buf_t* buf)
{
std::string line;
if (nread > 0) {
std::string strdata;
this->Conv.DecodeText(buf->base, static_cast<size_t>(nread), strdata);
cm::append(this->Output, strdata);
while (this->Output.GetLine(line)) {
this->Runner.CheckOutput(line);
line.clear();
}
return;
}
if (nread == 0) {
return;
}
// 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)) {
this->Runner.CheckOutput(line);
}
this->ReadHandleClosed = true;
this->PipeReader.reset();
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() != CM_PROCESS_BUF_SIZE) {
this->Buf.resize(CM_PROCESS_BUF_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()
{
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();
}
}
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 = 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
// running this may be even more. Make sure not to report a negative
// duration here.
if (this->TotalTime <= cmDuration::zero()) {
this->TotalTime = cmDuration::zero();
}
this->ProcessHandleClosed = true;
if (this->ReadHandleClosed) {
uv_timer_stop(this->Timer);
this->Runner.FinalizeTest();
}
}
cmProcess::State cmProcess::GetProcessStatus()
{
return this->ProcessState;
}
void cmProcess::ChangeTimeout(cmDuration t)
{
this->Timeout = t;
this->StartTimer();
}
void cmProcess::ResetStartTime()
{
this->StartTime = std::chrono::steady_clock::now();
}
cmProcess::Exception cmProcess::GetExitException()
{
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()
{
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];
const char* fmt = "Exit code 0x%" KWIML_INT_PRIx64 "\n";
_snprintf(buf, 1024, fmt, 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 = cmStrCat("Signal ", this->Signal);
}
#endif
return exception_str;
}