foundationdb/fdbcli/fdbcli.actor.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2219 lines
71 KiB
C++
Raw Normal View History

2017-05-26 04:48:44 +08:00
/*
* fdbcli.actor.cpp
*
* This source file is part of the FoundationDB open source project
*
2022-03-22 04:36:23 +08:00
* Copyright 2013-2022 Apple Inc. and the FoundationDB project authors
*
2017-05-26 04:48:44 +08:00
* 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
*
2017-05-26 04:48:44 +08:00
* http://www.apache.org/licenses/LICENSE-2.0
*
2017-05-26 04:48:44 +08:00
* 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"
2020-09-10 02:54:58 +08:00
#include "fdbclient/FDBTypes.h"
#include "fdbclient/IClientApi.h"
#include "fdbclient/MultiVersionTransaction.h"
2017-05-26 04:48:44 +08:00
#include "fdbclient/Status.h"
#include "fdbclient/KeyBackedTypes.h"
2017-05-26 04:48:44 +08:00
#include "fdbclient/StatusClient.h"
#include "fdbclient/DatabaseContext.h"
#include "fdbclient/GlobalConfig.actor.h"
2021-06-03 14:40:52 +08:00
#include "fdbclient/IKnobCollection.h"
#include "fdbclient/NativeAPI.actor.h"
2017-05-26 04:48:44 +08:00
#include "fdbclient/ClusterInterface.h"
#include "fdbclient/ManagementAPI.actor.h"
#include "fdbclient/Schemas.h"
2017-05-26 04:48:44 +08:00
#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"
2017-05-26 04:48:44 +08:00
#include "fdbclient/ThreadSafeTransaction.h"
#include "flow/flow.h"
#include "flow/ApiVersion.h"
#include "flow/ArgParseUtil.h"
2017-05-26 04:48:44 +08:00
#include "flow/DeterministicRandom.h"
2021-07-13 14:04:23 +08:00
#include "flow/FastRef.h"
#include "flow/Platform.h"
#include "flow/SystemMonitor.h"
#include "flow/CodeProbe.h"
2017-05-26 04:48:44 +08:00
#include "flow/TLSConfig.actor.h"
#include "flow/ThreadHelper.actor.h"
2022-06-28 07:05:55 +08:00
#include "SimpleOpt/SimpleOpt.h"
2017-05-26 04:48:44 +08:00
#include "fdbcli/FlowLineNoise.h"
#include "fdbcli/fdbcli.actor.h"
2017-05-26 04:48:44 +08:00
#include <cinttypes>
2022-02-20 07:25:51 +08:00
#include <cstdio>
#include <type_traits>
2017-05-26 04:48:44 +08:00
#include <signal.h>
#ifdef __unixish__
#include <stdio.h>
2022-06-28 09:20:18 +08:00
#include "linenoise/linenoise.h"
2017-05-26 04:48:44 +08:00
#endif
#include "fdbclient/versions.h"
2020-09-11 04:54:33 +08:00
#include "fdbclient/BuildFlags.h"
2017-05-26 04:48:44 +08:00
#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)
2019-11-16 04:26:51 +08:00
extern const char* getSourceVersion();
2017-05-26 04:48:44 +08:00
std::vector<std::string> validOptions;
2019-07-01 01:24:55 +08:00
enum {
OPT_CONNFILE,
OPT_DATABASE,
OPT_HELP,
OPT_TRACE,
OPT_TRACE_DIR,
2022-02-01 01:47:39 +08:00
OPT_LOGGROUP,
2019-07-01 01:24:55 +08:00
OPT_TIMEOUT,
OPT_EXEC,
OPT_NO_STATUS,
OPT_NO_HINTS,
2019-07-01 01:24:55 +08:00
OPT_STATUS_FROM_JSON,
OPT_VERSION,
2020-09-11 04:54:33 +08:00
OPT_BUILD_FLAGS,
OPT_TRACE_FORMAT,
OPT_KNOB,
2021-10-06 03:06:12 +08:00
OPT_DEBUG_TLS,
OPT_API_VERSION,
OPT_MEMORY,
OPT_USE_FUTURE_PROTOCOL_VERSION
2019-07-01 01:24:55 +08:00
};
CSimpleOpt::SOption g_rgOptions[] = { { OPT_CONNFILE, "-C", SO_REQ_SEP },
Unify flags (#25) * Unify flags implementation and change help text in backup.actor.cpp Description Testing * Keep LOG_GROUP unchanged Description Testing * Transfer the hyphens to underscores for internal options and user's input, EXCEPT leading hyphens Description Testing * Use a deep copy of the user's input flag to do the match Description Testing * Convert the _ to - in Option arrays of backup.actor.cpp Description Testing * Transter _ to - for files: TLSConfig.actor.h, fdbcli.actor.cpp, fdbserver.actor.cpp, FileConverter.h, FileConverter.cpp Description Testing * Change another way to unify flag: using SO_O_ICASE_HYPHEN_AND_UNDERSCORE to determine whether we do the conversion in function IsEqual Description Testing * Change the config command's name from SO_O_ICASE_HYPHEN_AND_UNDERSCORE to SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Update the comment for the SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Fix left underscore in SOption arrays Description Testing * Convert _ to - in several files for commands Description Testing * Make the FDBService and fdbmonitor backward compatible Description Testing * Fix bugs about pointers Description Testing * Check underscore and hyphen at the same time for --knob_, --localily_ and --test_ And fix bugs in fdbmonitor and FDBService Description Testing * Simplify the function in fdbmonitor and FDBService about retrieving arguments. And fix some documents in masterserver.actor.cpp Description Testing * Convert _ to - for knob in the setKnob functions Description Testing * Convert - to _ in the setKnob functions Description Since key in the knob related maps only contain _ Testing * Rename varialbe name in the fdbmonitor and FDBService for clarification Description Testing Co-authored-by: Chang Liu <chang.liu@snowflake.com>
2021-12-15 00:44:39 +08:00
{ OPT_CONNFILE, "--cluster-file", SO_REQ_SEP },
2019-07-01 01:24:55 +08:00
{ OPT_DATABASE, "-d", SO_REQ_SEP },
{ OPT_TRACE, "--log", SO_NONE },
{ OPT_TRACE_DIR, "--log-dir", SO_REQ_SEP },
2022-02-01 01:47:39 +08:00
{ OPT_LOGGROUP, "--log-group", SO_REQ_SEP },
2019-07-01 01:24:55 +08:00
{ 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 },
2019-07-01 01:24:55 +08:00
{ 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 },
Unify flags (#25) * Unify flags implementation and change help text in backup.actor.cpp Description Testing * Keep LOG_GROUP unchanged Description Testing * Transfer the hyphens to underscores for internal options and user's input, EXCEPT leading hyphens Description Testing * Use a deep copy of the user's input flag to do the match Description Testing * Convert the _ to - in Option arrays of backup.actor.cpp Description Testing * Transter _ to - for files: TLSConfig.actor.h, fdbcli.actor.cpp, fdbserver.actor.cpp, FileConverter.h, FileConverter.cpp Description Testing * Change another way to unify flag: using SO_O_ICASE_HYPHEN_AND_UNDERSCORE to determine whether we do the conversion in function IsEqual Description Testing * Change the config command's name from SO_O_ICASE_HYPHEN_AND_UNDERSCORE to SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Update the comment for the SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Fix left underscore in SOption arrays Description Testing * Convert _ to - in several files for commands Description Testing * Make the FDBService and fdbmonitor backward compatible Description Testing * Fix bugs about pointers Description Testing * Check underscore and hyphen at the same time for --knob_, --localily_ and --test_ And fix bugs in fdbmonitor and FDBService Description Testing * Simplify the function in fdbmonitor and FDBService about retrieving arguments. And fix some documents in masterserver.actor.cpp Description Testing * Convert _ to - for knob in the setKnob functions Description Testing * Convert - to _ in the setKnob functions Description Since key in the knob related maps only contain _ Testing * Rename varialbe name in the fdbmonitor and FDBService for clarification Description Testing Co-authored-by: Chang Liu <chang.liu@snowflake.com>
2021-12-15 00:44:39 +08:00
{ 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 },
2021-10-06 03:06:12 +08:00
{ 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 };
2017-05-26 04:48:44 +08:00
void printAtCol(const char* text, int col, FILE* stream = stdout) {
2017-05-26 04:48:44 +08:00
const char* iter = text;
const char* start = text;
2020-08-19 05:30:20 +08:00
const char* space = nullptr;
2017-05-26 04:48:44 +08:00
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);
2017-05-26 04:48:44 +08:00
start = space;
if (*start == ' ' || *start == '\n')
start++;
2020-08-19 05:30:20 +08:00
space = nullptr;
2017-05-26 04:48:44 +08:00
}
} 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,
2017-05-26 04:48:44 +08:00
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());
2017-05-26 04:48:44 +08:00
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>());
}
}
2017-05-26 04:48:44 +08:00
// Returns true if any options have been set
2020-12-27 13:46:20 +08:00
bool hasAnyOptionsEnabled() const { return !transactionOptions.options.empty(); }
2017-05-26 04:48:44 +08:00
// Prints a list of enabled options, along with their parameters (if any)
2020-12-27 13:46:20 +08:00
void print() const {
2017-05-26 04:48:44 +08:00
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
2020-12-27 13:46:20 +08:00
std::vector<std::string> getValidOptions() const { return transactionOptions.getValidOptions(); }
2017-05-26 04:48:44 +08:00
// Prints the help string obtained by invoking `help options'
2020-12-27 13:46:20 +08:00
void printHelpString() const { transactionOptions.printHelpString(); }
2017-05-26 04:48:44 +08:00
private:
// Sets a transaction option. If intrans == true, then this option is also applied to the passed in transaction.
void setTransactionOption(Reference<ITransaction> tr,
2017-05-26 04:48:44 +08:00
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();
}
}
2017-05-26 04:48:44 +08:00
}
if (intrans) {
2017-05-26 04:48:44 +08:00
tr->setOption(option, arg);
}
2017-05-26 04:48:44 +08:00
transactionOptions.setOption(option, enabled, arg.castTo<StringRef>());
2017-05-26 04:48:44 +08:00
}
// 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>>();
2017-05-26 04:48:44 +08:00
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
2020-12-27 13:46:20 +08:00
bool print() const {
2017-05-26 04:48:44 +08:00
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
2020-12-27 13:46:20 +08:00
bool isDocumented(typename T::Option option) const {
FDBOptionInfo info = T::optionInfo.getMustExist(option);
2017-05-26 04:48:44 +08:00
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
2020-12-27 13:46:20 +08:00
std::vector<std::string> getValidOptions() const {
2017-05-26 04:48:44 +08:00
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.
2020-12-27 13:46:20 +08:00
void printHelpString() const {
2017-05-26 04:48:44 +08:00
for (auto itr = legalOptions.begin(); itr != legalOptions.end(); ++itr) {
if (isDocumented(itr->second)) {
FDBOptionInfo info = T::optionInfo.getMustExist(itr->second);
2017-05-26 04:48:44 +08:00
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
2017-05-26 04:48:44 +08:00
i++;
break;
2017-05-26 04:48:44 +08:00
case '"':
quoted = !quoted;
line.erase(i, 1);
forcetoken = true;
break;
2017-05-26 04:48:44 +08:00
case ' ':
case '\n':
case '\t':
case '\r':
2017-05-26 04:48:44 +08:00
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);
2017-05-26 04:48:44 +08:00
forcetoken = false;
} else
i++;
break;
case '\\':
if (i + 2 > line.length()) {
err = true;
ret.push_back(std::move(buf));
return ret;
}
2017-05-26 04:48:44 +08:00
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;
2017-05-26 04:48:44 +08:00
default:
err = true;
ret.push_back(std::move(buf));
return ret;
}
default:
2017-05-26 04:48:44 +08:00
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"
2019-07-01 01:24:55 +08:00
" --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"
2022-02-01 01:47:39 +08:00
" --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"
Unify flags (#25) * Unify flags implementation and change help text in backup.actor.cpp Description Testing * Keep LOG_GROUP unchanged Description Testing * Transfer the hyphens to underscores for internal options and user's input, EXCEPT leading hyphens Description Testing * Use a deep copy of the user's input flag to do the match Description Testing * Convert the _ to - in Option arrays of backup.actor.cpp Description Testing * Transter _ to - for files: TLSConfig.actor.h, fdbcli.actor.cpp, fdbserver.actor.cpp, FileConverter.h, FileConverter.cpp Description Testing * Change another way to unify flag: using SO_O_ICASE_HYPHEN_AND_UNDERSCORE to determine whether we do the conversion in function IsEqual Description Testing * Change the config command's name from SO_O_ICASE_HYPHEN_AND_UNDERSCORE to SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Update the comment for the SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Fix left underscore in SOption arrays Description Testing * Convert _ to - in several files for commands Description Testing * Make the FDBService and fdbmonitor backward compatible Description Testing * Fix bugs about pointers Description Testing * Check underscore and hyphen at the same time for --knob_, --localily_ and --test_ And fix bugs in fdbmonitor and FDBService Description Testing * Simplify the function in fdbmonitor and FDBService about retrieving arguments. And fix some documents in masterserver.actor.cpp Description Testing * Convert _ to - for knob in the setKnob functions Description Testing * Convert - to _ in the setKnob functions Description Since key in the knob related maps only contain _ Testing * Rename varialbe name in the fdbmonitor and FDBService for clarification Description Testing Co-authored-by: Chang Liu <chang.liu@snowflake.com>
2021-12-15 00:44:39 +08:00
" --trace-format FORMAT\n"
2019-07-01 01:24:55 +08:00
" 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"
2021-10-06 03:06:12 +08:00
" --api-version APIVERSION\n"
" Specifies the version of the API for the CLI to use.\n" TLS_HELP
Unify flags (#25) * Unify flags implementation and change help text in backup.actor.cpp Description Testing * Keep LOG_GROUP unchanged Description Testing * Transfer the hyphens to underscores for internal options and user's input, EXCEPT leading hyphens Description Testing * Use a deep copy of the user's input flag to do the match Description Testing * Convert the _ to - in Option arrays of backup.actor.cpp Description Testing * Transter _ to - for files: TLSConfig.actor.h, fdbcli.actor.cpp, fdbserver.actor.cpp, FileConverter.h, FileConverter.cpp Description Testing * Change another way to unify flag: using SO_O_ICASE_HYPHEN_AND_UNDERSCORE to determine whether we do the conversion in function IsEqual Description Testing * Change the config command's name from SO_O_ICASE_HYPHEN_AND_UNDERSCORE to SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Update the comment for the SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Fix left underscore in SOption arrays Description Testing * Convert _ to - in several files for commands Description Testing * Make the FDBService and fdbmonitor backward compatible Description Testing * Fix bugs about pointers Description Testing * Check underscore and hyphen at the same time for --knob_, --localily_ and --test_ And fix bugs in fdbmonitor and FDBService Description Testing * Simplify the function in fdbmonitor and FDBService about retrieving arguments. And fix some documents in masterserver.actor.cpp Description Testing * Convert _ to - for knob in the setKnob functions Description Testing * Convert - to _ in the setKnob functions Description Since key in the knob related maps only contain _ Testing * Rename varialbe name in the fdbmonitor and FDBService for clarification Description Testing Co-authored-by: Chang Liu <chang.liu@snowflake.com>
2021-12-15 00:44:39 +08:00
" --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"
Unify flags (#25) * Unify flags implementation and change help text in backup.actor.cpp Description Testing * Keep LOG_GROUP unchanged Description Testing * Transfer the hyphens to underscores for internal options and user's input, EXCEPT leading hyphens Description Testing * Use a deep copy of the user's input flag to do the match Description Testing * Convert the _ to - in Option arrays of backup.actor.cpp Description Testing * Transter _ to - for files: TLSConfig.actor.h, fdbcli.actor.cpp, fdbserver.actor.cpp, FileConverter.h, FileConverter.cpp Description Testing * Change another way to unify flag: using SO_O_ICASE_HYPHEN_AND_UNDERSCORE to determine whether we do the conversion in function IsEqual Description Testing * Change the config command's name from SO_O_ICASE_HYPHEN_AND_UNDERSCORE to SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Update the comment for the SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Fix left underscore in SOption arrays Description Testing * Convert _ to - in several files for commands Description Testing * Make the FDBService and fdbmonitor backward compatible Description Testing * Fix bugs about pointers Description Testing * Check underscore and hyphen at the same time for --knob_, --localily_ and --test_ And fix bugs in fdbmonitor and FDBService Description Testing * Simplify the function in fdbmonitor and FDBService about retrieving arguments. And fix some documents in masterserver.actor.cpp Description Testing * Convert _ to - for knob in the setKnob functions Description Testing * Convert - to _ in the setKnob functions Description Since key in the knob related maps only contain _ Testing * Rename varialbe name in the fdbmonitor and FDBService for clarification Description Testing Co-authored-by: Chang Liu <chang.liu@snowflake.com>
2021-12-15 00:44:39 +08:00
" --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"
2019-07-01 01:24:55 +08:00
" -v, --version Print FoundationDB CLI version information and exit.\n"
" -h, --help Display this help and exit.\n");
2017-05-26 04:48:44 +08:00
}
#define ESCAPINGK "\n\nFor information on escaping keys, type `help escaping'."
#define ESCAPINGKV "\n\nFor information on escaping keys and values, type `help escaping'."
2021-03-30 03:51:32 +08:00
using namespace fdb_cli;
std::map<std::string, CommandHelp>& helpMap = CommandFactory::commands();
std::set<std::string>& hiddenCommands = CommandFactory::hiddenCommands();
2017-05-26 04:48:44 +08:00
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();
2019-08-17 09:13:35 +08:00
helpMap["sleep"] = CommandHelp("sleep <SECONDS>", "sleep for a period of time", "");
2017-05-26 04:48:44 +08:00
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);
2020-03-31 08:10:00 +08:00
helpMap["getversion"] =
CommandHelp("getversion",
"Fetch the current read version",
"Displays the current read version of the database or currently running transaction.");
2022-09-26 12:00:11 +08:00
helpMap["quota"] = CommandHelp("quota",
"quota [get <tag> [reserved_throughput|total_throughput] | set <tag> "
"[reserved_throughput|total_throughput] <value>]",
"Get or modify the throughput quota for the specified tag.");
2017-05-26 04:48:44 +08:00
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",
2017-05-26 04:48:44 +08:00
"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.");
2022-02-20 07:25:51 +08:00
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.");
2017-05-26 04:48:44 +08:00
}
void printVersion() {
printf("FoundationDB CLI " FDB_VT_PACKAGE_NAME " (v" FDB_VT_VERSION ")\n");
2019-11-16 04:26:51 +08:00
printf("source version %s\n", getSourceVersion());
printf("protocol %" PRIx64 "\n", currentProtocolVersion().version());
2017-05-26 04:48:44 +08:00
}
2020-09-11 04:54:33 +08:00
void printBuildInformation() {
printf("%s", jsonBuildInformation().c_str());
}
2017-05-26 04:48:44 +08:00
void printHelpOverview() {
printf("\nList of commands:\n\n");
2020-12-27 13:46:20 +08:00
for (const auto& [command, help] : helpMap) {
if (help.short_desc.size())
printf(" %s:\n %s\n", command.c_str(), help.short_desc.c_str());
}
2017-05-26 04:48:44 +08:00
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));
2017-05-26 04:48:44 +08:00
fputs(msg, stderr);
return Void();
}
ACTOR Future<Void> checkStatus(Future<Void> f,
Reference<IDatabase> db,
Database localDb,
bool displayDatabaseAvailable = true) {
wait(f);
2021-07-13 14:04:23 +08:00
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("\xff\xff/status/json"_sr);
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());
2021-07-13 14:04:23 +08:00
}
2017-05-26 04:48:44 +08:00
printf("\n");
printStatus(s, StatusClient::MINIMAL, displayDatabaseAvailable);
2017-05-26 04:48:44 +08:00
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)) {
2017-05-26 04:48:44 +08:00
f.cancel();
throw operation_cancelled();
}
}
}
ACTOR Future<Void> commitTransaction(Reference<ITransaction> tr) {
wait(makeInterruptable(safeThreadFutureToFuture(tr->commit())));
2017-05-26 04:48:44 +08:00
auto ver = tr->getCommittedVersion();
if (ver != invalidVersion)
fmt::print("Committed ({})\n", ver);
2017-05-26 04:48:44 +08:00
else
fmt::print("Nothing to commit\n");
2017-05-26 04:48:44 +08:00
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(" "_sr);
}
}
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,
2022-02-20 07:25:51 +08:00
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) {
2022-02-20 07:25:51 +08:00
if (tenant) {
tr = tenant->createTransaction();
} else {
tr = db->createTransaction();
}
options->apply(tr);
}
return tr;
}
std::string newCompletion(const char* base, const char* name) {
2017-05-26 04:48:44 +08:00
return format("%s%s ", base, name);
}
void compGenerator(const char* text, bool help, std::vector<std::string>& lc) {
2017-05-26 04:48:44 +08:00
std::map<std::string, CommandHelp>::const_iterator iter;
int len = strlen(text);
2020-08-19 05:30:20 +08:00
const char* helpExtra[] = { "escaping", "options", nullptr };
2017-05-26 04:48:44 +08:00
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));
2017-05-26 04:48:44 +08:00
}
}
if (help) {
while (*he) {
const char* name = *he;
he++;
if (!strncmp(name, text, len))
lc.push_back(newCompletion("help ", name));
2017-05-26 04:48:44 +08:00
}
}
}
void cmdGenerator(const char* text, std::vector<std::string>& lc) {
compGenerator(text, false, lc);
2017-05-26 04:48:44 +08:00
}
void helpGenerator(const char* text, std::vector<std::string>& lc) {
compGenerator(text, true, lc);
2017-05-26 04:48:44 +08:00
}
void optionGenerator(const char* text, const char* line, std::vector<std::string>& lc) {
2017-05-26 04:48:44 +08:00
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));
2017-05-26 04:48:44 +08:00
}
}
}
namespace fdb_cli {
void arrayGenerator(const char* text, const char* line, const char** options, std::vector<std::string>& lc) {
2017-05-26 04:48:44 +08:00
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));
2017-05-26 04:48:44 +08:00
}
}
}
} // namespace fdb_cli
2017-05-26 04:48:44 +08:00
void onOffGenerator(const char* text, const char* line, std::vector<std::string>& lc) {
const char* opts[] = { "on", "off", nullptr };
arrayGenerator(text, line, opts, lc);
2017-05-26 04:48:44 +08:00
}
void fdbcliCompCmd(std::string const& text, std::vector<std::string>& lc) {
2017-05-26 04:48:44 +08:00
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());
2017-05-26 04:48:44 +08:00
// }
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());
2017-05-26 04:48:44 +08:00
if (!count) {
cmdGenerator(ntext.c_str(), lc);
2017-05-26 04:48:44 +08:00
return;
}
if (tokencmp(tokens[0], "help") && count == 1) {
helpGenerator(ntext.c_str(), lc);
2017-05-26 04:48:44 +08:00
return;
}
if (tokencmp(tokens[0], "option")) {
if (count == 1)
onOffGenerator(ntext.c_str(), base_input.c_str(), lc);
2017-05-26 04:48:44 +08:00
if (count == 2)
optionGenerator(ntext.c_str(), base_input.c_str(), lc);
2017-05-26 04:48:44 +08:00
}
if (tokencmp(tokens[0], "writemode") && count == 1) {
onOffGenerator(ntext.c_str(), base_input.c_str(), lc);
2017-05-26 04:48:44 +08:00
}
auto itr = CommandFactory::completionGenerators().find(tokens[0].toString());
if (itr != CommandFactory::completionGenerators().end()) {
itr->second(ntext.c_str(), base_input.c_str(), lc, tokens);
}
}
2017-05-26 04:48:44 +08:00
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);
2017-05-26 04:48:44 +08:00
}
struct CLIOptions {
std::string program_name;
int exit_code = -1;
2017-05-26 04:48:44 +08:00
std::string commandLine;
std::string clusterFile;
bool trace = false;
2017-05-26 04:48:44 +08:00
std::string traceDir;
std::string traceFormat;
2022-02-01 01:47:39 +08:00
std::string logGroup;
int exit_timeout = 0;
2017-05-26 04:48:44 +08:00
Optional<std::string> exec;
bool initialStatusCheck = true;
bool cliHints = true;
bool useFutureProtocolVersion = false;
bool debugTLS = false;
2017-05-26 04:48:44 +08:00
std::string tlsCertPath;
std::string tlsKeyPath;
std::string tlsVerifyPeers;
std::string tlsCAPath;
2018-05-09 11:46:31 +08:00
std::string tlsPassword;
uint64_t memLimit = 8uLL << 30;
2017-05-26 04:48:44 +08:00
std::vector<std::pair<std::string, std::string>> knobs;
2021-10-06 03:06:12 +08:00
// api version, using the latest version by default
int apiVersion = ApiVersion::LATEST_VERSION;
2021-10-06 03:06:12 +08:00
CLIOptions(int argc, char* argv[]) {
2017-05-26 04:48:44 +08:00
program_name = argv[0];
for (int a = 0; a < argc; a++) {
if (a)
commandLine += ' ';
commandLine += argv[a];
}
Unify flags (#25) * Unify flags implementation and change help text in backup.actor.cpp Description Testing * Keep LOG_GROUP unchanged Description Testing * Transfer the hyphens to underscores for internal options and user's input, EXCEPT leading hyphens Description Testing * Use a deep copy of the user's input flag to do the match Description Testing * Convert the _ to - in Option arrays of backup.actor.cpp Description Testing * Transter _ to - for files: TLSConfig.actor.h, fdbcli.actor.cpp, fdbserver.actor.cpp, FileConverter.h, FileConverter.cpp Description Testing * Change another way to unify flag: using SO_O_ICASE_HYPHEN_AND_UNDERSCORE to determine whether we do the conversion in function IsEqual Description Testing * Change the config command's name from SO_O_ICASE_HYPHEN_AND_UNDERSCORE to SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Update the comment for the SO_O_HYPHEN_TO_UNDERSCORE Description Testing * Fix left underscore in SOption arrays Description Testing * Convert _ to - in several files for commands Description Testing * Make the FDBService and fdbmonitor backward compatible Description Testing * Fix bugs about pointers Description Testing * Check underscore and hyphen at the same time for --knob_, --localily_ and --test_ And fix bugs in fdbmonitor and FDBService Description Testing * Simplify the function in fdbmonitor and FDBService about retrieving arguments. And fix some documents in masterserver.actor.cpp Description Testing * Convert _ to - for knob in the setKnob functions Description Testing * Convert - to _ in the setKnob functions Description Since key in the knob related maps only contain _ Testing * Rename varialbe name in the fdbmonitor and FDBService for clarification Description Testing Co-authored-by: Chang Liu <chang.liu@snowflake.com>
2021-12-15 00:44:39 +08:00
CSimpleOpt args(argc, argv, g_rgOptions, SO_O_HYPHEN_TO_UNDERSCORE);
2017-05-26 04:48:44 +08:00
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;
2017-05-26 04:48:44 +08:00
return;
}
2021-11-17 02:39:22 +08:00
}
2021-11-17 02:39:22 +08:00
void setupKnobs() {
IKnobCollection::setupKnobs(knobs);
2020-04-02 04:59:06 +08:00
// Reinitialize knobs in order to update knobs that are dependent on explicitly set knobs
IKnobCollection::getMutableGlobalKnobCollection().initialize(Randomize::False, IsSimulated::False);
2017-05-26 04:48:44 +08:00
}
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;
2021-10-06 03:06:12 +08:00
case OPT_API_VERSION: {
char* endptr;
apiVersion = strtoul((char*)args.OptionArg(), &endptr, 10);
2021-10-06 03:06:12 +08:00
if (*endptr != '\0') {
fprintf(stderr, "ERROR: invalid client version %s\n", args.OptionArg());
return 1;
} else if (apiVersion < 700 || apiVersion > ApiVersion::LATEST_VERSION) {
2021-10-06 03:06:12 +08:00
// 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);
2021-10-06 03:06:12 +08:00
return 1;
}
break;
}
case OPT_MEMORY: {
std::string memoryArg(args.OptionArg());
memLimit = parse_with_suffix(memoryArg, "MiB").orDefault(8uLL << 30);
break;
}
2017-05-26 04:48:44 +08:00
case OPT_TRACE:
trace = true;
break;
case OPT_TRACE_DIR:
traceDir = args.OptionArg();
break;
2022-02-01 01:47:39 +08:00
case OPT_LOGGROUP:
logGroup = args.OptionArg();
break;
2017-05-26 04:48:44 +08:00
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;
}
2017-05-26 04:48:44 +08:00
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;
2017-05-26 04:48:44 +08:00
// 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;
2022-05-03 13:15:27 +08:00
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;
2020-09-11 04:54:33 +08:00
case OPT_BUILD_FLAGS:
printBuildInformation();
return FDB_EXIT_SUCCESS;
}
return -1;
2017-05-26 04:48:44 +08:00
}
};
ACTOR template <class T>
Future<T> stopNetworkAfter(Future<T> what) {
try {
T t = wait(what);
API->stopNetwork();
2017-05-26 04:48:44 +08:00
return t;
} catch (...) {
API->stopNetwork();
2017-05-26 04:48:44 +08:00
throw;
}
}
ACTOR Future<int> cli(CLIOptions opt, LineNoise* plinenoise, Reference<ClusterConnectionFile> ccf) {
2017-05-26 04:48:44 +08:00
state LineNoise& linenoise = *plinenoise;
state bool intrans = false;
2021-09-09 01:42:46 +08:00
state Database localDb;
state Reference<IDatabase> db;
2022-02-20 07:25:51 +08:00
state Reference<ITenant> tenant;
state Optional<TenantName> tenantName;
2022-02-20 07:25:51 +08:00
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;
2017-05-26 04:48:44 +08:00
state bool writeMode = false;
state std::map<Key, std::pair<Value, ClientLeaderRegInterface>> address_interface;
2017-05-26 04:48:44 +08:00
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());
}
2022-07-08 05:48:36 +08:00
db = API->createDatabase(opt.clusterFile.c_str());
} catch (Error& e) {
2021-04-23 16:38:25 +08:00
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;
}
2017-05-26 04:48:44 +08:00
if (opt.trace) {
TraceEvent("CLIProgramStart")
.setMaxEventLength(12000)
2019-11-16 04:26:51 +08:00
.detail("SourceVersion", getSourceVersion())
2017-05-26 04:48:44 +08:00
.detail("Version", FDB_VT_VERSION)
.detail("PackageName", FDB_VT_PACKAGE_NAME)
2020-08-19 05:30:20 +08:00
.detailf("ActualTime", "%lld", DEBUG_DETERMINISM ? 0 : time(nullptr))
.detail("ClusterFile", ccf->toString())
2017-05-26 04:48:44 +08:00
.detail("ConnectionString", ccf->getConnectionString().toString())
.setMaxFieldLength(10000)
2017-05-26 04:48:44 +08:00
.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 {
2022-02-20 07:25:51 +08:00
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) {
2021-09-09 01:42:46 +08:00
Future<Void> checkStatusF = checkStatus(Void(), db, localDb);
wait(makeInterruptable(success(checkStatusF)));
} else {
printf("\n");
2017-05-26 04:48:44 +08:00
}
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
2020-04-02 08:39:16 +08:00
if (line.find("writemode") == std::string::npos && line.find("expensive_data_check") == std::string::npos &&
2021-07-07 03:49:36 +08:00
line.find("unlock") == std::string::npos && line.find("blobrange") == std::string::npos)
2017-05-26 04:48:44 +08:00
linenoise.historyAdd(line);
}
2021-09-09 01:42:46 +08:00
warn = checkStatus(timeWarning(5.0, "\nWARNING: Long delay (Ctrl-C to interrupt)\n"), db, localDb);
2017-05-26 04:48:44 +08:00
try {
state UID randomID = deterministicRandom()->randomUniqueID();
TraceEvent(SevInfo, "CLICommandLog", randomID).detail("Command", line);
2017-05-26 04:48:44 +08:00
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")));
}
2017-05-26 04:48:44 +08:00
}
state bool multi = parsed.size() > 1;
is_error = false;
2017-05-26 04:48:44 +08:00
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) {
2017-05-26 04:48:44 +08:00
printf("WARNING: the previous command failed, the remaining commands will not be executed.\n");
break;
2017-05-26 04:48:44 +08:00
}
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;
}
2017-05-26 04:48:44 +08:00
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());
2017-05-26 04:48:44 +08:00
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")) {
2021-09-09 01:42:46 +08:00
wait(makeInterruptable(localDb->onConnected()));
2017-05-26 04:48:44 +08:00
continue;
}
if (tokencmp(tokens[0], "waitopen")) {
2022-02-20 07:25:51 +08:00
wait(makeInterruptable(success(safeThreadFutureToFuture(
getTransaction(db, managementTenant, tr, options, intrans)->getReadVersion()))));
2017-05-26 04:48:44 +08:00
continue;
}
2019-08-17 09:13:35 +08:00
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;
}
2017-05-26 04:48:44 +08:00
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");
2021-09-09 01:56:32 +08:00
bool _result = wait(makeInterruptable(statusCommandActor(db, localDb, tokens, opt.exec.present())));
2021-07-13 14:04:23 +08:00
if (!_result)
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "triggerddteaminfolog")) {
wait(success(makeInterruptable(triggerddteaminfologCommandActor(db))));
continue;
}
if (tokencmp(tokens[0], "tssq")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(tssqCommandActor(db, tokens)));
if (!_result)
is_error = true;
2021-03-05 13:31:21 +08:00
continue;
2021-03-05 11:14:54 +08:00
}
2017-05-26 04:48:44 +08:00
if (tokencmp(tokens[0], "configure")) {
bool _result =
wait(makeInterruptable(configureCommandActor(db, localDb, tokens, &linenoise, warn)));
if (!_result)
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "fileconfigure")) {
if (tokens.size() == 2 ||
(tokens.size() == 3 && (tokens[1] == "new"_sr || tokens[1] == "FORCE"_sr))) {
bool _result = wait(makeInterruptable(fileConfigureCommandActor(
db, tokens.back().toString(), tokens[1] == "new"_sr, tokens[1] == "FORCE"_sr)));
2021-09-21 06:40:53 +08:00
if (!_result)
is_error = true;
} else {
printUsage(tokens[0]);
is_error = true;
}
continue;
}
2017-05-26 04:48:44 +08:00
if (tokencmp(tokens[0], "coordinators")) {
bool _result = wait(makeInterruptable(coordinatorsCommandActor(db, tokens)));
if (!_result)
is_error = true;
2017-05-26 04:48:44 +08:00
continue;
}
if (tokencmp(tokens[0], "exclude")) {
2021-07-18 17:27:25 +08:00
bool _result = wait(makeInterruptable(excludeCommandActor(db, tokens, warn)));
if (!_result)
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "include")) {
bool _result = wait(makeInterruptable(includeCommandActor(db, tokens)));
if (!_result)
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "snapshot")) {
bool _result = wait(makeInterruptable(snapshotCommandActor(db, tokens)));
if (!_result)
is_error = true;
continue;
}
2019-08-28 04:15:30 +08:00
if (tokencmp(tokens[0], "lock")) {
bool _result = wait(makeInterruptable(lockCommandActor(db, tokens)));
if (!_result)
2021-10-20 04:56:52 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "changefeed")) {
2022-02-20 07:25:51 +08:00
bool _result = wait(makeInterruptable(changeFeedCommandActor(localDb, tenantEntry, tokens, warn)));
2021-10-20 04:56:52 +08:00
if (!_result)
2019-08-28 04:15:30 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "blobrange")) {
2022-02-20 07:25:51 +08:00
bool _result = wait(makeInterruptable(blobRangeCommandActor(localDb, tenantEntry, tokens)));
if (!_result)
2019-08-28 04:15:30 +08:00
is_error = true;
continue;
}
if (tokencmp(tokens[0], "blobkey")) {
bool _result = wait(makeInterruptable(blobKeyCommandActor(localDb, tenantEntry, tokens)));
if (!_result)
is_error = true;
continue;
}
2020-04-02 08:39:16 +08:00
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);
2021-06-17 08:35:44 +08:00
Optional<std::string> input =
wait(linenoise.read(format("Repeat the above passphrase if you would like to proceed:")));
2021-09-09 01:56:32 +08:00
warn =
checkStatus(timeWarning(5.0, "\nWARNING: Long delay (Ctrl-C to interrupt)\n"), db, localDb);
2020-04-02 08:39:16 +08:00
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;
2020-04-02 08:39:16 +08:00
} else {
fprintf(stderr, "ERROR: Incorrect passphrase entered.\n");
2020-04-02 08:39:16 +08:00
is_error = true;
}
}
continue;
}
2017-05-26 04:48:44 +08:00
if (tokencmp(tokens[0], "setclass")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(setClassCommandActor(db, tokens)));
2021-07-09 06:00:05 +08:00
if (!_result)
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
is_error = true;
} else {
activeOptions = FdbOptions(globalOptions);
options = &activeOptions;
2022-02-20 07:25:51 +08:00
getTransaction(db, tenant, tr, options, false);
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
is_error = true;
} else {
wait(commitTransaction(tr));
2017-05-26 04:48:44 +08:00
intrans = false;
options = &globalOptions;
}
continue;
}
2022-06-17 05:07:16 +08:00
if (tokencmp(tokens[0], "quota")) {
bool _result = wait(makeInterruptable(quotaCommandActor(db, tokens)));
if (!_result) {
is_error = true;
}
continue;
}
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
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 =
2022-02-20 07:25:51 +08:00
getTransaction(db, tenant, tr, options, intrans)->get(tokens[1]);
Optional<Standalone<StringRef>> v = wait(makeInterruptable(safeThreadFutureToFuture(valueF)));
2017-05-26 04:48:44 +08:00
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;
}
2020-03-31 08:10:00 +08:00
if (tokencmp(tokens[0], "getversion")) {
if (tokens.size() != 1) {
printUsage(tokens[0]);
is_error = true;
} else {
2022-02-20 07:25:51 +08:00
Version v = wait(makeInterruptable(safeThreadFutureToFuture(
getTransaction(db, tenant, tr, options, intrans)->getReadVersion())));
fmt::print("{}\n", v);
2020-03-31 08:10:00 +08:00
}
continue;
}
2020-04-16 11:01:01 +08:00
if (tokencmp(tokens[0], "advanceversion")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(advanceVersionCommandActor(db, tokens)));
2021-05-18 15:22:17 +08:00
if (!_result)
2020-04-16 11:01:01 +08:00
is_error = true;
continue;
}
Add fdbcli command to read/write version epoch (#6480) * Initialize cluster version at wall-clock time Previously, new clusters would begin at version 0. After this change, clusters will initialize at a version matching wall-clock time. Instead of using the Unix epoch (or Windows epoch), FDB clusters will use a new epoch, defaulting to January 1, 2010, 01:00:00+00:00. In the future, this base epoch will be modifiable through fdbcli, allowing administrators to advance the cluster version. Basing the version off of time allows different FDB clusters to share data without running into version issues. * Send version epoch to master * Cleanup * Update fdbserver/storageserver.actor.cpp Co-authored-by: A.J. Beamon <aj.beamon@snowflake.com> * Jump directly to expected version if possible * Fix initial version issue on storage servers * Add random recovery offset to start version in simulation * Type fixes * Disable reference time by default Enable on a cluster using the fdbcli command `versionepoch add 0`. * Use correct recoveryTransactionVersion when recovering * Allow version epoch to be adjusted forwards (to decrease the version) * Set version epoch in simulation * Add quiet database check to ensure small version offset * Fix initial version issue on storage servers * Disable reference time by default Enable on a cluster using the fdbcli command `versionepoch add 0`. * Add fdbcli command to read/write version epoch * Cause recovery when version epoch is set * Handle optional version epoch key * Add ability to clear the version epoch This causes version advancement to revert to the old methodology whereas versions attempt to advance by about a million versions per second, instead of trying to match the clock. * Update transaction access * Modify version epoch to use microseconds instead of seconds * Modify fdbcli version target API Move commands from `versionepoch` to `targetversion` top level command. * Add fdbcli tests for * Temporarily disable targetversion cli tests * Fix version epoch fetch issue * Fix Arena issue * Reduce max version jump in simulation to 1,000,000 * Rework fdbcli API It now requires two commands to fully switch a cluster to using the version epoch. First, enable the version epoch with `versionepoch enable` or `versionepoch set <versionepoch>`. At this point, versions will be given out at a faster or slower rate in an attempt to reach the expected version. Then, run `versionepoch commit` to perform a one time jump to the expected version. This is essentially irreversible. * Temporarily disable old targetversion tests * Cleanup * Move version epoch buggify to sequencer This will cause some issues with the QuietDatabase check for the version offset - namely, it won't do anything, since the version epoch is not being written to the txnStateStore in simulation. This will get fixed in the future. Co-authored-by: A.J. Beamon <aj.beamon@snowflake.com>
2022-04-09 03:33:19 +08:00
if (tokencmp(tokens[0], "versionepoch")) {
bool _result = wait(makeInterruptable(versionEpochCommandActor(db, localDb, tokens)));
if (!_result)
is_error = true;
continue;
}
2017-05-26 04:48:44 +08:00
if (tokencmp(tokens[0], "kill")) {
2022-02-20 07:25:51 +08:00
getTransaction(db, managementTenant, tr, options, intrans);
bool _result = wait(makeInterruptable(killCommandActor(db, tr, tokens, &address_interface)));
if (!_result)
is_error = true;
2017-05-26 04:48:44 +08:00
continue;
}
if (tokencmp(tokens[0], "suspend")) {
2022-02-20 07:25:51 +08:00
getTransaction(db, managementTenant, tr, options, intrans);
2021-09-09 01:42:46 +08:00
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")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(forceRecoveryWithDataLossCommandActor(db, tokens)));
if (!_result)
is_error = true;
continue;
}
if (tokencmp(tokens[0], "maintenance")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(maintenanceCommandActor(db, tokens)));
if (!_result)
is_error = true;
continue;
}
if (tokencmp(tokens[0], "consistencycheck")) {
2022-02-20 07:25:51 +08:00
getTransaction(db, managementTenant, tr, options, intrans);
bool _result = wait(makeInterruptable(consistencyCheckCommandActor(tr, tokens, intrans)));
if (!_result)
is_error = true;
continue;
}
if (tokencmp(tokens[0], "consistencyscan")) {
bool _result = wait(makeInterruptable(consistencyScanCommandActor(localDb, tokens)));
if (!_result)
is_error = true;
continue;
}
if (tokencmp(tokens[0], "profile")) {
2022-02-20 07:25:51 +08:00
getTransaction(db, managementTenant, tr, options, intrans);
bool _result = wait(makeInterruptable(profileCommandActor(localDb, tr, tokens, intrans)));
if (!_result)
is_error = true;
continue;
}
2017-05-26 04:48:44 +08:00
if (tokencmp(tokens[0], "expensive_data_check")) {
2022-02-20 07:25:51 +08:00
getTransaction(db, managementTenant, tr, options, intrans);
bool _result =
2021-09-09 01:42:46 +08:00
wait(makeInterruptable(expensiveDataCheckCommandActor(db, tr, tokens, &address_interface)));
if (!_result)
is_error = true;
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
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]);
}
2022-02-20 07:25:51 +08:00
getTransaction(db, tenant, tr, options, intrans);
state ThreadFuture<RangeResult> kvsF = tr->getRange(KeyRangeRef(tokens[1], endKey), limit);
RangeResult kvs = wait(makeInterruptable(safeThreadFutureToFuture(kvsF)));
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
2017-05-26 04:48:44 +08:00
if (tokens.size() != 3) {
printUsage(tokens[0]);
is_error = true;
} else {
2022-02-20 07:25:51 +08:00
getTransaction(db, tenant, tr, options, intrans);
2017-05-26 04:48:44 +08:00
tr->set(tokens[1], tokens[2]);
if (!intrans) {
wait(commitTransaction(tr));
2017-05-26 04:48:44 +08:00
}
}
continue;
}
if (tokencmp(tokens[0], "clear")) {
if (!writeMode) {
fprintf(stderr, "ERROR: writemode must be enabled to set or clear keys in the database.\n");
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
2017-05-26 04:48:44 +08:00
if (tokens.size() != 2) {
printUsage(tokens[0]);
is_error = true;
} else {
2022-02-20 07:25:51 +08:00
getTransaction(db, tenant, tr, options, intrans);
2017-05-26 04:48:44 +08:00
tr->clear(tokens[1]);
if (!intrans) {
wait(commitTransaction(tr));
2017-05-26 04:48:44 +08:00
}
}
continue;
}
if (tokencmp(tokens[0], "clearrange")) {
if (!writeMode) {
fprintf(stderr, "ERROR: writemode must be enabled to set or clear keys in the database.\n");
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
2017-05-26 04:48:44 +08:00
if (tokens.size() != 3) {
printUsage(tokens[0]);
is_error = true;
} else {
2022-02-20 07:25:51 +08:00
getTransaction(db, tenant, tr, options, intrans);
2017-05-26 04:48:44 +08:00
tr->clear(KeyRangeRef(tokens[1], tokens[2]));
if (!intrans) {
wait(commitTransaction(tr));
2017-05-26 04:48:44 +08:00
}
}
continue;
}
if (tokencmp(tokens[0], "datadistribution")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(dataDistributionCommandActor(db, tokens)));
2021-06-24 03:49:19 +08:00
if (!_result)
2021-07-07 03:49:36 +08:00
is_error = true;
continue;
}
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
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");
2017-05-26 04:48:44 +08:00
is_error = true;
continue;
}
if (tokens.size() > 3) {
fprintf(stderr, "ERROR: Cannot specify option argument when turning option off\n");
2017-05-26 04:48:44 +08:00
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());
2017-05-26 04:48:44 +08:00
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]);
2017-05-26 04:48:44 +08:00
is_error = true;
}
}
continue;
}
if (tokencmp(tokens[0], "throttle")) {
bool _result = wait(makeInterruptable(throttleCommandActor(db, tokens)));
2021-07-28 01:58:11 +08:00
if (!_result)
is_error = true;
continue;
}
2021-07-28 01:58:11 +08:00
if (tokencmp(tokens[0], "cache_range")) {
2021-09-09 01:42:46 +08:00
bool _result = wait(makeInterruptable(cacheRangeCommandActor(db, tokens)));
2021-07-09 03:28:46 +08:00
if (!_result)
is_error = true;
continue;
}
2022-02-20 07:25:51 +08:00
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])));
2022-02-20 07:25:51 +08:00
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], "tenant")) {
bool _result = wait(makeInterruptable(tenantCommand(db, tokens)));
if (!_result) {
2022-02-20 07:25:51 +08:00
is_error = true;
} else if (tokens.size() >= 3 && tenantName.present() && tokencmp(tokens[1], "delete") &&
tokens[2] == tenantName.get()) {
printAtCol("WARNING: the active tenant was deleted. Use the `usetenant' or `defaulttenant' "
"command to choose a new tenant.\n",
80);
}
2022-02-20 07:25:51 +08:00
2022-07-09 06:56:22 +08:00
continue;
}
if (tokencmp(tokens[0], "createtenant") || tokencmp(tokens[0], "deletetenant") ||
tokencmp(tokens[0], "listtenants") || tokencmp(tokens[0], "gettenant") ||
tokencmp(tokens[0], "configuretenant") || tokencmp(tokens[0], "renametenant")) {
bool _result = wait(makeInterruptable(tenantCommandForwarder(db, tokens)));
if (!_result) {
is_error = true;
}
continue;
}
if (tokencmp(tokens[0], "tenantgroup")) {
bool _result = wait(makeInterruptable(tenantGroupCommand(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());
2017-05-26 04:48:44 +08:00
is_error = true;
}
TraceEvent(SevInfo, "CLICommandLog", randomID).detail("Command", line).detail("IsError", is_error);
2017-05-26 04:48:44 +08:00
} catch (Error& e) {
if (e.code() == error_code_operation_cancelled) {
throw;
}
2022-02-20 07:25:51 +08:00
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());
2022-02-20 07:25:51 +08:00
}
2017-05-26 04:48:44 +08:00
is_error = true;
if (intrans) {
printf("Rolling back current transaction\n");
intrans = false;
options = &globalOptions;
options->apply(tr);
2017-05-26 04:48:44 +08:00
}
}
if (opt.exec.present()) {
return is_error ? 1 : 0;
}
}
}
ACTOR Future<int> runCli(CLIOptions opt, Reference<ClusterConnectionFile> ccf) {
state LineNoise linenoise(
2017-05-26 04:48:44 +08:00
[](std::string const& line, std::vector<std::string>& completions) { fdbcliCompCmd(line, completions); },
[enabled = opt.cliHints](std::string const& line) -> LineNoise::Hint {
2020-03-04 10:14:57 +08:00
if (!enabled) {
return LineNoise::Hint();
}
2020-03-04 10:14:57 +08:00
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.
2020-03-04 10:14:57 +08:00
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);
2017-05-26 04:48:44 +08:00
},
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();
2017-05-26 04:48:44 +08:00
}
state int result = wait(cli(opt, &linenoise, ccf));
2017-05-26 04:48:44 +08:00
if (!historyFilename.empty()) {
try {
linenoise.historySave(historyFilename);
} catch (Error& e) {
TraceEvent(SevWarnAlways, "ErrorSavingCliHistory")
.error(e)
.detail("Filename", historyFilename)
.GetLastError();
2017-05-26 04:48:44 +08:00
}
}
return result;
}
ACTOR Future<Void> timeExit(double duration) {
wait(delay(duration));
2017-05-26 04:48:44 +08:00
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 == totalAddrs) {
return "fdbcli is not configured with TLS, but all of the coordinators have TLS addresses.";
} else {
return nullptr;
}
}
2017-05-26 04:48:44 +08:00
int main(int argc, char** argv) {
platformInit();
Error::init();
std::set_new_handler(&platform::outOfMemory);
2017-05-26 04:48:44 +08:00
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;
2020-08-19 05:30:20 +08:00
sigaction(SIGINT, &act, nullptr);
2017-05-26 04:48:44 +08:00
#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));
}
2017-05-26 04:48:44 +08:00
setNetworkOption(FDBNetworkOptions::ENABLE_SLOW_TASK_PROFILING);
2022-02-01 01:47:39 +08:00
if (!opt.logGroup.empty()) {
setNetworkOption(FDBNetworkOptions::TRACE_LOG_GROUP, StringRef(opt.logGroup));
}
2017-05-26 04:48:44 +08:00
}
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;
}
}
2017-05-26 04:48:44 +08:00
if (opt.tlsKeyPath.size()) {
try {
2018-05-09 11:46:31 +08:00
if (opt.tlsPassword.size())
2018-05-24 06:32:56 +08:00
setNetworkOption(FDBNetworkOptions::TLS_PASSWORD, opt.tlsPassword);
2018-05-09 11:46:31 +08:00
2017-05-26 04:48:44 +08:00
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;
}
2017-05-26 04:48:44 +08:00
try {
API->selectApiVersion(opt.apiVersion);
if (opt.useFutureProtocolVersion) {
API->useFutureProtocolVersion();
}
API->setupNetwork();
2021-11-17 02:39:22 +08:00
opt.setupKnobs();
if (opt.exit_code != -1) {
return opt.exit_code;
}
Future<Void> memoryUsageMonitor = startMemoryUsageMonitor(opt.memLimit);
Future<int> cliFuture = runCli(opt, ccf);
2017-05-26 04:48:44 +08:00
Future<Void> timeoutFuture = opt.exit_timeout ? timeExit(opt.exit_timeout) : Never();
auto f = stopNetworkAfter(success(cliFuture) || timeoutFuture);
API->runNetwork();
2017-05-26 04:48:44 +08:00
if (cliFuture.isReady()) {
return cliFuture.get();
} else {
return 1;
}
} catch (Error& e) {
fprintf(stderr, "ERROR: %s (%d)\n", e.what(), e.code());
2017-05-26 04:48:44 +08:00
return 1;
}
}