summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSara Golemon <sara.golemon@mongodb.com>2018-08-13 19:58:28 +0000
committerSara Golemon <sara.golemon@mongodb.com>2018-09-18 12:53:49 +0000
commitad8d4ad25ce0781995edcac9d25dce7a739b8315 (patch)
treea6b6f28849a10a6acc46fb7c8e537666a6ce4e92
parent790cdfebf736064d3ab1368ce3a5c9bde71ef7c0 (diff)
downloadmongo-ad8d4ad25ce0781995edcac9d25dce7a739b8315.tar.gz
SERVER-36614 Add handling of __exec config expansions
-rw-r--r--jstests/libs/python.js19
-rw-r--r--jstests/noPassthrough/configExpand_exec_noexpand.js27
-rw-r--r--jstests/noPassthrough/configExpand_exec_timeeout.js31
-rw-r--r--jstests/noPassthrough/configExpand_exec_values.js28
-rw-r--r--jstests/noPassthrough/configExpand_exec_wholeconfig.js14
-rw-r--r--jstests/noPassthrough/libs/configExpand/lib.js42
-rw-r--r--jstests/noPassthrough/libs/configExpand/reflect.py29
-rw-r--r--src/mongo/SConscript1
-rw-r--r--src/mongo/db/server_options_server_helpers.cpp2
-rw-r--r--src/mongo/util/options_parser/options_parser.cpp93
-rw-r--r--src/mongo/util/options_parser/options_parser.h1
-rw-r--r--src/mongo/util/shell_exec.cpp296
-rw-r--r--src/mongo/util/shell_exec.h44
13 files changed, 583 insertions, 44 deletions
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<OptionDescription>& 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<std::string> {
+ bool allowed) -> boost::optional<std::string> {
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<YAML::Node> runYAMLExpansion(const YAML::Node& node,
@@ -478,10 +498,21 @@ StatusWith<YAML::Node> 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<OptionsParser::ConfigExpand> 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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 <memory>
+
+#ifdef _WIN32
+#include <processthreadsapi.h>
+#include <synchapi.h>
+#else
+#include <poll.h>
+#include <stdio.h>
+#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<wchar_t*>(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<Milliseconds>(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<size_t>(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<Milliseconds>(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<size_t>(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<std::string> mongo::shellExec(const std::string& cmd,
+ Milliseconds timeout,
+ size_t maxlen) try {
+ if (durationCount<Milliseconds>(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<size_t>(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 <http://www.gnu.org/licenses/>.
+ *
+ * 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 <string>
+
+#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<std::string> shellExec(const std::string&, Milliseconds timeout, size_t maxlen);
+
+} // namespace mongo