From ad8d4ad25ce0781995edcac9d25dce7a739b8315 Mon Sep 17 00:00:00 2001 From: Sara Golemon Date: Mon, 13 Aug 2018 19:58:28 +0000 Subject: SERVER-36614 Add handling of __exec config expansions --- jstests/libs/python.js | 19 ++ .../noPassthrough/configExpand_exec_noexpand.js | 27 ++ .../noPassthrough/configExpand_exec_timeeout.js | 31 +++ jstests/noPassthrough/configExpand_exec_values.js | 28 ++ .../noPassthrough/configExpand_exec_wholeconfig.js | 14 + jstests/noPassthrough/libs/configExpand/lib.js | 42 ++- jstests/noPassthrough/libs/configExpand/reflect.py | 29 ++ src/mongo/SConscript | 1 + src/mongo/db/server_options_server_helpers.cpp | 2 +- src/mongo/util/options_parser/options_parser.cpp | 93 ++++--- src/mongo/util/options_parser/options_parser.h | 1 + src/mongo/util/shell_exec.cpp | 296 +++++++++++++++++++++ src/mongo/util/shell_exec.h | 44 +++ 13 files changed, 583 insertions(+), 44 deletions(-) create mode 100644 jstests/libs/python.js create mode 100644 jstests/noPassthrough/configExpand_exec_noexpand.js create mode 100644 jstests/noPassthrough/configExpand_exec_timeeout.js create mode 100644 jstests/noPassthrough/configExpand_exec_values.js create mode 100644 jstests/noPassthrough/configExpand_exec_wholeconfig.js create mode 100644 jstests/noPassthrough/libs/configExpand/reflect.py create mode 100644 src/mongo/util/shell_exec.cpp create mode 100644 src/mongo/util/shell_exec.h diff --git a/jstests/libs/python.js b/jstests/libs/python.js new file mode 100644 index 00000000000..d220bda0c08 --- /dev/null +++ b/jstests/libs/python.js @@ -0,0 +1,19 @@ +// Helper for finding the local python binary. + +function getPython3Binary() { + 'use strict'; + + let cmd = '/opt/mongodbtoolchain/v2/bin/python3'; + if (_isWindows()) { + const paths = ["c:/python36/python.exe", "c:/python/python36/python.exe"]; + for (let p of paths) { + if (fileExists(p)) { + cmd = p; + break; + } + } + } + + assert(fileExists(cmd), "Python3 interpreter not found"); + return cmd; +} diff --git a/jstests/noPassthrough/configExpand_exec_noexpand.js b/jstests/noPassthrough/configExpand_exec_noexpand.js new file mode 100644 index 00000000000..03e147f036a --- /dev/null +++ b/jstests/noPassthrough/configExpand_exec_noexpand.js @@ -0,0 +1,27 @@ +// Test config file expansion using EXEC. + +(function() { + 'use strict'; + + load('jstests/noPassthrough/libs/configExpand/lib.js'); + + // Unexpected elements. + configExpandFailure({ + setParameter: { + scramIterationCount: {__exec: makeReflectionCmd('12345'), foo: 'bar'}, + } + }, + /expansion block must contain only '__exec'/); + + const sicReflect = {setParameter: {scramIterationCount: {__exec: makeReflectionCmd('12345')}}}; + + // Positive test just to be sure this works in a basic case before testing negatives. + configExpandSuccess(sicReflect); + + // Expansion not enabled. + configExpandFailure(sicReflect, /__exec support has not been enabled/, {configExpand: 'none'}); + + // Expansion enabled, but not recursively. + configExpandFailure({__exec: makeReflectionCmd(jsToYaml(sicReflect)), type: 'yaml'}, + /__exec support has not been enabled/); +})(); diff --git a/jstests/noPassthrough/configExpand_exec_timeeout.js b/jstests/noPassthrough/configExpand_exec_timeeout.js new file mode 100644 index 00000000000..7434790fc3f --- /dev/null +++ b/jstests/noPassthrough/configExpand_exec_timeeout.js @@ -0,0 +1,31 @@ +// Test config file expansion using EXEC. + +(function() { + 'use strict'; + + load('jstests/noPassthrough/libs/configExpand/lib.js'); + + // Sleep 10 seconds during request. + configExpandSuccess({ + setParameter: { + scramIterationCount: {__exec: makeReflectionCmd('12345', {sleep: 10})}, + } + }); + + // Sleep 40 seconds during request, with default 30 second timeout. + configExpandFailure({ + setParameter: { + scramIterationCount: {__exec: makeReflectionCmd('12345', {sleep: 40})}, + } + }, + /Timeout expired/); + + // Sleep 10 seconds during request, with custom 5 second timeout. + configExpandFailure({ + setParameter: { + scramIterationCount: {__exec: makeReflectionCmd('12345', {sleep: 10})}, + } + }, + /Timeout expired/, + {configExpandTimeoutSecs: 5}); +})(); diff --git a/jstests/noPassthrough/configExpand_exec_values.js b/jstests/noPassthrough/configExpand_exec_values.js new file mode 100644 index 00000000000..21b9e493ea1 --- /dev/null +++ b/jstests/noPassthrough/configExpand_exec_values.js @@ -0,0 +1,28 @@ +// Test config file expansion using EXEC. + +(function() { + 'use strict'; + + load('jstests/noPassthrough/libs/configExpand/lib.js'); + + // Basic success case + configExpandSuccess( + { + setParameter: { + scramIterationCount: {__exec: makeReflectionCmd('12345')}, + scramSHA256IterationCount: + {__exec: makeReflectionCmd("23456\n"), type: 'string', trim: 'whitespace'} + } + }, + function(admin) { + const response = assert.commandWorked(admin.runCommand( + {getParameter: 1, scramIterationCount: 1, scramSHA256IterationCount: 1})); + assert.eq(response.scramIterationCount, + 12345, + "Incorrect derived config value for scramIterationCount"); + assert.eq(response.scramSHA256IterationCount, + 23456, + "Incorrect derived config value scramSHA256IterationCount"); + }); + +})(); diff --git a/jstests/noPassthrough/configExpand_exec_wholeconfig.js b/jstests/noPassthrough/configExpand_exec_wholeconfig.js new file mode 100644 index 00000000000..9fac3848271 --- /dev/null +++ b/jstests/noPassthrough/configExpand_exec_wholeconfig.js @@ -0,0 +1,14 @@ +// Test config file expansion using EXEC at top level. + +(function() { + 'use strict'; + + load('jstests/noPassthrough/libs/configExpand/lib.js'); + + const yamlConfig = jsToYaml({setParameter: {scramIterationCount: 12345}}); + configExpandSuccess({__exec: makeReflectionCmd(yamlConfig), type: 'yaml'}, function(admin) { + const response = + assert.commandWorked(admin.runCommand({getParameter: 1, scramIterationCount: 1})); + assert.eq(response.scramIterationCount, 12345, "Incorrect derived config value"); + }); +})(); diff --git a/jstests/noPassthrough/libs/configExpand/lib.js b/jstests/noPassthrough/libs/configExpand/lib.js index e2b98bce554..96dcfa4049d 100644 --- a/jstests/noPassthrough/libs/configExpand/lib.js +++ b/jstests/noPassthrough/libs/configExpand/lib.js @@ -7,18 +7,10 @@ class ConfigExpandRestServer { * Create a new webserver. */ constructor() { - this.python = "/opt/mongodbtoolchain/v2/bin/python3"; - - if (_isWindows()) { - const paths = ["c:\\python36\\python.exe", "c:\\python\\python36\\python.exe"]; - for (let p of paths) { - if (fileExists(p)) { - this.python = p; - } - } - } - + load('jstests/libs/python.js'); + this.python = getPython3Binary(); print("Using python interpreter: " + this.python); + this.web_server_py = "jstests/noPassthrough/libs/configExpand/rest_server.py"; this.pid = undefined; @@ -88,6 +80,30 @@ class ConfigExpandRestServer { } } +function makeReflectionCmd(arg, opts = {}) { + 'use strict'; + + load('jstests/libs/python.js'); + let cmd = getPython3Binary(); + if (_isWindows()) { + cmd = '"' + cmd + '"'; + } + cmd += ' jstests/noPassthrough/libs/configExpand/reflect.py'; + + if (opts.sleep && (opts.sleep > 0)) { + cmd += ' -s ' + Number(opts.sleep); + } + + // Escape arguments to the shell by wrapping in OS appropriate quotes. + if (_isWindows()) { + cmd += ' ' + arg.split('"').map(v => '"' + v + '"').join('\\"'); + } else { + cmd += ' ' + arg.split("'").map(v => "'" + v + "'").join("\\'"); + } + + return cmd; +} + function jsToYaml(config, toplevel = true) { if (typeof config === 'object') { if (Array.isArray(config)) { @@ -124,7 +140,7 @@ function configExpandSuccess(config, test = null, opts = {}) { writeFile(configFile, jsToYaml(config)); const mongod = MongoRunner.runMongod(Object.assign({ - configExpand: 'rest', + configExpand: 'rest,exec', config: configFile, }, opts)); @@ -143,7 +159,7 @@ function configExpandFailure(config, test = null, opts = {}) { writeFile(configFile, jsToYaml(config)); const options = Object.assign({ - configExpand: 'rest', + configExpand: 'rest,exec', config: configFile, port: allocatePort(), }, diff --git a/jstests/noPassthrough/libs/configExpand/reflect.py b/jstests/noPassthrough/libs/configExpand/reflect.py new file mode 100644 index 00000000000..d4e0f9a88d4 --- /dev/null +++ b/jstests/noPassthrough/libs/configExpand/reflect.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python3 +"""Simple reflection script. + Sends argument back as provided. + Optionally sleeps for `--sleep` seconds.""" + +import argparse +import sys +import time + +def main(): + """Main Method.""" + + parser = argparse.ArgumentParser(description='MongoDB Mock Config Expandsion EXEC Endpoint.') + parser.add_argument('-s', '--sleep', type=int, default=0, help="Add artificial delay for timeout testing") + parser.add_argument('value', type=str, help="Content to reflect to stdout") + + args = parser.parse_args() + + if args.sleep > 0: + try: + time.sleep(args.sleep) + except BrokenPipeError: + # Let our caller kill us while we sleep + pass + + sys.stdout.write(args.value) + +if __name__ == '__main__': + main() diff --git a/src/mongo/SConscript b/src/mongo/SConscript index dca2d87d1be..2021798f780 100644 --- a/src/mongo/SConscript +++ b/src/mongo/SConscript @@ -123,6 +123,7 @@ env.Library( 'util/itoa.cpp', 'util/log.cpp', 'util/platform_init.cpp', + 'util/shell_exec.cpp', 'util/signal_handlers_synchronous.cpp', 'util/stacktrace.cpp', 'util/stacktrace_${TARGET_OS_FAMILY}.cpp', diff --git a/src/mongo/db/server_options_server_helpers.cpp b/src/mongo/db/server_options_server_helpers.cpp index 6ba968ad126..c5d071b78a7 100644 --- a/src/mongo/db/server_options_server_helpers.cpp +++ b/src/mongo/db/server_options_server_helpers.cpp @@ -101,7 +101,7 @@ Status addGeneralServerOptions(moe::OptionSection* options) { ->addOptionChaining("configExpand", "configExpand", moe::String, - "Process expansion directives in config file (none, rest)") + "Process expansion directives in config file (none, exec, rest)") .setSources(moe::SourceCommandLine); options diff --git a/src/mongo/util/options_parser/options_parser.cpp b/src/mongo/util/options_parser/options_parser.cpp index 75c27f2c4ae..61692acc396 100644 --- a/src/mongo/util/options_parser/options_parser.cpp +++ b/src/mongo/util/options_parser/options_parser.cpp @@ -51,6 +51,7 @@ #include "mongo/util/options_parser/option_description.h" #include "mongo/util/options_parser/option_section.h" #include "mongo/util/scopeguard.h" +#include "mongo/util/shell_exec.h" #include "mongo/util/text.h" namespace mongo { @@ -245,6 +246,7 @@ bool OptionIsStringMap(const std::vector& options_vector, con Status parseYAMLConfigFile(const std::string&, YAML::Node*); /* Searches a YAML node for configuration expansion directives such as: * __rest: https://example.com/path?query=val + * __exec: '/usr/bin/getConfig param' * * and optionally the fields `type` and `trim`. * @@ -276,39 +278,53 @@ public: } const auto getStringField = [&node, &prefix](const std::string& fieldName, - bool allowed) -> StatusWith { + bool allowed) -> boost::optional { try { const auto& strField = node[fieldName]; if (!strField.IsDefined()) { - return {ErrorCodes::NoSuchKey, "Not a configuration expansion block"}; - } - if (!allowed) { - return {ErrorCodes::BadValue, - str::stream() << prefix << fieldName - << " support has not been enabled"}; - } - if (!strField.IsScalar()) { - return {ErrorCodes::BadValue, - str::stream() << prefix << fieldName << " must be a string"}; + return {boost::none}; } + uassert(ErrorCodes::BadValue, + str::stream() << prefix << fieldName << " support has not been enabled", + allowed); + uassert(ErrorCodes::BadValue, + str::stream() << prefix << fieldName << " must be a string", + strField.IsScalar()); return strField.Scalar(); } catch (const YAML::InvalidNode&) { // Not this kind of expansion block. - return {ErrorCodes::NoSuchKey, "Not a configuration expansion block"}; + return {boost::none}; } }; _expansion = ExpansionType::kRest; - _action = uassertStatusOK(getStringField("__rest", configExpand.rest)); + auto optRestAction = getStringField("__rest", configExpand.rest); + auto optExecAction = getStringField("__exec", configExpand.exec); + uassert(ErrorCodes::NoSuchKey, + "Neither __exec nor __rest specified for config expansion", + optRestAction || optExecAction); + uassert(ErrorCodes::BadValue, + "Must not specify both __rest and __exec in a single config expansion", + !optRestAction || !optExecAction); + + if (optRestAction) { + invariant(!optExecAction); + _expansion = ExpansionType::kRest; + _action = std::move(*optRestAction); + } else { + invariant(optExecAction); + _expansion = ExpansionType::kExec; + _action = std::move(*optExecAction); + } // Parse optional fields, keeping track of how many we've handled. // If there are additional (unknown) fields beyond that, raise an error. size_t numVisitedFields = 1; - auto swType = getStringField("type", true); - if (swType.isOK()) { + auto optType = getStringField("type", true); + if (optType) { ++numVisitedFields; - auto typeField = std::move(swType.getValue()); + auto typeField = std::move(*optType); if (typeField == "string") { _type = ContentType::kString; } else if (typeField == "yaml") { @@ -317,14 +333,12 @@ public: uasserted(ErrorCodes::BadValue, str::stream() << prefix << "type must be either 'string' or 'yaml'"); } - } else if (swType.getStatus().code() != ErrorCodes::NoSuchKey) { - uassertStatusOK(swType.getStatus()); - } // else not specified, assume string. + } - auto swTrim = getStringField("trim", true); - if (swTrim.isOK()) { + auto optTrim = getStringField("trim", true); + if (optTrim) { ++numVisitedFields; - auto trimField = std::move(swTrim.getValue()); + auto trimField = std::move(*optTrim); if (trimField == "none") { _trim = Trim::kNone; } else if (trimField == "whitespace") { @@ -333,9 +347,7 @@ public: uasserted(ErrorCodes::BadValue, str::stream() << prefix << "trim must be either 'whitespace' or 'none'"); } - } else if (swTrim.getStatus().code() != ErrorCodes::NoSuchKey) { - uassertStatusOK(swTrim.getStatus()); - } // else not specified, assume none. + } uassert(ErrorCodes::BadValue, str::stream() << nodeName << " expansion block must contain only '" @@ -349,11 +361,15 @@ public: } std::string getExpansionName() const { - return "__rest"; + return isRestExpansion() ? "__rest" : "__exec"; } bool isRestExpansion() const { - return true; + return _expansion == ExpansionType::kRest; + } + + bool isExecExpansion() const { + return _expansion == ExpansionType::kExec; } std::string getAction() const { @@ -401,6 +417,7 @@ private: // The type of expansion represented. enum class ExpansionType { kRest, + kExec, }; ExpansionType _expansion = ExpansionType::kRest; @@ -458,6 +475,9 @@ std::string runYAMLRestExpansion(StringData url, Seconds timeout) { * If a __rest configuration expansion directive is found, * mongo::HttpClient will be invoked to fetch the resource via GET request. * + * If an __exec configuration expansion directive is found, + * mongo::shellExec() will be invoked to execute the process. + * * See the comment for class ConfigExpandNode for more details. */ StatusWith runYAMLExpansion(const YAML::Node& node, @@ -478,10 +498,21 @@ StatusWith runYAMLExpansion(const YAML::Node& node, const auto action = expansion.getAction(); LOG(2) << prefix << expansion.getExpansionName() << ": " << action; - invariant(expansion.isRestExpansion()); - auto output = runYAMLRestExpansion(action, configExpand.timeout); + if (expansion.isRestExpansion()) { + return expansion.process(runYAMLRestExpansion(action, configExpand.timeout)); + } + + invariant(expansion.isExecExpansion()); + // Hard-cap shell expansion at 128MB + const size_t kShellExpandMaxLenBytes = 128 << 20; + auto swOutput = shellExec(action, configExpand.timeout, kShellExpandMaxLenBytes); + if (!swOutput.isOK()) { + return {ErrorCodes::OperationFailed, + str::stream() << "Failed expanding __exec section: " + << swOutput.getStatus().reason()}; + } + return expansion.process(std::move(swOutput.getValue())); - return expansion.process(std::move(output)); } catch (...) { return exceptionToStatus(); } @@ -1515,6 +1546,8 @@ StatusWith parseConfigExpand(const Environment& cli } if (elem == "rest") { ret.rest = true; + } else if (elem == "exec") { + ret.exec = true; } else { return {ErrorCodes::BadValue, str::stream() << "Invalid value for --configExpand: '" << elem << "'"}; diff --git a/src/mongo/util/options_parser/options_parser.h b/src/mongo/util/options_parser/options_parser.h index e75bc5454a6..d6bf2032919 100644 --- a/src/mongo/util/options_parser/options_parser.h +++ b/src/mongo/util/options_parser/options_parser.h @@ -128,6 +128,7 @@ public: */ struct ConfigExpand { bool rest = false; + bool exec = false; Seconds timeout = kDefaultConfigExpandTimeout; }; diff --git a/src/mongo/util/shell_exec.cpp b/src/mongo/util/shell_exec.cpp new file mode 100644 index 00000000000..9e5e6344252 --- /dev/null +++ b/src/mongo/util/shell_exec.cpp @@ -0,0 +1,296 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/util/shell_exec.h" + +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +#include "mongo/util/errno_util.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/text.h" +#include "mongo/util/time_support.h" + +namespace mongo { +namespace { +constexpr size_t kExecBufferSizeBytes = 1024; + +#ifdef _WIN32 +class ProcessStream { +public: + ProcessStream(const std::string& cmd) { + ZeroMemory(&_startup, sizeof(_startup)); + ZeroMemory(&_process, sizeof(_process)); + _startup.cb = sizeof(_startup); + _startup.dwFlags = STARTF_USESTDHANDLES; + + SECURITY_ATTRIBUTES sa; + ZeroMemory(&sa, sizeof(sa)); + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = nullptr; + sa.bInheritHandle = true; + + // Close our end of stdin immediately to signal child we have no data. + HANDLE dummy = nullptr; + uassert(ErrorCodes::OperationFailed, + str::stream() << "Unable to create stdin pipe for subprocess: " + << errnoWithDescription(), + CreatePipe(&dummy, &_startup.hStdInput, &sa, kExecBufferSizeBytes)); + CloseHandle(dummy); + + uassert(ErrorCodes::OperationFailed, + str::stream() << "Unable to create stdout pipe for subprocess: " + << errnoWithDescription(), + CreatePipe(&_stdout, &_startup.hStdOutput, &sa, kExecBufferSizeBytes)); + + uassert(ErrorCodes::OperationFailed, + str::stream() << "Unable to create stderr pipe for subprocess: " + << errnoWithDescription(), + CreatePipe(&_stderr, &_startup.hStdError, &sa, kExecBufferSizeBytes)); + + DWORD mode = PIPE_NOWAIT; + uassert(ErrorCodes::OperationFailed, + str::stream() << "Unable to set non-blocking for subprocess: " + << errnoWithDescription(), + SetNamedPipeHandleState(_stdout, &mode, nullptr, nullptr)); + + auto wideCmd = toWideString(cmd.c_str()); + uassert(ErrorCodes::OperationFailed, + str::stream() << "Unable to launch command: " << errnoWithDescription(), + CreateProcessW(nullptr, + const_cast(wideCmd.c_str()), + &sa, + &sa, + true, + CREATE_NO_WINDOW, + nullptr, + nullptr, + &_startup, + &_process)); + } + + int close() { + if (_exitcode != STILL_ACTIVE) { + return _exitcode; + } + + uassert(ErrorCodes::OperationFailed, + str::stream() << "Failed retreiving exit code from subprocess: " + << errnoWithDescription(), + GetExitCodeProcess(_process.hProcess, &_exitcode)); + + if (_exitcode == STILL_ACTIVE) { + uassert(ErrorCodes::OperationFailed, + str::stream() << "Failed terminating subprocess: " << errnoWithDescription(), + TerminateProcess(_process.hProcess, 1)); + _exitcode = 1; + } + + return _exitcode; + } + + bool eof() { + if (_exitcode != STILL_ACTIVE) { + return true; + } + + uassert(ErrorCodes::OperationFailed, + str::stream() << "Failed retreiving status of subprocess: " + << errnoWithDescription(), + GetExitCodeProcess(_process.hProcess, &_exitcode)); + + return _exitcode != STILL_ACTIVE; + } + + Status wait(Milliseconds duration) { + auto ret = WaitForSingleObject(_process.hProcess, durationCount(duration)); + if (ret == WAIT_OBJECT_0) { + return Status::OK(); + } else if (ret == WAIT_TIMEOUT) { + return {ErrorCodes::OperationFailed, "Timeout expired"}; + } else { + return {ErrorCodes::OperationFailed, errnoWithDescription()}; + } + } + + void read(StringBuilder& sb, size_t len) { + constexpr DWORD kPipeReadyTimeoutMS = 10; + if (!_stdout || (WAIT_OBJECT_0 != WaitForSingleObject(_stdout, kPipeReadyTimeoutMS))) { + return; + } + + char buf[kExecBufferSizeBytes]; + DWORD read = 0; + uassert(ErrorCodes::OperationFailed, + str::stream() << "Failed reading from subprocess: " << errnoWithDescription(), + ReadFile(_stdout, buf, std::min(sizeof(buf), len), &read, nullptr)); + + if (read == 0) { + CloseHandle(_stdout); + _stdout = nullptr; + } else { + sb << StringData(buf, read); + } + } + + ~ProcessStream() { + if (_startup.hStdInput) { + CloseHandle(_startup.hStdInput); + } + if (_startup.hStdOutput) { + CloseHandle(_startup.hStdOutput); + } + if (_startup.hStdError) { + CloseHandle(_startup.hStdError); + } + if (_stdout) { + CloseHandle(_stdout); + } + if (_stderr) { + CloseHandle(_stderr); + } + if (_process.hProcess) { + CloseHandle(_process.hProcess); + } + if (_process.hThread) { + CloseHandle(_process.hThread); + } + } + +private: + ProcessStream() = delete; + + HANDLE _stdout, _stderr; + STARTUPINFO _startup; + PROCESS_INFORMATION _process; + DWORD _exitcode = STILL_ACTIVE; +}; +#else +class ProcessStream { +public: + ProcessStream(const std::string& cmd) { + _fp = ::popen(cmd.c_str(), "r"); + uassert(ErrorCodes::OperationFailed, + str::stream() << "Unable to launch command: " << errnoWithDescription(), + _fp); + _fd = fileno(_fp); + } + + int close() { + if (!_fp) { + return _exitcode; + } + _exitcode = ::pclose(_fp); + _fp = nullptr; + return _exitcode; + } + + bool eof() { + return feof(_fp); + } + + Status wait(Milliseconds duration) { + struct pollfd fds; + fds.fd = _fd; + fds.events = POLLIN | POLLHUP; + fds.revents = 0; + + auto ret = poll(&fds, 1, durationCount(duration)); + if (ret < 0) { + return {ErrorCodes::OperationFailed, errnoWithDescription()}; + } else if (ret == 0) { + return {ErrorCodes::OperationFailed, "Timeout expired"}; + } else { + return Status::OK(); + } + } + + void read(StringBuilder& sb, size_t len) { + char buf[kExecBufferSizeBytes]; + len = fread(buf, 1, std::min(sizeof(buf), len), _fp); + sb << StringData(buf, len); + } + + ~ProcessStream() { + if (_fp) { + ::pclose(_fp); + } + } + +private: + ProcessStream() = delete; + + FILE* _fp; + int _fd; + int _exitcode = 1; +}; +#endif +} // namespace +} // namespace mongo +mongo::StatusWith mongo::shellExec(const std::string& cmd, + Milliseconds timeout, + size_t maxlen) try { + if (durationCount(timeout) <= 0) { + return {ErrorCodes::OperationFailed, str::stream() << "Invalid timeout: " << timeout}; + } + auto end = Date_t::now() + timeout; + + ProcessStream process(cmd); + StringBuilder sb; + while (!process.eof()) { + auto status = process.wait(end - Date_t::now()); + if (!status.isOK()) { + return status; + } + + process.read(sb, maxlen - sb.len()); + if (static_cast(sb.len()) >= maxlen) { + // Truncate at maxlen + break; + } + } + + auto exitcode = process.close(); + if (exitcode) { + return {ErrorCodes::OperationFailed, + str::stream() << "Process returned non-zero exit code: " << exitcode}; + } + + return sb.str(); +} catch (...) { + return mongo::exceptionToStatus(); +} diff --git a/src/mongo/util/shell_exec.h b/src/mongo/util/shell_exec.h new file mode 100644 index 00000000000..31e9344e110 --- /dev/null +++ b/src/mongo/util/shell_exec.h @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include + +#include "mongo/base/status_with.h" +#include "mongo/util/duration.h" + +namespace mongo { + +/** + * Execute a shell command and return its output. + * Returns CommandExecutionFailure on non-zero exit code. + */ +StatusWith shellExec(const std::string&, Milliseconds timeout, size_t maxlen); + +} // namespace mongo -- cgit v1.2.1