Merge pull request #7269 from y-zeng/cli_call

Support server reflection in CLI
pull/7500/head
kpayson64 8 years ago committed by GitHub
commit b8208ff79a
  1. 8
      Makefile
  2. 4
      build.yaml
  3. 27
      doc/command_line_tool.md
  4. 89
      test/cpp/util/grpc_cli.cc
  5. 47
      test/cpp/util/proto_file_parser.cc
  6. 11
      test/cpp/util/proto_file_parser.h
  7. 12
      test/cpp/util/proto_reflection_descriptor_database.cc
  8. 9
      tools/run_tests/sources_and_headers.json
  9. 6
      vsprojects/vcxproj/grpc_cli_libs/grpc_cli_libs.vcxproj
  10. 6
      vsprojects/vcxproj/grpc_cli_libs/grpc_cli_libs.vcxproj.filters
  11. 3
      vsprojects/vcxproj/test/grpc_cli/grpc_cli.vcxproj

@ -4090,6 +4090,7 @@ endif
LIBGRPC_CLI_LIBS_SRC = \
test/cpp/util/cli_call.cc \
test/cpp/util/proto_file_parser.cc \
test/cpp/util/proto_reflection_descriptor_database.cc \
PUBLIC_HEADERS_CXX += \
@ -10997,16 +10998,16 @@ $(BINDIR)/$(CONFIG)/grpc_cli: protobuf_dep_error
else
$(BINDIR)/$(CONFIG)/grpc_cli: $(PROTOBUF_DEP) $(GRPC_CLI_OBJS) $(LIBDIR)/$(CONFIG)/libgrpc_cli_libs.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc++.a $(LIBDIR)/$(CONFIG)/libgrpc.a $(LIBDIR)/$(CONFIG)/libgpr_test_util.a $(LIBDIR)/$(CONFIG)/libgpr.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_config.a
$(BINDIR)/$(CONFIG)/grpc_cli: $(PROTOBUF_DEP) $(GRPC_CLI_OBJS) $(LIBDIR)/$(CONFIG)/libgrpc_cli_libs.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc++_reflection.a $(LIBDIR)/$(CONFIG)/libgrpc++.a $(LIBDIR)/$(CONFIG)/libgrpc.a $(LIBDIR)/$(CONFIG)/libgpr_test_util.a $(LIBDIR)/$(CONFIG)/libgpr.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_config.a
$(E) "[LD] Linking $@"
$(Q) mkdir -p `dirname $@`
$(Q) $(LDXX) $(LDFLAGS) $(GRPC_CLI_OBJS) $(LIBDIR)/$(CONFIG)/libgrpc_cli_libs.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc++.a $(LIBDIR)/$(CONFIG)/libgrpc.a $(LIBDIR)/$(CONFIG)/libgpr_test_util.a $(LIBDIR)/$(CONFIG)/libgpr.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_config.a $(LDLIBSXX) $(LDLIBS_PROTOBUF) $(LDLIBS) $(LDLIBS_SECURE) $(GTEST_LIB) -o $(BINDIR)/$(CONFIG)/grpc_cli
$(Q) $(LDXX) $(LDFLAGS) $(GRPC_CLI_OBJS) $(LIBDIR)/$(CONFIG)/libgrpc_cli_libs.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc++_reflection.a $(LIBDIR)/$(CONFIG)/libgrpc++.a $(LIBDIR)/$(CONFIG)/libgrpc.a $(LIBDIR)/$(CONFIG)/libgpr_test_util.a $(LIBDIR)/$(CONFIG)/libgpr.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_config.a $(LDLIBSXX) $(LDLIBS_PROTOBUF) $(LDLIBS) $(LDLIBS_SECURE) $(GTEST_LIB) -o $(BINDIR)/$(CONFIG)/grpc_cli
endif
endif
$(OBJDIR)/$(CONFIG)/test/cpp/util/grpc_cli.o: $(LIBDIR)/$(CONFIG)/libgrpc_cli_libs.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc++.a $(LIBDIR)/$(CONFIG)/libgrpc.a $(LIBDIR)/$(CONFIG)/libgpr_test_util.a $(LIBDIR)/$(CONFIG)/libgpr.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_config.a
$(OBJDIR)/$(CONFIG)/test/cpp/util/grpc_cli.o: $(LIBDIR)/$(CONFIG)/libgrpc_cli_libs.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc_test_util.a $(LIBDIR)/$(CONFIG)/libgrpc++_reflection.a $(LIBDIR)/$(CONFIG)/libgrpc++.a $(LIBDIR)/$(CONFIG)/libgrpc.a $(LIBDIR)/$(CONFIG)/libgpr_test_util.a $(LIBDIR)/$(CONFIG)/libgpr.a $(LIBDIR)/$(CONFIG)/libgrpc++_test_config.a
deps_grpc_cli: $(GRPC_CLI_OBJS:.o=.dep)
@ -14920,6 +14921,7 @@ test/cpp/util/byte_buffer_proto_helper.cc: $(OPENSSL_DEP)
test/cpp/util/cli_call.cc: $(OPENSSL_DEP)
test/cpp/util/create_test_channel.cc: $(OPENSSL_DEP)
test/cpp/util/proto_file_parser.cc: $(OPENSSL_DEP)
test/cpp/util/proto_reflection_descriptor_database.cc: $(OPENSSL_DEP)
test/cpp/util/string_ref_helper.cc: $(OPENSSL_DEP)
test/cpp/util/subprocess.cc: $(OPENSSL_DEP)
test/cpp/util/test_config.cc: $(OPENSSL_DEP)

