Files
CMake/Source/cmExecuteProcessCommand.cxx
Brad King bcbb212df7 Revert use of libuv for process execution for 3.28
Wide use of CMake 3.28.{1,0[-rcN]} has uncovered some hangs and crashes
in libuv SIGCHLD handling on some platforms, particularly in virtualization
environments on macOS hosts.  Although the bug does not seem to be in CMake,
we can restore stability in the CMake 3.28 release series for users of such
platforms by reverting our new uses of libuv for process execution.

Revert implementation changes merged by commit 4771544386 (Merge topic
'replace-cmsysprocess-with-cmuvprocesschain', 2023-09-06, v3.28.0-rc1~138),
but keep test suite updates.

Issue: #25414, #25500, #25562, #25589
2024-01-24 17:10:00 -05:00

543 lines
18 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmExecuteProcessCommand.h"
#include <algorithm>
#include <cctype> /* isspace */
#include <cstdio>
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <utility>
#include <vector>
#include <cm/string_view>
#include <cmext/algorithm>
#include <cmext/string_view>
#include "cmsys/Process.h"
#include "cmArgumentParser.h"
#include "cmExecutionStatus.h"
#include "cmList.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmProcessOutput.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
namespace {
bool cmExecuteProcessCommandIsWhitespace(char c)
{
return (isspace(static_cast<int>(c)) || c == '\n' || c == '\r');
}
void cmExecuteProcessCommandFixText(std::vector<char>& output,
bool strip_trailing_whitespace);
void cmExecuteProcessCommandAppend(std::vector<char>& output, const char* data,
int length);
}
// cmExecuteProcessCommand
bool cmExecuteProcessCommand(std::vector<std::string> const& args,
cmExecutionStatus& status)
{
if (args.empty()) {
status.SetError("called with incorrect number of arguments");
return false;
}
struct Arguments : public ArgumentParser::ParseResult
{
std::vector<std::vector<std::string>> Commands;
std::string OutputVariable;
std::string ErrorVariable;
std::string ResultVariable;
std::string ResultsVariable;
std::string WorkingDirectory;
std::string InputFile;
std::string OutputFile;
std::string ErrorFile;
std::string Timeout;
std::string CommandEcho;
bool OutputQuiet = false;
bool ErrorQuiet = false;
bool OutputStripTrailingWhitespace = false;
bool ErrorStripTrailingWhitespace = false;
bool EchoOutputVariable = false;
bool EchoErrorVariable = false;
std::string Encoding;
std::string CommandErrorIsFatal;
};
static auto const parser =
cmArgumentParser<Arguments>{}
.Bind("COMMAND"_s, &Arguments::Commands)
.Bind("COMMAND_ECHO"_s, &Arguments::CommandEcho)
.Bind("OUTPUT_VARIABLE"_s, &Arguments::OutputVariable)
.Bind("ERROR_VARIABLE"_s, &Arguments::ErrorVariable)
.Bind("RESULT_VARIABLE"_s, &Arguments::ResultVariable)
.Bind("RESULTS_VARIABLE"_s, &Arguments::ResultsVariable)
.Bind("WORKING_DIRECTORY"_s, &Arguments::WorkingDirectory)
.Bind("INPUT_FILE"_s, &Arguments::InputFile)
.Bind("OUTPUT_FILE"_s, &Arguments::OutputFile)
.Bind("ERROR_FILE"_s, &Arguments::ErrorFile)
.Bind("TIMEOUT"_s, &Arguments::Timeout)
.Bind("OUTPUT_QUIET"_s, &Arguments::OutputQuiet)
.Bind("ERROR_QUIET"_s, &Arguments::ErrorQuiet)
.Bind("OUTPUT_STRIP_TRAILING_WHITESPACE"_s,
&Arguments::OutputStripTrailingWhitespace)
.Bind("ERROR_STRIP_TRAILING_WHITESPACE"_s,
&Arguments::ErrorStripTrailingWhitespace)
.Bind("ENCODING"_s, &Arguments::Encoding)
.Bind("ECHO_OUTPUT_VARIABLE"_s, &Arguments::EchoOutputVariable)
.Bind("ECHO_ERROR_VARIABLE"_s, &Arguments::EchoErrorVariable)
.Bind("COMMAND_ERROR_IS_FATAL"_s, &Arguments::CommandErrorIsFatal);
std::vector<std::string> unparsedArguments;
Arguments const arguments = parser.Parse(args, &unparsedArguments);
if (arguments.MaybeReportError(status.GetMakefile())) {
return true;
}
if (!unparsedArguments.empty()) {
status.SetError(" given unknown argument \"" + unparsedArguments.front() +
"\".");
return false;
}
std::string inputFilename = arguments.InputFile;
std::string outputFilename = arguments.OutputFile;
std::string errorFilename = arguments.ErrorFile;
if (!arguments.WorkingDirectory.empty()) {
if (!inputFilename.empty()) {
inputFilename = cmSystemTools::CollapseFullPath(
inputFilename, arguments.WorkingDirectory);
}
if (!outputFilename.empty()) {
outputFilename = cmSystemTools::CollapseFullPath(
outputFilename, arguments.WorkingDirectory);
}
if (!errorFilename.empty()) {
errorFilename = cmSystemTools::CollapseFullPath(
errorFilename, arguments.WorkingDirectory);
}
}
if (!status.GetMakefile().CanIWriteThisFile(outputFilename)) {
status.SetError("attempted to output into a file: " + outputFilename +
" into a source directory.");
cmSystemTools::SetFatalErrorOccurred();
return false;
}
// Check for commands given.
if (arguments.Commands.empty()) {
status.SetError(" called with no COMMAND argument.");
return false;
}
for (std::vector<std::string> const& cmd : arguments.Commands) {
if (cmd.empty()) {
status.SetError(" given COMMAND argument with no value.");
return false;
}
}
// Parse the timeout string.
double timeout = -1;
if (!arguments.Timeout.empty()) {
if (sscanf(arguments.Timeout.c_str(), "%lg", &timeout) != 1) {
status.SetError(" called with TIMEOUT value that could not be parsed.");
return false;
}
}
if (!arguments.CommandErrorIsFatal.empty()) {
if (arguments.CommandErrorIsFatal != "ANY"_s &&
arguments.CommandErrorIsFatal != "LAST"_s) {
status.SetError("COMMAND_ERROR_IS_FATAL option can be ANY or LAST");
return false;
}
}
// Create a process instance.
std::unique_ptr<cmsysProcess, void (*)(cmsysProcess*)> cp_ptr(
cmsysProcess_New(), cmsysProcess_Delete);
cmsysProcess* cp = cp_ptr.get();
// Set the command sequence.
for (std::vector<std::string> const& cmd : arguments.Commands) {
std::vector<const char*> argv(cmd.size() + 1);
std::transform(cmd.begin(), cmd.end(), argv.begin(),
[](std::string const& s) { return s.c_str(); });
argv.back() = nullptr;
cmsysProcess_AddCommand(cp, argv.data());
}
// Set the process working directory.
if (!arguments.WorkingDirectory.empty()) {
cmsysProcess_SetWorkingDirectory(cp, arguments.WorkingDirectory.c_str());
}
// Always hide the process window.
cmsysProcess_SetOption(cp, cmsysProcess_Option_HideWindow, 1);
// Check the output variables.
bool merge_output = false;
if (!arguments.InputFile.empty()) {
cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDIN,
arguments.InputFile.c_str());
}
if (!arguments.OutputFile.empty()) {
cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDOUT,
arguments.OutputFile.c_str());
}
if (!arguments.ErrorFile.empty()) {
if (arguments.ErrorFile == arguments.OutputFile) {
merge_output = true;
} else {
cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDERR,
arguments.ErrorFile.c_str());
}
}
if (!arguments.OutputVariable.empty() &&
arguments.OutputVariable == arguments.ErrorVariable) {
merge_output = true;
}
if (merge_output) {
cmsysProcess_SetOption(cp, cmsysProcess_Option_MergeOutput, 1);
}
// Set the timeout if any.
if (timeout >= 0) {
cmsysProcess_SetTimeout(cp, timeout);
}
bool echo_stdout = false;
bool echo_stderr = false;
bool echo_output_from_variable = true;
std::string echo_output = status.GetMakefile().GetSafeDefinition(
"CMAKE_EXECUTE_PROCESS_COMMAND_ECHO");
if (!arguments.CommandEcho.empty()) {
echo_output_from_variable = false;
echo_output = arguments.CommandEcho;
}
if (!echo_output.empty()) {
if (echo_output == "STDERR") {
echo_stderr = true;
} else if (echo_output == "STDOUT") {
echo_stdout = true;
} else if (echo_output != "NONE") {
std::string error;
if (echo_output_from_variable) {
error = "CMAKE_EXECUTE_PROCESS_COMMAND_ECHO set to '";
} else {
error = " called with '";
}
error += echo_output;
error += "' expected STDERR|STDOUT|NONE";
if (!echo_output_from_variable) {
error += " for COMMAND_ECHO.";
}
status.GetMakefile().IssueMessage(MessageType::FATAL_ERROR, error);
return true;
}
}
if (echo_stdout || echo_stderr) {
std::string command;
for (const auto& cmd : arguments.Commands) {
command += "'";
command += cmJoin(cmd, "' '");
command += "'";
command += "\n";
}
if (echo_stdout) {
std::cout << command;
} else if (echo_stderr) {
std::cerr << command;
}
}
// Start the process.
cmsysProcess_Execute(cp);
// Read the process output.
std::vector<char> tempOutput;
std::vector<char> tempError;
int length;
char* data;
int p;
cmProcessOutput processOutput(
cmProcessOutput::FindEncoding(arguments.Encoding));
std::string strdata;
while ((p = cmsysProcess_WaitForData(cp, &data, &length, nullptr))) {
// Put the output in the right place.
if (p == cmsysProcess_Pipe_STDOUT && !arguments.OutputQuiet) {
if (arguments.OutputVariable.empty() || arguments.EchoOutputVariable) {
processOutput.DecodeText(data, length, strdata, 1);
cmSystemTools::Stdout(strdata);
}
if (!arguments.OutputVariable.empty()) {
cmExecuteProcessCommandAppend(tempOutput, data, length);
}
} else if (p == cmsysProcess_Pipe_STDERR && !arguments.ErrorQuiet) {
if (arguments.ErrorVariable.empty() || arguments.EchoErrorVariable) {
processOutput.DecodeText(data, length, strdata, 2);
cmSystemTools::Stderr(strdata);
}
if (!arguments.ErrorVariable.empty()) {
cmExecuteProcessCommandAppend(tempError, data, length);
}
}
}
if (!arguments.OutputQuiet &&
(arguments.OutputVariable.empty() || arguments.EchoOutputVariable)) {
processOutput.DecodeText(std::string(), strdata, 1);
if (!strdata.empty()) {
cmSystemTools::Stdout(strdata);
}
}
if (!arguments.ErrorQuiet &&
(arguments.ErrorVariable.empty() || arguments.EchoErrorVariable)) {
processOutput.DecodeText(std::string(), strdata, 2);
if (!strdata.empty()) {
cmSystemTools::Stderr(strdata);
}
}
// All output has been read. Wait for the process to exit.
cmsysProcess_WaitForExit(cp, nullptr);
processOutput.DecodeText(tempOutput, tempOutput);
processOutput.DecodeText(tempError, tempError);
// Fix the text in the output strings.
cmExecuteProcessCommandFixText(tempOutput,
arguments.OutputStripTrailingWhitespace);
cmExecuteProcessCommandFixText(tempError,
arguments.ErrorStripTrailingWhitespace);
// Store the output obtained.
if (!arguments.OutputVariable.empty() && !tempOutput.empty()) {
status.GetMakefile().AddDefinition(arguments.OutputVariable,
tempOutput.data());
}
if (!merge_output && !arguments.ErrorVariable.empty() &&
!tempError.empty()) {
status.GetMakefile().AddDefinition(arguments.ErrorVariable,
tempError.data());
}
// Store the result of running the process.
if (!arguments.ResultVariable.empty()) {
switch (cmsysProcess_GetState(cp)) {
case cmsysProcess_State_Exited: {
int v = cmsysProcess_GetExitValue(cp);
char buf[16];
snprintf(buf, sizeof(buf), "%d", v);
status.GetMakefile().AddDefinition(arguments.ResultVariable, buf);
} break;
case cmsysProcess_State_Exception:
status.GetMakefile().AddDefinition(
arguments.ResultVariable, cmsysProcess_GetExceptionString(cp));
break;
case cmsysProcess_State_Error:
status.GetMakefile().AddDefinition(arguments.ResultVariable,
cmsysProcess_GetErrorString(cp));
break;
case cmsysProcess_State_Expired:
status.GetMakefile().AddDefinition(
arguments.ResultVariable, "Process terminated due to timeout");
break;
}
}
// Store the result of running the processes.
if (!arguments.ResultsVariable.empty()) {
switch (cmsysProcess_GetState(cp)) {
case cmsysProcess_State_Exited: {
std::vector<std::string> res;
for (size_t i = 0; i < arguments.Commands.size(); ++i) {
switch (cmsysProcess_GetStateByIndex(cp, static_cast<int>(i))) {
case kwsysProcess_StateByIndex_Exited: {
int exitCode =
cmsysProcess_GetExitValueByIndex(cp, static_cast<int>(i));
char buf[16];
snprintf(buf, sizeof(buf), "%d", exitCode);
res.emplace_back(buf);
} break;
case kwsysProcess_StateByIndex_Exception:
res.emplace_back(cmsysProcess_GetExceptionStringByIndex(
cp, static_cast<int>(i)));
break;
case kwsysProcess_StateByIndex_Error:
default:
res.emplace_back("Error getting the child return code");
break;
}
}
status.GetMakefile().AddDefinition(arguments.ResultsVariable,
cmList::to_string(res));
} break;
case cmsysProcess_State_Exception:
status.GetMakefile().AddDefinition(
arguments.ResultsVariable, cmsysProcess_GetExceptionString(cp));
break;
case cmsysProcess_State_Error:
status.GetMakefile().AddDefinition(arguments.ResultsVariable,
cmsysProcess_GetErrorString(cp));
break;
case cmsysProcess_State_Expired:
status.GetMakefile().AddDefinition(
arguments.ResultsVariable, "Process terminated due to timeout");
break;
}
}
auto queryProcessStatusByIndex = [&cp](int index) -> std::string {
std::string processStatus;
switch (cmsysProcess_GetStateByIndex(cp, static_cast<int>(index))) {
case kwsysProcess_StateByIndex_Exited: {
int exitCode = cmsysProcess_GetExitValueByIndex(cp, index);
if (exitCode) {
processStatus = "Child return code: " + std::to_string(exitCode);
}
} break;
case kwsysProcess_StateByIndex_Exception: {
processStatus = cmStrCat(
"Abnormal exit with child return code: ",
cmsysProcess_GetExceptionStringByIndex(cp, static_cast<int>(index)));
break;
}
case kwsysProcess_StateByIndex_Error:
default:
processStatus = "Error getting the child return code";
break;
}
return processStatus;
};
if (arguments.CommandErrorIsFatal == "ANY"_s) {
bool ret = true;
switch (cmsysProcess_GetState(cp)) {
case cmsysProcess_State_Exited: {
std::map<int, std::string> failureIndices;
for (int i = 0; i < static_cast<int>(arguments.Commands.size()); ++i) {
std::string processStatus = queryProcessStatusByIndex(i);
if (!processStatus.empty()) {
failureIndices[i] = processStatus;
}
if (!failureIndices.empty()) {
std::ostringstream oss;
oss << "failed command indexes:\n";
for (auto const& e : failureIndices) {
oss << " " << e.first + 1 << ": \"" << e.second << "\"\n";
}
status.SetError(oss.str());
ret = false;
}
}
} break;
case cmsysProcess_State_Exception:
status.SetError(
cmStrCat("abnormal exit: ", cmsysProcess_GetExceptionString(cp)));
ret = false;
break;
case cmsysProcess_State_Error:
status.SetError(cmStrCat("error getting child return code: ",
cmsysProcess_GetErrorString(cp)));
ret = false;
break;
case cmsysProcess_State_Expired:
status.SetError("Process terminated due to timeout");
ret = false;
break;
}
if (!ret) {
cmSystemTools::SetFatalErrorOccurred();
return false;
}
}
if (arguments.CommandErrorIsFatal == "LAST"_s) {
bool ret = true;
switch (cmsysProcess_GetState(cp)) {
case cmsysProcess_State_Exited: {
int lastIndex = static_cast<int>(arguments.Commands.size() - 1);
const std::string processStatus = queryProcessStatusByIndex(lastIndex);
if (!processStatus.empty()) {
status.SetError("last command failed");
ret = false;
}
} break;
case cmsysProcess_State_Exception:
status.SetError(
cmStrCat("Abnormal exit: ", cmsysProcess_GetExceptionString(cp)));
ret = false;
break;
case cmsysProcess_State_Error:
status.SetError(cmStrCat("Error getting child return code: ",
cmsysProcess_GetErrorString(cp)));
ret = false;
break;
case cmsysProcess_State_Expired:
status.SetError("Process terminated due to timeout");
ret = false;
break;
}
if (!ret) {
cmSystemTools::SetFatalErrorOccurred();
return false;
}
}
return true;
}
namespace {
void cmExecuteProcessCommandFixText(std::vector<char>& output,
bool strip_trailing_whitespace)
{
// Remove \0 characters and the \r part of \r\n pairs.
unsigned int in_index = 0;
unsigned int out_index = 0;
while (in_index < output.size()) {
char c = output[in_index++];
if ((c != '\r' ||
!(in_index < output.size() && output[in_index] == '\n')) &&
c != '\0') {
output[out_index++] = c;
}
}
// Remove trailing whitespace if requested.
if (strip_trailing_whitespace) {
while (out_index > 0 &&
cmExecuteProcessCommandIsWhitespace(output[out_index - 1])) {
--out_index;
}
}
// Shrink the vector to the size needed.
output.resize(out_index);
// Put a terminator on the text string.
output.push_back('\0');
}
void cmExecuteProcessCommandAppend(std::vector<char>& output, const char* data,
int length)
{
#if defined(__APPLE__)
// HACK on Apple to work around bug with inserting at the
// end of an empty vector. This resulted in random failures
// that were hard to reproduce.
if (output.empty() && length > 0) {
output.push_back(data[0]);
++data;
--length;
}
#endif
cm::append(output, data, data + length);
}
}