/* * 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/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 #include #include #include #ifdef __unixish__ #include #include "linenoise/linenoise.h" #endif #include "fdbclient/versions.h" #include "fdbclient/BuildFlags.h" #include "flow/actorcompiler.h" // This must be the last #include. #define FDB_API_VERSION 720 /* * 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 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, }; 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 }, 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 tr, StringRef optionStr, bool enabled, Optional 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 tr) { for (const auto& [name, value] : transactionOptions.options) { tr->setOption(name, value.castTo()); } } // 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 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 tr, FDBTransactionOptions::Option option, bool enabled, Optional 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(&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()); } // A group of enabled options (of type T::Option) as well as a legal options map from string to T::Option template struct OptionGroup { std::map>> options; std::map legalOptions; OptionGroup() {} OptionGroup(OptionGroup& 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 arg) { auto optionItr = options.find(option); if (enabled && (optionItr == options.end() || Optional>(optionItr->second).castTo() != arg)) { options[option] = arg.castTo>(); 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 getValidOptions() const { std::vector 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 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> parseLine(std::string& line, bool& err, bool& partial) { err = false; partial = false; bool quoted = false; std::vector buf; std::vector> 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" " -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& helpMap = CommandFactory::commands(); std::set& 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 ", "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 ", "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 ", "sleep for a period of time", ""); helpMap["get"] = CommandHelp("get ", "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 [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 [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 [reserved|total] [read|write]|set [reserved|total] [read|write] ]", "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 ", "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