@ -1030,10 +1030,13 @@ libs:
headers:
- test/cpp/util/cli_call.h
- test/cpp/util/proto_file_parser.h
- test/cpp/util/proto_reflection_descriptor_database.h
src:
- test/cpp/util/cli_call.cc
- test/cpp/util/proto_file_parser.cc
- test/cpp/util/proto_reflection_descriptor_database.cc
deps:
- grpc++_reflection
- grpc++
- grpc_plugin_support
- name: grpc_plugin_support
@ -2657,6 +2660,7 @@ targets:
- grpc_cli_libs
- grpc++_test_util
- grpc_test_util
- grpc++_reflection
- grpc++
- grpc
- gpr_test_util

@ -15,6 +15,7 @@ The command line tool can do the following things:
- Send unary rpc.
- Attach metadata and display received metadata.
- Handle common authentication to server.
- Infer request/response types from server reflection result.
- Find the request/response types from a given proto file.
- Read proto request in text form.
- Read request in wire form (for protobuf messages, this means serialized binary form).
@ -24,7 +25,6 @@ The command line tool can do the following things:
The command line tool should support the following things:
- List server services and methods through server reflection.
- Infer request/response types from server reflection result.
- Fine-grained auth control (such as, use this oauth token to talk to the server).
- Send streaming rpc.
@ -46,8 +46,8 @@ https://github.com/grpc/grpc/blob/master/test/cpp/util/grpc_cli.cc
Send a rpc to a helloworld server at `localhost:50051`:
```
$ bins/opt/grpc_cli call localhost:50051 SayHello examples/protos/helloworld.proto \
"name: 'world'" --enable_ssl=false
$ bins/opt/grpc_cli call localhost:50051 SayHello "name: 'world'" \
--enable_ssl=false
```
On success, the tool will print out
@ -59,11 +59,22 @@ Response:
```
The `localhost:50051` part indicates the server you are connecting to. `SayHello` is (part of) the
gRPC method string. Then there is the path to the proto file containing the service definition,
if it is not under current directory, you can use `--proto_path` to specify a new search root.
`"name: 'world'"` is the text format of the request proto message.
We are not using ssl here by `--enable_ssl=false`. For information on more
flags, look at the comments of `grpc_cli.cc`.
gRPC method string. Then `"name: 'world'"` is the text format of the request proto message. We are
not using ssl here by `--enable_ssl=false`. For information on more flags, look at the comments of `grpc_cli.cc`.
### Use local proto files
If the server does not have the server reflection service, you will need to provide local proto
files containing the service definition. The tool will try to find request/response types from
them.
```
$ bins/opt/grpc_cli call localhost:50051 SayHello "name: 'world'" \
--protofiles=examples/protos/helloworld.proto --enable_ssl=false
```
If the proto files is not under current directory, you can use `--proto_path` to specify a new
search root.
### Send non-proto rpc

@ -34,18 +34,21 @@
/*
A command line tool to talk to a grpc server.
Example of talking to grpc interop server:
grpc_cli call localhost:50051 UnaryCall src/proto/grpc/testing/test.proto \
"response_size:10" --enable_ssl=false
grpc_cli call localhost:50051 UnaryCall "response_size:10" \
--protofiles=src/proto/grpc/testing/test.proto --enable_ssl=false
Options:
1. --proto_path, if your proto file is not under current working directory,
1. --protofiles, use this flag to provide a proto file if the server does
does not have the reflection service.
2. --proto_path, if your proto file is not under current working directory,
use this flag to provide a search root. It should work similar to the
counterpart in protoc.
2. --metadata specifies metadata to be sent to the server, such as:
counterpart in protoc. This option is valid only when protofiles is
provided.
3. --metadata specifies metadata to be sent to the server, such as:
--metadata="MyHeaderKey1:Value1:MyHeaderKey2:Value2"
3. --enable_ssl, whether to use tls.
4. --use_auth, if set to true, attach a GoogleDefaultCredentials to the call
3. --input_binary_file, a file containing the serialized request. The file
4. --enable_ssl, whether to use tls.
5. --use_auth, if set to true, attach a GoogleDefaultCredentials to the call
6. --input_binary_file, a file containing the serialized request. The file
can be generated by calling something like:
protoc --proto_path=src/proto/grpc/testing/ \
--encode=grpc.testing.SimpleRequest \
@ -53,7 +56,7 @@
< input.txt > input.bin
If this is used and no proto file is provided in the argument list, the
method string has to be exact in the form of /package.service/method.
4. --output_binary_file, a file to write binary format response into, it can
7. --output_binary_file, a file to write binary format response into, it can
be later decoded using protoc:
protoc --proto_path=src/proto/grpc/testing/ \
--decode=grpc.testing.SimpleResponse \
@ -61,6 +64,7 @@
< output.bin > output.txt
*/
#include <unistd.h>
#include <fstream>
#include <iostream>
#include <sstream>
@ -86,6 +90,8 @@ DEFINE_string(output_binary_file, "",
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.");
// TODO(zyc): support a list of input proto files
DEFINE_string(protofiles, "", "Name of the proto file.");
void ParseMetadataFlag(
std::multimap<grpc::string, grpc::string>* client_metadata) {
@ -129,35 +135,61 @@ void PrintMetadata(const T& m, const grpc::string& message) {
int main(int argc, char** argv) {
grpc::testing::InitTest(&argc, &argv, true);
if (argc < 4 || argc == 5 || grpc::string(argv[1]) != "call") {
if (argc < 4 || grpc::string(argv[1]) != "call") {
std::cout << "Usage: grpc_cli call server_host:port method_name "
<< "[proto file] [text format request] [<options>]" << std::endl;
return 1;
}
grpc::string file_name;
grpc::string request_text;
grpc::string server_address(argv[2]);
grpc::string method_name(argv[3]);
std::unique_ptr<grpc::testing::ProtoFileParser> parser;
grpc::string serialized_request_proto;
if (argc == 6) {
file_name = argv[4];
// TODO(yangg) read from stdin as well?
request_text = argv[5];
if (argc == 5) {
request_text = argv[4];
}
std::shared_ptr<grpc::ChannelCredentials> creds;
if (!FLAGS_enable_ssl) {
creds = grpc::InsecureChannelCredentials();
} else {
if (FLAGS_use_auth) {
creds = grpc::GoogleDefaultCredentials();
} else {
creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
}
}
std::shared_ptr<grpc::Channel> channel =
grpc::CreateChannel(server_address, creds);
if (request_text.empty() && FLAGS_input_binary_file.empty()) {
std::cout << "Missing input. Use text format input or "
<< "--input_binary_file for serialized request" << std::endl;
return 1;
} else if (!request_text.empty()) {
parser.reset(new grpc::testing::ProtoFileParser(FLAGS_proto_path, file_name,
method_name));
if (isatty(STDIN_FILENO)) {
std::cout << "reading request message from stdin..." << std::endl;
}
std::stringstream input_stream;
input_stream << std::cin.rdbuf();
request_text = input_stream.str();
}
if (!request_text.empty()) {
if (!FLAGS_protofiles.empty()) {
parser.reset(new grpc::testing::ProtoFileParser(
FLAGS_proto_path, FLAGS_protofiles, method_name));
} else {
parser.reset(new grpc::testing::ProtoFileParser(channel, method_name));
}
method_name = parser->GetFullMethodName();
if (parser->HasError()) {
return 1;
}
if (!FLAGS_input_binary_file.empty()) {
std::cout
<< "warning: request given in argv, ignoring --input_binary_file"
<< std::endl;
}
}
if (parser) {
@ -175,19 +207,6 @@ int main(int argc, char** argv) {
}
std::cout << "connecting to " << server_address << std::endl;
std::shared_ptr<grpc::ChannelCredentials> creds;
if (!FLAGS_enable_ssl) {
creds = grpc::InsecureChannelCredentials();
} else {
if (FLAGS_use_auth) {
creds = grpc::GoogleDefaultCredentials();
} else {
creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
}
}
std::shared_ptr<grpc::Channel> channel =
grpc::CreateChannel(server_address, creds);
grpc::string serialized_response_proto;
std::multimap<grpc::string, grpc::string> client_metadata;
std::multimap<grpc::string_ref, grpc::string_ref> server_initial_metadata,
@ -219,7 +238,7 @@ int main(int argc, char** argv) {
}
} else {
std::cout << "Rpc failed with status code " << s.error_code()
<< " error message " << s.error_message() << std::endl;
<< ", error message: " << s.error_message() << std::endl;
}
return 0;

@ -95,9 +95,48 @@ ProtoFileParser::ProtoFileParser(const grpc::string& proto_path,
dynamic_factory_.reset(
new google::protobuf::DynamicMessageFactory(importer_->pool()));
std::vector<const google::protobuf::ServiceDescriptor*> service_desc_list;
for (int i = 0; i < file_desc->service_count(); i++) {
service_desc_list.push_back(file_desc->service(i));
}
InitProtoFileParser(method, service_desc_list);
}
ProtoFileParser::ProtoFileParser(std::shared_ptr<grpc::Channel> channel,
const grpc::string& method)
: has_error_(false),
desc_db_(new grpc::ProtoReflectionDescriptorDatabase(channel)),
desc_pool_(new google::protobuf::DescriptorPool(desc_db_.get())) {
std::vector<std::string> service_list;
if (!desc_db_->GetServices(&service_list)) {
LogError(
"Failed to get services from the server, "
"it may not have the reflection service.\n"
"Please try to use the --protofiles option to provide a proto file.");
}
if (has_error_) {
return;
}
dynamic_factory_.reset(
new google::protobuf::DynamicMessageFactory(desc_pool_.get()));
std::vector<const google::protobuf::ServiceDescriptor*> service_desc_list;
for (auto it = service_list.begin(); it != service_list.end(); it++) {
service_desc_list.push_back(desc_pool_->FindServiceByName(*it));
}
InitProtoFileParser(method, service_desc_list);
}
ProtoFileParser::~ProtoFileParser() {}
void ProtoFileParser::InitProtoFileParser(
const grpc::string& method,
const std::vector<const google::protobuf::ServiceDescriptor*>
service_desc_list) {
const google::protobuf::MethodDescriptor* method_descriptor = nullptr;
for (int i = 0; !method_descriptor && i < file_desc->service_count(); i++) {
const auto* service_desc = file_desc->service(i);
for (auto it = service_desc_list.begin(); it != service_desc_list.end();
it++) {
const auto* service_desc = *it;
for (int j = 0; j < service_desc->method_count(); j++) {
const auto* method_desc = service_desc->method(j);
if (MethodNameMatch(method_desc->full_name(), method)) {
@ -130,8 +169,6 @@ ProtoFileParser::ProtoFileParser(const grpc::string& proto_path,
dynamic_factory_->GetPrototype(method_descriptor->output_type())->New());
}
ProtoFileParser::~ProtoFileParser() {}
grpc::string ProtoFileParser::GetSerializedProto(
const grpc::string& text_format_proto, bool is_request) {
grpc::string serialized;
@ -143,7 +180,7 @@ grpc::string ProtoFileParser::GetSerializedProto(
LogError("Failed to parse text format to proto.");
return "";
}
ok = request_prototype_->SerializeToString(&serialized);
ok = msg->SerializeToString(&serialized);
if (!ok) {
LogError("Failed to serialize proto.");
return "";

@ -38,8 +38,10 @@
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
#include <grpc++/channel.h>
#include "src/compiler/config.h"
#include "test/cpp/util/proto_reflection_descriptor_database.h"
namespace grpc {
namespace testing {
@ -53,6 +55,9 @@ class ProtoFileParser {
// even just Method. It will log an error if there is ambiguity.
ProtoFileParser(const grpc::string& proto_path, const grpc::string& file_name,
const grpc::string& method);
ProtoFileParser(std::shared_ptr<grpc::Channel> channel,
const grpc::string& method);
~ProtoFileParser();
grpc::string GetFullMethodName() const { return full_method_name_; }
@ -68,12 +73,18 @@ class ProtoFileParser {
void LogError(const grpc::string& error_msg);
private:
void InitProtoFileParser(
const grpc::string& method,
const std::vector<const google::protobuf::ServiceDescriptor*> services);
bool has_error_;
grpc::string request_text_;
grpc::string full_method_name_;
google::protobuf::compiler::DiskSourceTree source_tree_;
std::unique_ptr<ErrorPrinter> error_printer_;
std::unique_ptr<google::protobuf::compiler::Importer> importer_;
std::unique_ptr<grpc::ProtoReflectionDescriptorDatabase> desc_db_;
std::unique_ptr<google::protobuf::DescriptorPool> desc_pool_;
std::unique_ptr<google::protobuf::DynamicMessageFactory> dynamic_factory_;
std::unique_ptr<grpc::protobuf::Message> request_prototype_;
std::unique_ptr<grpc::protobuf::Message> response_prototype_;

@ -53,7 +53,17 @@ ProtoReflectionDescriptorDatabase::ProtoReflectionDescriptorDatabase(
std::shared_ptr<grpc::Channel> channel)
: stub_(ServerReflection::NewStub(channel)) {}
ProtoReflectionDescriptorDatabase::~ProtoReflectionDescriptorDatabase() {}
ProtoReflectionDescriptorDatabase::~ProtoReflectionDescriptorDatabase() {
if (stream_) {
stream_->WritesDone();
Status status = stream_->Finish();
if (!status.ok()) {
gpr_log(GPR_ERROR,
"ServerReflectionInfo rpc failed. Error code: %d, details: %s",
(int)status.error_code(), status.error_message().c_str());
}
}
}
bool ProtoReflectionDescriptorDatabase::FindFileByName(
const string& filename, google::protobuf::FileDescriptorProto* output) {

@ -2133,6 +2133,7 @@
"gpr_test_util",
"grpc",
"grpc++",
"grpc++_reflection",
"grpc++_test_config",
"grpc++_test_util",
"grpc_cli_libs",
@ -4460,11 +4461,13 @@
{
"deps": [
"grpc++",
"grpc++_reflection",
"grpc_plugin_support"
],
"headers": [
"test/cpp/util/cli_call.h",
"test/cpp/util/proto_file_parser.h"
"test/cpp/util/proto_file_parser.h",
"test/cpp/util/proto_reflection_descriptor_database.h"
],
"language": "c++",
"name": "grpc_cli_libs",
@ -4472,7 +4475,9 @@
"test/cpp/util/cli_call.cc",
"test/cpp/util/cli_call.h",
"test/cpp/util/proto_file_parser.cc",
"test/cpp/util/proto_file_parser.h"
"test/cpp/util/proto_file_parser.h",
"test/cpp/util/proto_reflection_descriptor_database.cc",
"test/cpp/util/proto_reflection_descriptor_database.h"
],
"third_party": false,
"type": "lib"

@ -149,14 +149,20 @@
<ItemGroup>
<ClInclude Include="$(SolutionDir)\..\test\cpp\util\cli_call.h" />
<ClInclude Include="$(SolutionDir)\..\test\cpp\util\proto_file_parser.h" />
<ClInclude Include="$(SolutionDir)\..\test\cpp\util\proto_reflection_descriptor_database.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="$(SolutionDir)\..\test\cpp\util\cli_call.cc">
</ClCompile>
<ClCompile Include="$(SolutionDir)\..\test\cpp\util\proto_file_parser.cc">
</ClCompile>
<ClCompile Include="$(SolutionDir)\..\test\cpp\util\proto_reflection_descriptor_database.cc">
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\..\vsprojects\vcxproj\.\grpc++_reflection\grpc++_reflection.vcxproj">
<Project>{5F575402-3F89-5D1A-6910-9DB8BF5D2BAB}</Project>
</ProjectReference>
<ProjectReference Include="$(SolutionDir)\..\vsprojects\vcxproj\.\grpc++\grpc++.vcxproj">
<Project>{C187A093-A0FE-489D-A40A-6E33DE0F9FEB}</Project>
</ProjectReference>

@ -7,6 +7,9 @@
<ClCompile Include="$(SolutionDir)\..\test\cpp\util\proto_file_parser.cc">
<Filter>test\cpp\util</Filter>
</ClCompile>
<ClCompile Include="$(SolutionDir)\..\test\cpp\util\proto_reflection_descriptor_database.cc">
<Filter>test\cpp\util</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="$(SolutionDir)\..\test\cpp\util\cli_call.h">
@ -15,6 +18,9 @@
<ClInclude Include="$(SolutionDir)\..\test\cpp\util\proto_file_parser.h">
<Filter>test\cpp\util</Filter>
</ClInclude>
<ClInclude Include="$(SolutionDir)\..\test\cpp\util\proto_reflection_descriptor_database.h">
<Filter>test\cpp\util</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>

@ -173,6 +173,9 @@
<ProjectReference Include="$(SolutionDir)\..\vsprojects\vcxproj\.\grpc_test_util\grpc_test_util.vcxproj">
<Project>{17BCAFC0-5FDC-4C94-AEB9-95F3E220614B}</Project>
</ProjectReference>
<ProjectReference Include="$(SolutionDir)\..\vsprojects\vcxproj\.\grpc++_reflection\grpc++_reflection.vcxproj">
<Project>{5F575402-3F89-5D1A-6910-9DB8BF5D2BAB}</Project>
</ProjectReference>
<ProjectReference Include="$(SolutionDir)\..\vsprojects\vcxproj\.\grpc++\grpc++.vcxproj">
<Project>{C187A093-A0FE-489D-A40A-6E33DE0F9FEB}</Project>
</ProjectReference>

Loading…
Cancel
Save