2226 lines
71 KiB
C++
2226 lines
71 KiB
C++
/*
|
|
* fdbcli.actor.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "boost/lexical_cast.hpp"
|
|
#include "fmt/format.h"
|
|
#include "fdbclient/ClusterConnectionFile.h"
|
|
#include "fdbclient/NativeAPI.actor.h"
|
|
#include "fdbclient/FDBTypes.h"
|
|
#include "fdbclient/IClientApi.h"
|
|
#include "fdbclient/MultiVersionTransaction.h"
|
|
#include "fdbclient/Status.h"
|
|
#include "fdbclient/KeyBackedTypes.h"
|
|
#include "fdbclient/StatusClient.h"
|
|
#include "fdbclient/DatabaseContext.h"
|
|
#include "fdbclient/GlobalConfig.actor.h"
|
|
#include "fdbclient/IKnobCollection.h"
|
|
#include "fdbclient/NativeAPI.actor.h"
|
|
#include "fdbclient/ClusterInterface.h"
|
|
#include "fdbclient/ManagementAPI.actor.h"
|
|
#include "fdbclient/Schemas.h"
|
|
#include "fdbclient/CoordinationInterface.h"
|
|
#include "fdbclient/FDBOptions.g.h"
|
|
#include "fdbclient/SystemData.h"
|
|
#include "fdbclient/TagThrottle.actor.h"
|
|
#include "fdbclient/TenantManagement.actor.h"
|
|
#include "fdbclient/Tuple.h"
|
|
|
|
#include "fdbclient/ThreadSafeTransaction.h"
|
|
#include "flow/flow.h"
|
|
#include "flow/ApiVersion.h"
|
|
#include "flow/ArgParseUtil.h"
|
|
#include "flow/DeterministicRandom.h"
|
|
#include "flow/FastRef.h"
|
|
#include "flow/Platform.h"
|
|
#include "flow/SystemMonitor.h"
|
|
#include "flow/CodeProbe.h"
|
|
|
|
#include "flow/TLSConfig.actor.h"
|
|
#include "flow/ThreadHelper.actor.h"
|
|
#include "SimpleOpt/SimpleOpt.h"
|
|
|
|
#include "fdbcli/FlowLineNoise.h"
|
|
#include "fdbcli/fdbcli.actor.h"
|
|
|
|
#include <cinttypes>
|
|
#include <cstdio>
|
|
#include <type_traits>
|
|
#include <signal.h>
|
|
|
|
#ifdef __unixish__
|
|
#include <stdio.h>
|
|
#include "linenoise/linenoise.h"
|
|
#endif
|
|
|
|
#include "fdbclient/versions.h"
|
|
#include "fdbclient/BuildFlags.h"
|
|
|
|
#include "flow/actorcompiler.h" // This must be the last #include.
|
|
|
|
/*
|
|
* While we could just use the MultiVersionApi instance directly, this #define allows us to swap in any other IClientApi
|
|
* instance (e.g. from ThreadSafeApi)
|
|
*/
|
|
#define API ((IClientApi*)MultiVersionApi::api)
|
|
|
|
extern const char* getSourceVersion();
|
|
|
|
std::vector<std::string> validOptions;
|
|
|
|
enum {
|
|
OPT_CONNFILE,
|
|
OPT_DATABASE,
|
|
OPT_HELP,
|
|
OPT_TRACE,
|
|
OPT_TRACE_DIR,
|
|
OPT_LOGGROUP,
|
|
OPT_TIMEOUT,
|
|
OPT_EXEC,
|
|
OPT_NO_STATUS,
|
|
OPT_NO_HINTS,
|
|
OPT_STATUS_FROM_JSON,
|
|
OPT_VERSION,
|
|
OPT_BUILD_FLAGS,
|
|
OPT_TRACE_FORMAT,
|
|
OPT_KNOB,
|
|
OPT_DEBUG_TLS,
|
|
OPT_API_VERSION,
|
|
OPT_MEMORY,
|
|
OPT_USE_FUTURE_PROTOCOL_VERSION
|
|
};
|
|
|
|
CSimpleOpt::SOption g_rgOptions[] = { { OPT_CONNFILE, "-C", SO_REQ_SEP },
|
|
{ OPT_CONNFILE, "--cluster-file", SO_REQ_SEP },
|
|
{ OPT_DATABASE, "-d", SO_REQ_SEP },
|
|
{ OPT_TRACE, "--log", SO_NONE },
|
|
{ OPT_TRACE_DIR, "--log-dir", SO_REQ_SEP },
|
|
{ OPT_LOGGROUP, "--log-group", SO_REQ_SEP },
|
|
{ OPT_TIMEOUT, "--timeout", SO_REQ_SEP },
|
|
{ OPT_EXEC, "--exec", SO_REQ_SEP },
|
|
{ OPT_NO_STATUS, "--no-status", SO_NONE },
|
|
{ OPT_NO_HINTS, "--no-hints", SO_NONE },
|
|
{ OPT_HELP, "-?", SO_NONE },
|
|
{ OPT_HELP, "-h", SO_NONE },
|
|
{ OPT_HELP, "--help", SO_NONE },
|
|
{ OPT_STATUS_FROM_JSON, "--status-from-json", SO_REQ_SEP },
|
|
{ OPT_VERSION, "--version", SO_NONE },
|
|
{ OPT_VERSION, "-v", SO_NONE },
|
|
{ OPT_BUILD_FLAGS, "--build-flags", SO_NONE },
|
|
{ OPT_TRACE_FORMAT, "--trace-format", SO_REQ_SEP },
|
|
{ OPT_KNOB, "--knob-", SO_REQ_SEP },
|
|
{ OPT_DEBUG_TLS, "--debug-tls", SO_NONE },
|
|
{ OPT_API_VERSION, "--api-version", SO_REQ_SEP },
|
|
{ OPT_MEMORY, "--memory", SO_REQ_SEP },
|
|
{ OPT_USE_FUTURE_PROTOCOL_VERSION, "--use-future-protocol-version", SO_NONE },
|
|
TLS_OPTION_FLAGS,
|
|
SO_END_OF_OPTIONS };
|
|
|
|
void printAtCol(const char* text, int col, FILE* stream = stdout) {
|
|
const char* iter = text;
|
|
const char* start = text;
|
|
const char* space = nullptr;
|
|
|
|
do {
|
|
iter++;
|
|
if (*iter == '\n' || *iter == ' ' || *iter == '\0')
|
|
space = iter;
|
|
if (*iter == '\n' || *iter == '\0' || (iter - start == col)) {
|
|
if (!space)
|
|
space = iter;
|
|
fprintf(stream, "%.*s\n", (int)(space - start), start);
|
|
start = space;
|
|
if (*start == ' ' || *start == '\n')
|
|
start++;
|
|
space = nullptr;
|
|
}
|
|
} while (*iter);
|
|
}
|
|
|
|
class FdbOptions {
|
|
public:
|
|
// Prints an error and throws invalid_option or invalid_option_value if the option could not be set
|
|
void setOption(Reference<ITransaction> tr,
|
|
StringRef optionStr,
|
|
bool enabled,
|
|
Optional<StringRef> arg,
|
|
bool intrans) {
|
|
auto transactionItr = transactionOptions.legalOptions.find(optionStr.toString());
|
|
if (transactionItr != transactionOptions.legalOptions.end())
|
|
setTransactionOption(tr, transactionItr->second, enabled, arg, intrans);
|
|
else {
|
|
fprintf(stderr,
|
|
"ERROR: invalid option '%s'. Try `help options' for a list of available options.\n",
|
|
optionStr.toString().c_str());
|
|
throw invalid_option();
|
|
}
|
|
}
|
|
|
|
// Applies all enabled transaction options to the given transaction
|
|
void apply(Reference<ITransaction> tr) {
|
|
for (const auto& [name, value] : transactionOptions.options) {
|
|
tr->setOption(name, value.castTo<StringRef>());
|
|
}
|
|
}
|
|
|
|
// Returns true if any options have been set
|
|
bool hasAnyOptionsEnabled() const { return !transactionOptions.options.empty(); }
|
|
|
|
// Prints a list of enabled options, along with their parameters (if any)
|
|
void print() const {
|
|
bool found = false;
|
|
found = found || transactionOptions.print();
|
|
|
|
if (!found)
|
|
printf("There are no options enabled\n");
|
|
}
|
|
|
|
// Returns a vector of the names of all documented options
|
|
std::vector<std::string> getValidOptions() const { return transactionOptions.getValidOptions(); }
|
|
|
|
// Prints the help string obtained by invoking `help options'
|
|
void printHelpString() const { transactionOptions.printHelpString(); }
|
|
|
|
private:
|
|
// Sets a transaction option. If intrans == true, then this option is also applied to the passed in transaction.
|
|
void setTransactionOption(Reference<ITransaction> tr,
|
|
FDBTransactionOptions::Option option,
|
|
bool enabled,
|
|
Optional<StringRef> arg,
|
|
bool intrans) {
|
|
// If the parameter type is an int, we will extract into this variable and reference its memory with a StringRef
|
|
int64_t parsedInt = 0;
|
|
|
|
if (enabled) {
|
|
auto optionInfo = FDBTransactionOptions::optionInfo.getMustExist(option);
|
|
if (arg.present() != optionInfo.hasParameter) {
|
|
fprintf(stderr, "ERROR: option %s a parameter\n", arg.present() ? "did not expect" : "expected");
|
|
throw invalid_option_value();
|
|
}
|
|
if (arg.present() && optionInfo.paramType == FDBOptionInfo::ParamType::Int) {
|
|
try {
|
|
size_t nextIdx;
|
|
std::string value = arg.get().toString();
|
|
parsedInt = std::stoll(value, &nextIdx);
|
|
if (nextIdx != value.length()) {
|
|
fprintf(
|
|
stderr, "ERROR: could not parse value `%s' as an integer\n", arg.get().toString().c_str());
|
|
throw invalid_option_value();
|
|
}
|
|
arg = StringRef(reinterpret_cast<uint8_t*>(&parsedInt), 8);
|
|
} catch (std::exception e) {
|
|
fprintf(stderr, "ERROR: could not parse value `%s' as an integer\n", arg.get().toString().c_str());
|
|
throw invalid_option_value();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (intrans) {
|
|
tr->setOption(option, arg);
|
|
}
|
|
|
|
transactionOptions.setOption(option, enabled, arg.castTo<StringRef>());
|
|
}
|
|
|
|
// A group of enabled options (of type T::Option) as well as a legal options map from string to T::Option
|
|
template <class T>
|
|
struct OptionGroup {
|
|
std::map<typename T::Option, Optional<Standalone<StringRef>>> options;
|
|
std::map<std::string, typename T::Option> legalOptions;
|
|
|
|
OptionGroup<T>() {}
|
|
OptionGroup<T>(OptionGroup<T>& base)
|
|
: options(base.options.begin(), base.options.end()), legalOptions(base.legalOptions) {}
|
|
|
|
// Enable or disable an option. Returns true if option value changed
|
|
bool setOption(typename T::Option option, bool enabled, Optional<StringRef> arg) {
|
|
auto optionItr = options.find(option);
|
|
if (enabled && (optionItr == options.end() ||
|
|
Optional<Standalone<StringRef>>(optionItr->second).castTo<StringRef>() != arg)) {
|
|
options[option] = arg.castTo<Standalone<StringRef>>();
|
|
return true;
|
|
} else if (!enabled && optionItr != options.end()) {
|
|
options.erase(optionItr);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Prints a list of all enabled options in this group
|
|
bool print() const {
|
|
bool found = false;
|
|
|
|
for (auto itr = legalOptions.begin(); itr != legalOptions.end(); ++itr) {
|
|
auto optionItr = options.find(itr->second);
|
|
if (optionItr != options.end()) {
|
|
if (optionItr->second.present())
|
|
printf("%s: `%s'\n", itr->first.c_str(), formatStringRef(optionItr->second.get()).c_str());
|
|
else
|
|
printf("%s\n", itr->first.c_str());
|
|
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
// Returns true if the specified option is documented
|
|
bool isDocumented(typename T::Option option) const {
|
|
FDBOptionInfo info = T::optionInfo.getMustExist(option);
|
|
|
|
std::string deprecatedStr = "Deprecated";
|
|
return !info.comment.empty() && info.comment.substr(0, deprecatedStr.size()) != deprecatedStr;
|
|
}
|
|
|
|
// Returns a vector of the names of all documented options
|
|
std::vector<std::string> getValidOptions() const {
|
|
std::vector<std::string> ret;
|
|
|
|
for (auto itr = legalOptions.begin(); itr != legalOptions.end(); ++itr)
|
|
if (isDocumented(itr->second))
|
|
ret.push_back(itr->first);
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Prints a help string for each option in this group. Any options with no comment
|
|
// are excluded from this help string. Lines are wrapped to 80 characters.
|
|
void printHelpString() const {
|
|
for (auto itr = legalOptions.begin(); itr != legalOptions.end(); ++itr) {
|
|
if (isDocumented(itr->second)) {
|
|
FDBOptionInfo info = T::optionInfo.getMustExist(itr->second);
|
|
std::string helpStr = info.name + " - " + info.comment;
|
|
if (info.hasParameter)
|
|
helpStr += " " + info.parameterComment;
|
|
helpStr += "\n";
|
|
|
|
printAtCol(helpStr.c_str(), 80);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
OptionGroup<FDBTransactionOptions> transactionOptions;
|
|
|
|
public:
|
|
FdbOptions() {
|
|
for (auto itr = FDBTransactionOptions::optionInfo.begin(); itr != FDBTransactionOptions::optionInfo.end();
|
|
++itr)
|
|
transactionOptions.legalOptions[itr->second.name] = itr->first;
|
|
}
|
|
|
|
FdbOptions(FdbOptions& base) : transactionOptions(base.transactionOptions) {}
|
|
};
|
|
|
|
static std::string formatStringRef(StringRef item, bool fullEscaping = false) {
|
|
std::string ret;
|
|
|
|
for (int i = 0; i < item.size(); i++) {
|
|
if (fullEscaping && item[i] == '\\')
|
|
ret += "\\\\";
|
|
else if (fullEscaping && item[i] == '"')
|
|
ret += "\\\"";
|
|
else if (fullEscaping && item[i] == ' ')
|
|
ret += format("\\x%02x", item[i]);
|
|
else if (item[i] >= 32 && item[i] < 127)
|
|
ret += item[i];
|
|
else
|
|
ret += format("\\x%02x", item[i]);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static std::vector<std::vector<StringRef>> parseLine(std::string& line, bool& err, bool& partial) {
|
|
err = false;
|
|
partial = false;
|
|
|
|
bool quoted = false;
|
|
std::vector<StringRef> buf;
|
|
std::vector<std::vector<StringRef>> ret;
|
|
|
|
size_t i = line.find_first_not_of(' ');
|
|
size_t offset = i;
|
|
|
|
bool forcetoken = false;
|
|
|
|
while (i <= line.length()) {
|
|
switch (line[i]) {
|
|
case ';':
|
|
if (!quoted) {
|
|
if (i > offset || (forcetoken && i == offset))
|
|
buf.push_back(StringRef((uint8_t*)(line.data() + offset), i - offset));
|
|
ret.push_back(std::move(buf));
|
|
offset = i = line.find_first_not_of(' ', i + 1);
|
|
forcetoken = false;
|
|
} else
|
|
i++;
|
|
break;
|
|
case '"':
|
|
quoted = !quoted;
|
|
line.erase(i, 1);
|
|
forcetoken = true;
|
|
break;
|
|
case ' ':
|
|
case '\n':
|
|
case '\t':
|
|
case '\r':
|
|
if (!quoted) {
|
|
if (i > offset || (forcetoken && i == offset))
|
|
buf.push_back(StringRef((uint8_t*)(line.data() + offset), i - offset));
|
|
offset = i = line.find_first_not_of(" \n\t\r", i);
|
|
forcetoken = false;
|
|
} else
|
|
i++;
|
|
break;
|
|
case '\\':
|
|
if (i + 2 > line.length()) {
|
|
err = true;
|
|
ret.push_back(std::move(buf));
|
|
return ret;
|
|
}
|
|
switch (line[i + 1]) {
|
|
char ent, save;
|
|
case '"':
|
|
case '\\':
|
|
case ' ':
|
|
case ';':
|
|
line.erase(i, 1);
|
|
break;
|
|
case 'x':
|
|
if (i + 4 > line.length()) {
|
|
err = true;
|
|
ret.push_back(std::move(buf));
|
|
return ret;
|
|
}
|
|
char* pEnd;
|
|
save = line[i + 4];
|
|
line[i + 4] = 0;
|
|
ent = char(strtoul(line.data() + i + 2, &pEnd, 16));
|
|
if (*pEnd) {
|
|
err = true;
|
|
ret.push_back(std::move(buf));
|
|
return ret;
|
|
}
|
|
line[i + 4] = save;
|
|
line.replace(i, 4, 1, ent);
|
|
break;
|
|
default:
|
|
err = true;
|
|
ret.push_back(std::move(buf));
|
|
return ret;
|
|
}
|
|
default:
|
|
i++;
|
|
}
|
|
}
|
|
|
|
i -= 1;
|
|
if (i > offset || (forcetoken && i == offset))
|
|
buf.push_back(StringRef((uint8_t*)(line.data() + offset), i - offset));
|
|
|
|
ret.push_back(std::move(buf));
|
|
|
|
if (quoted)
|
|
partial = true;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void printProgramUsage(const char* name) {
|
|
printf("FoundationDB CLI " FDB_VT_PACKAGE_NAME " (v" FDB_VT_VERSION ")\n"
|
|
"usage: %s [OPTIONS]\n"
|
|
"\n",
|
|
name);
|
|
printf(" -C CONNFILE The path of a file containing the connection string for the\n"
|
|
" FoundationDB cluster. The default is first the value of the\n"
|
|
" FDB_CLUSTER_FILE environment variable, then `./fdb.cluster',\n"
|
|
" then `%s'.\n",
|
|
platform::getDefaultClusterFilePath().c_str());
|
|
printf(" --log Enables trace file logging for the CLI session.\n"
|
|
" --log-dir PATH Specifes the output directory for trace files. If\n"
|
|
" unspecified, defaults to the current directory. Has\n"
|
|
" no effect unless --log is specified.\n"
|
|
" --log-group LOG_GROUP\n"
|
|
" Sets the LogGroup field with the specified value for all\n"
|
|
" events in the trace output (defaults to `default').\n"
|
|
" --trace-format FORMAT\n"
|
|
" Select the format of the log files. xml (the default) and json\n"
|
|
" are supported. Has no effect unless --log is specified.\n"
|
|
" --exec CMDS Immediately executes the semicolon separated CLI commands\n"
|
|
" and then exits.\n"
|
|
" --no-status Disables the initial status check done when starting\n"
|
|
" the CLI.\n"
|
|
" --api-version APIVERSION\n"
|
|
" Specifies the version of the API for the CLI to use.\n" TLS_HELP
|
|
" --knob-KNOBNAME KNOBVALUE\n"
|
|
" Changes a knob option. KNOBNAME should be lowercase.\n"
|
|
" --debug-tls Prints the TLS configuration and certificate chain, then exits.\n"
|
|
" Useful in reporting and diagnosing TLS issues.\n"
|
|
" --build-flags Print build information and exit.\n"
|
|
" --memory Resident memory limit of the CLI (defaults to 8GiB).\n"
|
|
" --use-future-protocol-version\n"
|
|
" Use the simulated future protocol version to connect to the cluster.\n"
|
|
" This option can be used testing purposes only!\n"
|
|
" -v, --version Print FoundationDB CLI version information and exit.\n"
|
|
" -h, --help Display this help and exit.\n");
|
|
}
|
|
|
|
#define ESCAPINGK "\n\nFor information on escaping keys, type `help escaping'."
|
|
#define ESCAPINGKV "\n\nFor information on escaping keys and values, type `help escaping'."
|
|
|
|
using namespace fdb_cli;
|
|
std::map<std::string, CommandHelp>& helpMap = CommandFactory::commands();
|
|
std::set<std::string>& hiddenCommands = CommandFactory::hiddenCommands();
|
|
|
|
void initHelp() {
|
|
helpMap["begin"] =
|
|
CommandHelp("begin",
|
|
"begin a new transaction",
|
|
"By default, the fdbcli operates in autocommit mode. All operations are performed in their own "
|
|
"transaction, and are automatically committed for you. By explicitly beginning a transaction, "
|
|
"successive operations are all performed as part of a single transaction.\n\nTo commit the "
|
|
"transaction, use the commit command. To discard the transaction, use the reset command.");
|
|
helpMap["commit"] = CommandHelp("commit",
|
|
"commit the current transaction",
|
|
"Any sets or clears executed after the start of the current transaction will be "
|
|
"committed to the database. On success, the committed version number is displayed. "
|
|
"If commit fails, the error is displayed and the transaction must be retried.");
|
|
helpMap["clear"] = CommandHelp(
|
|
"clear <KEY>",
|
|
"clear a key from the database",
|
|
"Clear succeeds even if the specified key is not present, but may fail because of conflicts." ESCAPINGK);
|
|
helpMap["clearrange"] = CommandHelp(
|
|
"clearrange <BEGINKEY> <ENDKEY>",
|
|
"clear a range of keys from the database",
|
|
"All keys between BEGINKEY (inclusive) and ENDKEY (exclusive) are cleared from the database. This command will "
|
|
"succeed even if the specified range is empty, but may fail because of conflicts." ESCAPINGK);
|
|
helpMap["exit"] = CommandHelp("exit", "exit the CLI", "");
|
|
helpMap["quit"] = CommandHelp();
|
|
helpMap["waitconnected"] = CommandHelp();
|
|
helpMap["waitopen"] = CommandHelp();
|
|
helpMap["sleep"] = CommandHelp("sleep <SECONDS>", "sleep for a period of time", "");
|
|
helpMap["get"] =
|
|
CommandHelp("get <KEY>",
|
|
"fetch the value for a given key",
|
|
"Displays the value of KEY in the database, or `not found' if KEY is not present." ESCAPINGK);
|
|
helpMap["getrange"] =
|
|
CommandHelp("getrange <BEGINKEY> [ENDKEY] [LIMIT]",
|
|
"fetch key/value pairs in a range of keys",
|
|
"Displays up to LIMIT keys and values for keys between BEGINKEY (inclusive) and ENDKEY "
|
|
"(exclusive). If ENDKEY is omitted, then the range will include all keys starting with BEGINKEY. "
|
|
"LIMIT defaults to 25 if omitted." ESCAPINGK);
|
|
helpMap["getrangekeys"] = CommandHelp(
|
|
"getrangekeys <BEGINKEY> [ENDKEY] [LIMIT]",
|
|
"fetch keys in a range of keys",
|
|
"Displays up to LIMIT keys for keys between BEGINKEY (inclusive) and ENDKEY (exclusive). If ENDKEY is omitted, "
|
|
"then the range will include all keys starting with BEGINKEY. LIMIT defaults to 25 if omitted." ESCAPINGK);
|
|
helpMap["getversion"] =
|
|
CommandHelp("getversion",
|
|
"Fetch the current read version",
|
|
"Displays the current read version of the database or currently running transaction.");
|
|
helpMap["quota"] =
|
|
CommandHelp("quota",
|
|
"quota [get <tag> [reserved|total] [read|write]|set <tag> [reserved|total] [read|write] <value>]",
|
|
"Get or modify the throughput quota for the specified tag.");
|
|
helpMap["reset"] =
|
|
CommandHelp("reset",
|
|
"reset the current transaction",
|
|
"Any sets or clears executed after the start of the active transaction will be discarded.");
|
|
helpMap["rollback"] = CommandHelp("rollback",
|
|
"rolls back the current transaction",
|
|
"The active transaction will be discarded, including any sets or clears executed "
|
|
"since the transaction was started.");
|
|
helpMap["set"] = CommandHelp("set <KEY> <VALUE>",
|
|
"set a value for a given key",
|
|
"If KEY is not already present in the database, it will be created." ESCAPINGKV);
|
|
helpMap["option"] = CommandHelp(
|
|
"option <STATE> <OPTION> <ARG>",
|
|
"enables or disables an option",
|
|
"If STATE is `on', then the option OPTION will be enabled with optional parameter ARG, if required. If STATE "
|
|
"is `off', then OPTION will be disabled.\n\nIf there is no active transaction, then the option will be applied "
|
|
"to all operations as well as all subsequently created transactions (using `begin').\n\nIf there is an active "
|
|
"transaction (one created with `begin'), then enabled options apply only to that transaction. Options cannot "
|
|
"be disabled on an active transaction.\n\nCalling `option' with no parameters prints a list of all enabled "
|
|
"options.\n\nFor information about specific options that can be set, type `help options'.");
|
|
helpMap["help"] = CommandHelp("help [<topic>]", "get help about a topic or command", "");
|
|
helpMap["writemode"] = CommandHelp("writemode <on|off>",
|
|
"enables or disables sets and clears",
|
|
"Setting or clearing keys from the CLI is not recommended.");
|
|
helpMap["usetenant"] =
|
|
CommandHelp("usetenant [NAME]",
|
|
"prints or configures the tenant used for transactions",
|
|
"If no name is given, prints the name of the current active tenant. Otherwise, all commands that "
|
|
"are used to read or write keys are done inside the tenant with the specified NAME. By default, "
|
|
"no tenant is configured and operations are performed on the raw key-space. The tenant cannot be "
|
|
"configured while a transaction started with `begin' is open.");
|
|
helpMap["defaulttenant"] =
|
|
CommandHelp("defaulttenant",
|
|
"configures transactions to not use a named tenant",
|
|
"All commands that are used to read or write keys will be done without a tenant and will operate "
|
|
"on the raw key-space. This is the default behavior. The tenant cannot be configured while a "
|
|
"transaction started with `begin' is open.");
|
|
}
|
|
|
|
void printVersion() {
|
|
printf("FoundationDB CLI " FDB_VT_PACKAGE_NAME " (v" FDB_VT_VERSION ")\n");
|
|
printf("source version %s\n", getSourceVersion());
|
|
printf("protocol %" PRIx64 "\n", currentProtocolVersion().version());
|
|
}
|
|
|
|
void printBuildInformation() {
|
|
printf("%s", jsonBuildInformation().c_str());
|
|
}
|
|
|
|
void printHelpOverview() {
|
|
printf("\nList of commands:\n\n");
|
|
for (const auto& [command, help] : helpMap) {
|
|
if (help.short_desc.size())
|
|
printf(" %s:\n %s\n", command.c_str(), help.short_desc.c_str());
|
|
}
|
|
printf("\nFor information on a specific command, type `help <command>'.");
|
|
printf("\nFor information on escaping keys and values, type `help escaping'.");
|
|
printf("\nFor information on available options, type `help options'.\n\n");
|
|
}
|
|
|
|
void printHelp(StringRef command) {
|
|
auto i = helpMap.find(command.toString());
|
|
if (i != helpMap.end() && i->second.short_desc.size()) {
|
|
printf("\n%s\n\n", i->second.usage.c_str());
|
|
auto cstr = i->second.short_desc.c_str();
|
|
printf("%c%s.\n", toupper(cstr[0]), cstr + 1);
|
|
if (!i->second.long_desc.empty()) {
|
|
printf("\n");
|
|
printAtCol(i->second.long_desc.c_str(), 80);
|
|
}
|
|
printf("\n");
|
|
} else
|
|
printf("I don't know anything about `%s'\n", formatStringRef(command).c_str());
|
|
}
|
|
|
|
int printStatusFromJSON(std::string const& jsonFileName) {
|
|
try {
|
|
json_spirit::mValue value;
|
|
json_spirit::read_string(readFileBytes(jsonFileName, 10000000), value);
|
|
|
|
printStatus(value.get_obj(), StatusClient::DETAILED, false, true);
|
|
|
|
return 0;
|
|
} catch (std::exception& e) {
|
|
printf("Exception printing status: %s\n", e.what());
|
|
return 1;
|
|
} catch (Error& e) {
|
|
printf("Error printing status: %d %s\n", e.code(), e.what());
|
|
return 2;
|
|
} catch (...) {
|
|
printf("Unknown exception printing status.\n");
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
ACTOR Future<Void> timeWarning(double when, const char* msg) {
|
|
wait(delay(when));
|
|
fputs(msg, stderr);
|
|
|
|
return Void();
|
|
}
|
|
|
|
ACTOR Future<Void> checkStatus(Future<Void> f,
|
|
Reference<IDatabase> db,
|
|
Database localDb,
|
|
bool displayDatabaseAvailable = true) {
|
|
wait(f);
|
|
state Reference<ITransaction> tr = db->createTransaction();
|
|
state StatusObject s;
|
|
if (!tr->isValid()) {
|
|
StatusObject _s = wait(StatusClient::statusFetcher(localDb));
|
|
s = _s;
|
|
} else {
|
|
state ThreadFuture<Optional<Value>> statusValueF = tr->get(LiteralStringRef("\xff\xff/status/json"));
|
|
Optional<Value> statusValue = wait(safeThreadFutureToFuture(statusValueF));
|
|
if (!statusValue.present()) {
|
|
fprintf(stderr, "ERROR: Failed to get status json from the cluster\n");
|
|
return Void();
|
|
}
|
|
json_spirit::mValue mv;
|
|
json_spirit::read_string(statusValue.get().toString(), mv);
|
|
s = StatusObject(mv.get_obj());
|
|
}
|
|
printf("\n");
|
|
printStatus(s, StatusClient::MINIMAL, displayDatabaseAvailable);
|
|
printf("\n");
|
|
return Void();
|
|
}
|
|
|
|
ACTOR template <class T>
|
|
Future<T> makeInterruptable(Future<T> f) {
|
|
Future<Void> interrupt = LineNoise::onKeyboardInterrupt();
|
|
choose {
|
|
when(T t = wait(f)) { return t; }
|
|
when(wait(interrupt)) {
|
|
f.cancel();
|
|
throw operation_cancelled();
|
|
}
|
|
}
|
|
}
|
|
|
|
ACTOR Future<Void> commitTransaction(Reference<ITransaction> tr) {
|
|
wait(makeInterruptable(safeThreadFutureToFuture(tr->commit())));
|
|
auto ver = tr->getCommittedVersion();
|
|
if (ver != invalidVersion)
|
|
fmt::print("Committed ({})\n", ver);
|
|
else
|
|
fmt::print("Nothing to commit\n");
|
|
return Void();
|
|
}
|
|
|
|
ACTOR Future<bool> createSnapshot(Database db, std::vector<StringRef> tokens) {
|
|
state Standalone<StringRef> snapCmd;
|
|
state UID snapUID = deterministicRandom()->randomUniqueID();
|
|
for (int i = 1; i < tokens.size(); i++) {
|
|
snapCmd = snapCmd.withSuffix(tokens[i]);
|
|
if (i != tokens.size() - 1) {
|
|
snapCmd = snapCmd.withSuffix(LiteralStringRef(" "));
|
|
}
|
|
}
|
|
try {
|
|
wait(makeInterruptable(mgmtSnapCreate(db, snapCmd, snapUID)));
|
|
printf("Snapshot command succeeded with UID %s\n", snapUID.toString().c_str());
|
|
} catch (Error& e) {
|
|
fprintf(stderr,
|
|
"Snapshot command failed %d (%s)."
|
|
" Please cleanup any instance level snapshots created with UID %s.\n",
|
|
e.code(),
|
|
e.what(),
|
|
snapUID.toString().c_str());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO: Update the function to get rid of the Database after refactoring
|
|
Reference<ITransaction> getTransaction(Reference<IDatabase> db,
|
|
Reference<ITenant> tenant,
|
|
Reference<ITransaction>& tr,
|
|
FdbOptions* options,
|
|
bool intrans) {
|
|
// Update "tr" to point to a brand new transaction object when it's not initialized or "intrans" flag is "false",
|
|
// which indicates we need a new transaction object
|
|
if (!tr || !intrans) {
|
|
if (tenant) {
|
|
tr = tenant->createTransaction();
|
|
} else {
|
|
tr = db->createTransaction();
|
|
}
|
|
options->apply(tr);
|
|
}
|
|
|
|
return tr;
|
|
}
|
|
|
|
std::string newCompletion(const char* base, const char* name) {
|
|
return format("%s%s ", base, name);
|
|
}
|
|
|
|
void compGenerator(const char* text, bool help, std::vector<std::string>& lc) {
|
|
std::map<std::string, CommandHelp>::const_iterator iter;
|
|
int len = strlen(text);
|
|
|
|
const char* helpExtra[] = { "escaping", "options", nullptr };
|
|
|
|
const char** he = helpExtra;
|
|
|
|
for (auto iter = helpMap.begin(); iter != helpMap.end(); ++iter) {
|
|
const char* name = (*iter).first.c_str();
|
|
if (!strncmp(name, text, len)) {
|
|
lc.push_back(newCompletion(help ? "help " : "", name));
|
|
}
|
|
}
|
|
|
|
if (help) {
|
|
while (*he) {
|
|
const char* name = *he;
|
|
he++;
|
|
if (!strncmp(name, text, len))
|
|
lc.push_back(newCompletion("help ", name));
|
|
}
|
|
}
|
|
}
|
|
|
|
void cmdGenerator(const char* text, std::vector<std::string>& lc) {
|
|
compGenerator(text, false, lc);
|
|
}
|
|
|
|
void helpGenerator(const char* text, std::vector<std::string>& lc) {
|
|
compGenerator(text, true, lc);
|
|
}
|
|
|
|
void optionGenerator(const char* text, const char* line, std::vector<std::string>& lc) {
|
|
int len = strlen(text);
|
|
|
|
for (auto iter = validOptions.begin(); iter != validOptions.end(); ++iter) {
|
|
const char* name = (*iter).c_str();
|
|
if (!strncmp(name, text, len)) {
|
|
lc.push_back(newCompletion(line, name));
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace fdb_cli {
|
|
void arrayGenerator(const char* text, const char* line, const char** options, std::vector<std::string>& lc) {
|
|
const char** iter = options;
|
|
int len = strlen(text);
|
|
|
|
while (*iter) {
|
|
const char* name = *iter;
|
|
iter++;
|
|
if (!strncmp(name, text, len)) {
|
|
lc.push_back(newCompletion(line, name));
|
|
}
|
|
}
|
|
}
|
|
} // namespace fdb_cli
|
|
|
|
void onOffGenerator(const char* text, const char* line, std::vector<std::string>& lc) {
|
|
const char* opts[] = { "on", "off", nullptr };
|
|
arrayGenerator(text, line, opts, lc);
|
|
}
|
|
|
|
void fdbcliCompCmd(std::string const& text, std::vector<std::string>& lc) {
|
|
bool err, partial;
|
|
std::string whole_line = text;
|
|
auto parsed = parseLine(whole_line, err, partial);
|
|
if (err || partial) // If there was an error, or we are partially through a quoted sequence
|
|
return;
|
|
|
|
auto tokens = parsed.back();
|
|
int count = tokens.size();
|
|
|
|
// for(int i = 0; i < count; i++) {
|
|
// printf("Token (%d): `%s'\n", i, tokens[i].toString().c_str());
|
|
// }
|
|
|
|
std::string ntext = "";
|
|
std::string base_input = text;
|
|
|
|
// If there is a token and the input does not end in a space
|
|
if (count && text.size() > 0 && text[text.size() - 1] != ' ') {
|
|
count--; // Ignore the last token for purposes of later code
|
|
ntext = tokens.back().toString();
|
|
base_input = whole_line.substr(0, whole_line.rfind(ntext));
|
|
}
|
|
|
|
// printf("final text (%d tokens): `%s' & `%s'\n", count, base_input.c_str(), ntext.c_str());
|
|
|
|
if (!count) {
|
|
cmdGenerator(ntext.c_str(), lc);
|
|
return;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "help") && count == 1) {
|
|
helpGenerator(ntext.c_str(), lc);
|
|
return;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "option")) {
|
|
if (count == 1)
|
|
onOffGenerator(ntext.c_str(), base_input.c_str(), lc);
|
|
if (count == 2)
|
|
optionGenerator(ntext.c_str(), base_input.c_str(), lc);
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "writemode") && count == 1) {
|
|
onOffGenerator(ntext.c_str(), base_input.c_str(), lc);
|
|
}
|
|
|
|
auto itr = CommandFactory::completionGenerators().find(tokens[0].toString());
|
|
if (itr != CommandFactory::completionGenerators().end()) {
|
|
itr->second(ntext.c_str(), base_input.c_str(), lc, tokens);
|
|
}
|
|
}
|
|
|
|
void LogCommand(std::string line, UID randomID, std::string errMsg) {
|
|
printf("%s\n", errMsg.c_str());
|
|
TraceEvent(SevInfo, "CLICommandLog", randomID).detail("Command", line).detail("Error", errMsg);
|
|
}
|
|
|
|
struct CLIOptions {
|
|
std::string program_name;
|
|
int exit_code = -1;
|
|
|
|
std::string commandLine;
|
|
|
|
std::string clusterFile;
|
|
bool trace = false;
|
|
std::string traceDir;
|
|
std::string traceFormat;
|
|
std::string logGroup;
|
|
int exit_timeout = 0;
|
|
Optional<std::string> exec;
|
|
bool initialStatusCheck = true;
|
|
bool cliHints = true;
|
|
bool useFutureProtocolVersion = false;
|
|
bool debugTLS = false;
|
|
std::string tlsCertPath;
|
|
std::string tlsKeyPath;
|
|
std::string tlsVerifyPeers;
|
|
std::string tlsCAPath;
|
|
std::string tlsPassword;
|
|
uint64_t memLimit = 8uLL << 30;
|
|
|
|
std::vector<std::pair<std::string, std::string>> knobs;
|
|
|
|
// api version, using the latest version by default
|
|
int apiVersion = ApiVersion::LATEST_VERSION;
|
|
|
|
CLIOptions(int argc, char* argv[]) {
|
|
program_name = argv[0];
|
|
for (int a = 0; a < argc; a++) {
|
|
if (a)
|
|
commandLine += ' ';
|
|
commandLine += argv[a];
|
|
}
|
|
|
|
CSimpleOpt args(argc, argv, g_rgOptions, SO_O_HYPHEN_TO_UNDERSCORE);
|
|
|
|
while (args.Next()) {
|
|
int ec = processArg(args);
|
|
if (ec != -1) {
|
|
exit_code = ec;
|
|
return;
|
|
}
|
|
}
|
|
if (exit_timeout && !exec.present()) {
|
|
fprintf(stderr, "ERROR: --timeout may only be specified with --exec\n");
|
|
exit_code = FDB_EXIT_ERROR;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void setupKnobs() {
|
|
IKnobCollection::setupKnobs(knobs);
|
|
|
|
// Reinitialize knobs in order to update knobs that are dependent on explicitly set knobs
|
|
IKnobCollection::getMutableGlobalKnobCollection().initialize(Randomize::False, IsSimulated::False);
|
|
}
|
|
|
|
int processArg(CSimpleOpt& args) {
|
|
if (args.LastError() != SO_SUCCESS) {
|
|
printProgramUsage(program_name.c_str());
|
|
return 1;
|
|
}
|
|
|
|
switch (args.OptionId()) {
|
|
case OPT_CONNFILE:
|
|
clusterFile = args.OptionArg();
|
|
break;
|
|
case OPT_API_VERSION: {
|
|
char* endptr;
|
|
apiVersion = strtoul((char*)args.OptionArg(), &endptr, 10);
|
|
if (*endptr != '\0') {
|
|
fprintf(stderr, "ERROR: invalid client version %s\n", args.OptionArg());
|
|
return 1;
|
|
} else if (apiVersion < 700 || apiVersion > ApiVersion::LATEST_VERSION) {
|
|
// multi-version fdbcli only available after 7.0
|
|
fprintf(stderr,
|
|
"ERROR: api version %s is not supported. (Min: 700, Max: %d)\n",
|
|
args.OptionArg(),
|
|
ApiVersion::LATEST_VERSION);
|
|
return 1;
|
|
}
|
|
break;
|
|
}
|
|
case OPT_MEMORY: {
|
|
std::string memoryArg(args.OptionArg());
|
|
memLimit = parse_with_suffix(memoryArg, "MiB").orDefault(8uLL << 30);
|
|
break;
|
|
}
|
|
case OPT_TRACE:
|
|
trace = true;
|
|
break;
|
|
case OPT_TRACE_DIR:
|
|
traceDir = args.OptionArg();
|
|
break;
|
|
case OPT_LOGGROUP:
|
|
logGroup = args.OptionArg();
|
|
break;
|
|
case OPT_TIMEOUT: {
|
|
char* endptr;
|
|
exit_timeout = strtoul((char*)args.OptionArg(), &endptr, 10);
|
|
if (*endptr != '\0') {
|
|
fprintf(stderr, "ERROR: invalid timeout %s\n", args.OptionArg());
|
|
return 1;
|
|
}
|
|
break;
|
|
}
|
|
case OPT_EXEC:
|
|
exec = args.OptionArg();
|
|
break;
|
|
case OPT_NO_STATUS:
|
|
initialStatusCheck = false;
|
|
break;
|
|
case OPT_NO_HINTS:
|
|
cliHints = false;
|
|
break;
|
|
case OPT_USE_FUTURE_PROTOCOL_VERSION:
|
|
useFutureProtocolVersion = true;
|
|
break;
|
|
|
|
// TLS Options
|
|
case TLSConfig::OPT_TLS_PLUGIN:
|
|
args.OptionArg();
|
|
break;
|
|
case TLSConfig::OPT_TLS_CERTIFICATES:
|
|
tlsCertPath = args.OptionArg();
|
|
break;
|
|
case TLSConfig::OPT_TLS_CA_FILE:
|
|
tlsCAPath = args.OptionArg();
|
|
break;
|
|
case TLSConfig::OPT_TLS_KEY:
|
|
tlsKeyPath = args.OptionArg();
|
|
break;
|
|
case TLSConfig::OPT_TLS_PASSWORD:
|
|
tlsPassword = args.OptionArg();
|
|
break;
|
|
case TLSConfig::OPT_TLS_VERIFY_PEERS:
|
|
tlsVerifyPeers = args.OptionArg();
|
|
break;
|
|
|
|
case OPT_HELP:
|
|
printProgramUsage(program_name.c_str());
|
|
return 0;
|
|
case OPT_STATUS_FROM_JSON:
|
|
return printStatusFromJSON(args.OptionArg());
|
|
case OPT_TRACE_FORMAT:
|
|
if (!validateTraceFormat(args.OptionArg())) {
|
|
fprintf(stderr, "WARNING: Unrecognized trace format `%s'\n", args.OptionArg());
|
|
}
|
|
traceFormat = args.OptionArg();
|
|
break;
|
|
case OPT_KNOB: {
|
|
Optional<std::string> knobName = extractPrefixedArgument("--knob", args.OptionSyntax());
|
|
if (!knobName.present()) {
|
|
fprintf(stderr, "ERROR: unable to parse knob option '%s'\n", args.OptionSyntax());
|
|
return FDB_EXIT_ERROR;
|
|
}
|
|
knobs.emplace_back(knobName.get(), args.OptionArg());
|
|
break;
|
|
}
|
|
case OPT_DEBUG_TLS:
|
|
debugTLS = true;
|
|
break;
|
|
case OPT_VERSION:
|
|
printVersion();
|
|
return FDB_EXIT_SUCCESS;
|
|
case OPT_BUILD_FLAGS:
|
|
printBuildInformation();
|
|
return FDB_EXIT_SUCCESS;
|
|
}
|
|
return -1;
|
|
}
|
|
};
|
|
|
|
ACTOR template <class T>
|
|
Future<T> stopNetworkAfter(Future<T> what) {
|
|
try {
|
|
T t = wait(what);
|
|
API->stopNetwork();
|
|
return t;
|
|
} catch (...) {
|
|
API->stopNetwork();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise, Reference<ClusterConnectionFile> ccf) {
|
|
state LineNoise& linenoise = *plinenoise;
|
|
state bool intrans = false;
|
|
|
|
state Database localDb;
|
|
state Reference<IDatabase> db;
|
|
state Reference<ITenant> tenant;
|
|
state Optional<TenantName> tenantName;
|
|
state Optional<TenantMapEntry> tenantEntry;
|
|
|
|
// This tenant is kept empty for operations that perform management tasks (e.g. killing a process)
|
|
state const Reference<ITenant> managementTenant;
|
|
|
|
state Reference<ITransaction> tr;
|
|
state Transaction trx;
|
|
|
|
state bool writeMode = false;
|
|
|
|
state std::map<Key, std::pair<Value, ClientLeaderRegInterface>> address_interface;
|
|
|
|
state FdbOptions globalOptions;
|
|
state FdbOptions activeOptions;
|
|
|
|
state FdbOptions* options = &globalOptions;
|
|
|
|
// Ordinarily, this is done when the network is run. However, network thread should be set before TraceEvents are
|
|
// logged. This thread will eventually run the network, so call it now.
|
|
TraceEvent::setNetworkThread();
|
|
|
|
try {
|
|
localDb = Database::createDatabase(ccf, opt.apiVersion, IsInternal::False);
|
|
if (!opt.exec.present()) {
|
|
printf("Using cluster file `%s'.\n", ccf->getLocation().c_str());
|
|
}
|
|
db = API->createDatabase(opt.clusterFile.c_str());
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: %s (%d)\n", e.what(), e.code());
|
|
printf("Unable to connect to cluster from `%s'\n", ccf->getLocation().c_str());
|
|
return 1;
|
|
}
|
|
|
|
if (opt.trace) {
|
|
TraceEvent("CLIProgramStart")
|
|
.setMaxEventLength(12000)
|
|
.detail("SourceVersion", getSourceVersion())
|
|
.detail("Version", FDB_VT_VERSION)
|
|
.detail("PackageName", FDB_VT_PACKAGE_NAME)
|
|
.detailf("ActualTime", "%lld", DEBUG_DETERMINISM ? 0 : time(nullptr))
|
|
.detail("ClusterFile", ccf->toString())
|
|
.detail("ConnectionString", ccf->getConnectionString().toString())
|
|
.setMaxFieldLength(10000)
|
|
.detail("CommandLine", opt.commandLine)
|
|
.trackLatest("ProgramStart");
|
|
}
|
|
|
|
// used to catch the first cluster_version_changed error when using external clients
|
|
// when using external clients, it will throw cluster_version_changed for the first time establish the connection to
|
|
// the cluster. Thus, we catch it by doing a get version request to establish the connection
|
|
// The 3.0 timeout is a guard to avoid waiting forever when the cli cannot talk to any coordinators
|
|
loop {
|
|
try {
|
|
getTransaction(db, managementTenant, tr, options, intrans);
|
|
tr->setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
wait(delay(3.0) || success(safeThreadFutureToFuture(tr->getReadVersion())));
|
|
break;
|
|
} catch (Error& e) {
|
|
if (e.code() == error_code_operation_cancelled) {
|
|
throw;
|
|
}
|
|
if (e.code() == error_code_cluster_version_changed) {
|
|
wait(safeThreadFutureToFuture(tr->onError(e)));
|
|
} else {
|
|
// unexpected errors
|
|
fprintf(stderr, "ERROR: unexpected error %d while initializing the multiversion database\n", e.code());
|
|
tr->reset();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!opt.exec.present()) {
|
|
if (opt.initialStatusCheck) {
|
|
Future<Void> checkStatusF = checkStatus(Void(), db, localDb);
|
|
wait(makeInterruptable(success(checkStatusF)));
|
|
} else {
|
|
printf("\n");
|
|
}
|
|
|
|
printf("Welcome to the fdbcli. For help, type `help'.\n");
|
|
validOptions = options->getValidOptions();
|
|
}
|
|
|
|
state bool is_error = false;
|
|
|
|
state Future<Void> warn;
|
|
loop {
|
|
if (warn.isValid())
|
|
warn.cancel();
|
|
|
|
state std::string line;
|
|
|
|
if (opt.exec.present()) {
|
|
line = opt.exec.get();
|
|
} else {
|
|
Optional<std::string> rawline = wait(linenoise.read("fdb> "));
|
|
if (!rawline.present()) {
|
|
printf("\n");
|
|
return 0;
|
|
}
|
|
line = rawline.get();
|
|
|
|
if (!line.size())
|
|
continue;
|
|
|
|
// Don't put dangerous commands in the command history
|
|
if (line.find("writemode") == std::string::npos && line.find("expensive_data_check") == std::string::npos &&
|
|
line.find("unlock") == std::string::npos && line.find("blobrange") == std::string::npos)
|
|
linenoise.historyAdd(line);
|
|
}
|
|
|
|
warn = checkStatus(timeWarning(5.0, "\nWARNING: Long delay (Ctrl-C to interrupt)\n"), db, localDb);
|
|
|
|
try {
|
|
state UID randomID = deterministicRandom()->randomUniqueID();
|
|
TraceEvent(SevInfo, "CLICommandLog", randomID).detail("Command", line);
|
|
|
|
bool malformed, partial;
|
|
state std::vector<std::vector<StringRef>> parsed = parseLine(line, malformed, partial);
|
|
if (malformed)
|
|
LogCommand(line, randomID, "ERROR: malformed escape sequence");
|
|
if (partial)
|
|
LogCommand(line, randomID, "ERROR: unterminated quote");
|
|
if (malformed || partial) {
|
|
if (parsed.size() > 0) {
|
|
// Denote via a special token that the command was a parse failure.
|
|
auto& last_command = parsed.back();
|
|
last_command.insert(last_command.begin(),
|
|
StringRef((const uint8_t*)"parse_error", strlen("parse_error")));
|
|
}
|
|
}
|
|
|
|
state bool multi = parsed.size() > 1;
|
|
is_error = false;
|
|
|
|
state std::vector<std::vector<StringRef>>::iterator iter;
|
|
for (iter = parsed.begin(); iter != parsed.end(); ++iter) {
|
|
state std::vector<StringRef> tokens = *iter;
|
|
|
|
if (is_error) {
|
|
printf("WARNING: the previous command failed, the remaining commands will not be executed.\n");
|
|
break;
|
|
}
|
|
|
|
if (!tokens.size())
|
|
continue;
|
|
|
|
if (tokencmp(tokens[0], "parse_error")) {
|
|
fprintf(stderr, "ERROR: Command failed to completely parse.\n");
|
|
if (tokens.size() > 1) {
|
|
fprintf(stderr, "ERROR: Not running partial or malformed command:");
|
|
for (auto t = tokens.begin() + 1; t != tokens.end(); ++t)
|
|
printf(" %s", formatStringRef(*t, true).c_str());
|
|
printf("\n");
|
|
}
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (multi) {
|
|
printf(">>>");
|
|
for (auto t = tokens.begin(); t != tokens.end(); ++t)
|
|
printf(" %s", formatStringRef(*t, true).c_str());
|
|
printf("\n");
|
|
}
|
|
|
|
if (!helpMap.count(tokens[0].toString()) && !hiddenCommands.count(tokens[0].toString())) {
|
|
fprintf(stderr, "ERROR: Unknown command `%s'. Try `help'?\n", formatStringRef(tokens[0]).c_str());
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "exit") || tokencmp(tokens[0], "quit")) {
|
|
return 0;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "help")) {
|
|
if (tokens.size() == 1) {
|
|
printHelpOverview();
|
|
} else if (tokens.size() == 2) {
|
|
if (tokencmp(tokens[1], "escaping"))
|
|
printf("\n"
|
|
"When parsing commands, fdbcli considers a space to delimit individual tokens.\n"
|
|
"To include a space in a single token, you may either enclose the token in\n"
|
|
"quotation marks (\"hello world\"), prefix the space with a backslash\n"
|
|
"(hello\\ world), or encode the space as a hex byte (hello\\x20world).\n"
|
|
"\n"
|
|
"To include a literal quotation mark in a token, precede it with a backslash\n"
|
|
"(\\\"hello\\ world\\\").\n"
|
|
"\n"
|
|
"To express a binary value, encode each byte as a two-digit hex byte, preceded\n"
|
|
"by \\x (e.g. \\x20 for a space character, or \\x0a\\x00\\x00\\x00 for a\n"
|
|
"32-bit, little-endian representation of the integer 10).\n"
|
|
"\n"
|
|
"All keys and values are displayed by the fdbcli with non-printable characters\n"
|
|
"and spaces encoded as two-digit hex bytes.\n\n");
|
|
else if (tokencmp(tokens[1], "options")) {
|
|
printf("\n"
|
|
"The following options are available to be set using the `option' command:\n"
|
|
"\n");
|
|
options->printHelpString();
|
|
} else if (tokencmp(tokens[1], "help"))
|
|
printHelpOverview();
|
|
else
|
|
printHelp(tokens[1]);
|
|
} else
|
|
printf("Usage: help [topic]\n");
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "waitconnected")) {
|
|
wait(makeInterruptable(localDb->onConnected()));
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "waitopen")) {
|
|
wait(makeInterruptable(success(safeThreadFutureToFuture(
|
|
getTransaction(db, managementTenant, tr, options, intrans)->getReadVersion()))));
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "sleep")) {
|
|
if (tokens.size() != 2) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
double v;
|
|
int n = 0;
|
|
if (sscanf(tokens[1].toString().c_str(), "%lf%n", &v, &n) != 1 || n != tokens[1].size()) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
wait(delay(v));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "status")) {
|
|
// Warn at 7 seconds since status will spend as long as 5 seconds trying to read/write from the
|
|
// database
|
|
warn = timeWarning(7.0, "\nWARNING: Long delay (Ctrl-C to interrupt)\n");
|
|
bool _result = wait(makeInterruptable(statusCommandActor(db, localDb, tokens, opt.exec.present())));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "triggerddteaminfolog")) {
|
|
wait(success(makeInterruptable(triggerddteaminfologCommandActor(db))));
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "tssq")) {
|
|
bool _result = wait(makeInterruptable(tssqCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "configure")) {
|
|
bool _result =
|
|
wait(makeInterruptable(configureCommandActor(db, localDb, tokens, &linenoise, warn)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "fileconfigure")) {
|
|
if (tokens.size() == 2 || (tokens.size() == 3 && (tokens[1] == LiteralStringRef("new") ||
|
|
tokens[1] == LiteralStringRef("FORCE")))) {
|
|
bool _result =
|
|
wait(makeInterruptable(fileConfigureCommandActor(db,
|
|
tokens.back().toString(),
|
|
tokens[1] == LiteralStringRef("new"),
|
|
tokens[1] == LiteralStringRef("FORCE"))));
|
|
if (!_result)
|
|
is_error = true;
|
|
} else {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "coordinators")) {
|
|
bool _result = wait(makeInterruptable(coordinatorsCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "exclude")) {
|
|
bool _result = wait(makeInterruptable(excludeCommandActor(db, tokens, warn)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "include")) {
|
|
bool _result = wait(makeInterruptable(includeCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "snapshot")) {
|
|
bool _result = wait(makeInterruptable(snapshotCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "lock")) {
|
|
bool _result = wait(makeInterruptable(lockCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "changefeed")) {
|
|
bool _result = wait(makeInterruptable(changeFeedCommandActor(localDb, tenantEntry, tokens, warn)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "blobrange")) {
|
|
bool _result = wait(makeInterruptable(blobRangeCommandActor(localDb, tenantEntry, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "unlock")) {
|
|
if ((tokens.size() != 2) || (tokens[1].size() != 32) ||
|
|
!std::all_of(tokens[1].begin(), tokens[1].end(), &isxdigit)) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
state std::string passPhrase = deterministicRandom()->randomAlphaNumeric(10);
|
|
warn.cancel(); // don't warn while waiting on user input
|
|
printf("Unlocking the database is a potentially dangerous operation.\n");
|
|
printf("%s\n", passPhrase.c_str());
|
|
fflush(stdout);
|
|
Optional<std::string> input =
|
|
wait(linenoise.read(format("Repeat the above passphrase if you would like to proceed:")));
|
|
warn =
|
|
checkStatus(timeWarning(5.0, "\nWARNING: Long delay (Ctrl-C to interrupt)\n"), db, localDb);
|
|
if (input.present() && input.get() == passPhrase) {
|
|
UID unlockUID = UID::fromString(tokens[1].toString());
|
|
bool _result = wait(makeInterruptable(unlockDatabaseActor(db, unlockUID)));
|
|
if (!_result)
|
|
is_error = true;
|
|
} else {
|
|
fprintf(stderr, "ERROR: Incorrect passphrase entered.\n");
|
|
is_error = true;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "setclass")) {
|
|
bool _result = wait(makeInterruptable(setClassCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "begin")) {
|
|
if (tokens.size() != 1) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else if (intrans) {
|
|
fprintf(stderr, "ERROR: Already in transaction\n");
|
|
is_error = true;
|
|
} else {
|
|
activeOptions = FdbOptions(globalOptions);
|
|
options = &activeOptions;
|
|
getTransaction(db, tenant, tr, options, false);
|
|
intrans = true;
|
|
printf("Transaction started\n");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "commit")) {
|
|
if (tokens.size() != 1) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else if (!intrans) {
|
|
fprintf(stderr, "ERROR: No active transaction\n");
|
|
is_error = true;
|
|
} else {
|
|
wait(commitTransaction(tr));
|
|
intrans = false;
|
|
options = &globalOptions;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "quota")) {
|
|
bool _result = wait(makeInterruptable(quotaCommandActor(db, tokens)));
|
|
if (!_result) {
|
|
is_error = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "reset")) {
|
|
if (tokens.size() != 1) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else if (!intrans) {
|
|
fprintf(stderr, "ERROR: No active transaction\n");
|
|
is_error = true;
|
|
} else {
|
|
tr->reset();
|
|
activeOptions = FdbOptions(globalOptions);
|
|
options = &activeOptions;
|
|
options->apply(tr);
|
|
printf("Transaction reset\n");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "rollback")) {
|
|
if (tokens.size() != 1) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else if (!intrans) {
|
|
fprintf(stderr, "ERROR: No active transaction\n");
|
|
is_error = true;
|
|
} else {
|
|
intrans = false;
|
|
options = &globalOptions;
|
|
printf("Transaction rolled back\n");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "get")) {
|
|
if (tokens.size() != 2) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
state ThreadFuture<Optional<Value>> valueF =
|
|
getTransaction(db, tenant, tr, options, intrans)->get(tokens[1]);
|
|
Optional<Standalone<StringRef>> v = wait(makeInterruptable(safeThreadFutureToFuture(valueF)));
|
|
|
|
if (v.present())
|
|
printf("`%s' is `%s'\n", printable(tokens[1]).c_str(), printable(v.get()).c_str());
|
|
else
|
|
printf("`%s': not found\n", printable(tokens[1]).c_str());
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "getversion")) {
|
|
if (tokens.size() != 1) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
Version v = wait(makeInterruptable(safeThreadFutureToFuture(
|
|
getTransaction(db, tenant, tr, options, intrans)->getReadVersion())));
|
|
fmt::print("{}\n", v);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "advanceversion")) {
|
|
bool _result = wait(makeInterruptable(advanceVersionCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "versionepoch")) {
|
|
bool _result = wait(makeInterruptable(versionEpochCommandActor(db, localDb, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "kill")) {
|
|
getTransaction(db, managementTenant, tr, options, intrans);
|
|
bool _result = wait(makeInterruptable(killCommandActor(db, tr, tokens, &address_interface)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "suspend")) {
|
|
getTransaction(db, managementTenant, tr, options, intrans);
|
|
bool _result = wait(makeInterruptable(suspendCommandActor(db, tr, tokens, &address_interface)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "force_recovery_with_data_loss")) {
|
|
bool _result = wait(makeInterruptable(forceRecoveryWithDataLossCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "maintenance")) {
|
|
bool _result = wait(makeInterruptable(maintenanceCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "consistencycheck")) {
|
|
getTransaction(db, managementTenant, tr, options, intrans);
|
|
bool _result = wait(makeInterruptable(consistencyCheckCommandActor(tr, tokens, intrans)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "profile")) {
|
|
getTransaction(db, managementTenant, tr, options, intrans);
|
|
bool _result = wait(makeInterruptable(profileCommandActor(localDb, tr, tokens, intrans)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "expensive_data_check")) {
|
|
getTransaction(db, managementTenant, tr, options, intrans);
|
|
bool _result =
|
|
wait(makeInterruptable(expensiveDataCheckCommandActor(db, tr, tokens, &address_interface)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "getrange") ||
|
|
tokencmp(tokens[0], "getrangekeys")) { // FIXME: support byte limits, and reverse range reads
|
|
if (tokens.size() < 2 || tokens.size() > 4) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
state int limit;
|
|
bool valid = true;
|
|
|
|
if (tokens.size() == 4) {
|
|
// INT_MAX is 10 digits; rather than
|
|
// worrying about overflow we'll just cap
|
|
// limit at the (already absurd)
|
|
// nearly-a-billion
|
|
if (tokens[3].size() > 9) {
|
|
fprintf(stderr, "ERROR: bad limit\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
limit = 0;
|
|
int place = 1;
|
|
for (int i = tokens[3].size(); i > 0; i--) {
|
|
int val = int(tokens[3][i - 1]) - int('0');
|
|
if (val < 0 || val > 9) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
limit += val * place;
|
|
place *= 10;
|
|
}
|
|
if (!valid) {
|
|
fprintf(stderr, "ERROR: bad limit\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
} else {
|
|
limit = 25;
|
|
}
|
|
|
|
Standalone<StringRef> endKey;
|
|
if (tokens.size() >= 3) {
|
|
endKey = tokens[2];
|
|
} else if (tokens[1].size() == 0) {
|
|
endKey = normalKeys.end;
|
|
} else if (tokens[1] == systemKeys.begin) {
|
|
endKey = systemKeys.end;
|
|
} else if (tokens[1] >= allKeys.end) {
|
|
throw key_outside_legal_range();
|
|
} else {
|
|
endKey = strinc(tokens[1]);
|
|
}
|
|
|
|
getTransaction(db, tenant, tr, options, intrans);
|
|
state ThreadFuture<RangeResult> kvsF = tr->getRange(KeyRangeRef(tokens[1], endKey), limit);
|
|
RangeResult kvs = wait(makeInterruptable(safeThreadFutureToFuture(kvsF)));
|
|
|
|
printf("\nRange limited to %d keys\n", limit);
|
|
for (auto iter = kvs.begin(); iter < kvs.end(); iter++) {
|
|
if (tokencmp(tokens[0], "getrangekeys"))
|
|
printf("`%s'\n", printable((*iter).key).c_str());
|
|
else
|
|
printf(
|
|
"`%s' is `%s'\n", printable((*iter).key).c_str(), printable((*iter).value).c_str());
|
|
}
|
|
printf("\n");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "writemode")) {
|
|
if (tokens.size() != 2) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
if (tokencmp(tokens[1], "on")) {
|
|
writeMode = true;
|
|
} else if (tokencmp(tokens[1], "off")) {
|
|
writeMode = false;
|
|
} else {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "set")) {
|
|
if (!writeMode) {
|
|
fprintf(stderr, "ERROR: writemode must be enabled to set or clear keys in the database.\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokens.size() != 3) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
getTransaction(db, tenant, tr, options, intrans);
|
|
tr->set(tokens[1], tokens[2]);
|
|
|
|
if (!intrans) {
|
|
wait(commitTransaction(tr));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "clear")) {
|
|
if (!writeMode) {
|
|
fprintf(stderr, "ERROR: writemode must be enabled to set or clear keys in the database.\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokens.size() != 2) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
getTransaction(db, tenant, tr, options, intrans);
|
|
tr->clear(tokens[1]);
|
|
|
|
if (!intrans) {
|
|
wait(commitTransaction(tr));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "clearrange")) {
|
|
if (!writeMode) {
|
|
fprintf(stderr, "ERROR: writemode must be enabled to set or clear keys in the database.\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokens.size() != 3) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
getTransaction(db, tenant, tr, options, intrans);
|
|
tr->clear(KeyRangeRef(tokens[1], tokens[2]));
|
|
|
|
if (!intrans) {
|
|
wait(commitTransaction(tr));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "datadistribution")) {
|
|
bool _result = wait(makeInterruptable(dataDistributionCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "option")) {
|
|
if (tokens.size() == 2 || tokens.size() > 4) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else {
|
|
if (tokens.size() == 1) {
|
|
if (options->hasAnyOptionsEnabled()) {
|
|
printf("\nCurrently enabled options:\n\n");
|
|
options->print();
|
|
printf("\n");
|
|
} else
|
|
fprintf(stderr, "There are no options enabled\n");
|
|
|
|
continue;
|
|
}
|
|
bool isOn;
|
|
if (tokencmp(tokens[1], "on")) {
|
|
isOn = true;
|
|
} else if (tokencmp(tokens[1], "off")) {
|
|
if (intrans) {
|
|
fprintf(
|
|
stderr,
|
|
"ERROR: Cannot turn option off when using a transaction created with `begin'\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
if (tokens.size() > 3) {
|
|
fprintf(stderr, "ERROR: Cannot specify option argument when turning option off\n");
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
isOn = false;
|
|
} else {
|
|
fprintf(stderr,
|
|
"ERROR: Invalid option state `%s': option must be turned `on' or `off'\n",
|
|
formatStringRef(tokens[1]).c_str());
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
Optional<StringRef> arg = (tokens.size() > 3) ? tokens[3] : Optional<StringRef>();
|
|
|
|
try {
|
|
options->setOption(tr, tokens[2], isOn, arg, intrans);
|
|
printf("Option %s for %s\n",
|
|
isOn ? "enabled" : "disabled",
|
|
intrans ? "current transaction" : "all transactions");
|
|
} catch (Error& e) {
|
|
// options->setOption() prints error message
|
|
TraceEvent(SevWarn, "CLISetOptionError").error(e).detail("Option", tokens[2]);
|
|
is_error = true;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "throttle")) {
|
|
bool _result = wait(makeInterruptable(throttleCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "cache_range")) {
|
|
bool _result = wait(makeInterruptable(cacheRangeCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "usetenant")) {
|
|
if (tokens.size() > 2) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else if (intrans && tokens.size() == 2) {
|
|
fprintf(stderr, "ERROR: Tenant cannot be changed while a transaction is open\n");
|
|
is_error = true;
|
|
} else if (tokens.size() == 1) {
|
|
if (!tenantName.present()) {
|
|
printf("Using the default tenant\n");
|
|
} else {
|
|
printf("Using tenant `%s'\n", printable(tenantName.get()).c_str());
|
|
}
|
|
} else {
|
|
Optional<TenantMapEntry> entry =
|
|
wait(makeInterruptable(TenantAPI::tryGetTenant(db, tokens[1])));
|
|
if (!entry.present()) {
|
|
fprintf(stderr, "ERROR: Tenant `%s' does not exist\n", printable(tokens[1]).c_str());
|
|
is_error = true;
|
|
} else {
|
|
tenant = db->openTenant(tokens[1]);
|
|
tenantName = tokens[1];
|
|
tenantEntry = entry;
|
|
printf("Using tenant `%s'\n", printable(tokens[1]).c_str());
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "defaulttenant")) {
|
|
if (tokens.size() != 1) {
|
|
printUsage(tokens[0]);
|
|
is_error = true;
|
|
} else if (intrans) {
|
|
fprintf(stderr, "ERROR: Tenant cannot be changed while a transaction is open\n");
|
|
is_error = true;
|
|
} else {
|
|
tenant = Reference<ITenant>();
|
|
tenantName = Optional<Standalone<StringRef>>();
|
|
tenantEntry = Optional<TenantMapEntry>();
|
|
printf("Using the default tenant\n");
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "createtenant")) {
|
|
bool _result = wait(makeInterruptable(createTenantCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "deletetenant")) {
|
|
bool _result = wait(makeInterruptable(deleteTenantCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
else if (tenantName.present() && tokens[1] == tenantName.get()) {
|
|
printAtCol("WARNING: the active tenant was deleted. Use the `usetenant' or `defaulttenant' "
|
|
"command to choose a new tenant.\n",
|
|
80);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "listtenants")) {
|
|
bool _result = wait(makeInterruptable(listTenantsCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "gettenant")) {
|
|
bool _result = wait(makeInterruptable(getTenantCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "configuretenant")) {
|
|
bool _result = wait(makeInterruptable(configureTenantCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "renametenant")) {
|
|
bool _result = wait(makeInterruptable(renameTenantCommandActor(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
if (tokencmp(tokens[0], "metacluster")) {
|
|
bool _result = wait(makeInterruptable(metaclusterCommand(db, tokens)));
|
|
if (!_result)
|
|
is_error = true;
|
|
continue;
|
|
}
|
|
|
|
fprintf(stderr, "ERROR: Unknown command `%s'. Try `help'?\n", formatStringRef(tokens[0]).c_str());
|
|
is_error = true;
|
|
}
|
|
|
|
TraceEvent(SevInfo, "CLICommandLog", randomID).detail("Command", line).detail("IsError", is_error);
|
|
|
|
} catch (Error& e) {
|
|
if (e.code() == error_code_operation_cancelled) {
|
|
throw;
|
|
}
|
|
if (e.code() == error_code_tenant_name_required) {
|
|
printAtCol("ERROR: tenant name required. Use the `usetenant' command to select a tenant or enable the "
|
|
"`RAW_ACCESS' option to read raw keys.",
|
|
80,
|
|
stderr);
|
|
} else if (e.code() != error_code_actor_cancelled) {
|
|
fprintf(stderr, "ERROR: %s (%d)\n", e.what(), e.code());
|
|
}
|
|
is_error = true;
|
|
if (intrans) {
|
|
printf("Rolling back current transaction\n");
|
|
intrans = false;
|
|
options = &globalOptions;
|
|
options->apply(tr);
|
|
}
|
|
}
|
|
|
|
if (opt.exec.present()) {
|
|
return is_error ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
ACTOR Future<int> runCli(CLIOptions opt, Reference<ClusterConnectionFile> ccf) {
|
|
state LineNoise linenoise(
|
|
[](std::string const& line, std::vector<std::string>& completions) { fdbcliCompCmd(line, completions); },
|
|
[enabled = opt.cliHints](std::string const& line) -> LineNoise::Hint {
|
|
if (!enabled) {
|
|
return LineNoise::Hint();
|
|
}
|
|
|
|
bool error = false;
|
|
bool partial = false;
|
|
std::string linecopy = line;
|
|
std::vector<std::vector<StringRef>> parsed = parseLine(linecopy, error, partial);
|
|
if (parsed.size() == 0 || parsed.back().size() == 0)
|
|
return LineNoise::Hint();
|
|
StringRef command = parsed.back().front();
|
|
int finishedParameters = parsed.back().size() + error;
|
|
|
|
// As a user is typing an escaped character, e.g. \", after the \ and before the " is typed
|
|
// the string will be a parse error. Ignore this parse error to avoid flipping the hint to
|
|
// {malformed escape sequence} and back to the original hint for the span of one character
|
|
// being entered.
|
|
if (error && line.back() != '\\')
|
|
return LineNoise::Hint(std::string(" {malformed escape sequence}"), 90, false);
|
|
|
|
bool inArgument = *(line.end() - 1) != ' ';
|
|
std::string hintLine = inArgument ? " " : "";
|
|
auto itr = CommandFactory::hintGenerators().find(command.toString());
|
|
if (itr != CommandFactory::hintGenerators().end()) {
|
|
std::vector<const char*> hintItems = itr->second(parsed.back(), inArgument);
|
|
if (hintItems.empty()) {
|
|
return LineNoise::Hint();
|
|
}
|
|
for (auto item : hintItems) {
|
|
hintLine = hintLine + item + " ";
|
|
}
|
|
} else {
|
|
auto iter = helpMap.find(command.toString());
|
|
if (iter != helpMap.end()) {
|
|
std::string helpLine = iter->second.usage;
|
|
std::vector<std::vector<StringRef>> parsedHelp = parseLine(helpLine, error, partial);
|
|
for (int i = finishedParameters; i < parsedHelp.back().size(); i++) {
|
|
hintLine = hintLine + parsedHelp.back()[i].toString() + " ";
|
|
}
|
|
} else {
|
|
return LineNoise::Hint();
|
|
}
|
|
}
|
|
|
|
return LineNoise::Hint(hintLine, 90, false);
|
|
},
|
|
1000,
|
|
false);
|
|
|
|
state std::string historyFilename;
|
|
try {
|
|
historyFilename = joinPath(getUserHomeDirectory(), ".fdbcli_history");
|
|
linenoise.historyLoad(historyFilename);
|
|
} catch (Error& e) {
|
|
TraceEvent(SevWarnAlways, "ErrorLoadingCliHistory")
|
|
.error(e)
|
|
.detail("Filename", historyFilename.empty() ? "<unknown>" : historyFilename)
|
|
.GetLastError();
|
|
}
|
|
|
|
state int result = wait(cli(opt, &linenoise, ccf));
|
|
|
|
if (!historyFilename.empty()) {
|
|
try {
|
|
linenoise.historySave(historyFilename);
|
|
} catch (Error& e) {
|
|
TraceEvent(SevWarnAlways, "ErrorSavingCliHistory")
|
|
.error(e)
|
|
.detail("Filename", historyFilename)
|
|
.GetLastError();
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
ACTOR Future<Void> timeExit(double duration) {
|
|
wait(delay(duration));
|
|
fprintf(stderr, "Specified timeout reached -- exiting...\n");
|
|
return Void();
|
|
}
|
|
|
|
const char* checkTlsConfigAgainstCoordAddrs(const ClusterConnectionString& ccs) {
|
|
// Resolve TLS config and inspect whether any of the certificate, key, ca bytes has been set
|
|
extern TLSConfig tlsConfig;
|
|
auto const loaded = tlsConfig.loadSync();
|
|
const bool tlsConfigured =
|
|
!loaded.getCertificateBytes().empty() || !loaded.getKeyBytes().empty() || !loaded.getCABytes().empty();
|
|
int tlsAddrs = 0;
|
|
int totalAddrs = 0;
|
|
for (const auto& addr : ccs.coords) {
|
|
if (addr.isTLS())
|
|
tlsAddrs++;
|
|
totalAddrs++;
|
|
}
|
|
for (const auto& host : ccs.hostnames) {
|
|
if (host.isTLS)
|
|
tlsAddrs++;
|
|
totalAddrs++;
|
|
}
|
|
if (tlsConfigured && tlsAddrs == 0) {
|
|
return "fdbcli is configured with TLS, but none of the coordinators have TLS addresses.";
|
|
} else if (!tlsConfigured && tlsAddrs == totalAddrs) {
|
|
return "fdbcli is not configured with TLS, but all of the coordinators have TLS addresses.";
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
platformInit();
|
|
Error::init();
|
|
std::set_new_handler(&platform::outOfMemory);
|
|
|
|
registerCrashHandler();
|
|
|
|
#ifdef __unixish__
|
|
struct sigaction act;
|
|
|
|
// We don't want ctrl-c to quit
|
|
sigemptyset(&act.sa_mask);
|
|
act.sa_flags = 0;
|
|
act.sa_handler = SIG_IGN;
|
|
sigaction(SIGINT, &act, nullptr);
|
|
#endif
|
|
|
|
CLIOptions opt(argc, argv);
|
|
if (opt.exit_code != -1)
|
|
return opt.exit_code;
|
|
|
|
if (opt.trace) {
|
|
if (opt.traceDir.empty())
|
|
setNetworkOption(FDBNetworkOptions::TRACE_ENABLE);
|
|
else
|
|
setNetworkOption(FDBNetworkOptions::TRACE_ENABLE, StringRef(opt.traceDir));
|
|
|
|
if (!opt.traceFormat.empty()) {
|
|
setNetworkOption(FDBNetworkOptions::TRACE_FORMAT, StringRef(opt.traceFormat));
|
|
}
|
|
setNetworkOption(FDBNetworkOptions::ENABLE_SLOW_TASK_PROFILING);
|
|
|
|
if (!opt.logGroup.empty()) {
|
|
setNetworkOption(FDBNetworkOptions::TRACE_LOG_GROUP, StringRef(opt.logGroup));
|
|
}
|
|
}
|
|
initHelp();
|
|
|
|
// deferred TLS options
|
|
if (opt.tlsCertPath.size()) {
|
|
try {
|
|
setNetworkOption(FDBNetworkOptions::TLS_CERT_PATH, opt.tlsCertPath);
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: cannot set TLS certificate path to `%s' (%s)\n", opt.tlsCertPath.c_str(), e.what());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (opt.tlsCAPath.size()) {
|
|
try {
|
|
setNetworkOption(FDBNetworkOptions::TLS_CA_PATH, opt.tlsCAPath);
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: cannot set TLS CA path to `%s' (%s)\n", opt.tlsCAPath.c_str(), e.what());
|
|
return 1;
|
|
}
|
|
}
|
|
if (opt.tlsKeyPath.size()) {
|
|
try {
|
|
if (opt.tlsPassword.size())
|
|
setNetworkOption(FDBNetworkOptions::TLS_PASSWORD, opt.tlsPassword);
|
|
|
|
setNetworkOption(FDBNetworkOptions::TLS_KEY_PATH, opt.tlsKeyPath);
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: cannot set TLS key path to `%s' (%s)\n", opt.tlsKeyPath.c_str(), e.what());
|
|
return 1;
|
|
}
|
|
}
|
|
if (opt.tlsVerifyPeers.size()) {
|
|
try {
|
|
setNetworkOption(FDBNetworkOptions::TLS_VERIFY_PEERS, opt.tlsVerifyPeers);
|
|
} catch (Error& e) {
|
|
fprintf(
|
|
stderr, "ERROR: cannot set TLS peer verification to `%s' (%s)\n", opt.tlsVerifyPeers.c_str(), e.what());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
try {
|
|
setNetworkOption(FDBNetworkOptions::DISABLE_CLIENT_STATISTICS_LOGGING);
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: cannot disable logging client related information (%s)\n", e.what());
|
|
return 1;
|
|
}
|
|
|
|
if (opt.debugTLS) {
|
|
// Backdoor into NativeAPI's tlsConfig, which is where the above network option settings ended up.
|
|
extern TLSConfig tlsConfig;
|
|
printf("TLS Configuration:\n");
|
|
printf("\tCertificate Path: %s\n", tlsConfig.getCertificatePathSync().c_str());
|
|
printf("\tKey Path: %s\n", tlsConfig.getKeyPathSync().c_str());
|
|
printf("\tCA Path: %s\n", tlsConfig.getCAPathSync().c_str());
|
|
try {
|
|
LoadedTLSConfig loaded = tlsConfig.loadSync();
|
|
printf("\tPassword: %s\n", loaded.getPassword().empty() ? "Not configured" : "Exists, but redacted");
|
|
printf("\n");
|
|
loaded.print(stdout);
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: %s (%d)\n", e.what(), e.code());
|
|
printf("Use --log and look at the trace logs for more detailed information on the failure.\n");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
Reference<ClusterConnectionFile> ccf;
|
|
std::pair<std::string, bool> resolvedClusterFile = ClusterConnectionFile::lookupClusterFileName(opt.clusterFile);
|
|
|
|
try {
|
|
ccf = makeReference<ClusterConnectionFile>(resolvedClusterFile.first);
|
|
} catch (Error& e) {
|
|
if (e.code() == error_code_operation_cancelled) {
|
|
throw;
|
|
}
|
|
fprintf(stderr, "%s\n", ClusterConnectionFile::getErrorString(resolvedClusterFile, e).c_str());
|
|
return 1;
|
|
}
|
|
|
|
// Make sure that TLS configuration lines up with ":tls" prefix on coordinator addresses
|
|
if (auto errorMsg = checkTlsConfigAgainstCoordAddrs(ccf->getConnectionString())) {
|
|
fprintf(stderr, "ERROR: %s\n", errorMsg);
|
|
return 1;
|
|
}
|
|
|
|
try {
|
|
API->selectApiVersion(opt.apiVersion);
|
|
if (opt.useFutureProtocolVersion) {
|
|
API->useFutureProtocolVersion();
|
|
}
|
|
API->setupNetwork();
|
|
opt.setupKnobs();
|
|
if (opt.exit_code != -1) {
|
|
return opt.exit_code;
|
|
}
|
|
Future<Void> memoryUsageMonitor = startMemoryUsageMonitor(opt.memLimit);
|
|
Future<int> cliFuture = runCli(opt, ccf);
|
|
Future<Void> timeoutFuture = opt.exit_timeout ? timeExit(opt.exit_timeout) : Never();
|
|
auto f = stopNetworkAfter(success(cliFuture) || timeoutFuture);
|
|
API->runNetwork();
|
|
|
|
if (cliFuture.isReady()) {
|
|
return cliFuture.get();
|
|
} else {
|
|
return 1;
|
|
}
|
|
} catch (Error& e) {
|
|
fprintf(stderr, "ERROR: %s (%d)\n", e.what(), e.code());
|
|
return 1;
|
|
}
|
|
}
|