/* * * Copyright 2016, Google Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ #include "test/cpp/util/grpc_tool.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "test/cpp/util/cli_call.h" #include "test/cpp/util/proto_file_parser.h" #include "test/cpp/util/proto_reflection_descriptor_database.h" #include "test/cpp/util/service_describer.h" namespace grpc { namespace testing { DEFINE_bool(l, false, "Use a long listing format"); DEFINE_bool(remotedb, true, "Use server types to parse and format messages"); DEFINE_string(metadata, "", "Metadata to send to server, in the form of key1:val1:key2:val2"); DEFINE_string(proto_path, ".", "Path to look for the proto file."); DEFINE_string(protofiles, "", "Name of the proto file."); DEFINE_bool(binary_input, false, "Input in binary format"); DEFINE_bool(binary_output, false, "Output in binary format"); DEFINE_string(infile, "", "Input file (default is stdin)"); namespace { class GrpcTool { public: explicit GrpcTool(); virtual ~GrpcTool() {} bool Help(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); bool CallMethod(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); bool ListServices(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); bool PrintType(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); // TODO(zyc): implement the following methods // bool ListServices(int argc, const char** argv, GrpcToolOutputCallback // callback); // bool PrintTypeId(int argc, const char** argv, GrpcToolOutputCallback // callback); bool ParseMessage(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); bool ToText(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); bool ToBinary(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback); void SetPrintCommandMode(int exit_status) { print_command_usage_ = true; usage_exit_status_ = exit_status; } private: void CommandUsage(const grpc::string& usage) const; bool print_command_usage_; int usage_exit_status_; const grpc::string cred_usage_; }; template std::function BindWith5Args(T&& func) { return std::bind(std::forward(func), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5); } template size_t ArraySize(T& a) { return ((sizeof(a) / sizeof(*(a))) / static_cast(!(sizeof(a) % sizeof(*(a))))); } void ParseMetadataFlag( std::multimap* client_metadata) { if (FLAGS_metadata.empty()) { return; } std::vector fields; const char* delim = ":"; size_t cur, next = -1; do { cur = next + 1; next = FLAGS_metadata.find_first_of(delim, cur); fields.push_back(FLAGS_metadata.substr(cur, next - cur)); } while (next != grpc::string::npos); if (fields.size() % 2) { fprintf(stderr, "Failed to parse metadata flag.\n"); exit(1); } for (size_t i = 0; i < fields.size(); i += 2) { client_metadata->insert( std::pair(fields[i], fields[i + 1])); } } template void PrintMetadata(const T& m, const grpc::string& message) { if (m.empty()) { return; } fprintf(stderr, "%s\n", message.c_str()); grpc::string pair; for (typename T::const_iterator iter = m.begin(); iter != m.end(); ++iter) { pair.clear(); pair.append(iter->first.data(), iter->first.size()); pair.append(" : "); pair.append(iter->second.data(), iter->second.size()); fprintf(stderr, "%s\n", pair.c_str()); } } void ReadResponse(CliCall* call, const grpc::string& method_name, GrpcToolOutputCallback callback, ProtoFileParser* parser, gpr_mu* parser_mu, bool print_mode) { grpc::string serialized_response_proto; std::multimap server_initial_metadata; for (bool receive_initial_metadata = true; call->ReadAndMaybeNotifyWrite( &serialized_response_proto, receive_initial_metadata ? &server_initial_metadata : nullptr); receive_initial_metadata = false) { fprintf(stderr, "got response.\n"); if (!FLAGS_binary_output) { gpr_mu_lock(parser_mu); serialized_response_proto = parser->GetTextFormatFromMethod( method_name, serialized_response_proto, false /* is_request */); if (parser->HasError() && print_mode) { fprintf(stderr, "Failed to parse response.\n"); } gpr_mu_unlock(parser_mu); } if (receive_initial_metadata) { PrintMetadata(server_initial_metadata, "Received initial metadata from server:"); } if (!callback(serialized_response_proto) && print_mode) { fprintf(stderr, "Failed to output response.\n"); } } } struct Command { const char* command; std::function function; int min_args; int max_args; }; const Command ops[] = { {"help", BindWith5Args(&GrpcTool::Help), 0, INT_MAX}, {"ls", BindWith5Args(&GrpcTool::ListServices), 1, 3}, {"list", BindWith5Args(&GrpcTool::ListServices), 1, 3}, {"call", BindWith5Args(&GrpcTool::CallMethod), 2, 3}, {"type", BindWith5Args(&GrpcTool::PrintType), 2, 2}, {"parse", BindWith5Args(&GrpcTool::ParseMessage), 2, 3}, {"totext", BindWith5Args(&GrpcTool::ToText), 2, 3}, {"tobinary", BindWith5Args(&GrpcTool::ToBinary), 2, 3}, }; void Usage(const grpc::string& msg) { fprintf( stderr, "%s\n" " grpc_cli ls ... ; List services\n" " grpc_cli call ... ; Call method\n" " grpc_cli type ... ; Print type\n" " grpc_cli parse ... ; Parse message\n" " grpc_cli totext ... ; Convert binary message to text\n" " grpc_cli tobinary ... ; Convert text message to binary\n" " grpc_cli help ... ; Print this message, or per-command usage\n" "\n", msg.c_str()); exit(1); } const Command* FindCommand(const grpc::string& name) { for (int i = 0; i < (int)ArraySize(ops); i++) { if (name == ops[i].command) { return &ops[i]; } } return NULL; } } // namespace int GrpcToolMainLib(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { if (argc < 2) { Usage("No command specified"); } grpc::string command = argv[1]; argc -= 2; argv += 2; const Command* cmd = FindCommand(command); if (cmd != NULL) { GrpcTool grpc_tool; if (argc < cmd->min_args || argc > cmd->max_args) { // Force the command to print its usage message fprintf(stderr, "\nWrong number of arguments for %s\n", command.c_str()); grpc_tool.SetPrintCommandMode(1); return cmd->function(&grpc_tool, -1, NULL, cred, callback); } const bool ok = cmd->function(&grpc_tool, argc, argv, cred, callback); return ok ? 0 : 1; } else { Usage("Invalid command '" + grpc::string(command.c_str()) + "'"); } return 1; } GrpcTool::GrpcTool() : print_command_usage_(false), usage_exit_status_(0) {} void GrpcTool::CommandUsage(const grpc::string& usage) const { if (print_command_usage_) { fprintf(stderr, "\n%s%s\n", usage.c_str(), (usage.empty() || usage[usage.size() - 1] != '\n') ? "\n" : ""); exit(usage_exit_status_); } } bool GrpcTool::Help(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "Print help\n" " grpc_cli help [subcommand]\n"); if (argc == 0) { Usage(""); } else { const Command* cmd = FindCommand(argv[0]); if (cmd == NULL) { Usage("Unknown command '" + grpc::string(argv[0]) + "'"); } SetPrintCommandMode(0); cmd->function(this, -1, NULL, cred, callback); } return true; } bool GrpcTool::ListServices(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "List services\n" " grpc_cli ls
[[/]]\n" "
; host:port\n" " ; Exported service name\n" " ; Method name\n" " --l ; Use a long listing format\n" " --outfile ; Output filename (defaults to stdout)\n" + cred.GetCredentialUsage()); grpc::string server_address(argv[0]); std::shared_ptr channel = grpc::CreateChannel(server_address, cred.GetCredentials()); grpc::ProtoReflectionDescriptorDatabase desc_db(channel); grpc::protobuf::DescriptorPool desc_pool(&desc_db); std::vector service_list; if (!desc_db.GetServices(&service_list)) { return false; } // If no service is specified, dump the list of services. grpc::string output; if (argc < 2) { // List all services, if --l is passed, then include full description, // otherwise include a summarized list only. if (FLAGS_l) { output = DescribeServiceList(service_list, desc_pool); } else { for (auto it = service_list.begin(); it != service_list.end(); it++) { auto const& service = *it; output.append(service); output.append("\n"); } } } else { grpc::string service_name; grpc::string method_name; std::stringstream ss(argv[1]); // Remove leading slashes. while (ss.peek() == '/') { ss.get(); } // Parse service and method names. Support the following patterns: // Service // Service Method // Service.Method // Service/Method if (argc == 3) { std::getline(ss, service_name, '/'); method_name = argv[2]; } else { if (std::getline(ss, service_name, '/')) { std::getline(ss, method_name); } } const grpc::protobuf::ServiceDescriptor* service = desc_pool.FindServiceByName(service_name); if (service != nullptr) { if (method_name.empty()) { output = FLAGS_l ? DescribeService(service) : SummarizeService(service); } else { method_name.insert(0, 1, '.'); method_name.insert(0, service_name); const grpc::protobuf::MethodDescriptor* method = desc_pool.FindMethodByName(method_name); if (method != nullptr) { output = FLAGS_l ? DescribeMethod(method) : SummarizeMethod(method); } else { fprintf(stderr, "Method %s not found in service %s.\n", method_name.c_str(), service_name.c_str()); return false; } } } else { if (!method_name.empty()) { fprintf(stderr, "Service %s not found.\n", service_name.c_str()); return false; } else { const grpc::protobuf::MethodDescriptor* method = desc_pool.FindMethodByName(service_name); if (method != nullptr) { output = FLAGS_l ? DescribeMethod(method) : SummarizeMethod(method); } else { fprintf(stderr, "Service or method %s not found.\n", service_name.c_str()); return false; } } } } return callback(output); } bool GrpcTool::PrintType(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "Print type\n" " grpc_cli type
\n" "
; host:port\n" " ; Protocol buffer type name\n" + cred.GetCredentialUsage()); grpc::string server_address(argv[0]); std::shared_ptr channel = grpc::CreateChannel(server_address, cred.GetCredentials()); grpc::ProtoReflectionDescriptorDatabase desc_db(channel); grpc::protobuf::DescriptorPool desc_pool(&desc_db); grpc::string output; const grpc::protobuf::Descriptor* descriptor = desc_pool.FindMessageTypeByName(argv[1]); if (descriptor != nullptr) { output = descriptor->DebugString(); } else { fprintf(stderr, "Type %s not found.\n", argv[1]); return false; } return callback(output); } bool GrpcTool::CallMethod(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "Call method\n" " grpc_cli call
[.] \n" "
; host:port\n" " ; Exported service name\n" " ; Method name\n" " ; Text protobuffer (overrides infile)\n" " --protofiles ; Comma separated proto files used as a" " fallback when parsing request/response\n" " --proto_path ; The search path of proto files, valid" " only when --protofiles is given\n" " --metadata ; The metadata to be sent to the server\n" " --infile ; Input filename (defaults to stdin)\n" " --outfile ; Output filename (defaults to stdout)\n" " --binary_input ; Input in binary format\n" " --binary_output ; Output in binary format\n" + cred.GetCredentialUsage()); std::stringstream output_ss; grpc::string request_text; grpc::string server_address(argv[0]); grpc::string method_name(argv[1]); grpc::string formatted_method_name; std::unique_ptr parser; grpc::string serialized_request_proto; bool print_mode = false; std::shared_ptr channel = FLAGS_remotedb ? grpc::CreateChannel(server_address, cred.GetCredentials()) : nullptr; parser.reset(new grpc::testing::ProtoFileParser(channel, FLAGS_proto_path, FLAGS_protofiles)); if (FLAGS_binary_input) { formatted_method_name = method_name; } else { formatted_method_name = parser->GetFormattedMethodName(method_name); } if (parser->HasError()) { return false; } if (parser->IsStreaming(method_name, true /* is_request */)) { std::istream* input_stream; std::ifstream input_file; if (argc == 3) { request_text = argv[2]; } std::multimap client_metadata; ParseMetadataFlag(&client_metadata); PrintMetadata(client_metadata, "Sending client initial metadata:"); CliCall call(channel, formatted_method_name, client_metadata); if (FLAGS_infile.empty()) { if (isatty(STDIN_FILENO)) { print_mode = true; fprintf(stderr, "reading streaming request message from stdin...\n"); } input_stream = &std::cin; } else { input_file.open(FLAGS_infile, std::ios::in | std::ios::binary); input_stream = &input_file; } gpr_mu parser_mu; gpr_mu_init(&parser_mu); std::thread read_thread(ReadResponse, &call, method_name, callback, parser.get(), &parser_mu, print_mode); std::stringstream request_ss; grpc::string line; while (!request_text.empty() || (!input_stream->eof() && getline(*input_stream, line))) { if (!request_text.empty()) { if (FLAGS_binary_input) { serialized_request_proto = request_text; request_text.clear(); } else { gpr_mu_lock(&parser_mu); serialized_request_proto = parser->GetSerializedProtoFromMethod( method_name, request_text, true /* is_request */); request_text.clear(); if (parser->HasError()) { if (print_mode) { fprintf(stderr, "Failed to parse request.\n"); } gpr_mu_unlock(&parser_mu); continue; } gpr_mu_unlock(&parser_mu); } call.WriteAndWait(serialized_request_proto); if (print_mode) { fprintf(stderr, "Request sent.\n"); } } else { if (line.length() == 0) { request_text = request_ss.str(); request_ss.str(grpc::string()); request_ss.clear(); } else { request_ss << line << ' '; } } } if (input_file.is_open()) { input_file.close(); } call.WritesDoneAndWait(); read_thread.join(); std::multimap server_trailing_metadata; Status status = call.Finish(&server_trailing_metadata); PrintMetadata(server_trailing_metadata, "Received trailing metadata from server:"); if (status.ok()) { fprintf(stderr, "Stream RPC succeeded with OK status\n"); return true; } else { fprintf(stderr, "Rpc failed with status code %d, error message: %s\n", status.error_code(), status.error_message().c_str()); return false; } } else { // parser->IsStreaming(method_name, true /* is_request */) if (argc == 3) { request_text = argv[2]; if (!FLAGS_infile.empty()) { fprintf(stderr, "warning: request given in argv, ignoring --infile\n"); } } else { std::stringstream input_stream; if (FLAGS_infile.empty()) { if (isatty(STDIN_FILENO)) { fprintf(stderr, "reading request message from stdin...\n"); } input_stream << std::cin.rdbuf(); } else { std::ifstream input_file(FLAGS_infile, std::ios::in | std::ios::binary); input_stream << input_file.rdbuf(); input_file.close(); } request_text = input_stream.str(); } if (FLAGS_binary_input) { serialized_request_proto = request_text; // formatted_method_name = method_name; } else { // formatted_method_name = parser->GetFormattedMethodName(method_name); serialized_request_proto = parser->GetSerializedProtoFromMethod( method_name, request_text, true /* is_request */); if (parser->HasError()) { return false; } } fprintf(stderr, "connecting to %s\n", server_address.c_str()); grpc::string serialized_response_proto; std::multimap client_metadata; std::multimap server_initial_metadata, server_trailing_metadata; ParseMetadataFlag(&client_metadata); PrintMetadata(client_metadata, "Sending client initial metadata:"); CliCall call(channel, formatted_method_name, client_metadata); call.Write(serialized_request_proto); call.WritesDone(); for (bool receive_initial_metadata = true; call.Read( &serialized_response_proto, receive_initial_metadata ? &server_initial_metadata : nullptr); receive_initial_metadata = false) { if (!FLAGS_binary_output) { serialized_response_proto = parser->GetTextFormatFromMethod( method_name, serialized_response_proto, false /* is_request */); if (parser->HasError()) { return false; } } if (receive_initial_metadata) { PrintMetadata(server_initial_metadata, "Received initial metadata from server:"); } if (!callback(serialized_response_proto)) { return false; } } Status status = call.Finish(&server_trailing_metadata); if (status.ok()) { fprintf(stderr, "Rpc succeeded with OK status\n"); return true; } else { fprintf(stderr, "Rpc failed with status code %d, error message: %s\n", status.error_code(), status.error_message().c_str()); return false; } } GPR_UNREACHABLE_CODE(return false); } bool GrpcTool::ParseMessage(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "Parse message\n" " grpc_cli parse
[]\n" "
; host:port\n" " ; Protocol buffer type name\n" " ; Text protobuffer (overrides --infile)\n" " --protofiles ; Comma separated proto files used as a" " fallback when parsing request/response\n" " --proto_path ; The search path of proto files, valid" " only when --protofiles is given\n" " --infile ; Input filename (defaults to stdin)\n" " --outfile ; Output filename (defaults to stdout)\n" " --binary_input ; Input in binary format\n" " --binary_output ; Output in binary format\n" + cred.GetCredentialUsage()); std::stringstream output_ss; grpc::string message_text; grpc::string server_address(argv[0]); grpc::string type_name(argv[1]); std::unique_ptr parser; grpc::string serialized_request_proto; if (argc == 3) { message_text = argv[2]; if (!FLAGS_infile.empty()) { fprintf(stderr, "warning: message given in argv, ignoring --infile.\n"); } } else { std::stringstream input_stream; if (FLAGS_infile.empty()) { if (isatty(STDIN_FILENO)) { fprintf(stderr, "reading request message from stdin...\n"); } input_stream << std::cin.rdbuf(); } else { std::ifstream input_file(FLAGS_infile, std::ios::in | std::ios::binary); input_stream << input_file.rdbuf(); input_file.close(); } message_text = input_stream.str(); } if (!FLAGS_binary_input || !FLAGS_binary_output) { std::shared_ptr channel = grpc::CreateChannel(server_address, cred.GetCredentials()); parser.reset( new grpc::testing::ProtoFileParser(FLAGS_remotedb ? channel : nullptr, FLAGS_proto_path, FLAGS_protofiles)); if (parser->HasError()) { return false; } } if (FLAGS_binary_input) { serialized_request_proto = message_text; } else { serialized_request_proto = parser->GetSerializedProtoFromMessageType(type_name, message_text); if (parser->HasError()) { return false; } } if (FLAGS_binary_output) { output_ss << serialized_request_proto; } else { grpc::string output_text = parser->GetTextFormatFromMessageType( type_name, serialized_request_proto); if (parser->HasError()) { return false; } output_ss << output_text << std::endl; } return callback(output_ss.str()); } bool GrpcTool::ToText(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "Convert binary message to text\n" " grpc_cli totext \n" " ; Comma separated list of proto files\n" " ; Protocol buffer type name\n" " --proto_path ; The search path of proto files\n" " --infile ; Input filename (defaults to stdin)\n" " --outfile ; Output filename (defaults to stdout)\n"); FLAGS_protofiles = argv[0]; FLAGS_remotedb = false; FLAGS_binary_input = true; FLAGS_binary_output = false; return ParseMessage(argc, argv, cred, callback); } bool GrpcTool::ToBinary(int argc, const char** argv, const CliCredentials& cred, GrpcToolOutputCallback callback) { CommandUsage( "Convert text message to binary\n" " grpc_cli tobinary []\n" " ; Comma separated list of proto files\n" " ; Protocol buffer type name\n" " --proto_path ; The search path of proto files\n" " --infile ; Input filename (defaults to stdin)\n" " --outfile ; Output filename (defaults to stdout)\n"); FLAGS_protofiles = argv[0]; FLAGS_remotedb = false; FLAGS_binary_input = false; FLAGS_binary_output = true; return ParseMessage(argc, argv, cred, callback); } } // namespace testing } // namespace grpc