| /* 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 <utility> |
| |
| #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 <cm3p/kwiml/int.h> |
| #endif |
| |
| #define CM_PROCESS_BUF_SIZE 65536 |
| |
| cmProcess::cmProcess(std::unique_ptr<cmCTestRunTest> runner) |
| : Runner(std::move(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->Finish(); |
| } |
| } |
| |
| 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->Finish(); |
| } |
| } |
| |
| 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->ProcessHandleClosed = true; |
| if (this->ReadHandleClosed) { |
| uv_timer_stop(this->Timer); |
| this->Finish(); |
| } |
| } |
| |
| void cmProcess::Finish() |
| { |
| 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->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 = "Subprocess aborted"; |
| break; |
| # endif |
| # ifdef SIGKILL |
| case SIGKILL: |
| exception_str = "Subprocess killed"; |
| break; |
| # endif |
| # ifdef SIGTERM |
| case SIGTERM: |
| exception_str = "Subprocess 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; |
| } |