aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/csharp
diff options
context:
space:
mode:
authorGravatar Yash Tibrewal <yashkt@google.com>2018-11-16 10:58:12 -0800
committerGravatar Yash Tibrewal <yashkt@google.com>2018-11-16 11:11:04 -0800
commitfc332d2c9247832af90792a59ff6d391e84bc8ae (patch)
tree4bd1db687960ca851f87d237a36f55190ac52f27 /src/csharp
parent0eb9a3e783237cd46c8ba6d3b33228f537cafbfc (diff)
parent9cfacc48ee2e9f8db083d578c84881551734b1f0 (diff)
Merge master
Diffstat (limited to 'src/csharp')
-rw-r--r--src/csharp/.editorconfig23
-rw-r--r--src/csharp/BUILD-INTEGRATION.md357
-rw-r--r--src/csharp/Grpc.Core.Tests/ChannelConnectivityTest.cs20
-rw-r--r--src/csharp/Grpc.Core.Tests/ChannelCredentialsTest.cs37
-rw-r--r--src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs119
-rw-r--r--src/csharp/Grpc.Core.Tests/FakeCredentials.cs10
-rw-r--r--src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs75
-rw-r--r--src/csharp/Grpc.Core.Tests/Internal/FakeNativeCall.cs23
-rw-r--r--src/csharp/Grpc.Core.Tests/MarshallerTest.cs105
-rw-r--r--src/csharp/Grpc.Core.Tests/MetadataTest.cs19
-rw-r--r--src/csharp/Grpc.Core.Tests/SanityTest.cs1
-rw-r--r--src/csharp/Grpc.Core/Channel.cs12
-rw-r--r--src/csharp/Grpc.Core/ChannelCredentials.cs39
-rw-r--r--src/csharp/Grpc.Core/ChannelOptions.cs56
-rw-r--r--src/csharp/Grpc.Core/ClientBase.cs20
-rw-r--r--src/csharp/Grpc.Core/DeserializationContext.cs46
-rw-r--r--src/csharp/Grpc.Core/Internal/AsyncCall.cs234
-rw-r--r--src/csharp/Grpc.Core/Internal/AsyncCallBase.cs34
-rw-r--r--src/csharp/Grpc.Core/Internal/AsyncCallServer.cs10
-rw-r--r--src/csharp/Grpc.Core/Internal/MarshalUtils.cs7
-rw-r--r--src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs7
-rw-r--r--src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs6
-rw-r--r--src/csharp/Grpc.Core/Internal/ServerCredentialsSafeHandle.cs4
-rw-r--r--src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs40
-rw-r--r--src/csharp/Grpc.Core/Marshaller.cs126
-rw-r--r--src/csharp/Grpc.Core/Metadata.cs52
-rw-r--r--src/csharp/Grpc.Core/NativeDeps.Linux.csproj.include2
-rw-r--r--src/csharp/Grpc.Core/NativeDeps.Mac.csproj.include2
-rw-r--r--src/csharp/Grpc.Core/RpcException.cs20
-rw-r--r--src/csharp/Grpc.Core/SerializationContext.cs34
-rw-r--r--src/csharp/Grpc.Core/ServerCredentials.cs98
-rwxr-xr-xsrc/csharp/Grpc.Core/Version.csproj.include2
-rw-r--r--src/csharp/Grpc.Core/VersionInfo.cs4
-rwxr-xr-xsrc/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj2
-rw-r--r--src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs4
-rw-r--r--src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs143
-rw-r--r--src/csharp/Grpc.Microbenchmarks/Grpc.Microbenchmarks.csproj2
-rw-r--r--src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs85
-rw-r--r--src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs88
-rw-r--r--src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs146
-rw-r--r--src/csharp/Grpc.Tools.Tests/GeneratorTest.cs55
-rw-r--r--src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj78
-rw-r--r--src/csharp/Grpc.Tools.Tests/NUnitMain.cs33
-rw-r--r--src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs76
-rw-r--r--src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs179
-rw-r--r--src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs51
-rw-r--r--src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs139
-rw-r--r--src/csharp/Grpc.Tools.Tests/Utils.cs46
-rw-r--r--src/csharp/Grpc.Tools.nuspec33
-rw-r--r--src/csharp/Grpc.Tools/Common.cs114
-rw-r--r--src/csharp/Grpc.Tools/DepFileUtil.cs273
-rw-r--r--src/csharp/Grpc.Tools/GeneratorServices.cs194
-rw-r--r--src/csharp/Grpc.Tools/Grpc.Tools.csproj101
-rw-r--r--src/csharp/Grpc.Tools/ProtoCompile.cs441
-rw-r--r--src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs86
-rw-r--r--src/csharp/Grpc.Tools/ProtoReadDependencies.cs78
-rw-r--r--src/csharp/Grpc.Tools/ProtoToolsPlatform.cs63
-rw-r--r--src/csharp/Grpc.Tools/build/Grpc.Tools.props11
-rw-r--r--src/csharp/Grpc.Tools/build/Grpc.Tools.targets11
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml30
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/README3
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props6
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets48
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props24
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets384
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml99
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/README1
-rw-r--r--src/csharp/Grpc.Tools/build/native/Grpc.Tools.props17
-rw-r--r--src/csharp/Grpc.sln12
-rw-r--r--src/csharp/README.md1
-rwxr-xr-xsrc/csharp/build_packages_dotnetcli.bat4
-rw-r--r--src/csharp/build_unitypackage.bat2
-rw-r--r--src/csharp/doc/README.md19
-rwxr-xr-xsrc/csharp/doc/generate_reference_docs.sh38
-rw-r--r--src/csharp/doc/integration.md-fig.1-classic.pngbin0 -> 15266 bytes
-rw-r--r--src/csharp/doc/integration.md-fig.2-sdk.pngbin0 -> 15863 bytes
-rw-r--r--src/csharp/experimental/README.md4
-rw-r--r--src/csharp/ext/grpc_csharp_ext.c11
-rw-r--r--src/csharp/tests.json12
79 files changed, 4661 insertions, 230 deletions
diff --git a/src/csharp/.editorconfig b/src/csharp/.editorconfig
index fabce7f5ba..c9a2c48a7d 100644
--- a/src/csharp/.editorconfig
+++ b/src/csharp/.editorconfig
@@ -6,3 +6,26 @@ indent_style = space
indent_size = 4
insert_final_newline = true
tab_width = 4
+
+; https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference
+[*.cs]
+dotnet_sort_system_directives_first = true
+csharp_new_line_before_open_brace = accessors, anonymous_methods, control_blocks, events, indexers, local_functions, methods, properties, types
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
diff --git a/src/csharp/BUILD-INTEGRATION.md b/src/csharp/BUILD-INTEGRATION.md
new file mode 100644
index 0000000000..3addc2403c
--- /dev/null
+++ b/src/csharp/BUILD-INTEGRATION.md
@@ -0,0 +1,357 @@
+Protocol Buffers/gRPC Integration Into .NET Build
+=================================================
+
+With Grpc.Tools package version 1.17 we made it easier to compile .proto files
+in your project using the `dotnet build` command, Visual Studio, or command-line
+MSBuild. You need to configure the .csproj project according to the way you want
+to integrate Protocol Buffer files into your build. If you are upgrading an
+existing project, read through this list of common scenarios and decide if any
+one of them matches your approach. The protoc command line migration is
+explained near the end of this document; this migration may be the quickest but
+not the long-term solution.
+
+There is also a Reference section at the end of the file.
+
+Reporting issues
+----------------
+
+First thing first, if you found a bug in this new build system, or have a
+scenario that is not easily covered, please open an [issue in the gRPC
+repository](https://github.com/grpc/grpc/issues), and **tag the user @kkm000**
+somewhere in the text (for example, include `/cc @kkm000` at end of the issue
+text) to seize his immediate attention.
+
+Common scenarios
+----------------
+
+### I just want to compile .proto files into my library
+
+This is the approach taken by the examples in the `csharp/examples` directory.
+Protoc output files (for example, `Helloworld.cs` and `HelloworldGrpc.cs`
+compiled from `helloworld.proto`) are placed among *object* and other temporary
+files of your project, and automatically provided as inputs to the C# compiler.
+As with other automatically generated .cs files, they are included in the source
+and symbols NuGet package, if you build one.
+
+Simply reference your .proto files in a `<Protobuf>` item group. The following
+example will add all .proto files in a project and all its subdirectories
+(excluding special directories such as `bin` and `obj`):
+
+```xml
+ <ItemGroup>
+ <Protobuf Include="**/*.proto" />
+ </ItemGroup>
+```
+
+You must add a reference to the NuGet packages Grpc.Tools and Grpc (the latter
+is a meta-package, in turn referencing Grpc.Core and Google.Protobuf packages).
+It is **very important** to mark Grpc.Tools as a development-only dependency, so
+that the *users* of your library do not fetch the tools package:
+
+* "Classic" .csproj with `packages.config` (Visual Studio, Mono): This is
+ handled automatically by NuGet. See the attribute added by Visual Studio to the
+ [packages.config](../../examples/csharp/HelloworldLegacyCsproj/Greeter/packages.config#L6)
+ file in the HelloworldLegacyCsproj/Greeter example.
+
+* "SDK" .csproj (Visual Studio, `dotnet new`): Add an attribute
+ `PrivateAssets="All"` to the Grpc.Tools package reference. See an example in the
+ [Greeter.csproj](../../examples/csharp/Helloworld/Greeter/Greeter.csproj#L10)
+ example project in this repository. If adding a package reference in Visual
+ Studio, edit the project file and add this attribute. [This is a bug in NuGet
+ client](https://github.com/NuGet/Home/issues/4125).
+
+If building a NuGet package from your library with the nuget command line tool
+from a .nuspec file, then the spec file may (and probably should) reference the
+Grpc metapackage, but **do not add a reference to Grpc.Tools** to it. .NET "SDK"
+projects handle this automatically when called from `dotnet pack` by excluding
+any packages with private assets, such as thus marked Grpc.Tools.
+
+#### Per-file options that can be set in Visual Studio
+
+For a "classic" project, you can only add .proto files with all options set to
+default (if you find it necessary to modify these options, then hand-edit the
+.csproj file). Click on the "show all files" button, add files to project, then
+change file type of the .proto files to "Protobuf" in the Properties window
+drop-down. This menu item will appear after you import the Grpc.Tools package:
+
+![Properties in a classic project](doc/integration.md-fig.1-classic.png)
+
+For an "SDK" project, you have more control of some frequently used options.
+**You may need to open and close Visual Studio** for this form to appear in the
+properties window after adding a reference to Grpc.Tools package (we do not know
+whether this is a bug or by design, but it looks like a bug):
+
+![Properties in an SDK project](doc/integration.md-fig.2-sdk.png)
+
+You can also change options of multiple files at once by selecting them in the
+Project Explorer together.
+
+See the Reference section at end of this file for options that can be set
+per-file by modifying the source .csproj directly.
+
+#### My .proto files are in a directory outside the project
+
+Refer to the example files
+[RouteGuide.csproj](../../examples/csharp/RouteGuide/RouteGuide/RouteGuide.csproj#L58-L60)
+and [Greeter.csproj](../../examples/csharp/Helloworld/Greeter/Greeter.csproj#L11)
+in this repository. For the files to show up in Visual Studio properly, add a
+`Link` attribute with just a filename to the `<Protobuf>` item. This will be the
+display name of the file. In the `Include` attribute, specify the complete path
+to file. A relative path is based off the project directory.
+
+Or, if using Visual Studio, add files _as links_ from outside directory. In the
+Add Files dialog, there is a little [down arrow near the Open
+button](https://stackoverflow.com/a/9770061). Click on it, and choose "Add as
+link". If you do not select this option, Visual Studio will copy files to the
+project directory instead.
+
+### I just want to generate proto and gRPC C# sources from my .proto files (no C# compile)
+
+Suppose you want to place generated files right beside each respective source
+.proto file. Create a .csproj library file in the common root of your .proto
+tree, and add a reference to Grpc.Tools package (this works in Windows too, `$`
+below stands for a command prompt in either platform):
+
+```
+/myproject/myprotofiles$ dotnet new classlib
+ . . .
+ Restoring packages for /myproject/myprotofiles/myprotofiles.csproj...
+ . . .
+/myproject/myprotofiles$ rm *.cs <-- remove all *.cs files from template;
+C:\myproject\myprotofiles> del *.cs /y <-- on Windows, use the del command instead.
+/myproject/myprotofiles$ dotnet add package Grpc.Tools
+```
+
+(the latter command also accepts an optional `--version X.Y` switch for a
+specific version of package, should you need one). Next open the generated
+.csproj file in a text editor.
+
+Since you are not building a package, you may not worry about adding
+`PrivateAssets="All"` attribute, but it will not hurt, in case you are
+repurposing the project at some time later. The important part is (1) tell the
+gRPC tools to select the whole directory of files; (2) order placement of each
+output besides its source, and (3) not compile the generated .cs files. Add the
+following stanza under the `<Project>` xml node:
+
+```xml
+ <ItemGroup>
+ <Protobuf Include="**/*.proto" OutputDir="%(RelativePath)" CompileOutputs="false" />
+ </ItemGroup>
+```
+
+The `Include` tells the build system to recursively examine project directory
+and its subdirectories (`**`) include all files matching the wildcard `*.proto`.
+You can instead selectively include your files or selectively exclude files from
+the glob pattern; [MSBuild documentation explains
+that](https://docs.microsoft.com/visualstudio/msbuild/msbuild-items). The
+`OutputDir="%(RelativePath)"` orders the output directory for each .cs file be
+same as the corresponding .proto directory. Finally, `CompileOutputs="false"`
+prevents compiling the generated files into an assembly.
+
+Note that an empty assembly is still generated, but you should ignore it. As
+with any build system, it is used to detect out-of-date dependencies and
+recompile them.
+
+#### I am getting a warning about a missing expected file!
+
+When we are preparing compile, there is no way to know whether a given proto
+file will produce a *Grpc.cs output or not. If the proto file has a `service`
+clause, it will; otherwise, it won't, but the build script cannot know that in
+advance. When we are treating generated .cs files as temporary, this is ok, but
+when generating them for you, creating empty files is probably not. You need to
+tell the compiler which files should be compiled with gRPC services, and which
+only contain protobuffer message definitions.
+
+One option is just ignore the warning. Another is quench it by setting the
+property `Protobuf_NoWarnMissingExpected` to `true`:
+
+```xml
+<PropertyGroup>
+ <Protobuf_NoWarnMissingExpected>true</Protobuf_NoWarnMissingExpected>
+</PropertyGroup>
+```
+
+For a small to medium projects this is sufficient. But because of a missing
+output dependency, the corresponding .proto file will be recompiled on every
+build. If your project is large, or if other large builds depend on generated
+files, and are also needlessly recompiled, you'll want to prevent these rebuilds
+when files have not in fact changed, as follows:
+
+##### Explicitly tell protoc for which files it should use the gRPC plugin
+
+You need to set the `Protobuf` item property `GrpcServices` to `None` for those
+.proto inputs which do not have a `service` declared (or, optionally, those
+which do but you do not want a service/client stub for). The default value for
+the `GrpcServices` is `Both` (both client and server stub are generated). This
+is easy enough to do with glob patterns if your files are laid out in
+directories according to their service use, for example:
+
+```xml
+ <ItemGroup>
+ <Protobuf Include="**/*.proto" OutputDir="%(RelativePath)"
+ CompileOutputs="false" GrpcServices="None" />
+ <Protobuf Update="**/hello/*.proto;**/bye/*.proto" GrpcServices="Both" />
+ </ItemGroup>
+```
+
+In this sample, all .proto files are compiled with `GrpcServices="None"`, except
+for .proto files in subdirectories on any tree level named `hello/` and `bye`,
+which will take `GrpcServices="Both"` Note the use of the `Update` attribute
+instead of `Include`. If you write `Include` by mistake, the files will be added
+to compile *twice*, once with, and once without GrpcServices. Pay attention not
+to do that!
+
+Another example would be the use of globbing if your service .proto files are
+named according to a pattern, for example `*_services.proto`. In this case, The
+`Update` attribute can be written as `Update="**/*_service.proto"`, to set the
+attribute `GrpcServices="Both"` only on these files.
+
+But what if no patterns work, and you cannot sort a large set of .proto file
+into those containing a service and those not? As a last resort,
+
+##### Force creating empty .cs files for missing outputs.
+
+Naturally, this results in a dirtier compiler output tree, but you may clean it
+using other ways (for example, by not copying zero-length .cs files to their
+final destination). Remember, though, that the files are still important to keep
+in their output locations to prevent needless recompilation. You may force
+generating empty files by setting the property `Protobuf_TouchMissingExpected`
+to `true`:
+
+```xml
+ <PropertyGroup>
+ <Protobuf_TouchMissingExpected>true</Protobuf_TouchMissingExpected>
+ </PropertyGroup>
+```
+
+#### But I do not use gRPC at all, I need only protobuffer messages compiled
+
+Set `GrpcServices="None"` on all proto files:
+
+```xml
+ <ItemGroup>
+ <Protobuf Include="**/*.proto" OutputDir="%(RelativeDir)"
+ CompileOutputs="false" GrpcServices="None" />
+ </ItemGroup>
+```
+
+#### That's good so far, but I do not want the `bin` and `obj` directories in my tree
+
+You may create the project in a subdirectory of the root of your files, such as,
+for example, `.build`. In this case, you want to refer to the proto files
+relative to that `.build/` directory as
+
+```xml
+ <ItemGroup>
+ <Protobuf Include="../**/*.proto" ProtoRoot=".."
+ OutputDir="%(RelativeDir)" CompileOutputs="false" />
+ </ItemGroup>
+```
+
+Pay attention to the `ProtoRoot` property. It needs to be set to the directory
+where `import` declarations in the .proto files are looking for files, since the
+project root is no longer the same as the proto root.
+
+Alternatively, you may place the project in a directory *above* your proto root,
+and refer to the files with a subdirectory name:
+
+```xml
+ <ItemGroup>
+ <Protobuf Include="proto_root/**/*.proto" ProtoRoot="proto_root"
+ OutputDir="%(RelativeDir)" CompileOutputs="false" />
+ </ItemGroup>
+```
+
+### Alas, this all is nice, but my scenario is more complex, -OR-
+### I'll investigate that when I have time. I just want to run protoc as I did before.
+
+One option is examine our [.targets and .props files](Grpc.Tools/build/) and see
+if you can create your own build sequence from the provided targets so that it
+fits your needs. Also please open an issue (and tag @kkm000 in it!) with your
+scenario. We'll try to support it if it appears general enough.
+
+But if you just want to run `protoc` using MsBuild `<Exec>` task, as you
+probably did before the version 1.17 of Grpc.Tools, we have a few build
+variables that point to resolved names of tools and common protoc imports.
+You'll have to roll your own dependency checking (or go with a full
+recompilation each time, if that works for you), but at the very least each
+version of the Tools package will point to the correct location of the files,
+and resolve the compiler and plugin executables appropriate for the host system.
+These property variables are:
+
+* `Protobuf_ProtocFullPath` points to the full path and filename of protoc executable, e. g.,
+ "C:\Users\kkm\.nuget\packages\grpc.tools\1.17.0\build\native\bin\windows\protoc.exe".
+
+* `gRPC_PluginFullPath` points to the full path and filename of gRPC plugin, such as
+ "C:\Users\kkm\.nuget\packages\grpc.tools\1.17.0\build\native\bin\windows\grpc_csharp_plugin.exe"
+
+* `Protobuf_StandardImportsPath` points to the standard proto import directory, for example,
+ "C:\Users\kkm\.nuget\packages\grpc.tools\1.17.0\build\native\include". This is
+ the directory where a declaration such as `import "google/protobuf/wrappers.proto";`
+ in a proto file would find its target.
+
+Use MSBuild property expansion syntax `$(VariableName)` in your protoc command
+line to substitute these variables, for instance,
+
+```xml
+ <Target Name="MyProtoCompile">
+ <PropertyGroup>
+ <ProtoCCommand>$(Protobuf_ProtocFullPath) --plugin=protoc-gen-grpc=$(gRPC_PluginFullPath) -I $(Protobuf_StandardImportsPath) ....rest of your command.... </ProtoCCommand>
+ </PropertyGroup>
+ <Message Importance="high" Text="$(ProtoCCommand)" />
+ <Exec Command="$(ProtoCCommand)" />
+ </Target>
+```
+
+Also make sure *not* to include any file names to the `Protobuf` item
+collection, otherwise they will be compiled by default. If, by any chance, you
+used that name for your build scripting, you must rename it.
+
+### What about C++ projects?
+
+This is in the works. Currently, the same variables as above are set to point to
+the protoc binary, C++ gRPC plugin and the standard imports, but nothing else.
+Do not use the `Protobuf` item collection name so that your project remains
+future-proof. We'll use it for C++ projects too.
+
+Reference
+---------
+
+### Protobuf item metadata reference
+
+The following metadata are recognized on the `<Protobuf>` items.
+
+| Name | Default | Value | Synopsis |
+|----------------|-----------|----------------------|----------------------------------|
+| Access | `public` | `public`, `internal` | Generated class access |
+| ProtoCompile | `true` | `true`, `false` | Pass files to protoc? |
+| ProtoRoot | See notes | A directory | Common root for set of files |
+| CompileOutputs | `true` | `true`, `false` | C#-compile generated files? |
+| OutputDir | See notes | A directory | Directory for generated C# files |
+| GrpcOutputDir | See notes | A directory | Directory for generated stubs |
+| GrpcServices | `both` | `none`, `client`, | Generated gRPC stubs |
+| | | `server`, `both` | |
+
+__Notes__
+
+* __ProtoRoot__
+For files _inside_ the project cone, `ProtoRoot` is set by default to the
+project directory. For every file _outside_ of the project directory, the value
+is set to this file's containing directory name, individually per file. If you
+include a subtree of proto files that lies outside of the project directory, you
+need to set this metadatum. There is an example in this file above. The path in
+this variable is relative to the project directory.
+
+* __OutputDir__
+The default value for this metadatum is the value of the property
+`Protobuf_OutputPath`. This property, in turn, unless you set it in your
+project, will be set to the value of the standard MSBuild property
+`IntermediateOutputPath`, which points to the location of compilation object
+outputs, such as "obj/Release/netstandard1.5/". The path in this property is
+considered relative to the project directory.
+
+* __GrpcOutputDir__
+Unless explicitly set, will follow `OutputDir` for any given file.
+
+* __Access__
+Sets generated class access on _both_ generated message and gRPC stub classes.
diff --git a/src/csharp/Grpc.Core.Tests/ChannelConnectivityTest.cs b/src/csharp/Grpc.Core.Tests/ChannelConnectivityTest.cs
index a43040f01a..0834ddadda 100644
--- a/src/csharp/Grpc.Core.Tests/ChannelConnectivityTest.cs
+++ b/src/csharp/Grpc.Core.Tests/ChannelConnectivityTest.cs
@@ -57,19 +57,23 @@ namespace Grpc.Core.Tests
[Test]
public async Task Channel_WaitForStateChangedAsync()
{
- helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
- {
- return Task.FromResult(request);
- });
-
Assert.ThrowsAsync(typeof(TaskCanceledException),
- async () => await channel.WaitForStateChangedAsync(channel.State, DateTime.UtcNow.AddMilliseconds(10)));
+ async () => await channel.WaitForStateChangedAsync(channel.State, DateTime.UtcNow.AddMilliseconds(0)));
var stateChangedTask = channel.WaitForStateChangedAsync(channel.State);
+ await channel.ConnectAsync(DateTime.UtcNow.AddMilliseconds(5000));
+ await stateChangedTask;
+ Assert.AreEqual(ChannelState.Ready, channel.State);
+ }
- await Calls.AsyncUnaryCall(helper.CreateUnaryCall(), "abc");
+ [Test]
+ public async Task Channel_TryWaitForStateChangedAsync()
+ {
+ Assert.IsFalse(await channel.TryWaitForStateChangedAsync(channel.State, DateTime.UtcNow.AddMilliseconds(0)));
- await stateChangedTask;
+ var stateChangedTask = channel.TryWaitForStateChangedAsync(channel.State);
+ await channel.ConnectAsync(DateTime.UtcNow.AddMilliseconds(5000));
+ Assert.IsTrue(await stateChangedTask);
Assert.AreEqual(ChannelState.Ready, channel.State);
}
diff --git a/src/csharp/Grpc.Core.Tests/ChannelCredentialsTest.cs b/src/csharp/Grpc.Core.Tests/ChannelCredentialsTest.cs
index 62cc904a61..843d88bfb6 100644
--- a/src/csharp/Grpc.Core.Tests/ChannelCredentialsTest.cs
+++ b/src/csharp/Grpc.Core.Tests/ChannelCredentialsTest.cs
@@ -17,13 +17,7 @@
#endregion
using System;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using Grpc.Core;
using Grpc.Core.Internal;
-using Grpc.Core.Utils;
using NUnit.Framework;
namespace Grpc.Core.Tests
@@ -44,9 +38,38 @@ namespace Grpc.Core.Tests
Assert.Throws(typeof(ArgumentNullException), () => ChannelCredentials.Create(null, new FakeCallCredentials()));
Assert.Throws(typeof(ArgumentNullException), () => ChannelCredentials.Create(new FakeChannelCredentials(true), null));
-
+
// forbid composing non-composable
Assert.Throws(typeof(ArgumentException), () => ChannelCredentials.Create(new FakeChannelCredentials(false), new FakeCallCredentials()));
}
+
+ [Test]
+ public void ChannelCredentials_NativeCredentialsAreReused()
+ {
+ // always returning the same native object is critical for subchannel sharing to work with secure channels
+ var creds = new SslCredentials();
+ var nativeCreds1 = creds.GetNativeCredentials();
+ var nativeCreds2 = creds.GetNativeCredentials();
+ Assert.AreSame(nativeCreds1, nativeCreds2);
+ }
+
+ [Test]
+ public void ChannelCredentials_CreateExceptionIsCached()
+ {
+ var creds = new ChannelCredentialsWithCreateNativeThrows();
+ var ex1 = Assert.Throws(typeof(Exception), () => creds.GetNativeCredentials());
+ var ex2 = Assert.Throws(typeof(Exception), () => creds.GetNativeCredentials());
+ Assert.AreSame(ex1, ex2);
+ }
+
+ internal class ChannelCredentialsWithCreateNativeThrows : ChannelCredentials
+ {
+ internal override bool IsComposable => false;
+
+ internal override ChannelCredentialsSafeHandle CreateNativeCredentials()
+ {
+ throw new Exception("Creation of native credentials has failed on purpose.");
+ }
+ }
}
}
diff --git a/src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs b/src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs
new file mode 100644
index 0000000000..c3aee726f2
--- /dev/null
+++ b/src/csharp/Grpc.Core.Tests/ContextualMarshallerTest.cs
@@ -0,0 +1,119 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC 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.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Grpc.Core;
+using Grpc.Core.Internal;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests
+{
+ public class ContextualMarshallerTest
+ {
+ const string Host = "127.0.0.1";
+
+ MockServiceHelper helper;
+ Server server;
+ Channel channel;
+
+ [SetUp]
+ public void Init()
+ {
+ var contextualMarshaller = new Marshaller<string>(
+ (str, serializationContext) =>
+ {
+ if (str == "UNSERIALIZABLE_VALUE")
+ {
+ // Google.Protobuf throws exception inherited from IOException
+ throw new IOException("Error serializing the message.");
+ }
+ if (str == "SERIALIZE_TO_NULL")
+ {
+ return;
+ }
+ var bytes = System.Text.Encoding.UTF8.GetBytes(str);
+ serializationContext.Complete(bytes);
+ },
+ (deserializationContext) =>
+ {
+ var buffer = deserializationContext.PayloadAsNewBuffer();
+ Assert.AreEqual(buffer.Length, deserializationContext.PayloadLength);
+ var s = System.Text.Encoding.UTF8.GetString(buffer);
+ if (s == "UNPARSEABLE_VALUE")
+ {
+ // Google.Protobuf throws exception inherited from IOException
+ throw new IOException("Error parsing the message.");
+ }
+ return s;
+ });
+ helper = new MockServiceHelper(Host, contextualMarshaller);
+ server = helper.GetServer();
+ server.Start();
+ channel = helper.GetChannel();
+ }
+
+ [TearDown]
+ public void Cleanup()
+ {
+ channel.ShutdownAsync().Wait();
+ server.ShutdownAsync().Wait();
+ }
+
+ [Test]
+ public void UnaryCall()
+ {
+ helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
+ {
+ return Task.FromResult(request);
+ });
+ Assert.AreEqual("ABC", Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "ABC"));
+ }
+
+ [Test]
+ public void ResponseParsingError_UnaryResponse()
+ {
+ helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
+ {
+ return Task.FromResult("UNPARSEABLE_VALUE");
+ });
+
+ var ex = Assert.Throws<RpcException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "REQUEST"));
+ Assert.AreEqual(StatusCode.Internal, ex.Status.StatusCode);
+ }
+
+ [Test]
+ public void RequestSerializationError_BlockingUnary()
+ {
+ Assert.Throws<IOException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "UNSERIALIZABLE_VALUE"));
+ }
+
+ [Test]
+ public void SerializationResultIsNull_BlockingUnary()
+ {
+ Assert.Throws<NullReferenceException>(() => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "SERIALIZE_TO_NULL"));
+ }
+ }
+}
diff --git a/src/csharp/Grpc.Core.Tests/FakeCredentials.cs b/src/csharp/Grpc.Core.Tests/FakeCredentials.cs
index 7d658576e5..f23c9e9757 100644
--- a/src/csharp/Grpc.Core.Tests/FakeCredentials.cs
+++ b/src/csharp/Grpc.Core.Tests/FakeCredentials.cs
@@ -16,15 +16,7 @@
#endregion
-using System;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Threading;
-using System.Threading.Tasks;
-using Grpc.Core;
using Grpc.Core.Internal;
-using Grpc.Core.Utils;
-using NUnit.Framework;
namespace Grpc.Core.Tests
{
@@ -42,7 +34,7 @@ namespace Grpc.Core.Tests
get { return composable; }
}
- internal override ChannelCredentialsSafeHandle ToNativeCredentials()
+ internal override ChannelCredentialsSafeHandle CreateNativeCredentials()
{
return null;
}
diff --git a/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs b/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs
index 9aab54d2d0..775849d89b 100644
--- a/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs
+++ b/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs
@@ -107,6 +107,42 @@ namespace Grpc.Core.Internal.Tests
}
[Test]
+ public void AsyncUnary_RequestSerializationExceptionDoesntLeakResources()
+ {
+ string nullRequest = null; // will throw when serializing
+ Assert.Throws(typeof(ArgumentNullException), () => asyncCall.UnaryCallAsync(nullRequest));
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
+ [Test]
+ public void AsyncUnary_StartCallFailureDoesntLeakResources()
+ {
+ fakeCall.MakeStartCallFail();
+ Assert.Throws(typeof(InvalidOperationException), () => asyncCall.UnaryCallAsync("request1"));
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
+ [Test]
+ public void SyncUnary_RequestSerializationExceptionDoesntLeakResources()
+ {
+ string nullRequest = null; // will throw when serializing
+ Assert.Throws(typeof(ArgumentNullException), () => asyncCall.UnaryCall(nullRequest));
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
+ [Test]
+ public void SyncUnary_StartCallFailureDoesntLeakResources()
+ {
+ fakeCall.MakeStartCallFail();
+ Assert.Throws(typeof(InvalidOperationException), () => asyncCall.UnaryCall("request1"));
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
+ [Test]
public void ClientStreaming_StreamingReadNotAllowed()
{
asyncCall.ClientStreamingCallAsync();
@@ -328,6 +364,15 @@ namespace Grpc.Core.Internal.Tests
}
[Test]
+ public void ClientStreaming_StartCallFailureDoesntLeakResources()
+ {
+ fakeCall.MakeStartCallFail();
+ Assert.Throws(typeof(InvalidOperationException), () => asyncCall.ClientStreamingCallAsync());
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
+ [Test]
public void ServerStreaming_StreamingSendNotAllowed()
{
asyncCall.StartServerStreamingCall("request1");
@@ -402,6 +447,27 @@ namespace Grpc.Core.Internal.Tests
}
[Test]
+ public void ServerStreaming_RequestSerializationExceptionDoesntLeakResources()
+ {
+ string nullRequest = null; // will throw when serializing
+ Assert.Throws(typeof(ArgumentNullException), () => asyncCall.StartServerStreamingCall(nullRequest));
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+
+ var responseStream = new ClientResponseStream<string, string>(asyncCall);
+ var readTask = responseStream.MoveNext();
+ }
+
+ [Test]
+ public void ServerStreaming_StartCallFailureDoesntLeakResources()
+ {
+ fakeCall.MakeStartCallFail();
+ Assert.Throws(typeof(InvalidOperationException), () => asyncCall.StartServerStreamingCall("request1"));
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
+ [Test]
public void DuplexStreaming_NoRequestNoResponse_Success()
{
asyncCall.StartDuplexStreamingCall();
@@ -558,6 +624,15 @@ namespace Grpc.Core.Internal.Tests
AssertStreamingResponseError(asyncCall, fakeCall, readTask2, StatusCode.Cancelled);
}
+ [Test]
+ public void DuplexStreaming_StartCallFailureDoesntLeakResources()
+ {
+ fakeCall.MakeStartCallFail();
+ Assert.Throws(typeof(InvalidOperationException), () => asyncCall.StartDuplexStreamingCall());
+ Assert.AreEqual(0, channel.GetCallReferenceCount());
+ Assert.IsTrue(fakeCall.IsDisposed);
+ }
+
ClientSideStatus CreateClientSideStatus(StatusCode statusCode)
{
return new ClientSideStatus(new Status(statusCode, ""), new Metadata());
diff --git a/src/csharp/Grpc.Core.Tests/Internal/FakeNativeCall.cs b/src/csharp/Grpc.Core.Tests/Internal/FakeNativeCall.cs
index 581ac3384b..ef67918dab 100644
--- a/src/csharp/Grpc.Core.Tests/Internal/FakeNativeCall.cs
+++ b/src/csharp/Grpc.Core.Tests/Internal/FakeNativeCall.cs
@@ -31,6 +31,7 @@ namespace Grpc.Core.Internal.Tests
/// </summary>
internal class FakeNativeCall : INativeCall
{
+ private bool shouldStartCallFail;
public IUnaryResponseClientCallback UnaryResponseClientCallback
{
get;
@@ -102,26 +103,31 @@ namespace Grpc.Core.Internal.Tests
public void StartUnary(IUnaryResponseClientCallback callback, byte[] payload, WriteFlags writeFlags, MetadataArraySafeHandle metadataArray, CallFlags callFlags)
{
+ StartCallMaybeFail();
UnaryResponseClientCallback = callback;
}
public void StartUnary(BatchContextSafeHandle ctx, byte[] payload, WriteFlags writeFlags, MetadataArraySafeHandle metadataArray, CallFlags callFlags)
{
+ StartCallMaybeFail();
throw new NotImplementedException();
}
public void StartClientStreaming(IUnaryResponseClientCallback callback, MetadataArraySafeHandle metadataArray, CallFlags callFlags)
{
+ StartCallMaybeFail();
UnaryResponseClientCallback = callback;
}
public void StartServerStreaming(IReceivedStatusOnClientCallback callback, byte[] payload, WriteFlags writeFlags, MetadataArraySafeHandle metadataArray, CallFlags callFlags)
{
+ StartCallMaybeFail();
ReceivedStatusOnClientCallback = callback;
}
public void StartDuplexStreaming(IReceivedStatusOnClientCallback callback, MetadataArraySafeHandle metadataArray, CallFlags callFlags)
{
+ StartCallMaybeFail();
ReceivedStatusOnClientCallback = callback;
}
@@ -165,5 +171,22 @@ namespace Grpc.Core.Internal.Tests
{
IsDisposed = true;
}
+
+ /// <summary>
+ /// Emulate CallSafeHandle.CheckOk() failure for all future attempts
+ /// to start a call.
+ /// </summary>
+ public void MakeStartCallFail()
+ {
+ shouldStartCallFail = true;
+ }
+
+ private void StartCallMaybeFail()
+ {
+ if (shouldStartCallFail)
+ {
+ throw new InvalidOperationException("Start call has failed.");
+ }
+ }
}
}
diff --git a/src/csharp/Grpc.Core.Tests/MarshallerTest.cs b/src/csharp/Grpc.Core.Tests/MarshallerTest.cs
new file mode 100644
index 0000000000..97f64a0575
--- /dev/null
+++ b/src/csharp/Grpc.Core.Tests/MarshallerTest.cs
@@ -0,0 +1,105 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC 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.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Grpc.Core;
+using Grpc.Core.Internal;
+using Grpc.Core.Utils;
+using NUnit.Framework;
+
+namespace Grpc.Core.Tests
+{
+ public class MarshallerTest
+ {
+ [Test]
+ public void ContextualSerializerEmulation()
+ {
+ Func<string, byte[]> simpleSerializer = System.Text.Encoding.UTF8.GetBytes;
+ Func<byte[], string> simpleDeserializer = System.Text.Encoding.UTF8.GetString;
+ var marshaller = new Marshaller<string>(simpleSerializer,
+ simpleDeserializer);
+
+ Assert.AreSame(simpleSerializer, marshaller.Serializer);
+ Assert.AreSame(simpleDeserializer, marshaller.Deserializer);
+
+ // test that emulated contextual serializer and deserializer work
+ string origMsg = "abc";
+ var serializationContext = new FakeSerializationContext();
+ marshaller.ContextualSerializer(origMsg, serializationContext);
+
+ var deserializationContext = new FakeDeserializationContext(serializationContext.Payload);
+ Assert.AreEqual(origMsg, marshaller.ContextualDeserializer(deserializationContext));
+ }
+
+ [Test]
+ public void SimpleSerializerEmulation()
+ {
+ Action<string, SerializationContext> contextualSerializer = (str, context) =>
+ {
+ var bytes = System.Text.Encoding.UTF8.GetBytes(str);
+ context.Complete(bytes);
+ };
+ Func<DeserializationContext, string> contextualDeserializer = (context) =>
+ {
+ return System.Text.Encoding.UTF8.GetString(context.PayloadAsNewBuffer());
+ };
+ var marshaller = new Marshaller<string>(contextualSerializer, contextualDeserializer);
+
+ Assert.AreSame(contextualSerializer, marshaller.ContextualSerializer);
+ Assert.AreSame(contextualDeserializer, marshaller.ContextualDeserializer);
+
+ // test that emulated serializer and deserializer work
+ var origMsg = "abc";
+ var serialized = marshaller.Serializer(origMsg);
+ Assert.AreEqual(origMsg, marshaller.Deserializer(serialized));
+ }
+
+ class FakeSerializationContext : SerializationContext
+ {
+ public byte[] Payload;
+ public override void Complete(byte[] payload)
+ {
+ this.Payload = payload;
+ }
+ }
+
+ class FakeDeserializationContext : DeserializationContext
+ {
+ public byte[] payload;
+
+ public FakeDeserializationContext(byte[] payload)
+ {
+ this.payload = payload;
+ }
+
+ public override int PayloadLength => payload.Length;
+
+ public override byte[] PayloadAsNewBuffer()
+ {
+ return payload;
+ }
+ }
+ }
+}
diff --git a/src/csharp/Grpc.Core.Tests/MetadataTest.cs b/src/csharp/Grpc.Core.Tests/MetadataTest.cs
index 8916731757..d85d7572a6 100644
--- a/src/csharp/Grpc.Core.Tests/MetadataTest.cs
+++ b/src/csharp/Grpc.Core.Tests/MetadataTest.cs
@@ -66,11 +66,30 @@ namespace Grpc.Core.Tests
new Metadata.Entry("0123456789abc", "XYZ");
new Metadata.Entry("-abc", "XYZ");
new Metadata.Entry("a_bc_", "XYZ");
+ new Metadata.Entry("abc.xyz", "XYZ");
+ new Metadata.Entry("abc.xyz-bin", new byte[] {1, 2, 3});
Assert.Throws(typeof(ArgumentException), () => new Metadata.Entry("abc[", "xyz"));
Assert.Throws(typeof(ArgumentException), () => new Metadata.Entry("abc/", "xyz"));
}
[Test]
+ public void KeysAreNormalized_UppercaseKey()
+ {
+ var uppercaseKey = "ABC";
+ var entry = new Metadata.Entry(uppercaseKey, "XYZ");
+ Assert.AreEqual("abc", entry.Key);
+ }
+
+ [Test]
+ public void KeysAreNormalized_LowercaseKey()
+ {
+ var lowercaseKey = "abc";
+ var entry = new Metadata.Entry(lowercaseKey, "XYZ");
+ // no allocation if key already lowercase
+ Assert.AreSame(lowercaseKey, entry.Key);
+ }
+
+ [Test]
public void Entry_ConstructionPreconditions()
{
Assert.Throws(typeof(ArgumentNullException), () => new Metadata.Entry(null, "xyz"));
diff --git a/src/csharp/Grpc.Core.Tests/SanityTest.cs b/src/csharp/Grpc.Core.Tests/SanityTest.cs
index eaad409ec0..0904453b6e 100644
--- a/src/csharp/Grpc.Core.Tests/SanityTest.cs
+++ b/src/csharp/Grpc.Core.Tests/SanityTest.cs
@@ -102,6 +102,7 @@ namespace Grpc.Core.Tests
"Grpc.HealthCheck.Tests",
"Grpc.IntegrationTesting",
"Grpc.Reflection.Tests",
+ "Grpc.Tools.Tests",
};
foreach (var assemblyName in otherAssemblies)
{
diff --git a/src/csharp/Grpc.Core/Channel.cs b/src/csharp/Grpc.Core/Channel.cs
index e9930b6fbc..7ce929dfa3 100644
--- a/src/csharp/Grpc.Core/Channel.cs
+++ b/src/csharp/Grpc.Core/Channel.cs
@@ -72,9 +72,9 @@ namespace Grpc.Core
this.environment = GrpcEnvironment.AddRef();
this.completionQueue = this.environment.PickCompletionQueue();
- using (var nativeCredentials = credentials.ToNativeCredentials())
using (var nativeChannelArgs = ChannelOptions.CreateChannelArgs(this.options.Values))
{
+ var nativeCredentials = credentials.GetNativeCredentials();
if (nativeCredentials != null)
{
this.handle = ChannelSafeHandle.CreateSecure(nativeCredentials, target, nativeChannelArgs);
@@ -136,7 +136,7 @@ namespace Grpc.Core
/// </summary>
public async Task WaitForStateChangedAsync(ChannelState lastObservedState, DateTime? deadline = null)
{
- var result = await WaitForStateChangedInternalAsync(lastObservedState, deadline).ConfigureAwait(false);
+ var result = await TryWaitForStateChangedAsync(lastObservedState, deadline).ConfigureAwait(false);
if (!result)
{
throw new TaskCanceledException("Reached deadline.");
@@ -147,7 +147,7 @@ namespace Grpc.Core
/// Returned tasks completes once channel state has become different from
/// given lastObservedState (<c>true</c> is returned) or if the wait has timed out (<c>false</c> is returned).
/// </summary>
- internal Task<bool> WaitForStateChangedInternalAsync(ChannelState lastObservedState, DateTime? deadline = null)
+ public Task<bool> TryWaitForStateChangedAsync(ChannelState lastObservedState, DateTime? deadline = null)
{
GrpcPreconditions.CheckArgument(lastObservedState != ChannelState.Shutdown,
"Shutdown is a terminal state. No further state changes can occur.");
@@ -297,6 +297,12 @@ namespace Grpc.Core
activeCallCounter.Decrement();
}
+ // for testing only
+ internal long GetCallReferenceCount()
+ {
+ return activeCallCounter.Count;
+ }
+
private ChannelState GetConnectivityState(bool tryToConnect)
{
try
diff --git a/src/csharp/Grpc.Core/ChannelCredentials.cs b/src/csharp/Grpc.Core/ChannelCredentials.cs
index ba482897d7..3ce32f31b7 100644
--- a/src/csharp/Grpc.Core/ChannelCredentials.cs
+++ b/src/csharp/Grpc.Core/ChannelCredentials.cs
@@ -31,6 +31,19 @@ namespace Grpc.Core
public abstract class ChannelCredentials
{
static readonly ChannelCredentials InsecureInstance = new InsecureCredentialsImpl();
+ readonly Lazy<ChannelCredentialsSafeHandle> cachedNativeCredentials;
+
+ /// <summary>
+ /// Creates a new instance of channel credentials
+ /// </summary>
+ public ChannelCredentials()
+ {
+ // Native credentials object need to be kept alive once initialized for subchannel sharing to work correctly
+ // with secure connections. See https://github.com/grpc/grpc/issues/15207.
+ // We rely on finalizer to clean up the native portion of ChannelCredentialsSafeHandle after the ChannelCredentials
+ // instance becomes unused.
+ this.cachedNativeCredentials = new Lazy<ChannelCredentialsSafeHandle>(() => CreateNativeCredentials());
+ }
/// <summary>
/// Returns instance of credentials that provides no security and
@@ -57,11 +70,22 @@ namespace Grpc.Core
}
/// <summary>
- /// Creates native object for the credentials. May return null if insecure channel
- /// should be created.
+ /// Gets native object for the credentials, creating one if it already doesn't exist. May return null if insecure channel
+ /// should be created. Caller must not call <c>Dispose()</c> on the returned native credentials as their lifetime
+ /// is managed by this class (and instances of native credentials are cached).
+ /// </summary>
+ /// <returns>The native credentials.</returns>
+ internal ChannelCredentialsSafeHandle GetNativeCredentials()
+ {
+ return cachedNativeCredentials.Value;
+ }
+
+ /// <summary>
+ /// Creates a new native object for the credentials. May return null if insecure channel
+ /// should be created. For internal use only, use <see cref="GetNativeCredentials"/> instead.
/// </summary>
/// <returns>The native credentials.</returns>
- internal abstract ChannelCredentialsSafeHandle ToNativeCredentials();
+ internal abstract ChannelCredentialsSafeHandle CreateNativeCredentials();
/// <summary>
/// Returns <c>true</c> if this credential type allows being composed by <c>CompositeCredentials</c>.
@@ -73,7 +97,7 @@ namespace Grpc.Core
private sealed class InsecureCredentialsImpl : ChannelCredentials
{
- internal override ChannelCredentialsSafeHandle ToNativeCredentials()
+ internal override ChannelCredentialsSafeHandle CreateNativeCredentials()
{
return null;
}
@@ -145,7 +169,7 @@ namespace Grpc.Core
get { return true; }
}
- internal override ChannelCredentialsSafeHandle ToNativeCredentials()
+ internal override ChannelCredentialsSafeHandle CreateNativeCredentials()
{
return ChannelCredentialsSafeHandle.CreateSslCredentials(rootCertificates, keyCertificatePair);
}
@@ -173,12 +197,11 @@ namespace Grpc.Core
GrpcPreconditions.CheckArgument(channelCredentials.IsComposable, "Supplied channel credentials do not allow composition.");
}
- internal override ChannelCredentialsSafeHandle ToNativeCredentials()
+ internal override ChannelCredentialsSafeHandle CreateNativeCredentials()
{
- using (var channelCreds = channelCredentials.ToNativeCredentials())
using (var callCreds = callCredentials.ToNativeCredentials())
{
- var nativeComposite = ChannelCredentialsSafeHandle.CreateComposite(channelCreds, callCreds);
+ var nativeComposite = ChannelCredentialsSafeHandle.CreateComposite(channelCredentials.GetNativeCredentials(), callCreds);
if (nativeComposite.IsInvalid)
{
throw new ArgumentException("Error creating native composite credentials. Likely, this is because you are trying to compose incompatible credentials.");
diff --git a/src/csharp/Grpc.Core/ChannelOptions.cs b/src/csharp/Grpc.Core/ChannelOptions.cs
index 6ad5d56cad..880f2bef5f 100644
--- a/src/csharp/Grpc.Core/ChannelOptions.cs
+++ b/src/csharp/Grpc.Core/ChannelOptions.cs
@@ -26,8 +26,10 @@ namespace Grpc.Core
/// <summary>
/// Channel option specified when creating a channel.
/// Corresponds to grpc_channel_args from grpc/grpc.h.
+ /// Commonly used channel option names are defined in <c>ChannelOptions</c>,
+ /// but any of the GRPC_ARG_* channel options names defined in grpc_types.h can be used.
/// </summary>
- public sealed class ChannelOption
+ public sealed class ChannelOption : IEquatable<ChannelOption>
{
/// <summary>
/// Type of <c>ChannelOption</c>.
@@ -119,10 +121,60 @@ namespace Grpc.Core
return stringValue;
}
}
+
+ /// <summary>
+ /// Determines whether the specified object is equal to the current object.
+ /// </summary>
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as ChannelOption);
+ }
+
+ /// <summary>
+ /// Determines whether the specified object is equal to the current object.
+ /// </summary>
+ public bool Equals(ChannelOption other)
+ {
+ return other != null &&
+ type == other.type &&
+ name == other.name &&
+ intValue == other.intValue &&
+ stringValue == other.stringValue;
+ }
+
+ /// <summary>
+ /// A hash code for the current object.
+ /// </summary>
+ public override int GetHashCode()
+ {
+ var hashCode = 1412678443;
+ hashCode = hashCode * -1521134295 + type.GetHashCode();
+ hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(name);
+ hashCode = hashCode * -1521134295 + intValue.GetHashCode();
+ hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(stringValue);
+ return hashCode;
+ }
+
+ /// <summary>
+ /// Equality operator.
+ /// </summary>
+ public static bool operator ==(ChannelOption option1, ChannelOption option2)
+ {
+ return EqualityComparer<ChannelOption>.Default.Equals(option1, option2);
+ }
+
+ /// <summary>
+ /// Inequality operator.
+ /// </summary>
+ public static bool operator !=(ChannelOption option1, ChannelOption option2)
+ {
+ return !(option1 == option2);
+ }
}
/// <summary>
- /// Defines names of supported channel options.
+ /// Defines names of most commonly used channel options.
+ /// Other supported options names can be found in grpc_types.h (GRPC_ARG_* definitions)
/// </summary>
public static class ChannelOptions
{
diff --git a/src/csharp/Grpc.Core/ClientBase.cs b/src/csharp/Grpc.Core/ClientBase.cs
index fac34071be..05edce7467 100644
--- a/src/csharp/Grpc.Core/ClientBase.cs
+++ b/src/csharp/Grpc.Core/ClientBase.cs
@@ -151,12 +151,12 @@ namespace Grpc.Core
{
private class ClientBaseConfigurationInterceptor : Interceptor
{
- readonly Func<IMethod, string, CallOptions, Tuple<string, CallOptions>> interceptor;
+ readonly Func<IMethod, string, CallOptions, ClientBaseConfigurationInfo> interceptor;
/// <summary>
/// Creates a new instance of ClientBaseConfigurationInterceptor given the specified header and host interceptor function.
/// </summary>
- public ClientBaseConfigurationInterceptor(Func<IMethod, string, CallOptions, Tuple<string, CallOptions>> interceptor)
+ public ClientBaseConfigurationInterceptor(Func<IMethod, string, CallOptions, ClientBaseConfigurationInfo> interceptor)
{
this.interceptor = GrpcPreconditions.CheckNotNull(interceptor, nameof(interceptor));
}
@@ -166,7 +166,7 @@ namespace Grpc.Core
where TResponse : class
{
var newHostAndCallOptions = interceptor(context.Method, context.Host, context.Options);
- return new ClientInterceptorContext<TRequest, TResponse>(context.Method, newHostAndCallOptions.Item1, newHostAndCallOptions.Item2);
+ return new ClientInterceptorContext<TRequest, TResponse>(context.Method, newHostAndCallOptions.Host, newHostAndCallOptions.CallOptions);
}
public override TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
@@ -195,6 +195,18 @@ namespace Grpc.Core
}
}
+ internal struct ClientBaseConfigurationInfo
+ {
+ internal readonly string Host;
+ internal readonly CallOptions CallOptions;
+
+ internal ClientBaseConfigurationInfo(string host, CallOptions callOptions)
+ {
+ Host = host;
+ CallOptions = callOptions;
+ }
+ }
+
readonly CallInvoker undecoratedCallInvoker;
readonly string host;
@@ -206,7 +218,7 @@ namespace Grpc.Core
internal CallInvoker CreateDecoratedCallInvoker()
{
- return undecoratedCallInvoker.Intercept(new ClientBaseConfigurationInterceptor((method, host, options) => Tuple.Create(this.host, options)));
+ return undecoratedCallInvoker.Intercept(new ClientBaseConfigurationInterceptor((method, host, options) => new ClientBaseConfigurationInfo(this.host, options)));
}
internal ClientBaseConfiguration WithHost(string host)
diff --git a/src/csharp/Grpc.Core/DeserializationContext.cs b/src/csharp/Grpc.Core/DeserializationContext.cs
new file mode 100644
index 0000000000..5b6372ef85
--- /dev/null
+++ b/src/csharp/Grpc.Core/DeserializationContext.cs
@@ -0,0 +1,46 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC 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.
+
+#endregion
+
+namespace Grpc.Core
+{
+ /// <summary>
+ /// Provides access to the payload being deserialized when deserializing messages.
+ /// </summary>
+ public abstract class DeserializationContext
+ {
+ /// <summary>
+ /// Get the total length of the payload in bytes.
+ /// </summary>
+ public abstract int PayloadLength { get; }
+
+ /// <summary>
+ /// Gets the entire payload as a newly allocated byte array.
+ /// Once the byte array is returned, the byte array becomes owned by the caller and won't be ever accessed or reused by gRPC again.
+ /// NOTE: Obtaining the buffer as a newly allocated byte array is the simplest way of accessing the payload,
+ /// but it can have important consequences in high-performance scenarios.
+ /// In particular, using this method usually requires copying of the entire buffer one extra time.
+ /// Also, allocating a new buffer each time can put excessive pressure on GC, especially if
+ /// the payload is more than 86700 bytes large (which means the newly allocated buffer will be placed in LOH,
+ /// and LOH object can only be garbage collected via a full ("stop the world") GC run).
+ /// NOTE: Deserializers are expected not to call this method more than once per received message
+ /// (as there is no practical reason for doing so) and <c>DeserializationContext</c> implementations are free to assume so.
+ /// </summary>
+ /// <returns>byte array containing the entire payload.</returns>
+ public abstract byte[] PayloadAsNewBuffer();
+ }
+}
diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs
index 3c9e090ba4..4cdf0ee6a7 100644
--- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs
+++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs
@@ -17,6 +17,7 @@
#endregion
using System;
+using System.Threading;
using System.Threading.Tasks;
using Grpc.Core.Logging;
using Grpc.Core.Profiling;
@@ -34,6 +35,8 @@ namespace Grpc.Core.Internal
readonly CallInvocationDetails<TRequest, TResponse> details;
readonly INativeCall injectedNativeCall; // for testing
+ bool registeredWithChannel;
+
// Dispose of to de-register cancellation token registration
IDisposable cancellationTokenRegistration;
@@ -77,43 +80,59 @@ namespace Grpc.Core.Internal
using (profiler.NewScope("AsyncCall.UnaryCall"))
using (CompletionQueueSafeHandle cq = CompletionQueueSafeHandle.CreateSync())
{
- byte[] payload = UnsafeSerialize(msg);
+ bool callStartedOk = false;
+ try
+ {
+ unaryResponseTcs = new TaskCompletionSource<TResponse>();
- unaryResponseTcs = new TaskCompletionSource<TResponse>();
+ lock (myLock)
+ {
+ GrpcPreconditions.CheckState(!started);
+ started = true;
+ Initialize(cq);
- lock (myLock)
- {
- GrpcPreconditions.CheckState(!started);
- started = true;
- Initialize(cq);
+ halfcloseRequested = true;
+ readingDone = true;
+ }
- halfcloseRequested = true;
- readingDone = true;
- }
+ byte[] payload = UnsafeSerialize(msg);
- using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
- {
- var ctx = details.Channel.Environment.BatchContextPool.Lease();
- try
+ using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
{
- call.StartUnary(ctx, payload, GetWriteFlagsForCall(), metadataArray, details.Options.Flags);
- var ev = cq.Pluck(ctx.Handle);
- bool success = (ev.success != 0);
+ var ctx = details.Channel.Environment.BatchContextPool.Lease();
try
{
- using (profiler.NewScope("AsyncCall.UnaryCall.HandleBatch"))
+ call.StartUnary(ctx, payload, GetWriteFlagsForCall(), metadataArray, details.Options.Flags);
+ callStartedOk = true;
+
+ var ev = cq.Pluck(ctx.Handle);
+ bool success = (ev.success != 0);
+ try
{
- HandleUnaryResponse(success, ctx.GetReceivedStatusOnClient(), ctx.GetReceivedMessage(), ctx.GetReceivedInitialMetadata());
+ using (profiler.NewScope("AsyncCall.UnaryCall.HandleBatch"))
+ {
+ HandleUnaryResponse(success, ctx.GetReceivedStatusOnClient(), ctx.GetReceivedMessage(), ctx.GetReceivedInitialMetadata());
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Exception occurred while invoking completion delegate.");
}
}
- catch (Exception e)
+ finally
{
- Logger.Error(e, "Exception occurred while invoking completion delegate.");
+ ctx.Recycle();
}
}
- finally
+ }
+ finally
+ {
+ if (!callStartedOk)
{
- ctx.Recycle();
+ lock (myLock)
+ {
+ OnFailedToStartCallLocked();
+ }
}
}
@@ -130,22 +149,35 @@ namespace Grpc.Core.Internal
{
lock (myLock)
{
- GrpcPreconditions.CheckState(!started);
- started = true;
+ bool callStartedOk = false;
+ try
+ {
+ GrpcPreconditions.CheckState(!started);
+ started = true;
- Initialize(details.Channel.CompletionQueue);
+ Initialize(details.Channel.CompletionQueue);
- halfcloseRequested = true;
- readingDone = true;
+ halfcloseRequested = true;
+ readingDone = true;
- byte[] payload = UnsafeSerialize(msg);
+ byte[] payload = UnsafeSerialize(msg);
- unaryResponseTcs = new TaskCompletionSource<TResponse>();
- using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ unaryResponseTcs = new TaskCompletionSource<TResponse>();
+ using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ {
+ call.StartUnary(UnaryResponseClientCallback, payload, GetWriteFlagsForCall(), metadataArray, details.Options.Flags);
+ callStartedOk = true;
+ }
+
+ return unaryResponseTcs.Task;
+ }
+ finally
{
- call.StartUnary(UnaryResponseClientCallback, payload, GetWriteFlagsForCall(), metadataArray, details.Options.Flags);
+ if (!callStartedOk)
+ {
+ OnFailedToStartCallLocked();
+ }
}
- return unaryResponseTcs.Task;
}
}
@@ -157,20 +189,32 @@ namespace Grpc.Core.Internal
{
lock (myLock)
{
- GrpcPreconditions.CheckState(!started);
- started = true;
+ bool callStartedOk = false;
+ try
+ {
+ GrpcPreconditions.CheckState(!started);
+ started = true;
- Initialize(details.Channel.CompletionQueue);
+ Initialize(details.Channel.CompletionQueue);
- readingDone = true;
+ readingDone = true;
+
+ unaryResponseTcs = new TaskCompletionSource<TResponse>();
+ using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ {
+ call.StartClientStreaming(UnaryResponseClientCallback, metadataArray, details.Options.Flags);
+ callStartedOk = true;
+ }
- unaryResponseTcs = new TaskCompletionSource<TResponse>();
- using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ return unaryResponseTcs.Task;
+ }
+ finally
{
- call.StartClientStreaming(UnaryResponseClientCallback, metadataArray, details.Options.Flags);
+ if (!callStartedOk)
+ {
+ OnFailedToStartCallLocked();
+ }
}
-
- return unaryResponseTcs.Task;
}
}
@@ -181,21 +225,33 @@ namespace Grpc.Core.Internal
{
lock (myLock)
{
- GrpcPreconditions.CheckState(!started);
- started = true;
+ bool callStartedOk = false;
+ try
+ {
+ GrpcPreconditions.CheckState(!started);
+ started = true;
- Initialize(details.Channel.CompletionQueue);
+ Initialize(details.Channel.CompletionQueue);
- halfcloseRequested = true;
+ halfcloseRequested = true;
- byte[] payload = UnsafeSerialize(msg);
+ byte[] payload = UnsafeSerialize(msg);
- streamingResponseCallFinishedTcs = new TaskCompletionSource<object>();
- using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ streamingResponseCallFinishedTcs = new TaskCompletionSource<object>();
+ using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ {
+ call.StartServerStreaming(ReceivedStatusOnClientCallback, payload, GetWriteFlagsForCall(), metadataArray, details.Options.Flags);
+ callStartedOk = true;
+ }
+ call.StartReceiveInitialMetadata(ReceivedResponseHeadersCallback);
+ }
+ finally
{
- call.StartServerStreaming(ReceivedStatusOnClientCallback, payload, GetWriteFlagsForCall(), metadataArray, details.Options.Flags);
+ if (!callStartedOk)
+ {
+ OnFailedToStartCallLocked();
+ }
}
- call.StartReceiveInitialMetadata(ReceivedResponseHeadersCallback);
}
}
@@ -207,17 +263,29 @@ namespace Grpc.Core.Internal
{
lock (myLock)
{
- GrpcPreconditions.CheckState(!started);
- started = true;
+ bool callStartedOk = false;
+ try
+ {
+ GrpcPreconditions.CheckState(!started);
+ started = true;
- Initialize(details.Channel.CompletionQueue);
+ Initialize(details.Channel.CompletionQueue);
- streamingResponseCallFinishedTcs = new TaskCompletionSource<object>();
- using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ streamingResponseCallFinishedTcs = new TaskCompletionSource<object>();
+ using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers))
+ {
+ call.StartDuplexStreaming(ReceivedStatusOnClientCallback, metadataArray, details.Options.Flags);
+ callStartedOk = true;
+ }
+ call.StartReceiveInitialMetadata(ReceivedResponseHeadersCallback);
+ }
+ finally
{
- call.StartDuplexStreaming(ReceivedStatusOnClientCallback, metadataArray, details.Options.Flags);
+ if (!callStartedOk)
+ {
+ OnFailedToStartCallLocked();
+ }
}
- call.StartReceiveInitialMetadata(ReceivedResponseHeadersCallback);
}
}
@@ -325,9 +393,22 @@ namespace Grpc.Core.Internal
}
}
- protected override void OnAfterReleaseResources()
+ protected override void OnAfterReleaseResourcesLocked()
{
- details.Channel.RemoveCallReference(this);
+ if (registeredWithChannel)
+ {
+ details.Channel.RemoveCallReference(this);
+ registeredWithChannel = false;
+ }
+ }
+
+ protected override void OnAfterReleaseResourcesUnlocked()
+ {
+ // If cancellation callback is in progress, this can block
+ // so we need to do this outside of call's lock to prevent
+ // deadlock.
+ // See https://github.com/grpc/grpc/issues/14777
+ // See https://github.com/dotnet/corefx/issues/14903
cancellationTokenRegistration?.Dispose();
}
@@ -385,10 +466,27 @@ namespace Grpc.Core.Internal
var call = CreateNativeCall(cq);
details.Channel.AddCallReference(this);
+ registeredWithChannel = true;
InitializeInternal(call);
+
RegisterCancellationCallback();
}
+ private void OnFailedToStartCallLocked()
+ {
+ ReleaseResources();
+
+ // We need to execute the hook that disposes the cancellation token
+ // registration, but it cannot be done from under a lock.
+ // To make things simple, we just schedule the unregistering
+ // on a threadpool.
+ // - Once the native call is disposed, the Cancel() calls are ignored anyway
+ // - We don't care about the overhead as OnFailedToStartCallLocked() only happens
+ // when something goes very bad when initializing a call and that should
+ // never happen when gRPC is used correctly.
+ ThreadPool.QueueUserWorkItem((state) => OnAfterReleaseResourcesUnlocked());
+ }
+
private INativeCall CreateNativeCall(CompletionQueueSafeHandle cq)
{
if (injectedNativeCall != null)
@@ -448,6 +546,7 @@ namespace Grpc.Core.Internal
TResponse msg = default(TResponse);
var deserializeException = TryDeserialize(receivedMessage, out msg);
+ bool releasedResources;
lock (myLock)
{
finished = true;
@@ -464,7 +563,12 @@ namespace Grpc.Core.Internal
streamingWriteTcs = null;
}
- ReleaseResourcesIfPossible();
+ releasedResources = ReleaseResourcesIfPossible();
+ }
+
+ if (releasedResources)
+ {
+ OnAfterReleaseResourcesUnlocked();
}
responseHeadersTcs.SetResult(responseHeaders);
@@ -494,6 +598,7 @@ namespace Grpc.Core.Internal
TaskCompletionSource<object> delayedStreamingWriteTcs = null;
+ bool releasedResources;
lock (myLock)
{
finished = true;
@@ -504,7 +609,12 @@ namespace Grpc.Core.Internal
streamingWriteTcs = null;
}
- ReleaseResourcesIfPossible();
+ releasedResources = ReleaseResourcesIfPossible();
+ }
+
+ if (releasedResources)
+ {
+ OnAfterReleaseResourcesUnlocked();
}
if (delayedStreamingWriteTcs != null)
diff --git a/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs b/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs
index 3273c26b88..a93dc34620 100644
--- a/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs
+++ b/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs
@@ -189,17 +189,21 @@ namespace Grpc.Core.Internal
/// </summary>
protected abstract Exception GetRpcExceptionClientOnly();
- private void ReleaseResources()
+ protected void ReleaseResources()
{
if (call != null)
{
call.Dispose();
}
disposed = true;
- OnAfterReleaseResources();
+ OnAfterReleaseResourcesLocked();
}
- protected virtual void OnAfterReleaseResources()
+ protected virtual void OnAfterReleaseResourcesLocked()
+ {
+ }
+
+ protected virtual void OnAfterReleaseResourcesUnlocked()
{
}
@@ -235,6 +239,7 @@ namespace Grpc.Core.Internal
{
bool delayCompletion = false;
TaskCompletionSource<object> origTcs = null;
+ bool releasedResources;
lock (myLock)
{
if (!success && !finished && IsClient) {
@@ -252,7 +257,12 @@ namespace Grpc.Core.Internal
streamingWriteTcs = null;
}
- ReleaseResourcesIfPossible();
+ releasedResources = ReleaseResourcesIfPossible();
+ }
+
+ if (releasedResources)
+ {
+ OnAfterReleaseResourcesUnlocked();
}
if (!success)
@@ -282,9 +292,15 @@ namespace Grpc.Core.Internal
/// </summary>
protected void HandleSendStatusFromServerFinished(bool success)
{
+ bool releasedResources;
lock (myLock)
{
- ReleaseResourcesIfPossible();
+ releasedResources = ReleaseResourcesIfPossible();
+ }
+
+ if (releasedResources)
+ {
+ OnAfterReleaseResourcesUnlocked();
}
if (!success)
@@ -310,6 +326,7 @@ namespace Grpc.Core.Internal
var deserializeException = (success && receivedMessage != null) ? TryDeserialize(receivedMessage, out msg) : null;
TaskCompletionSource<TRead> origTcs = null;
+ bool releasedResources;
lock (myLock)
{
origTcs = streamingReadTcs;
@@ -332,7 +349,12 @@ namespace Grpc.Core.Internal
streamingReadTcs = null;
}
- ReleaseResourcesIfPossible();
+ releasedResources = ReleaseResourcesIfPossible();
+ }
+
+ if (releasedResources)
+ {
+ OnAfterReleaseResourcesUnlocked();
}
if (deserializeException != null && !IsClient)
diff --git a/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs b/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs
index 11acb27533..0ceca4abb8 100644
--- a/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs
+++ b/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs
@@ -184,7 +184,7 @@ namespace Grpc.Core.Internal
throw new InvalidOperationException("Call be only called for client calls");
}
- protected override void OnAfterReleaseResources()
+ protected override void OnAfterReleaseResourcesLocked()
{
server.RemoveCallReference(this);
}
@@ -206,6 +206,7 @@ namespace Grpc.Core.Internal
{
// NOTE: because this event is a result of batch containing GRPC_OP_RECV_CLOSE_ON_SERVER,
// success will be always set to true.
+ bool releasedResources;
lock (myLock)
{
finished = true;
@@ -217,7 +218,12 @@ namespace Grpc.Core.Internal
streamingReadTcs = new TaskCompletionSource<TRequest>();
streamingReadTcs.SetResult(default(TRequest));
}
- ReleaseResourcesIfPossible();
+ releasedResources = ReleaseResourcesIfPossible();
+ }
+
+ if (releasedResources)
+ {
+ OnAfterReleaseResourcesUnlocked();
}
if (cancelled)
diff --git a/src/csharp/Grpc.Core/Internal/MarshalUtils.cs b/src/csharp/Grpc.Core/Internal/MarshalUtils.cs
index 3f9605bfdd..09ded1a036 100644
--- a/src/csharp/Grpc.Core/Internal/MarshalUtils.cs
+++ b/src/csharp/Grpc.Core/Internal/MarshalUtils.cs
@@ -35,6 +35,13 @@ namespace Grpc.Core.Internal
/// </summary>
public static string PtrToStringUTF8(IntPtr ptr, int len)
{
+ if (len == 0)
+ {
+ return "";
+ }
+
+ // TODO(jtattermusch): once Span dependency is added,
+ // use Span-based API to decode the string without copying the buffer.
var bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
return EncodingUTF8.GetString(bytes);
diff --git a/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs b/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs
index 4d695e8850..faeb51e6f7 100644
--- a/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs
+++ b/src/csharp/Grpc.Core/Internal/NativeMetadataCredentialsPlugin.cs
@@ -68,8 +68,8 @@ namespace Grpc.Core.Internal
}
catch (Exception e)
{
- Native.grpcsharp_metadata_credentials_notify_from_plugin(callbackPtr, userDataPtr, MetadataArraySafeHandle.Create(Metadata.Empty), StatusCode.Unknown, GetMetadataExceptionStatusMsg);
- Logger.Error(e, GetMetadataExceptionLogMsg);
+ // eat the exception, we must not throw when inside callback from native code.
+ Logger.Error(e, "Exception occurred while invoking native metadata interceptor handler.");
}
}
@@ -87,7 +87,8 @@ namespace Grpc.Core.Internal
}
catch (Exception e)
{
- Native.grpcsharp_metadata_credentials_notify_from_plugin(callbackPtr, userDataPtr, MetadataArraySafeHandle.Create(Metadata.Empty), StatusCode.Unknown, GetMetadataExceptionStatusMsg);
+ string detail = GetMetadataExceptionStatusMsg + " " + e.ToString();
+ Native.grpcsharp_metadata_credentials_notify_from_plugin(callbackPtr, userDataPtr, MetadataArraySafeHandle.Create(Metadata.Empty), StatusCode.Unknown, detail);
Logger.Error(e, GetMetadataExceptionLogMsg);
}
}
diff --git a/src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs b/src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs
index 153a52f947..a45cbe4107 100644
--- a/src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs
+++ b/src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs
@@ -505,7 +505,7 @@ namespace Grpc.Core.Internal
public delegate void grpcsharp_redirect_log_delegate(GprLogDelegate callback);
public delegate CallCredentialsSafeHandle grpcsharp_metadata_credentials_create_from_plugin_delegate(NativeMetadataInterceptor interceptor);
public delegate void grpcsharp_metadata_credentials_notify_from_plugin_delegate(IntPtr callbackPtr, IntPtr userData, MetadataArraySafeHandle metadataArray, StatusCode statusCode, string errorDetails);
- public delegate ServerCredentialsSafeHandle grpcsharp_ssl_server_credentials_create_delegate(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, UIntPtr numKeyCertPairs, int forceClientAuth);
+ public delegate ServerCredentialsSafeHandle grpcsharp_ssl_server_credentials_create_delegate(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, UIntPtr numKeyCertPairs, SslClientCertificateRequestType clientCertificateRequest);
public delegate void grpcsharp_server_credentials_release_delegate(IntPtr credentials);
public delegate ServerSafeHandle grpcsharp_server_create_delegate(ChannelArgsSafeHandle args);
public delegate void grpcsharp_server_register_completion_queue_delegate(ServerSafeHandle server, CompletionQueueSafeHandle cq);
@@ -752,7 +752,7 @@ namespace Grpc.Core.Internal
public static extern void grpcsharp_metadata_credentials_notify_from_plugin(IntPtr callbackPtr, IntPtr userData, MetadataArraySafeHandle metadataArray, StatusCode statusCode, string errorDetails);
[DllImport(ImportName)]
- public static extern ServerCredentialsSafeHandle grpcsharp_ssl_server_credentials_create(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, UIntPtr numKeyCertPairs, int forceClientAuth);
+ public static extern ServerCredentialsSafeHandle grpcsharp_ssl_server_credentials_create(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, UIntPtr numKeyCertPairs, SslClientCertificateRequestType clientCertificateRequest);
[DllImport(ImportName)]
public static extern void grpcsharp_server_credentials_release(IntPtr credentials);
@@ -1045,7 +1045,7 @@ namespace Grpc.Core.Internal
public static extern void grpcsharp_metadata_credentials_notify_from_plugin(IntPtr callbackPtr, IntPtr userData, MetadataArraySafeHandle metadataArray, StatusCode statusCode, string errorDetails);
[DllImport(ImportName)]
- public static extern ServerCredentialsSafeHandle grpcsharp_ssl_server_credentials_create(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, UIntPtr numKeyCertPairs, int forceClientAuth);
+ public static extern ServerCredentialsSafeHandle grpcsharp_ssl_server_credentials_create(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, UIntPtr numKeyCertPairs, SslClientCertificateRequestType clientCertificateRequest);
[DllImport(ImportName)]
public static extern void grpcsharp_server_credentials_release(IntPtr credentials);
diff --git a/src/csharp/Grpc.Core/Internal/ServerCredentialsSafeHandle.cs b/src/csharp/Grpc.Core/Internal/ServerCredentialsSafeHandle.cs
index 545e581f94..5f8c95c4ea 100644
--- a/src/csharp/Grpc.Core/Internal/ServerCredentialsSafeHandle.cs
+++ b/src/csharp/Grpc.Core/Internal/ServerCredentialsSafeHandle.cs
@@ -32,13 +32,13 @@ namespace Grpc.Core.Internal
{
}
- public static ServerCredentialsSafeHandle CreateSslCredentials(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, bool forceClientAuth)
+ public static ServerCredentialsSafeHandle CreateSslCredentials(string pemRootCerts, string[] keyCertPairCertChainArray, string[] keyCertPairPrivateKeyArray, SslClientCertificateRequestType clientCertificateRequest)
{
GrpcPreconditions.CheckArgument(keyCertPairCertChainArray.Length == keyCertPairPrivateKeyArray.Length);
return Native.grpcsharp_ssl_server_credentials_create(pemRootCerts,
keyCertPairCertChainArray, keyCertPairPrivateKeyArray,
new UIntPtr((ulong)keyCertPairCertChainArray.Length),
- forceClientAuth ? 1 : 0);
+ clientCertificateRequest);
}
protected override bool ReleaseHandle()
diff --git a/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs b/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs
index 7185d68efe..1786fc2e3f 100644
--- a/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs
+++ b/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs
@@ -51,11 +51,12 @@ namespace Grpc.Core.Internal
Logger.Debug("Attempting to load native library \"{0}\"", this.libraryPath);
- this.handle = PlatformSpecificLoadLibrary(this.libraryPath);
+ this.handle = PlatformSpecificLoadLibrary(this.libraryPath, out string loadLibraryErrorDetail);
if (this.handle == IntPtr.Zero)
{
- throw new IOException(string.Format("Error loading native library \"{0}\"", this.libraryPath));
+ throw new IOException(string.Format("Error loading native library \"{0}\". {1}",
+ this.libraryPath, loadLibraryErrorDetail));
}
}
@@ -129,31 +130,44 @@ namespace Grpc.Core.Internal
/// <summary>
/// Loads library in a platform specific way.
/// </summary>
- private static IntPtr PlatformSpecificLoadLibrary(string libraryPath)
+ private static IntPtr PlatformSpecificLoadLibrary(string libraryPath, out string errorMsg)
{
if (PlatformApis.IsWindows)
{
+ // TODO(jtattermusch): populate the error on Windows
+ errorMsg = null;
return Windows.LoadLibrary(libraryPath);
}
if (PlatformApis.IsLinux)
{
if (PlatformApis.IsMono)
{
- return Mono.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+ return LoadLibraryPosix(Mono.dlopen, Mono.dlerror, libraryPath, out errorMsg);
}
if (PlatformApis.IsNetCore)
{
- return CoreCLR.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+ return LoadLibraryPosix(CoreCLR.dlopen, CoreCLR.dlerror, libraryPath, out errorMsg);
}
- return Linux.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+ return LoadLibraryPosix(Linux.dlopen, Linux.dlerror, libraryPath, out errorMsg);
}
if (PlatformApis.IsMacOSX)
{
- return MacOSX.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+ return LoadLibraryPosix(MacOSX.dlopen, MacOSX.dlerror, libraryPath, out errorMsg);
}
throw new InvalidOperationException("Unsupported platform.");
}
+ private static IntPtr LoadLibraryPosix(Func<string, int, IntPtr> dlopenFunc, Func<IntPtr> dlerrorFunc, string libraryPath, out string errorMsg)
+ {
+ errorMsg = null;
+ IntPtr ret = dlopenFunc(libraryPath, RTLD_GLOBAL + RTLD_LAZY);
+ if (ret == IntPtr.Zero)
+ {
+ errorMsg = Marshal.PtrToStringAnsi(dlerrorFunc());
+ }
+ return ret;
+ }
+
private static string FirstValidLibraryPath(string[] libraryPathAlternatives)
{
GrpcPreconditions.CheckArgument(libraryPathAlternatives.Length > 0, "libraryPathAlternatives cannot be empty.");
@@ -184,6 +198,9 @@ namespace Grpc.Core.Internal
internal static extern IntPtr dlopen(string filename, int flags);
[DllImport("libdl.so")]
+ internal static extern IntPtr dlerror();
+
+ [DllImport("libdl.so")]
internal static extern IntPtr dlsym(IntPtr handle, string symbol);
}
@@ -193,6 +210,9 @@ namespace Grpc.Core.Internal
internal static extern IntPtr dlopen(string filename, int flags);
[DllImport("libSystem.dylib")]
+ internal static extern IntPtr dlerror();
+
+ [DllImport("libSystem.dylib")]
internal static extern IntPtr dlsym(IntPtr handle, string symbol);
}
@@ -209,6 +229,9 @@ namespace Grpc.Core.Internal
internal static extern IntPtr dlopen(string filename, int flags);
[DllImport("__Internal")]
+ internal static extern IntPtr dlerror();
+
+ [DllImport("__Internal")]
internal static extern IntPtr dlsym(IntPtr handle, string symbol);
}
@@ -223,6 +246,9 @@ namespace Grpc.Core.Internal
internal static extern IntPtr dlopen(string filename, int flags);
[DllImport("libcoreclr.so")]
+ internal static extern IntPtr dlerror();
+
+ [DllImport("libcoreclr.so")]
internal static extern IntPtr dlsym(IntPtr handle, string symbol);
}
}
diff --git a/src/csharp/Grpc.Core/Marshaller.cs b/src/csharp/Grpc.Core/Marshaller.cs
index 1d758ca935..0af9aa586b 100644
--- a/src/csharp/Grpc.Core/Marshaller.cs
+++ b/src/csharp/Grpc.Core/Marshaller.cs
@@ -29,36 +29,129 @@ namespace Grpc.Core
readonly Func<T, byte[]> serializer;
readonly Func<byte[], T> deserializer;
+ readonly Action<T, SerializationContext> contextualSerializer;
+ readonly Func<DeserializationContext, T> contextualDeserializer;
+
/// <summary>
- /// Initializes a new marshaller.
+ /// Initializes a new marshaller from simple serialize/deserialize functions.
/// </summary>
/// <param name="serializer">Function that will be used to serialize messages.</param>
/// <param name="deserializer">Function that will be used to deserialize messages.</param>
public Marshaller(Func<T, byte[]> serializer, Func<byte[], T> deserializer)
{
- this.serializer = GrpcPreconditions.CheckNotNull(serializer, "serializer");
- this.deserializer = GrpcPreconditions.CheckNotNull(deserializer, "deserializer");
+ this.serializer = GrpcPreconditions.CheckNotNull(serializer, nameof(serializer));
+ this.deserializer = GrpcPreconditions.CheckNotNull(deserializer, nameof(deserializer));
+ this.contextualSerializer = EmulateContextualSerializer;
+ this.contextualDeserializer = EmulateContextualDeserializer;
}
/// <summary>
- /// Gets the serializer function.
+ /// Initializes a new marshaller from serialize/deserialize fuctions that can access serialization and deserialization
+ /// context. Compared to the simple serializer/deserializer functions, using the contextual version provides more
+ /// flexibility and can lead to increased efficiency (and better performance).
+ /// Note: This constructor is part of an experimental API that can change or be removed without any prior notice.
/// </summary>
- public Func<T, byte[]> Serializer
+ /// <param name="serializer">Function that will be used to serialize messages.</param>
+ /// <param name="deserializer">Function that will be used to deserialize messages.</param>
+ public Marshaller(Action<T, SerializationContext> serializer, Func<DeserializationContext, T> deserializer)
{
- get
- {
- return this.serializer;
- }
+ this.contextualSerializer = GrpcPreconditions.CheckNotNull(serializer, nameof(serializer));
+ this.contextualDeserializer = GrpcPreconditions.CheckNotNull(deserializer, nameof(deserializer));
+ // TODO(jtattermusch): once gRPC C# library switches to using contextual (de)serializer,
+ // emulating the simple (de)serializer will become unnecessary.
+ this.serializer = EmulateSimpleSerializer;
+ this.deserializer = EmulateSimpleDeserializer;
}
/// <summary>
+ /// Gets the serializer function.
+ /// </summary>
+ public Func<T, byte[]> Serializer => this.serializer;
+
+ /// <summary>
/// Gets the deserializer function.
/// </summary>
- public Func<byte[], T> Deserializer
+ public Func<byte[], T> Deserializer => this.deserializer;
+
+ /// <summary>
+ /// Gets the serializer function.
+ /// Note: experimental API that can change or be removed without any prior notice.
+ /// </summary>
+ public Action<T, SerializationContext> ContextualSerializer => this.contextualSerializer;
+
+ /// <summary>
+ /// Gets the serializer function.
+ /// Note: experimental API that can change or be removed without any prior notice.
+ /// </summary>
+ public Func<DeserializationContext, T> ContextualDeserializer => this.contextualDeserializer;
+
+ // for backward compatibility, emulate the simple serializer using the contextual one
+ private byte[] EmulateSimpleSerializer(T msg)
{
- get
+ // TODO(jtattermusch): avoid the allocation by passing a thread-local instance
+ // This code will become unnecessary once gRPC C# library switches to using contextual (de)serializer.
+ var context = new EmulatedSerializationContext();
+ this.contextualSerializer(msg, context);
+ return context.GetPayload();
+ }
+
+ // for backward compatibility, emulate the simple deserializer using the contextual one
+ private T EmulateSimpleDeserializer(byte[] payload)
+ {
+ // TODO(jtattermusch): avoid the allocation by passing a thread-local instance
+ // This code will become unnecessary once gRPC C# library switches to using contextual (de)serializer.
+ var context = new EmulatedDeserializationContext(payload);
+ return this.contextualDeserializer(context);
+ }
+
+ // for backward compatibility, emulate the contextual serializer using the simple one
+ private void EmulateContextualSerializer(T message, SerializationContext context)
+ {
+ var payload = this.serializer(message);
+ context.Complete(payload);
+ }
+
+ // for backward compatibility, emulate the contextual deserializer using the simple one
+ private T EmulateContextualDeserializer(DeserializationContext context)
+ {
+ return this.deserializer(context.PayloadAsNewBuffer());
+ }
+
+ internal class EmulatedSerializationContext : SerializationContext
+ {
+ bool isComplete;
+ byte[] payload;
+
+ public override void Complete(byte[] payload)
+ {
+ GrpcPreconditions.CheckState(!isComplete);
+ this.isComplete = true;
+ this.payload = payload;
+ }
+
+ internal byte[] GetPayload()
+ {
+ return this.payload;
+ }
+ }
+
+ internal class EmulatedDeserializationContext : DeserializationContext
+ {
+ readonly byte[] payload;
+ bool alreadyCalledPayloadAsNewBuffer;
+
+ public EmulatedDeserializationContext(byte[] payload)
+ {
+ this.payload = GrpcPreconditions.CheckNotNull(payload);
+ }
+
+ public override int PayloadLength => payload.Length;
+
+ public override byte[] PayloadAsNewBuffer()
{
- return this.deserializer;
+ GrpcPreconditions.CheckState(!alreadyCalledPayloadAsNewBuffer);
+ alreadyCalledPayloadAsNewBuffer = true;
+ return payload;
}
}
}
@@ -77,6 +170,15 @@ namespace Grpc.Core
}
/// <summary>
+ /// Creates a marshaller from specified contextual serializer and deserializer.
+ /// Note: This method is part of an experimental API that can change or be removed without any prior notice.
+ /// </summary>
+ public static Marshaller<T> Create<T>(Action<T, SerializationContext> serializer, Func<DeserializationContext, T> deserializer)
+ {
+ return new Marshaller<T>(serializer, deserializer);
+ }
+
+ /// <summary>
/// Returns a marshaller for <c>string</c> type. This is useful for testing.
/// </summary>
public static Marshaller<string> StringMarshaller
diff --git a/src/csharp/Grpc.Core/Metadata.cs b/src/csharp/Grpc.Core/Metadata.cs
index 0e4456278c..bc263c3469 100644
--- a/src/csharp/Grpc.Core/Metadata.cs
+++ b/src/csharp/Grpc.Core/Metadata.cs
@@ -135,7 +135,7 @@ namespace Grpc.Core
}
/// <summary>
- /// <see cref="T:IList`1"/>
+ /// Adds a new ASCII-valued metadata entry. See <c>Metadata.Entry</c> constructor for params.
/// </summary>
public void Add(string key, string value)
{
@@ -143,7 +143,7 @@ namespace Grpc.Core
}
/// <summary>
- /// <see cref="T:IList`1"/>
+ /// Adds a new binary-valued metadata entry. See <c>Metadata.Entry</c> constructor for params.
/// </summary>
public void Add(string key, byte[] valueBytes)
{
@@ -225,8 +225,6 @@ namespace Grpc.Core
/// </summary>
public class Entry
{
- private static readonly Regex ValidKeyRegex = new Regex("^[a-z0-9_-]+$");
-
readonly string key;
readonly string value;
readonly byte[] valueBytes;
@@ -241,7 +239,7 @@ namespace Grpc.Core
/// <summary>
/// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with a binary value.
/// </summary>
- /// <param name="key">Metadata key, needs to have suffix indicating a binary valued metadata entry.</param>
+ /// <param name="key">Metadata key. Gets converted to lowercase. Needs to have suffix indicating a binary valued metadata entry. Can only contain lowercase alphanumeric characters, underscores, hyphens and dots.</param>
/// <param name="valueBytes">Value bytes.</param>
public Entry(string key, byte[] valueBytes)
{
@@ -255,9 +253,9 @@ namespace Grpc.Core
}
/// <summary>
- /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct holding an ASCII value.
+ /// Initializes a new instance of the <see cref="Grpc.Core.Metadata.Entry"/> struct with an ASCII value.
/// </summary>
- /// <param name="key">Metadata key, must not use suffix indicating a binary valued metadata entry.</param>
+ /// <param name="key">Metadata key. Gets converted to lowercase. Must not use suffix indicating a binary valued metadata entry. Can only contain lowercase alphanumeric characters, underscores, hyphens and dots.</param>
/// <param name="value">Value string. Only ASCII characters are allowed.</param>
public Entry(string key, string value)
{
@@ -358,10 +356,42 @@ namespace Grpc.Core
private static string NormalizeKey(string key)
{
- var normalized = GrpcPreconditions.CheckNotNull(key, "key").ToLowerInvariant();
- GrpcPreconditions.CheckArgument(ValidKeyRegex.IsMatch(normalized),
- "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores and hyphens.");
- return normalized;
+ GrpcPreconditions.CheckNotNull(key, "key");
+
+ GrpcPreconditions.CheckArgument(IsValidKey(key, out bool isLowercase),
+ "Metadata entry key not valid. Keys can only contain lowercase alphanumeric characters, underscores, hyphens and dots.");
+ if (isLowercase)
+ {
+ // save allocation of a new string if already lowercase
+ return key;
+ }
+
+ return key.ToLowerInvariant();
+ }
+
+ private static bool IsValidKey(string input, out bool isLowercase)
+ {
+ isLowercase = true;
+ for (int i = 0; i < input.Length; i++)
+ {
+ char c = input[i];
+ if ('a' <= c && c <= 'z' ||
+ '0' <= c && c <= '9' ||
+ c == '.' ||
+ c == '_' ||
+ c == '-' )
+ continue;
+
+ if ('A' <= c && c <= 'Z')
+ {
+ isLowercase = false;
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
}
/// <summary>
diff --git a/src/csharp/Grpc.Core/NativeDeps.Linux.csproj.include b/src/csharp/Grpc.Core/NativeDeps.Linux.csproj.include
index e3bbeb071e..af660064a4 100644
--- a/src/csharp/Grpc.Core/NativeDeps.Linux.csproj.include
+++ b/src/csharp/Grpc.Core/NativeDeps.Linux.csproj.include
@@ -1,6 +1,6 @@
<Project>
<ItemGroup>
- <Content Include="..\..\..\libs\$(NativeDependenciesConfigurationUnix)\libgrpc_csharp_ext.so">
+ <Content Include="..\..\..\cmake\build\libgrpc_csharp_ext.so">
<Link>libgrpc_csharp_ext.x64.so</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Pack>false</Pack>
diff --git a/src/csharp/Grpc.Core/NativeDeps.Mac.csproj.include b/src/csharp/Grpc.Core/NativeDeps.Mac.csproj.include
index 309e33d47e..570b0cd8b7 100644
--- a/src/csharp/Grpc.Core/NativeDeps.Mac.csproj.include
+++ b/src/csharp/Grpc.Core/NativeDeps.Mac.csproj.include
@@ -1,6 +1,6 @@
<Project>
<ItemGroup>
- <Content Include="..\..\..\libs\$(NativeDependenciesConfigurationUnix)\libgrpc_csharp_ext.dylib">
+ <Content Include="..\..\..\cmake\build\libgrpc_csharp_ext.dylib">
<Link>libgrpc_csharp_ext.x64.dylib</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Pack>false</Pack>
diff --git a/src/csharp/Grpc.Core/RpcException.cs b/src/csharp/Grpc.Core/RpcException.cs
index 94429d74ce..ff89897565 100644
--- a/src/csharp/Grpc.Core/RpcException.cs
+++ b/src/csharp/Grpc.Core/RpcException.cs
@@ -33,10 +33,8 @@ namespace Grpc.Core
/// Creates a new <c>RpcException</c> associated with given status.
/// </summary>
/// <param name="status">Resulting status of a call.</param>
- public RpcException(Status status) : base(status.ToString())
+ public RpcException(Status status) : this(status, Metadata.Empty, status.ToString())
{
- this.status = status;
- this.trailers = Metadata.Empty;
}
/// <summary>
@@ -44,10 +42,8 @@ namespace Grpc.Core
/// </summary>
/// <param name="status">Resulting status of a call.</param>
/// <param name="message">The exception message.</param>
- public RpcException(Status status, string message) : base(message)
+ public RpcException(Status status, string message) : this(status, Metadata.Empty, message)
{
- this.status = status;
- this.trailers = Metadata.Empty;
}
/// <summary>
@@ -55,7 +51,17 @@ namespace Grpc.Core
/// </summary>
/// <param name="status">Resulting status of a call.</param>
/// <param name="trailers">Response trailing metadata.</param>
- public RpcException(Status status, Metadata trailers) : base(status.ToString())
+ public RpcException(Status status, Metadata trailers) : this(status, trailers, status.ToString())
+ {
+ }
+
+ /// <summary>
+ /// Creates a new <c>RpcException</c> associated with given status, message and trailing response metadata.
+ /// </summary>
+ /// <param name="status">Resulting status of a call.</param>
+ /// <param name="trailers">Response trailing metadata.</param>
+ /// <param name="message">The exception message.</param>
+ public RpcException(Status status, Metadata trailers, string message) : base(message)
{
this.status = status;
this.trailers = GrpcPreconditions.CheckNotNull(trailers);
diff --git a/src/csharp/Grpc.Core/SerializationContext.cs b/src/csharp/Grpc.Core/SerializationContext.cs
new file mode 100644
index 0000000000..cf4d1595da
--- /dev/null
+++ b/src/csharp/Grpc.Core/SerializationContext.cs
@@ -0,0 +1,34 @@
+#region Copyright notice and license
+
+// Copyright 2018 The gRPC 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.
+
+#endregion
+
+namespace Grpc.Core
+{
+ /// <summary>
+ /// Provides storage for payload when serializing a message.
+ /// </summary>
+ public abstract class SerializationContext
+ {
+ /// <summary>
+ /// Use the byte array as serialized form of current message and mark serialization process as complete.
+ /// Complete() can only be called once. By calling this method the caller gives up the ownership of the
+ /// payload which must not be accessed afterwards.
+ /// </summary>
+ /// <param name="payload">the serialized form of current message</param>
+ public abstract void Complete(byte[] payload);
+ }
+}
diff --git a/src/csharp/Grpc.Core/ServerCredentials.cs b/src/csharp/Grpc.Core/ServerCredentials.cs
index 703f9ff6b3..8e4e44ba50 100644
--- a/src/csharp/Grpc.Core/ServerCredentials.cs
+++ b/src/csharp/Grpc.Core/ServerCredentials.cs
@@ -58,41 +58,106 @@ namespace Grpc.Core
}
/// <summary>
+ /// Modes of requesting client's SSL certificate by the server.
+ /// Corresponds to <c>grpc_ssl_client_certificate_request_type</c>.
+ /// </summary>
+ public enum SslClientCertificateRequestType {
+ /// <summary>
+ /// Server does not request client certificate.
+ /// The certificate presented by the client is not checked by the server at
+ /// all. (A client may present a self signed or signed certificate or not
+ /// present a certificate at all and any of those option would be accepted)
+ /// </summary>
+ DontRequest = 0,
+ /// <summary>
+ /// Server requests client certificate but does not enforce that the client
+ /// presents a certificate.
+ /// If the client presents a certificate, the client authentication is left to
+ /// the application (the necessary metadata will be available to the
+ /// application via authentication context properties, see grpc_auth_context).
+ /// The client's key certificate pair must be valid for the SSL connection to
+ /// be established.
+ ///</summary>
+ RequestButDontVerify,
+ /// <summary>
+ /// Server requests client certificate but does not enforce that the client
+ /// presents a certificate.
+ /// If the client presents a certificate, the client authentication is done by
+ /// the gRPC framework. (For a successful connection the client needs to either
+ /// present a certificate that can be verified against the root certificate
+ /// configured by the server or not present a certificate at all)
+ /// The client's key certificate pair must be valid for the SSL connection to
+ /// be established.
+ /// </summary>
+ RequestAndVerify,
+ /// <summary>
+ /// Server requests client certificate and enforces that the client presents a
+ /// certificate.
+ /// If the client presents a certificate, the client authentication is left to
+ /// the application (the necessary metadata will be available to the
+ /// application via authentication context properties, see grpc_auth_context).
+ /// The client's key certificate pair must be valid for the SSL connection to
+ /// be established.
+ ///</summary>
+ RequestAndRequireButDontVerify,
+ /// <summary>
+ /// Server requests client certificate and enforces that the client presents a
+ /// certificate.
+ /// The cerificate presented by the client is verified by the gRPC framework.
+ /// (For a successful connection the client needs to present a certificate that
+ /// can be verified against the root certificate configured by the server)
+ /// The client's key certificate pair must be valid for the SSL connection to
+ /// be established.
+ /// </summary>
+ RequestAndRequireAndVerify,
+ }
+ /// <summary>
/// Server-side SSL credentials.
/// </summary>
public class SslServerCredentials : ServerCredentials
{
readonly IList<KeyCertificatePair> keyCertificatePairs;
readonly string rootCertificates;
- readonly bool forceClientAuth;
+ readonly SslClientCertificateRequestType clientCertificateRequest;
/// <summary>
/// Creates server-side SSL credentials.
/// </summary>
/// <param name="keyCertificatePairs">Key-certificates to use.</param>
/// <param name="rootCertificates">PEM encoded client root certificates used to authenticate client.</param>
- /// <param name="forceClientAuth">If true, client will be rejected unless it proves its unthenticity using against rootCertificates.</param>
+ /// <param name="forceClientAuth">Deprecated, use clientCertificateRequest overload instead.</param>
public SslServerCredentials(IEnumerable<KeyCertificatePair> keyCertificatePairs, string rootCertificates, bool forceClientAuth)
+ : this(keyCertificatePairs, rootCertificates, forceClientAuth ? SslClientCertificateRequestType.RequestAndRequireAndVerify : SslClientCertificateRequestType.DontRequest)
+ {
+ }
+
+ /// <summary>
+ /// Creates server-side SSL credentials.
+ /// </summary>
+ /// <param name="keyCertificatePairs">Key-certificates to use.</param>
+ /// <param name="rootCertificates">PEM encoded client root certificates used to authenticate client.</param>
+ /// <param name="clientCertificateRequest">Options for requesting and verifying client certificate.</param>
+ public SslServerCredentials(IEnumerable<KeyCertificatePair> keyCertificatePairs, string rootCertificates, SslClientCertificateRequestType clientCertificateRequest)
{
this.keyCertificatePairs = new List<KeyCertificatePair>(keyCertificatePairs).AsReadOnly();
GrpcPreconditions.CheckArgument(this.keyCertificatePairs.Count > 0,
"At least one KeyCertificatePair needs to be provided.");
- if (forceClientAuth)
+ if (clientCertificateRequest == SslClientCertificateRequestType.RequestAndRequireAndVerify)
{
GrpcPreconditions.CheckNotNull(rootCertificates,
- "Cannot force client authentication unless you provide rootCertificates.");
+ "Cannot require and verify client certificate unless you provide rootCertificates.");
}
this.rootCertificates = rootCertificates;
- this.forceClientAuth = forceClientAuth;
+ this.clientCertificateRequest = clientCertificateRequest;
}
/// <summary>
/// Creates server-side SSL credentials.
- /// This constructor should be use if you do not wish to autheticate client
- /// using client root certificates.
+ /// This constructor should be used if you do not wish to authenticate the client.
+ /// (client certificate won't be requested and checked by the server at all).
/// </summary>
/// <param name="keyCertificatePairs">Key-certificates to use.</param>
- public SslServerCredentials(IEnumerable<KeyCertificatePair> keyCertificatePairs) : this(keyCertificatePairs, null, false)
+ public SslServerCredentials(IEnumerable<KeyCertificatePair> keyCertificatePairs) : this(keyCertificatePairs, null, SslClientCertificateRequestType.DontRequest)
{
}
@@ -119,13 +184,24 @@ namespace Grpc.Core
}
/// <summary>
- /// If true, the authenticity of client check will be enforced.
+ /// Deprecated. If true, the authenticity of client check will be enforced.
/// </summary>
public bool ForceClientAuthentication
{
get
{
- return this.forceClientAuth;
+ return this.clientCertificateRequest == SslClientCertificateRequestType.RequestAndRequireAndVerify;
+ }
+ }
+
+ /// <summary>
+ /// Mode of requesting certificate from client by the server.
+ /// </summary>
+ public SslClientCertificateRequestType ClientCertificateRequest
+ {
+ get
+ {
+ return this.clientCertificateRequest;
}
}
@@ -139,7 +215,7 @@ namespace Grpc.Core
certChains[i] = keyCertificatePairs[i].CertificateChain;
keys[i] = keyCertificatePairs[i].PrivateKey;
}
- return ServerCredentialsSafeHandle.CreateSslCredentials(rootCertificates, certChains, keys, forceClientAuth);
+ return ServerCredentialsSafeHandle.CreateSslCredentials(rootCertificates, certChains, keys, clientCertificateRequest);
}
}
}
diff --git a/src/csharp/Grpc.Core/Version.csproj.include b/src/csharp/Grpc.Core/Version.csproj.include
index 45bd8ebd85..ed0d884365 100755
--- a/src/csharp/Grpc.Core/Version.csproj.include
+++ b/src/csharp/Grpc.Core/Version.csproj.include
@@ -1,7 +1,7 @@
<!-- This file is generated -->
<Project>
<PropertyGroup>
- <GrpcCsharpVersion>1.15.0-dev</GrpcCsharpVersion>
+ <GrpcCsharpVersion>1.17.0-dev</GrpcCsharpVersion>
<GoogleProtobufVersion>3.6.1</GoogleProtobufVersion>
</PropertyGroup>
</Project>
diff --git a/src/csharp/Grpc.Core/VersionInfo.cs b/src/csharp/Grpc.Core/VersionInfo.cs
index 295e0f2577..14714c8c4a 100644
--- a/src/csharp/Grpc.Core/VersionInfo.cs
+++ b/src/csharp/Grpc.Core/VersionInfo.cs
@@ -33,11 +33,11 @@ namespace Grpc.Core
/// <summary>
/// Current <c>AssemblyFileVersion</c> of gRPC C# assemblies
/// </summary>
- public const string CurrentAssemblyFileVersion = "1.15.0.0";
+ public const string CurrentAssemblyFileVersion = "1.17.0.0";
/// <summary>
/// Current version of gRPC C#
/// </summary>
- public const string CurrentVersion = "1.15.0-dev";
+ public const string CurrentVersion = "1.17.0-dev";
}
}
diff --git a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
index ad7033b782..8daf3fa98b 100755
--- a/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
+++ b/src/csharp/Grpc.IntegrationTesting/Grpc.IntegrationTesting.csproj
@@ -18,7 +18,7 @@
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="$(GoogleProtobufVersion)" />
- <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
+ <PackageReference Include="CommandLineParser" Version="2.3.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="NUnitLite" Version="3.10.1" />
</ItemGroup>
diff --git a/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs b/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs
index c83ccd2612..40447854f4 100644
--- a/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs
+++ b/src/csharp/Grpc.IntegrationTesting/MetadataCredentialsTest.cs
@@ -153,9 +153,10 @@ namespace Grpc.IntegrationTesting
[Test]
public void MetadataCredentials_InterceptorThrows()
{
+ var authInterceptorExceptionMessage = "Auth interceptor throws";
var callCredentials = CallCredentials.FromInterceptor(new AsyncAuthInterceptor((context, metadata) =>
{
- throw new Exception("Auth interceptor throws");
+ throw new Exception(authInterceptorExceptionMessage);
}));
var channelCredentials = ChannelCredentials.Create(TestCredentials.CreateSslCredentials(), callCredentials);
channel = new Channel(Host, server.Ports.Single().BoundPort, channelCredentials, options);
@@ -163,6 +164,7 @@ namespace Grpc.IntegrationTesting
var ex = Assert.Throws<RpcException>(() => client.UnaryCall(new SimpleRequest { }));
Assert.AreEqual(StatusCode.Unavailable, ex.Status.StatusCode);
+ StringAssert.Contains(authInterceptorExceptionMessage, ex.Status.Detail);
}
private class FakeTestService : TestService.TestServiceBase
diff --git a/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs b/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs
index 152d8feab9..b3c47c2d8d 100644
--- a/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs
+++ b/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs
@@ -37,20 +37,24 @@ namespace Grpc.IntegrationTesting
public class SslCredentialsTest
{
const string Host = "localhost";
+ const string IsPeerAuthenticatedMetadataKey = "test_only_is_peer_authenticated";
Server server;
Channel channel;
TestService.TestServiceClient client;
- [OneTimeSetUp]
- public void Init()
+ string rootCert;
+ KeyCertificatePair keyCertPair;
+
+ public void InitClientAndServer(bool clientAddKeyCertPair,
+ SslClientCertificateRequestType clientCertRequestType)
{
- var rootCert = File.ReadAllText(TestCredentials.ClientCertAuthorityPath);
- var keyCertPair = new KeyCertificatePair(
+ rootCert = File.ReadAllText(TestCredentials.ClientCertAuthorityPath);
+ keyCertPair = new KeyCertificatePair(
File.ReadAllText(TestCredentials.ServerCertChainPath),
File.ReadAllText(TestCredentials.ServerPrivateKeyPath));
- var serverCredentials = new SslServerCredentials(new[] { keyCertPair }, rootCert, true);
- var clientCredentials = new SslCredentials(rootCert, keyCertPair);
+ var serverCredentials = new SslServerCredentials(new[] { keyCertPair }, rootCert, clientCertRequestType);
+ var clientCredentials = clientAddKeyCertPair ? new SslCredentials(rootCert, keyCertPair) : new SslCredentials(rootCert);
// Disable SO_REUSEPORT to prevent https://github.com/grpc/grpc/issues/10755
server = new Server(new[] { new ChannelOption(ChannelOptions.SoReuseport, 0) })
@@ -72,19 +76,133 @@ namespace Grpc.IntegrationTesting
[OneTimeTearDown]
public void Cleanup()
{
- channel.ShutdownAsync().Wait();
- server.ShutdownAsync().Wait();
+ if (channel != null)
+ {
+ channel.ShutdownAsync().Wait();
+ }
+ if (server != null)
+ {
+ server.ShutdownAsync().Wait();
+ }
}
[Test]
- public void AuthenticatedClientAndServer()
+ public async Task NoClientCert_DontRequestClientCertificate_Accepted()
{
- var response = client.UnaryCall(new SimpleRequest { ResponseSize = 10 });
- Assert.AreEqual(10, response.Payload.Body.Length);
+ InitClientAndServer(
+ clientAddKeyCertPair: false,
+ clientCertRequestType: SslClientCertificateRequestType.DontRequest);
+
+ await CheckAccepted(expectPeerAuthenticated: false);
}
[Test]
- public async Task AuthContextIsPopulated()
+ public async Task ClientWithCert_DontRequestClientCertificate_AcceptedButPeerNotAuthenticated()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: true,
+ clientCertRequestType: SslClientCertificateRequestType.DontRequest);
+
+ await CheckAccepted(expectPeerAuthenticated: false);
+ }
+
+ [Test]
+ public async Task NoClientCert_RequestClientCertificateButDontVerify_Accepted()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: false,
+ clientCertRequestType: SslClientCertificateRequestType.RequestButDontVerify);
+
+ await CheckAccepted(expectPeerAuthenticated: false);
+ }
+
+ [Test]
+ public async Task NoClientCert_RequestClientCertificateAndVerify_Accepted()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: false,
+ clientCertRequestType: SslClientCertificateRequestType.RequestAndVerify);
+
+ await CheckAccepted(expectPeerAuthenticated: false);
+ }
+
+ [Test]
+ public async Task ClientWithCert_RequestAndRequireClientCertificateButDontVerify_Accepted()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: true,
+ clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireButDontVerify);
+
+ await CheckAccepted(expectPeerAuthenticated: true);
+ await CheckAuthContextIsPopulated();
+ }
+
+ [Test]
+ public async Task ClientWithCert_RequestAndRequireClientCertificateAndVerify_Accepted()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: true,
+ clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireAndVerify);
+
+ await CheckAccepted(expectPeerAuthenticated: true);
+ await CheckAuthContextIsPopulated();
+ }
+
+ [Test]
+ public void NoClientCert_RequestAndRequireClientCertificateButDontVerify_Rejected()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: false,
+ clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireButDontVerify);
+
+ CheckRejected();
+ }
+
+ [Test]
+ public void NoClientCert_RequestAndRequireClientCertificateAndVerify_Rejected()
+ {
+ InitClientAndServer(
+ clientAddKeyCertPair: false,
+ clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireAndVerify);
+
+ CheckRejected();
+ }
+
+ [Test]
+ public void Constructor_LegacyForceClientAuth()
+ {
+ var creds = new SslServerCredentials(new[] { keyCertPair }, rootCert, true);
+ Assert.AreEqual(SslClientCertificateRequestType.RequestAndRequireAndVerify, creds.ClientCertificateRequest);
+
+ var creds2 = new SslServerCredentials(new[] { keyCertPair }, rootCert, false);
+ Assert.AreEqual(SslClientCertificateRequestType.DontRequest, creds2.ClientCertificateRequest);
+ }
+
+ [Test]
+ public void Constructor_NullRootCerts()
+ {
+ var keyCertPairs = new[] { keyCertPair };
+ Assert.DoesNotThrow(() => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.DontRequest));
+ Assert.DoesNotThrow(() => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.RequestAndVerify));
+ Assert.DoesNotThrow(() => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.RequestAndRequireButDontVerify));
+ Assert.Throws(typeof(ArgumentNullException), () => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.RequestAndRequireAndVerify));
+ }
+
+ private async Task CheckAccepted(bool expectPeerAuthenticated)
+ {
+ var call = client.UnaryCallAsync(new SimpleRequest { ResponseSize = 10 });
+ var response = await call;
+ Assert.AreEqual(10, response.Payload.Body.Length);
+ Assert.AreEqual(expectPeerAuthenticated.ToString(), call.GetTrailers().First((entry) => entry.Key == IsPeerAuthenticatedMetadataKey).Value);
+ }
+
+ private void CheckRejected()
+ {
+ var ex = Assert.Throws<RpcException>(() => client.UnaryCall(new SimpleRequest { ResponseSize = 10 }));
+ Assert.AreEqual(StatusCode.Unavailable, ex.Status.StatusCode);
+ }
+
+ private async Task CheckAuthContextIsPopulated()
{
var call = client.StreamingInputCall();
await call.RequestStream.CompleteAsync();
@@ -96,6 +214,7 @@ namespace Grpc.IntegrationTesting
{
public override Task<SimpleResponse> UnaryCall(SimpleRequest request, ServerCallContext context)
{
+ context.ResponseTrailers.Add(IsPeerAuthenticatedMetadataKey, context.AuthContext.IsPeerAuthenticated.ToString());
return Task.FromResult(new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) });
}
diff --git a/src/csharp/Grpc.Microbenchmarks/Grpc.Microbenchmarks.csproj b/src/csharp/Grpc.Microbenchmarks/Grpc.Microbenchmarks.csproj
index 8a629f9748..d39d46cf1b 100644
--- a/src/csharp/Grpc.Microbenchmarks/Grpc.Microbenchmarks.csproj
+++ b/src/csharp/Grpc.Microbenchmarks/Grpc.Microbenchmarks.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
+ <PackageReference Include="CommandLineParser" Version="2.3.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
diff --git a/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs
new file mode 100644
index 0000000000..e4c9b2fa84
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs
@@ -0,0 +1,85 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class CSharpGeneratorTest : GeneratorTest
+ {
+ GeneratorServices _generator;
+
+ [SetUp]
+ public new void SetUp()
+ {
+ _generator = GeneratorServices.GetForLanguage("CSharp", _log);
+ }
+
+ [TestCase("foo.proto", "Foo.cs", "FooGrpc.cs")]
+ [TestCase("sub/foo.proto", "Foo.cs", "FooGrpc.cs")]
+ [TestCase("one_two.proto", "OneTwo.cs", "OneTwoGrpc.cs")]
+ [TestCase("__one_two!.proto", "OneTwo!.cs", "OneTwo!Grpc.cs")]
+ [TestCase("one(two).proto", "One(two).cs", "One(two)Grpc.cs")]
+ [TestCase("one_(two).proto", "One(two).cs", "One(two)Grpc.cs")]
+ [TestCase("one two.proto", "One two.cs", "One twoGrpc.cs")]
+ [TestCase("one_ two.proto", "One two.cs", "One twoGrpc.cs")]
+ [TestCase("one .proto", "One .cs", "One Grpc.cs")]
+ public void NameMangling(string proto, string expectCs, string expectGrpcCs)
+ {
+ var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "grpcservices", "both"));
+ Assert.AreEqual(2, poss.Length);
+ Assert.Contains(expectCs, poss);
+ Assert.Contains(expectGrpcCs, poss);
+ }
+
+ [Test]
+ public void NoGrpcOneOutput()
+ {
+ var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto"));
+ Assert.AreEqual(1, poss.Length);
+ }
+
+ [TestCase("none")]
+ [TestCase("")]
+ public void GrpcNoneOneOutput(string grpc)
+ {
+ var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+ var poss = _generator.GetPossibleOutputs(item);
+ Assert.AreEqual(1, poss.Length);
+ }
+
+ [TestCase("client")]
+ [TestCase("server")]
+ [TestCase("both")]
+ public void GrpcEnabledTwoOutputs(string grpc)
+ {
+ var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+ var poss = _generator.GetPossibleOutputs(item);
+ Assert.AreEqual(2, poss.Length);
+ }
+
+ [Test]
+ public void OutputDirMetadataRecognized()
+ {
+ var item = Utils.MakeItem("foo.proto", "OutputDir", "out");
+ var poss = _generator.GetPossibleOutputs(item);
+ Assert.AreEqual(1, poss.Length);
+ Assert.That(poss[0], Is.EqualTo("out/Foo.cs") | Is.EqualTo("out\\Foo.cs"));
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs
new file mode 100644
index 0000000000..bd0405a03a
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs
@@ -0,0 +1,88 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.IO;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class CppGeneratorTest : GeneratorTest
+ {
+ GeneratorServices _generator;
+
+ [SetUp]
+ public new void SetUp()
+ {
+ _generator = GeneratorServices.GetForLanguage("Cpp", _log);
+ }
+
+ [TestCase("foo.proto", "", "foo")]
+ [TestCase("foo.proto", ".", "foo")]
+ [TestCase("foo.proto", "./", "foo")]
+ [TestCase("sub/foo.proto", "", "sub/foo")]
+ [TestCase("root/sub/foo.proto", "root", "sub/foo")]
+ [TestCase("root/sub/foo.proto", "root", "sub/foo")]
+ [TestCase("/root/sub/foo.proto", "/root", "sub/foo")]
+ public void RelativeDirectoryCompute(string proto, string root, string expectStem)
+ {
+ if (Path.DirectorySeparatorChar == '\\')
+ expectStem = expectStem.Replace('/', '\\');
+ var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "ProtoRoot", root));
+ Assert.AreEqual(2, poss.Length);
+ Assert.Contains(expectStem + ".pb.cc", poss);
+ Assert.Contains(expectStem + ".pb.h", poss);
+ }
+
+ [Test]
+ public void NoGrpcTwoOutputs()
+ {
+ var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto"));
+ Assert.AreEqual(2, poss.Length);
+ }
+
+ [TestCase("false")]
+ [TestCase("")]
+ public void GrpcDisabledTwoOutput(string grpc)
+ {
+ var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+ var poss = _generator.GetPossibleOutputs(item);
+ Assert.AreEqual(2, poss.Length);
+ }
+
+ [TestCase("true")]
+ public void GrpcEnabledFourOutputs(string grpc)
+ {
+ var item = Utils.MakeItem("foo.proto", "grpcservices", grpc);
+ var poss = _generator.GetPossibleOutputs(item);
+ Assert.AreEqual(4, poss.Length);
+ Assert.Contains("foo.pb.cc", poss);
+ Assert.Contains("foo.pb.h", poss);
+ Assert.Contains("foo_grpc.pb.cc", poss);
+ Assert.Contains("foo_grpc.pb.h", poss);
+ }
+
+ [Test]
+ public void OutputDirMetadataRecognized()
+ {
+ var item = Utils.MakeItem("foo.proto", "OutputDir", "out");
+ var poss = _generator.GetPossibleOutputs(item);
+ Assert.AreEqual(2, poss.Length);
+ Assert.That(Path.GetDirectoryName(poss[0]), Is.EqualTo("out"));
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs
new file mode 100644
index 0000000000..e89a8f4b5d
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs
@@ -0,0 +1,146 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.IO;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class DepFileUtilTest
+ {
+
+ [Test]
+ public void HashString64Hex_IsSane()
+ {
+ string hashFoo1 = DepFileUtil.HashString64Hex("foo");
+ string hashEmpty = DepFileUtil.HashString64Hex("");
+ string hashFoo2 = DepFileUtil.HashString64Hex("foo");
+
+ StringAssert.IsMatch("^[a-f0-9]{16}$", hashFoo1);
+ Assert.AreEqual(hashFoo1, hashFoo2);
+ Assert.AreNotEqual(hashFoo1, hashEmpty);
+ }
+
+ [Test]
+ public void GetDepFilenameForProto_IsSane()
+ {
+ StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}_foo.protodep$",
+ DepFileUtil.GetDepFilenameForProto("out", "foo.proto"));
+ StringAssert.IsMatch(@"^[a-f0-9]{16}_foo.protodep$",
+ DepFileUtil.GetDepFilenameForProto("", "foo.proto"));
+ }
+
+ [Test]
+ public void GetDepFilenameForProto_HashesDir()
+ {
+ string PickHash(string fname) =>
+ DepFileUtil.GetDepFilenameForProto("", fname).Substring(0, 16);
+
+ string same1 = PickHash("dir1/dir2/foo.proto");
+ string same2 = PickHash("dir1/dir2/proto.foo");
+ string same3 = PickHash("dir1/dir2/proto");
+ string same4 = PickHash("dir1/dir2/.proto");
+ string unsame1 = PickHash("dir2/foo.proto");
+ string unsame2 = PickHash("/dir2/foo.proto");
+
+ Assert.AreEqual(same1, same2);
+ Assert.AreEqual(same1, same3);
+ Assert.AreEqual(same1, same4);
+ Assert.AreNotEqual(same1, unsame1);
+ Assert.AreNotEqual(unsame1, unsame2);
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Full file reading tests
+
+ // Generated by protoc on Windows. Slashes vary.
+ const string depFile1 =
+ @"C:\projects\foo\src\./foo.grpc.pb.cc \
+C:\projects\foo\src\./foo.grpc.pb.h \
+C:\projects\foo\src\./foo.pb.cc \
+ C:\projects\foo\src\./foo.pb.h: C:/usr/include/google/protobuf/wrappers.proto\
+ C:/usr/include/google/protobuf/any.proto\
+C:/usr/include/google/protobuf/source_context.proto\
+ C:/usr/include/google/protobuf/type.proto\
+ foo.proto";
+
+ // This has a nasty output directory with a space.
+ const string depFile2 =
+ @"obj\Release x64\net45\/Foo.cs \
+obj\Release x64\net45\/FooGrpc.cs: C:/usr/include/google/protobuf/wrappers.proto\
+ C:/projects/foo/src//foo.proto";
+
+ [Test]
+ public void ReadDependencyInput_FullFile1()
+ {
+ string[] deps = ReadDependencyInputFromFileData(depFile1, "foo.proto");
+
+ Assert.NotNull(deps);
+ Assert.That(deps, Has.Length.InRange(4, 5)); // foo.proto may or may not be listed.
+ Assert.That(deps, Has.One.EndsWith("wrappers.proto"));
+ Assert.That(deps, Has.One.EndsWith("type.proto"));
+ Assert.That(deps, Has.None.StartWith(" "));
+ }
+
+ [Test]
+ public void ReadDependencyInput_FullFile2()
+ {
+ string[] deps = ReadDependencyInputFromFileData(depFile2, "C:/projects/foo/src/foo.proto");
+
+ Assert.NotNull(deps);
+ Assert.That(deps, Has.Length.InRange(1, 2));
+ Assert.That(deps, Has.One.EndsWith("wrappers.proto"));
+ Assert.That(deps, Has.None.StartWith(" "));
+ }
+
+ [Test]
+ public void ReadDependencyInput_FullFileUnparsable()
+ {
+ string[] deps = ReadDependencyInputFromFileData("a:/foo.proto", "/foo.proto");
+ Assert.NotNull(deps);
+ Assert.Zero(deps.Length);
+ }
+
+ // NB in our tests files are put into the temp directory but all have
+ // different names. Avoid adding files with the same directory path and
+ // name, or add reasonable handling for it if required. Tests are run in
+ // parallel and will collide otherwise.
+ private string[] ReadDependencyInputFromFileData(string fileData, string protoName)
+ {
+ string tempPath = Path.GetTempPath();
+ string tempfile = DepFileUtil.GetDepFilenameForProto(tempPath, protoName);
+ try
+ {
+ File.WriteAllText(tempfile, fileData);
+ var mockEng = new Moq.Mock<IBuildEngine>();
+ var log = new TaskLoggingHelper(mockEng.Object, "x");
+ return DepFileUtil.ReadDependencyInputs(tempPath, protoName, log);
+ }
+ finally
+ {
+ try
+ {
+ File.Delete(tempfile);
+ }
+ catch { }
+ }
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs
new file mode 100644
index 0000000000..8a8fc81aba
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs
@@ -0,0 +1,55 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class GeneratorTest
+ {
+ protected Mock<IBuildEngine> _mockEngine;
+ protected TaskLoggingHelper _log;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _mockEngine = new Mock<IBuildEngine>();
+ _log = new TaskLoggingHelper(_mockEngine.Object, "dummy");
+ }
+
+ [TestCase("csharp")]
+ [TestCase("CSharp")]
+ [TestCase("cpp")]
+ public void ValidLanguages(string lang)
+ {
+ Assert.IsNotNull(GeneratorServices.GetForLanguage(lang, _log));
+ _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Never);
+ }
+
+ [TestCase("")]
+ [TestCase("COBOL")]
+ public void InvalidLanguages(string lang)
+ {
+ Assert.IsNull(GeneratorServices.GetForLanguage(lang, _log));
+ _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Once);
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj
new file mode 100644
index 0000000000..a2d4874eec
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj
@@ -0,0 +1,78 @@
+<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+
+ <Import Project="..\Grpc.Core\Version.csproj.include" />
+
+ <PropertyGroup>
+ <TargetFrameworks>net45;netcoreapp1.0</TargetFrameworks>
+ <OutputType>Exe</OutputType>
+ </PropertyGroup>
+
+ <Import Project="..\Grpc.Core\SourceLink.csproj.include" />
+
+ <!-- This is copied verbatim from Grpc.Core/Common.csproj.include. Other settings
+ in that file conflict with the intent of this build, as it cannot be signed,
+ and may not compile Grpc.Core/Version.cs, as that file references constants
+ in Grpc.Core.dll.
+ TODO(kkm): Refactor imports. -->
+ <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' and '$(MSBuildRuntimeType)' == 'Core' ">
+ <!-- Use Mono reference assemblies in SDK build: https://github.com/dotnet/sdk/issues/335 -->
+ <FrameworkPathOverride Condition="Exists('/usr/lib/mono/4.5-api')">/usr/lib/mono/4.5-api</FrameworkPathOverride>
+ <FrameworkPathOverride Condition="Exists('/usr/local/lib/mono/4.5-api')">/usr/local/lib/mono/4.5-api</FrameworkPathOverride>
+ <FrameworkPathOverride Condition="Exists('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api</FrameworkPathOverride>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Grpc.Tools\Grpc.Tools.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Moq" Version="4.8.3" />
+ <PackageReference Include="NUnit; NUnitLite" Version="3.10.1" />
+ <PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
+ </ItemGroup>
+
+ <PropertyGroup Condition=" '$(TargetFramework)' != 'net45' ">
+ <DefineConstants>$(DefineConstants);NETCORE</DefineConstants>
+ </PropertyGroup>
+
+ <ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
+ <Reference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.v4.0" />
+ </ItemGroup>
+
+ <ItemGroup Condition=" '$(TargetFramework)' != 'net45' ">
+ <PackageReference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.Core" Version="15.6.*" />
+ </ItemGroup>
+
+ <!-- Groups below is a hack to allow the test to run under Mono Framework build.
+ ========================================================================== -->
+
+ <!-- Mono unfortunately comes with broken Microsoft.Build.* assemblies installed in
+ the GAC, but fortunately searches for runtime assemblies in a different order
+ than Windows CLR host: the GAC assemblies have the lowest search priority, (see
+ https://www.mono-project.com/docs/advanced/assemblies-and-the-gac/), not the
+ highest as is in Windows (documented at
+ https://docs.microsoft.com/dotnet/framework/deployment/how-the-runtime-locates-assemblies).
+ To run the tests under Mono, we need correct assemblies in the same directory as
+ the test executable. Correct versions are in the MSBuild directory under Mono. -->
+ <ItemGroup Condition=" '$(TargetFramework)' == 'net45' and '$(OS)' != 'Windows_NT' ">
+ <None Include="$(_MSBuildAssemblyPath)/Microsoft.Build.Framework.dll;
+ $(_MSBuildAssemblyPath)/Microsoft.Build.Utilities.v4.0.dll;
+ $(_MSBuildAssemblyPath)/Microsoft.Build.Utilities.Core.dll"
+ CopyToOutputDirectory="Always" Visible="false" />
+ </ItemGroup>
+ <PropertyGroup Condition=" '$(TargetFramework)' == 'net45' and '$(OS)' != 'Windows_NT' ">
+ <!-- The None items are included into assembly candidate resolution by default, and
+ we do not want that, as they are not valid as reference assemblies (the version of
+ Microsoft.Build.Utilities.v4.0 is a pure facade for Microsoft.Build.Utilities.Core,
+ and does not define any types at all). Exclude them from assembly resolution. See
+ https://github.com/Microsoft/msbuild/blob/50639058f/documentation/wiki/ResolveAssemblyReference.md -->
+ <AssemblySearchPaths>{HintPathFromItem};{TargetFrameworkDirectory};{RawFileName}</AssemblySearchPaths>
+ <!-- Mono knows better where its MSBuild is. -->
+ <_MSBuildAssemblyPath Condition=" '$(MSBuildRuntimeType)' != 'Core' "
+ >$(MSBuildToolsPath)</_MSBuildAssemblyPath>
+ <!-- Under dotnet, make the best guess we can. -->
+ <_MSBuildAssemblyPath Condition=" '$(MSBuildRuntimeType)' == 'Core' "
+ >$(FrameworkPathOverride)/../msbuild/$(MSBuildToolsVersion)/bin</_MSBuildAssemblyPath>
+ </PropertyGroup>
+
+</Project>
diff --git a/src/csharp/Grpc.Tools.Tests/NUnitMain.cs b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs
new file mode 100644
index 0000000000..418c33820e
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs
@@ -0,0 +1,33 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Reflection;
+using NUnitLite;
+
+namespace Grpc.Tools.Tests
+{
+ static class NUnitMain
+ {
+ public static int Main(string[] args) =>
+#if NETCOREAPP1_0 || NETCOREAPP1_1
+ new AutoRun(typeof(NUnitMain).GetTypeInfo().Assembly).Execute(args);
+#else
+ new AutoRun().Execute(args);
+#endif
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs
new file mode 100644
index 0000000000..ea763f4e40
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs
@@ -0,0 +1,76 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Reflection; // UWYU: Object.GetType() extension.
+using Microsoft.Build.Framework;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class ProtoCompileBasicTest
+ {
+ // Mock task class that stops right before invoking protoc.
+ public class ProtoCompileTestable : ProtoCompile
+ {
+ public string LastPathToTool { get; private set; }
+ public string[] LastResponseFile { get; private set; }
+
+ protected override int ExecuteTool(string pathToTool,
+ string response,
+ string commandLine)
+ {
+ // We should never be using command line commands.
+ Assert.That(commandLine, Is.Null | Is.Empty);
+
+ // Must receive a path to tool
+ Assert.That(pathToTool, Is.Not.Null & Is.Not.Empty);
+ Assert.That(response, Is.Not.Null & Does.EndWith("\n"));
+
+ LastPathToTool = pathToTool;
+ LastResponseFile = response.Remove(response.Length - 1).Split('\n');
+
+ // Do not run the tool, but pretend it ran successfully.
+ return 0;
+ }
+ };
+
+ protected Mock<IBuildEngine> _mockEngine;
+ protected ProtoCompileTestable _task;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _mockEngine = new Mock<IBuildEngine>();
+ _task = new ProtoCompileTestable {
+ BuildEngine = _mockEngine.Object
+ };
+ }
+
+ [TestCase("ProtoBuf")]
+ [TestCase("Generator")]
+ [TestCase("OutputDir")]
+ [Description("We trust MSBuild to initialize these properties.")]
+ public void RequiredAttributePresentOnProperty(string prop)
+ {
+ var pinfo = _task.GetType()?.GetProperty(prop);
+ Assert.NotNull(pinfo);
+ Assert.That(pinfo, Has.Attribute<RequiredAttribute>());
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs
new file mode 100644
index 0000000000..cac7146634
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs
@@ -0,0 +1,179 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.IO;
+using Microsoft.Build.Framework;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class ProtoCompileCommandLineGeneratorTest : ProtoCompileBasicTest
+ {
+ [SetUp]
+ public new void SetUp()
+ {
+ _task.Generator = "csharp";
+ _task.OutputDir = "outdir";
+ _task.ProtoBuf = Utils.MakeSimpleItems("a.proto");
+ }
+
+ void ExecuteExpectSuccess()
+ {
+ _mockEngine
+ .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
+ .Callback((BuildErrorEventArgs e) =>
+ Assert.Fail($"Error logged by build engine:\n{e.Message}"));
+ bool result = _task.Execute();
+ Assert.IsTrue(result);
+ }
+
+ [Test]
+ public void MinimalCompile()
+ {
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$"));
+ Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+ "--csharp_out=outdir", "a.proto" }));
+ }
+
+ [Test]
+ public void CompileTwoFiles()
+ {
+ _task.ProtoBuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto");
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+ "--csharp_out=outdir", "a.proto", "foo/b.proto" }));
+ }
+
+ [Test]
+ public void CompileWithProtoPaths()
+ {
+ _task.ProtoPath = new[] { "/path1", "/path2" };
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+ "--csharp_out=outdir", "--proto_path=/path1",
+ "--proto_path=/path2", "a.proto" }));
+ }
+
+ [TestCase("Cpp")]
+ [TestCase("CSharp")]
+ [TestCase("Java")]
+ [TestCase("Javanano")]
+ [TestCase("Js")]
+ [TestCase("Objc")]
+ [TestCase("Php")]
+ [TestCase("Python")]
+ [TestCase("Ruby")]
+ public void CompileWithOptions(string gen)
+ {
+ _task.Generator = gen;
+ _task.OutputOptions = new[] { "foo", "bar" };
+ ExecuteExpectSuccess();
+ gen = gen.ToLowerInvariant();
+ Assert.That(_task.LastResponseFile, Is.EqualTo(new[] {
+ $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "a.proto" }));
+ }
+
+ [Test]
+ public void OutputDependencyFile()
+ {
+ _task.DependencyOut = "foo/my.protodep";
+ // Task fails trying to read the non-generated file; we ignore that.
+ _task.Execute();
+ Assert.That(_task.LastResponseFile,
+ Does.Contain("--dependency_out=foo/my.protodep"));
+ }
+
+ [Test]
+ public void OutputDependencyWithProtoDepDir()
+ {
+ _task.ProtoDepDir = "foo";
+ // Task fails trying to read the non-generated file; we ignore that.
+ _task.Execute();
+ Assert.That(_task.LastResponseFile,
+ Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$"));
+ }
+
+ [Test]
+ public void GenerateGrpc()
+ {
+ _task.GrpcPluginExe = "/foo/grpcgen";
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
+ "--csharp_out=outdir", "--grpc_out=outdir",
+ "--plugin=protoc-gen-grpc=/foo/grpcgen" }));
+ }
+
+ [Test]
+ public void GenerateGrpcWithOutDir()
+ {
+ _task.GrpcPluginExe = "/foo/grpcgen";
+ _task.GrpcOutputDir = "gen-out";
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
+ "--csharp_out=outdir", "--grpc_out=gen-out" }));
+ }
+
+ [Test]
+ public void GenerateGrpcWithOptions()
+ {
+ _task.GrpcPluginExe = "/foo/grpcgen";
+ _task.GrpcOutputOptions = new[] { "baz", "quux" };
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile,
+ Does.Contain("--grpc_opt=baz,quux"));
+ }
+
+ [Test]
+ public void DirectoryArgumentsSlashTrimmed()
+ {
+ _task.GrpcPluginExe = "/foo/grpcgen";
+ _task.GrpcOutputDir = "gen-out/";
+ _task.OutputDir = "outdir/";
+ _task.ProtoPath = new[] { "/path1/", "/path2/" };
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] {
+ "--proto_path=/path1", "--proto_path=/path2",
+ "--csharp_out=outdir", "--grpc_out=gen-out" }));
+ }
+
+ [TestCase(".", ".")]
+ [TestCase("/", "/")]
+ [TestCase("//", "/")]
+ [TestCase("/foo/", "/foo")]
+ [TestCase("/foo", "/foo")]
+ [TestCase("foo/", "foo")]
+ [TestCase("foo//", "foo")]
+ [TestCase("foo/\\", "foo")]
+ [TestCase("foo\\/", "foo")]
+ [TestCase("C:\\foo", "C:\\foo")]
+ [TestCase("C:", "C:")]
+ [TestCase("C:\\", "C:\\")]
+ [TestCase("C:\\\\", "C:\\")]
+ public void DirectorySlashTrimmingCases(string given, string expect)
+ {
+ if (Path.DirectorySeparatorChar == '/')
+ expect = expect.Replace('\\', '/');
+ _task.OutputDir = given;
+ ExecuteExpectSuccess();
+ Assert.That(_task.LastResponseFile,
+ Does.Contain("--csharp_out=" + expect));
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs
new file mode 100644
index 0000000000..1773dcb875
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs
@@ -0,0 +1,51 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using Microsoft.Build.Framework;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class ProtoCompileCommandLinePrinterTest : ProtoCompileBasicTest
+ {
+ [SetUp]
+ public new void SetUp()
+ {
+ _task.Generator = "csharp";
+ _task.OutputDir = "outdir";
+ _task.ProtoBuf = Utils.MakeSimpleItems("a.proto");
+
+ _mockEngine
+ .Setup(me => me.LogMessageEvent(It.IsAny<BuildMessageEventArgs>()))
+ .Callback((BuildMessageEventArgs e) =>
+ Assert.Fail($"Error logged by build engine:\n{e.Message}"))
+ .Verifiable("Command line was not output by the task.");
+ }
+
+ void ExecuteExpectSuccess()
+ {
+ _mockEngine
+ .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
+ .Callback((BuildErrorEventArgs e) =>
+ Assert.Fail($"Error logged by build engine:\n{e.Message}"));
+ bool result = _task.Execute();
+ Assert.IsTrue(result);
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs
new file mode 100644
index 0000000000..54723b74fc
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs
@@ -0,0 +1,139 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Runtime.InteropServices; // For NETCORE tests.
+using Microsoft.Build.Framework;
+using Moq;
+using NUnit.Framework;
+
+namespace Grpc.Tools.Tests
+{
+ public class ProtoToolsPlatformTaskTest
+ {
+ ProtoToolsPlatform _task;
+ int _cpuMatched, _osMatched;
+
+ [OneTimeSetUp]
+ public void SetUp()
+ {
+ var mockEng = new Mock<IBuildEngine>();
+ _task = new ProtoToolsPlatform() { BuildEngine = mockEng.Object };
+ _task.Execute();
+ }
+
+ [OneTimeTearDown]
+ public void TearDown()
+ {
+ Assert.AreEqual(1, _cpuMatched, "CPU type detection failed");
+ Assert.AreEqual(1, _osMatched, "OS detection failed");
+ }
+
+#if NETCORE
+ // PlatformAttribute not yet available in NUnit, coming soon:
+ // https://github.com/nunit/nunit/pull/3003.
+ // Use same test case names as under the full framework.
+ [Test]
+ public void CpuIsX86()
+ {
+ if (RuntimeInformation.OSArchitecture == Architecture.X86)
+ {
+ _cpuMatched++;
+ Assert.AreEqual("x86", _task.Cpu);
+ }
+ }
+
+ [Test]
+ public void CpuIsX64()
+ {
+ if (RuntimeInformation.OSArchitecture == Architecture.X64)
+ {
+ _cpuMatched++;
+ Assert.AreEqual("x64", _task.Cpu);
+ }
+ }
+
+ [Test]
+ public void OsIsWindows()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ _osMatched++;
+ Assert.AreEqual("windows", _task.Os);
+ }
+ }
+
+ [Test]
+ public void OsIsLinux()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ _osMatched++;
+ Assert.AreEqual("linux", _task.Os);
+ }
+ }
+
+ [Test]
+ public void OsIsMacOsX()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ _osMatched++;
+ Assert.AreEqual("macosx", _task.Os);
+ }
+ }
+
+#else // !NETCORE, i.e. full framework.
+
+ [Test, Platform("32-Bit")]
+ public void CpuIsX86()
+ {
+ _cpuMatched++;
+ Assert.AreEqual("x86", _task.Cpu);
+ }
+
+ [Test, Platform("64-Bit")]
+ public void CpuIsX64()
+ {
+ _cpuMatched++;
+ Assert.AreEqual("x64", _task.Cpu);
+ }
+
+ [Test, Platform("Win")]
+ public void OsIsWindows()
+ {
+ _osMatched++;
+ Assert.AreEqual("windows", _task.Os);
+ }
+
+ [Test, Platform("Linux")]
+ public void OsIsLinux()
+ {
+ _osMatched++;
+ Assert.AreEqual("linux", _task.Os);
+ }
+
+ [Test, Platform("MacOSX")]
+ public void OsIsMacOsX()
+ {
+ _osMatched++;
+ Assert.AreEqual("macosx", _task.Os);
+ }
+
+#endif // NETCORE
+ };
+}
diff --git a/src/csharp/Grpc.Tools.Tests/Utils.cs b/src/csharp/Grpc.Tools.Tests/Utils.cs
new file mode 100644
index 0000000000..6e0f1cffd5
--- /dev/null
+++ b/src/csharp/Grpc.Tools.Tests/Utils.cs
@@ -0,0 +1,46 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools.Tests
+{
+ static class Utils
+ {
+ // Build an item with a name from args[0] and metadata key-value pairs
+ // from the rest of args, interleaved.
+ // This does not do any checking, and expects an odd number of args.
+ public static ITaskItem MakeItem(params string[] args)
+ {
+ var item = new TaskItem(args[0]);
+ for (int i = 1; i < args.Length; i += 2)
+ {
+ item.SetMetadata(args[i], args[i + 1]);
+ }
+ return item;
+ }
+
+ // Return an array of items from given itemspecs.
+ public static ITaskItem[] MakeSimpleItems(params string[] specs)
+ {
+ return specs.Select(s => new TaskItem(s)).ToArray();
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools.nuspec b/src/csharp/Grpc.Tools.nuspec
deleted file mode 100644
index 0cae5572fd..0000000000
--- a/src/csharp/Grpc.Tools.nuspec
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<package>
- <metadata>
- <id>Grpc.Tools</id>
- <title>gRPC C# Tools</title>
- <summary>Tools for C# implementation of gRPC - an RPC library and framework</summary>
- <description>Precompiled protobuf compiler and gRPC protobuf compiler plugin for generating gRPC client/server C# code. Binaries are available for Windows, Linux and MacOS.</description>
- <version>$version$</version>
- <authors>Google Inc.</authors>
- <owners>grpc-packages</owners>
- <licenseUrl>https://github.com/grpc/grpc/blob/master/LICENSE</licenseUrl>
- <projectUrl>https://github.com/grpc/grpc</projectUrl>
- <requireLicenseAcceptance>false</requireLicenseAcceptance>
- <releaseNotes>Release $version$</releaseNotes>
- <copyright>Copyright 2015, Google Inc.</copyright>
- <tags>gRPC RPC Protocol HTTP/2</tags>
- </metadata>
- <files>
- <!-- forward slashes in src path enable building on Linux -->
- <file src="protoc_plugins/protoc_windows_x86/protoc.exe" target="tools/windows_x86/protoc.exe" />
- <file src="protoc_plugins/protoc_windows_x86/grpc_csharp_plugin.exe" target="tools/windows_x86/grpc_csharp_plugin.exe" />
- <file src="protoc_plugins/protoc_windows_x64/protoc.exe" target="tools/windows_x64/protoc.exe" />
- <file src="protoc_plugins/protoc_windows_x64/grpc_csharp_plugin.exe" target="tools/windows_x64/grpc_csharp_plugin.exe" />
- <file src="protoc_plugins/protoc_linux_x86/protoc" target="tools/linux_x86/protoc" />
- <file src="protoc_plugins/protoc_linux_x86/grpc_csharp_plugin" target="tools/linux_x86/grpc_csharp_plugin" />
- <file src="protoc_plugins/protoc_linux_x64/protoc" target="tools/linux_x64/protoc" />
- <file src="protoc_plugins/protoc_linux_x64/grpc_csharp_plugin" target="tools/linux_x64/grpc_csharp_plugin" />
- <file src="protoc_plugins/protoc_macos_x86/protoc" target="tools/macosx_x86/protoc" />
- <file src="protoc_plugins/protoc_macos_x86/grpc_csharp_plugin" target="tools/macosx_x86/grpc_csharp_plugin" />
- <file src="protoc_plugins/protoc_macos_x64/protoc" target="tools/macosx_x64/protoc" />
- <file src="protoc_plugins/protoc_macos_x64/grpc_csharp_plugin" target="tools/macosx_x64/grpc_csharp_plugin" />
- </files>
-</package>
diff --git a/src/csharp/Grpc.Tools/Common.cs b/src/csharp/Grpc.Tools/Common.cs
new file mode 100644
index 0000000000..e6acdd6393
--- /dev/null
+++ b/src/csharp/Grpc.Tools/Common.cs
@@ -0,0 +1,114 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+
+[assembly: InternalsVisibleTo("Grpc.Tools.Tests")]
+
+namespace Grpc.Tools
+{
+ // Metadata names (MSBuild item attributes) that we refer to often.
+ static class Metadata
+ {
+ // On output dependency lists.
+ public static string Source = "Source";
+ // On ProtoBuf items.
+ public static string ProtoRoot = "ProtoRoot";
+ public static string OutputDir = "OutputDir";
+ public static string GrpcServices = "GrpcServices";
+ public static string GrpcOutputDir = "GrpcOutputDir";
+ };
+
+ // A few flags used to control the behavior under various platforms.
+ internal static class Platform
+ {
+ public enum OsKind { Unknown, Windows, Linux, MacOsX };
+ public static readonly OsKind Os;
+
+ public enum CpuKind { Unknown, X86, X64 };
+ public static readonly CpuKind Cpu;
+
+ // This is not necessarily true, but good enough. BCL lacks a per-FS
+ // API to determine file case sensitivity.
+ public static bool IsFsCaseInsensitive => Os == OsKind.Windows;
+ public static bool IsWindows => Os == OsKind.Windows;
+
+ static Platform()
+ {
+#if NETCORE
+ Os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? OsKind.Windows
+ : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OsKind.Linux
+ : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OsKind.MacOsX
+ : OsKind.Unknown;
+
+ switch (RuntimeInformation.OSArchitecture)
+ {
+ case Architecture.X86: Cpu = CpuKind.X86; break;
+ case Architecture.X64: Cpu = CpuKind.X64; break;
+ // We do not have build tools for other architectures.
+ default: Cpu = CpuKind.Unknown; break;
+ }
+#else
+ // Running under either Mono or full MS framework.
+ Os = OsKind.Windows;
+ if (Type.GetType("Mono.Runtime", throwOnError: false) != null)
+ {
+ // Congratulations. We are running under Mono.
+ var plat = Environment.OSVersion.Platform;
+ if (plat == PlatformID.MacOSX)
+ {
+ Os = OsKind.MacOsX;
+ }
+ else if (plat == PlatformID.Unix || (int)plat == 128)
+ {
+ // This is how Mono detects OSX internally.
+ Os = File.Exists("/usr/lib/libc.dylib") ? OsKind.MacOsX : OsKind.Linux;
+ }
+ }
+
+ // Hope we are not building on ARM under Xamarin!
+ Cpu = Environment.Is64BitOperatingSystem ? CpuKind.X64 : CpuKind.X86;
+#endif
+ }
+ };
+
+ // Exception handling helpers.
+ static class Exceptions
+ {
+ // Returns true iff the exception indicates an error from an I/O call. See
+ // https://github.com/Microsoft/msbuild/blob/v15.4.8.50001/src/Shared/ExceptionHandling.cs#L101
+ static public bool IsIoRelated(Exception ex) =>
+ ex is IOException ||
+ (ex is ArgumentException && !(ex is ArgumentNullException)) ||
+ ex is SecurityException ||
+ ex is UnauthorizedAccessException ||
+ ex is NotSupportedException;
+ };
+
+ // String helpers.
+ static class Strings
+ {
+ // Compare string to argument using OrdinalIgnoreCase comparison.
+ public static bool EqualNoCase(this string a, string b) =>
+ string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/csharp/Grpc.Tools/DepFileUtil.cs b/src/csharp/Grpc.Tools/DepFileUtil.cs
new file mode 100644
index 0000000000..440d3d535c
--- /dev/null
+++ b/src/csharp/Grpc.Tools/DepFileUtil.cs
@@ -0,0 +1,273 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ internal static class DepFileUtil
+ {
+ /*
+ Sample dependency files. Notable features we have to deal with:
+ * Slash doubling, must normalize them.
+ * Spaces in file names. Cannot just "unwrap" the line on backslash at eof;
+ rather, treat every line as containing one file name except for one with
+ the ':' separator, as containing exactly two.
+ * Deal with ':' also being drive letter separator (second example).
+
+ obj\Release\net45\/Foo.cs \
+ obj\Release\net45\/FooGrpc.cs: C:/foo/include/google/protobuf/wrappers.proto\
+ C:/projects/foo/src//foo.proto
+
+ C:\projects\foo\src\./foo.grpc.pb.cc \
+ C:\projects\foo\src\./foo.grpc.pb.h \
+ C:\projects\foo\src\./foo.pb.cc \
+ C:\projects\foo\src\./foo.pb.h: C:/foo/include/google/protobuf/wrappers.proto\
+ C:/foo/include/google/protobuf/any.proto\
+ C:/foo/include/google/protobuf/source_context.proto\
+ C:/foo/include/google/protobuf/type.proto\
+ foo.proto
+ */
+
+ /// <summary>
+ /// Read file names from the dependency file to the right of ':'
+ /// </summary>
+ /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param>
+ /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
+ /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param>
+ /// <returns>
+ /// Array of the proto file <b>input</b> dependencies as written by protoc, or empty
+ /// array if the dependency file does not exist or cannot be parsed.
+ /// </returns>
+ public static string[] ReadDependencyInputs(string protoDepDir, string proto,
+ TaskLoggingHelper log)
+ {
+ string depFilename = GetDepFilenameForProto(protoDepDir, proto);
+ string[] lines = ReadDepFileLines(depFilename, false, log);
+ if (lines.Length == 0)
+ {
+ return lines;
+ }
+
+ var result = new List<string>();
+ bool skip = true;
+ foreach (string line in lines)
+ {
+ // Start at the only line separating dependency outputs from inputs.
+ int ix = skip ? FindLineSeparator(line) : -1;
+ skip = skip && ix < 0;
+ if (skip) { continue; }
+ string file = ExtractFilenameFromLine(line, ix + 1, line.Length);
+ if (file == "")
+ {
+ log.LogMessage(MessageImportance.Low,
+ $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'");
+ return new string[0];
+ }
+
+ // Do not bend over backwards trying not to include a proto into its
+ // own list of dependencies. Since a file is not older than self,
+ // it is safe to add; this is purely a memory optimization.
+ if (file != proto)
+ {
+ result.Add(file);
+ }
+ }
+ return result.ToArray();
+ }
+
+ /// <summary>
+ /// Read file names from the dependency file to the left of ':'
+ /// </summary>
+ /// <param name="depFilename">Path to dependency file written by protoc</param>
+ /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param>
+ /// <returns>
+ /// Array of the protoc-generated outputs from the given dependency file
+ /// written by protoc, or empty array if the file does not exist or cannot
+ /// be parsed.
+ /// </returns>
+ /// <remarks>
+ /// Since this is called after a protoc invocation, an unparsable or missing
+ /// file causes an error-level message to be logged.
+ /// </remarks>
+ public static string[] ReadDependencyOutputs(string depFilename,
+ TaskLoggingHelper log)
+ {
+ string[] lines = ReadDepFileLines(depFilename, true, log);
+ if (lines.Length == 0)
+ {
+ return lines;
+ }
+
+ var result = new List<string>();
+ foreach (string line in lines)
+ {
+ int ix = FindLineSeparator(line);
+ string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length);
+ if (file == "")
+ {
+ log.LogError("Unable to parse generated dependency file {0}.\n" +
+ "Line with error: '{1}'", depFilename, line);
+ return new string[0];
+ }
+ result.Add(file);
+
+ // If this is the line with the separator, do not read further.
+ if (ix >= 0) { break; }
+ }
+ return result.ToArray();
+ }
+
+ /// <summary>
+ /// Construct relative dependency file name from directory hash and file name
+ /// </summary>
+ /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param>
+ /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
+ /// <returns>
+ /// Full relative path to the dependency file, e. g.
+ /// "out/deadbeef12345678_file.protodep"
+ /// </returns>
+ /// <remarks>
+ /// Since a project may contain proto files with the same filename but in different
+ /// directories, a unique filename for the dependency file is constructed based on the
+ /// proto file name both name and directory. The directory path can be arbitrary,
+ /// for example, it can be outside of the project, or an absolute path including
+ /// a drive letter, or a UNC network path. A name constructed from such a path by,
+ /// for example, replacing disallowed name characters with an underscore, may well
+ /// be over filesystem's allowed path length, since it will be located under the
+ /// project and solution directories, which are also some level deep from the root.
+ /// Instead of creating long and unwieldy names for these proto sources, we cache
+ /// the full path of the name without the filename, and append the filename to it,
+ /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678_file", where
+ /// "deadbeef12345678" is a presumed hash value of the string "foo/". This allows
+ /// the file names be short, unique (up to a hash collision), and still allowing
+ /// the user to guess their provenance.
+ /// </remarks>
+ public static string GetDepFilenameForProto(string protoDepDir, string proto)
+ {
+ string dirname = Path.GetDirectoryName(proto);
+ if (Platform.IsFsCaseInsensitive)
+ {
+ dirname = dirname.ToLowerInvariant();
+ }
+ string dirhash = HashString64Hex(dirname);
+ string filename = Path.GetFileNameWithoutExtension(proto);
+ return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep");
+ }
+
+ // Get a 64-bit hash for a directory string. We treat it as if it were
+ // unique, since there are not so many distinct proto paths in a project.
+ // We take the first 64 bit of the string SHA1.
+ // Internal for tests access only.
+ internal static string HashString64Hex(string str)
+ {
+ using (var sha1 = System.Security.Cryptography.SHA1.Create())
+ {
+ byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str));
+ var hashstr = new StringBuilder(16);
+ for (int i = 0; i < 8; i++)
+ {
+ hashstr.Append(hash[i].ToString("x2"));
+ }
+ return hashstr.ToString();
+ }
+ }
+
+ // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from
+ // line 'line', skipping over trailing and leading whitespace, and, when
+ // 'end' is immediately past end of line 'line', also final '\' (used
+ // as a line continuation token in the dep file).
+ // Returns an empty string if the filename cannot be extracted.
+ static string ExtractFilenameFromLine(string line, int beg, int end)
+ {
+ while (beg < end && char.IsWhiteSpace(line[beg])) beg++;
+ if (beg < end && end == line.Length && line[end - 1] == '\\') end--;
+ while (beg < end && char.IsWhiteSpace(line[end - 1])) end--;
+ if (beg == end) return "";
+
+ string filename = line.Substring(beg, end - beg);
+ try
+ {
+ // Normalize file name.
+ return Path.Combine(Path.GetDirectoryName(filename), Path.GetFileName(filename));
+ }
+ catch (Exception ex) when (Exceptions.IsIoRelated(ex))
+ {
+ return "";
+ }
+ }
+
+ // Finds the index of the ':' separating dependency clauses in the line,
+ // not taking Windows drive spec into account. Returns the index of the
+ // separating ':', or -1 if no separator found.
+ static int FindLineSeparator(string line)
+ {
+ // Mind this case where the first ':' is not separator:
+ // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\
+ int ix = line.IndexOf(':');
+ if (ix <= 0 || ix == line.Length - 1
+ || (line[ix + 1] != '/' && line[ix + 1] != '\\')
+ || !char.IsLetter(line[ix - 1]))
+ {
+ return ix; // Not a windows drive: no letter before ':', or no '\' after.
+ }
+ for (int j = ix - 1; --j >= 0;)
+ {
+ if (!char.IsWhiteSpace(line[j]))
+ {
+ return ix; // Not space or BOL only before "X:/".
+ }
+ }
+ return line.IndexOf(':', ix + 1);
+ }
+
+ // Read entire dependency file. The 'required' parameter controls error
+ // logging behavior in case the file not found. We require this file when
+ // compiling, but reading it is optional when computing dependencies.
+ static string[] ReadDepFileLines(string filename, bool required,
+ TaskLoggingHelper log)
+ {
+ try
+ {
+ var result = File.ReadAllLines(filename);
+ if (!required)
+ {
+ log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}");
+ }
+ return result;
+ }
+ catch (Exception ex) when (Exceptions.IsIoRelated(ex))
+ {
+ if (required)
+ {
+ log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}");
+ }
+ else
+ {
+ log.LogMessage(MessageImportance.Low, $"Skipping {filename}: {ex.Message}");
+ }
+ return new string[0];
+ }
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs
new file mode 100644
index 0000000000..536ec43c83
--- /dev/null
+++ b/src/csharp/Grpc.Tools/GeneratorServices.cs
@@ -0,0 +1,194 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ // Abstract class for language-specific analysis behavior, such
+ // as guessing the generated files the same way protoc does.
+ internal abstract class GeneratorServices
+ {
+ protected readonly TaskLoggingHelper Log;
+ protected GeneratorServices(TaskLoggingHelper log) { Log = log; }
+
+ // Obtain a service for the given language (csharp, cpp).
+ public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log)
+ {
+ if (lang.EqualNoCase("csharp")) { return new CSharpGeneratorServices(log); }
+ if (lang.EqualNoCase("cpp")) { return new CppGeneratorServices(log); }
+
+ log.LogError("Invalid value '{0}' for task property 'Generator'. " +
+ "Supported generator languages: CSharp, Cpp.", lang);
+ return null;
+ }
+
+ // Guess whether item's metadata suggests gRPC stub generation.
+ // When "gRPCServices" is not defined, assume gRPC is not used.
+ // When defined, C# uses "none" to skip gRPC, C++ uses "false", so
+ // recognize both. Since the value is tightly coupled to the scripts,
+ // we do not try to validate the value; scripts take care of that.
+ // It is safe to assume that gRPC is requested for any other value.
+ protected bool GrpcOutputPossible(ITaskItem proto)
+ {
+ string gsm = proto.GetMetadata(Metadata.GrpcServices);
+ return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none")
+ && !gsm.EqualNoCase("false");
+ }
+
+ public abstract string[] GetPossibleOutputs(ITaskItem proto);
+ };
+
+ // C# generator services.
+ internal class CSharpGeneratorServices : GeneratorServices
+ {
+ public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) { }
+
+ public override string[] GetPossibleOutputs(ITaskItem protoItem)
+ {
+ bool doGrpc = GrpcOutputPossible(protoItem);
+ string filename = LowerUnderscoreToUpperCamel(
+ Path.GetFileNameWithoutExtension(protoItem.ItemSpec));
+
+ var outputs = new string[doGrpc ? 2 : 1];
+ string outdir = protoItem.GetMetadata(Metadata.OutputDir);
+ string fileStem = Path.Combine(outdir, filename);
+ outputs[0] = fileStem + ".cs";
+ if (doGrpc)
+ {
+ // Override outdir if kGrpcOutputDir present, default to proto output.
+ outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
+ if (outdir != "")
+ {
+ fileStem = Path.Combine(outdir, filename);
+ }
+ outputs[1] = fileStem + "Grpc.cs";
+ }
+ return outputs;
+ }
+
+ string LowerUnderscoreToUpperCamel(string str)
+ {
+ // See src/compiler/generator_helpers.h:118
+ var result = new StringBuilder(str.Length, str.Length);
+ bool cap = true;
+ foreach (char c in str)
+ {
+ if (c == '_')
+ {
+ cap = true;
+ }
+ else if (cap)
+ {
+ result.Append(char.ToUpperInvariant(c));
+ cap = false;
+ }
+ else
+ {
+ result.Append(c);
+ }
+ }
+ return result.ToString();
+ }
+ };
+
+ // C++ generator services.
+ internal class CppGeneratorServices : GeneratorServices
+ {
+ public CppGeneratorServices(TaskLoggingHelper log) : base(log) { }
+
+ public override string[] GetPossibleOutputs(ITaskItem protoItem)
+ {
+ bool doGrpc = GrpcOutputPossible(protoItem);
+ string root = protoItem.GetMetadata(Metadata.ProtoRoot);
+ string proto = protoItem.ItemSpec;
+ string filename = Path.GetFileNameWithoutExtension(proto);
+ // E. g., ("foo/", "foo/bar/x.proto") => "bar"
+ string relative = GetRelativeDir(root, proto);
+
+ var outputs = new string[doGrpc ? 4 : 2];
+ string outdir = protoItem.GetMetadata(Metadata.OutputDir);
+ string fileStem = Path.Combine(outdir, relative, filename);
+ outputs[0] = fileStem + ".pb.cc";
+ outputs[1] = fileStem + ".pb.h";
+ if (doGrpc)
+ {
+ // Override outdir if kGrpcOutputDir present, default to proto output.
+ outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
+ if (outdir != "")
+ {
+ fileStem = Path.Combine(outdir, relative, filename);
+ }
+ outputs[2] = fileStem + "_grpc.pb.cc";
+ outputs[3] = fileStem + "_grpc.pb.h";
+ }
+ return outputs;
+ }
+
+ // Calculate part of proto path relative to root. Protoc is very picky
+ // about them matching exactly, so can be we. Expect root be exact prefix
+ // to proto, minus some slash normalization.
+ string GetRelativeDir(string root, string proto)
+ {
+ string protoDir = Path.GetDirectoryName(proto);
+ string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root)));
+ if (rootDir == s_dotSlash)
+ {
+ // Special case, otherwise we can return "./" instead of "" below!
+ return protoDir;
+ }
+ if (Platform.IsFsCaseInsensitive)
+ {
+ protoDir = protoDir.ToLowerInvariant();
+ rootDir = rootDir.ToLowerInvariant();
+ }
+ protoDir = EndWithSlash(protoDir);
+ if (!protoDir.StartsWith(rootDir))
+ {
+ Log.LogWarning("ProtoBuf item '{0}' has the ProtoRoot metadata '{1}' " +
+ "which is not prefix to its path. Cannot compute relative path.",
+ proto, root);
+ return "";
+ }
+ return protoDir.Substring(rootDir.Length);
+ }
+
+ // './' or '.\', normalized per system.
+ static string s_dotSlash = "." + Path.DirectorySeparatorChar;
+
+ static string EndWithSlash(string str)
+ {
+ if (str == "")
+ {
+ return s_dotSlash;
+ }
+ else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/')
+ {
+ return str + Path.DirectorySeparatorChar;
+ }
+ else
+ {
+ return str;
+ }
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools/Grpc.Tools.csproj b/src/csharp/Grpc.Tools/Grpc.Tools.csproj
new file mode 100644
index 0000000000..61fa75a4ec
--- /dev/null
+++ b/src/csharp/Grpc.Tools/Grpc.Tools.csproj
@@ -0,0 +1,101 @@
+<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+
+ <Import Project="..\Grpc.Core\Version.csproj.include" />
+
+ <PropertyGroup>
+ <AssemblyName>Protobuf.MSBuild</AssemblyName>
+ <Version>$(GrpcCsharpVersion)</Version>
+ <!-- If changing targets, change also paths in Google.Protobuf.Tools.targets. -->
+ <TargetFrameworks>net45;netstandard1.3</TargetFrameworks>
+ </PropertyGroup>
+
+ <!-- This is copied verbatim from Grpc.Core/Common.csproj.include. Other settings
+ in that file conflict with the intent of this build, as it cannot be signed,
+ and may not compile Grpc.Core/Version.cs, as that file references constants
+ in Grpc.Core.dll.
+ TODO(kkm): Refactor imports. -->
+ <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' and '$(MSBuildRuntimeType)' == 'Core' ">
+ <!-- Use Mono reference assemblies in SDK build: https://github.com/dotnet/sdk/issues/335 -->
+ <FrameworkPathOverride Condition="Exists('/usr/lib/mono/4.5-api')">/usr/lib/mono/4.5-api</FrameworkPathOverride>
+ <FrameworkPathOverride Condition="Exists('/usr/local/lib/mono/4.5-api')">/usr/local/lib/mono/4.5-api</FrameworkPathOverride>
+ <FrameworkPathOverride Condition="Exists('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api</FrameworkPathOverride>
+ </PropertyGroup>
+
+ <PropertyGroup Label="Asset root folders. TODO(kkm): Change with package separation.">
+ <!-- TODO(kkm): Rework whole section when splitting packages. -->
+ <!-- GRPC: ../../third_party/protobuf/src/google/protobuf/ -->
+ <!-- GPB: ../src/google/protobuf/ -->
+ <Assets_ProtoInclude>../../../third_party/protobuf/src/google/protobuf/</Assets_ProtoInclude>
+
+ <!-- GPB: ../protoc/ -->
+ <!-- GRPC: ../protoc_plugins/protoc_ -->
+ <Assets_ProtoCompiler>../protoc_plugins/protoc_</Assets_ProtoCompiler>
+
+ <!-- GRPC: ../protoc_plugins/ -->
+ <Assets_GrpcPlugins>../protoc_plugins/</Assets_GrpcPlugins>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(TargetFramework)' != 'net45' ">
+ <DefineConstants>$(DefineConstants);NETCORE</DefineConstants>
+ </PropertyGroup>
+
+ <PropertyGroup Label="NuGet package definition" Condition=" '$(Configuration)' == 'Release' ">
+ <!-- TODO(kkm): Change to "build\" after splitting. -->
+ <BuildOutputTargetFolder>build\_protobuf\</BuildOutputTargetFolder>
+ <DevelopmentDependency>true</DevelopmentDependency>
+ <NoPackageAnalysis>true</NoPackageAnalysis>
+ <PackageId>Grpc.Tools</PackageId>
+ <Description>gRPC and Protocol Buffer compiler for managed C# and native C++ projects.
+
+Add this package to a project that contains .proto files to be compiled to code.
+It contains the compilers, include files and project system integration for gRPC
+and Protocol buffer service description files necessary to build them on Windows,
+Linux and MacOS. Managed runtime is supplied separately in the Grpc.Core package.</Description>
+ <Copyright>Copyright 2018 gRPC authors</Copyright>
+ <Authors>gRPC authors</Authors>
+ <PackageLicenseUrl>https://github.com/grpc/grpc/blob/master/LICENSE</PackageLicenseUrl>
+ <PackageProjectUrl>https://github.com/grpc/grpc</PackageProjectUrl>
+ <PackageTags>gRPC RPC protocol HTTP/2</PackageTags>
+ </PropertyGroup>
+
+ <ItemGroup Label="NuGet package assets">
+ <None Pack="true" PackagePath="build\" Include="build\**\*.xml; build\**\*.props; build\**\*.targets;" />
+
+ <!-- Protobuf assets (for Google.Protobuf.Tools) -->
+ <_ProtoAssetName Include="any;api;descriptor;duration;empty;field_mask;
+ source_context;struct;timestamp;type;wrappers" />
+ <_Asset PackagePath="build/native/include/google/protobuf/" Include="@(_ProtoAssetName->'$(Assets_ProtoInclude)%(Identity).proto')" />
+
+ <!-- TODO(kkm): GPB builds assets into "macosx", GRPC into "macos". -->
+ <!-- TODO(kkm): Do not place non-tools under tools/, use build/native/bin/. -->
+ <!-- TODO(kkm): Do not package windows x64 builds (#13098). -->
+ <_Asset PackagePath="tools/windows_x86/protoc.exe" Include="$(Assets_ProtoCompiler)windows_x86/protoc.exe" />
+ <_Asset PackagePath="tools/windows_x64/protoc.exe" Include="$(Assets_ProtoCompiler)windows_x64/protoc.exe" />
+ <_Asset PackagePath="tools/linux_x86/protoc" Include="$(Assets_ProtoCompiler)linux_x86/protoc" />
+ <_Asset PackagePath="tools/linux_x64/protoc" Include="$(Assets_ProtoCompiler)linux_x64/protoc" />
+ <_Asset PackagePath="tools/macosx_x86/protoc" Include="$(Assets_ProtoCompiler)macos_x86/protoc" /> <!-- GPB: macosx-->
+ <_Asset PackagePath="tools/macosx_x64/protoc" Include="$(Assets_ProtoCompiler)macos_x64/protoc" /> <!-- GPB: macosx-->
+
+ <!-- gRPC assets (for Grpc.Tools) -->
+ <_Asset PackagePath="tools/windows_x86/grpc_csharp_plugin.exe" Include="$(Assets_GrpcPlugins)protoc_windows_x86/grpc_csharp_plugin.exe" />
+ <_Asset PackagePath="tools/windows_x64/grpc_csharp_plugin.exe" Include="$(Assets_GrpcPlugins)protoc_windows_x64/grpc_csharp_plugin.exe" />
+ <_Asset PackagePath="tools/linux_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x86/grpc_csharp_plugin" />
+ <_Asset PackagePath="tools/linux_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x64/grpc_csharp_plugin" />
+ <_Asset PackagePath="tools/macosx_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x86/grpc_csharp_plugin" />
+ <_Asset PackagePath="tools/macosx_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x64/grpc_csharp_plugin" />
+
+ <None Include="@(_Asset)" Pack="true" Visible="false" />
+ </ItemGroup>
+
+ <ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
+ <Reference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.v4.0" Pack="false" />
+ </ItemGroup>
+
+ <ItemGroup Condition=" '$(TargetFramework)' != 'net45' ">
+ <PackageReference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.Core" Version="15.6.*" />
+ <!-- Set PrivateAssets="All" on all items, even those implicitly added,
+ so that they do not become dependencies of this package. -->
+ <PackageReference Update="@(PackageReference)" PrivateAssets="All" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs
new file mode 100644
index 0000000000..93608e1ac0
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoCompile.cs
@@ -0,0 +1,441 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ /// <summary>
+ /// Run Google proto compiler (protoc).
+ ///
+ /// After a successful run, the task reads the dependency file if specified
+ /// to be saved by the compiler, and returns its output files.
+ ///
+ /// This task (unlike PrepareProtoCompile) does not attempt to guess anything
+ /// about language-specific behavior of protoc, and therefore can be used for
+ /// any language outputs.
+ /// </summary>
+ public class ProtoCompile : ToolTask
+ {
+ /*
+
+ Usage: /home/kkm/work/protobuf/src/.libs/lt-protoc [OPTION] PROTO_FILES
+ Parse PROTO_FILES and generate output based on the options given:
+ -IPATH, --proto_path=PATH Specify the directory in which to search for
+ imports. May be specified multiple times;
+ directories will be searched in order. If not
+ given, the current working directory is used.
+ --version Show version info and exit.
+ -h, --help Show this text and exit.
+ --encode=MESSAGE_TYPE Read a text-format message of the given type
+ from standard input and write it in binary
+ to standard output. The message type must
+ be defined in PROTO_FILES or their imports.
+ --decode=MESSAGE_TYPE Read a binary message of the given type from
+ standard input and write it in text format
+ to standard output. The message type must
+ be defined in PROTO_FILES or their imports.
+ --decode_raw Read an arbitrary protocol message from
+ standard input and write the raw tag/value
+ pairs in text format to standard output. No
+ PROTO_FILES should be given when using this
+ flag.
+ --descriptor_set_in=FILES Specifies a delimited list of FILES
+ each containing a FileDescriptorSet (a
+ protocol buffer defined in descriptor.proto).
+ The FileDescriptor for each of the PROTO_FILES
+ provided will be loaded from these
+ FileDescriptorSets. If a FileDescriptor
+ appears multiple times, the first occurrence
+ will be used.
+ -oFILE, Writes a FileDescriptorSet (a protocol buffer,
+ --descriptor_set_out=FILE defined in descriptor.proto) containing all of
+ the input files to FILE.
+ --include_imports When using --descriptor_set_out, also include
+ all dependencies of the input files in the
+ set, so that the set is self-contained.
+ --include_source_info When using --descriptor_set_out, do not strip
+ SourceCodeInfo from the FileDescriptorProto.
+ This results in vastly larger descriptors that
+ include information about the original
+ location of each decl in the source file as
+ well as surrounding comments.
+ --dependency_out=FILE Write a dependency output file in the format
+ expected by make. This writes the transitive
+ set of input file paths to FILE
+ --error_format=FORMAT Set the format in which to print errors.
+ FORMAT may be 'gcc' (the default) or 'msvs'
+ (Microsoft Visual Studio format).
+ --print_free_field_numbers Print the free field numbers of the messages
+ defined in the given proto files. Groups share
+ the same field number space with the parent
+ message. Extension ranges are counted as
+ occupied fields numbers.
+
+ --plugin=EXECUTABLE Specifies a plugin executable to use.
+ Normally, protoc searches the PATH for
+ plugins, but you may specify additional
+ executables not in the path using this flag.
+ Additionally, EXECUTABLE may be of the form
+ NAME=PATH, in which case the given plugin name
+ is mapped to the given executable even if
+ the executable's own name differs.
+ --cpp_out=OUT_DIR Generate C++ header and source.
+ --csharp_out=OUT_DIR Generate C# source file.
+ --java_out=OUT_DIR Generate Java source file.
+ --javanano_out=OUT_DIR Generate Java Nano source file.
+ --js_out=OUT_DIR Generate JavaScript source.
+ --objc_out=OUT_DIR Generate Objective C header and source.
+ --php_out=OUT_DIR Generate PHP source file.
+ --python_out=OUT_DIR Generate Python source file.
+ --ruby_out=OUT_DIR Generate Ruby source file.
+ @<filename> Read options and filenames from file. If a
+ relative file path is specified, the file
+ will be searched in the working directory.
+ The --proto_path option will not affect how
+ this argument file is searched. Content of
+ the file will be expanded in the position of
+ @<filename> as in the argument list. Note
+ that shell expansion is not applied to the
+ content of the file (i.e., you cannot use
+ quotes, wildcards, escapes, commands, etc.).
+ Each line corresponds to a single argument,
+ even if it contains spaces.
+ */
+ static string[] s_supportedGenerators = new[] { "cpp", "csharp", "java",
+ "javanano", "js", "objc",
+ "php", "python", "ruby" };
+
+ /// <summary>
+ /// Code generator.
+ /// </summary>
+ [Required]
+ public string Generator { get; set; }
+
+ /// <summary>
+ /// Protobuf files to compile.
+ /// </summary>
+ [Required]
+ public ITaskItem[] ProtoBuf { get; set; }
+
+ /// <summary>
+ /// Directory where protoc dependency files are cached. If provided, dependency
+ /// output filename is autogenerated from source directory hash and file name.
+ /// Mutually exclusive with DependencyOut.
+ /// Switch: --dependency_out (with autogenerated file name).
+ /// </summary>
+ public string ProtoDepDir { get; set; }
+
+ /// <summary>
+ /// Dependency file full name. Mutually exclusive with ProtoDepDir.
+ /// Autogenerated file name is available in this property after execution.
+ /// Switch: --dependency_out.
+ /// </summary>
+ [Output]
+ public string DependencyOut { get; set; }
+
+ /// <summary>
+ /// The directories to search for imports. Directories will be searched
+ /// in order. If not given, the current working directory is used.
+ /// Switch: --proto_path.
+ /// </summary>
+ public string[] ProtoPath { get; set; }
+
+ /// <summary>
+ /// Generated code directory. The generator property determines the language.
+ /// Switch: --GEN-out= (for different generators GEN).
+ /// </summary>
+ [Required]
+ public string OutputDir { get; set; }
+
+ /// <summary>
+ /// Codegen options. See also OptionsFromMetadata.
+ /// Switch: --GEN_out= (for different generators GEN).
+ /// </summary>
+ public string[] OutputOptions { get; set; }
+
+ /// <summary>
+ /// Full path to the gRPC plugin executable. If specified, gRPC generation
+ /// is enabled for the files.
+ /// Switch: --plugin=protoc-gen-grpc=
+ /// </summary>
+ public string GrpcPluginExe { get; set; }
+
+ /// <summary>
+ /// Generated gRPC directory. The generator property determines the
+ /// language. If gRPC is enabled but this is not given, OutputDir is used.
+ /// Switch: --grpc_out=
+ /// </summary>
+ public string GrpcOutputDir { get; set; }
+
+ /// <summary>
+ /// gRPC Codegen options. See also OptionsFromMetadata.
+ /// --grpc_opt=opt1,opt2=val (comma-separated).
+ /// </summary>
+ public string[] GrpcOutputOptions { get; set; }
+
+ /// <summary>
+ /// List of files written in addition to generated outputs. Includes a
+ /// single item for the dependency file if written.
+ /// </summary>
+ [Output]
+ public ITaskItem[] AdditionalFileWrites { get; private set; }
+
+ /// <summary>
+ /// List of language files generated by protoc. Empty unless DependencyOut
+ /// or ProtoDepDir is set, since the file writes are extracted from protoc
+ /// dependency output file.
+ /// </summary>
+ [Output]
+ public ITaskItem[] GeneratedFiles { get; private set; }
+
+ // Hide this property from MSBuild, we should never use a shell script.
+ private new bool UseCommandProcessor { get; set; }
+
+ protected override string ToolName => Platform.IsWindows ? "protoc.exe" : "protoc";
+
+ // Since we never try to really locate protoc.exe somehow, just try ToolExe
+ // as the full tool location. It will be either just protoc[.exe] from
+ // ToolName above if not set by the user, or a user-supplied full path. The
+ // base class will then resolve the former using system PATH.
+ protected override string GenerateFullPathToTool() => ToolExe;
+
+ // Log protoc errors with the High priority (bold white in MsBuild,
+ // printed with -v:n, and shown in the Output windows in VS).
+ protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;
+
+ // Called by base class to validate arguments and make them consistent.
+ protected override bool ValidateParameters()
+ {
+ // Part of proto command line switches, must be lowercased.
+ Generator = Generator.ToLowerInvariant();
+ if (!System.Array.Exists(s_supportedGenerators, g => g == Generator))
+ {
+ Log.LogError("Invalid value for Generator='{0}'. Supported generators: {1}",
+ Generator, string.Join(", ", s_supportedGenerators));
+ }
+
+ if (ProtoDepDir != null && DependencyOut != null)
+ {
+ Log.LogError("Properties ProtoDepDir and DependencyOut may not be both specified");
+ }
+
+ if (ProtoBuf.Length > 1 && (ProtoDepDir != null || DependencyOut != null))
+ {
+ Log.LogError("Proto compiler currently allows only one input when " +
+ "--dependency_out is specified (via ProtoDepDir or DependencyOut). " +
+ "Tracking issue: https://github.com/google/protobuf/pull/3959");
+ }
+
+ // Use ProtoDepDir to autogenerate DependencyOut
+ if (ProtoDepDir != null)
+ {
+ DependencyOut = DepFileUtil.GetDepFilenameForProto(ProtoDepDir, ProtoBuf[0].ItemSpec);
+ }
+
+ if (GrpcPluginExe == null)
+ {
+ GrpcOutputOptions = null;
+ GrpcOutputDir = null;
+ }
+ else if (GrpcOutputDir == null)
+ {
+ // Use OutputDir for gRPC output if not specified otherwise by user.
+ GrpcOutputDir = OutputDir;
+ }
+
+ return !Log.HasLoggedErrors && base.ValidateParameters();
+ }
+
+ // Protoc chokes on BOM, naturally. I would!
+ static readonly Encoding s_utf8WithoutBom = new UTF8Encoding(false);
+ protected override Encoding ResponseFileEncoding => s_utf8WithoutBom;
+
+ // Protoc takes one argument per line from the response file, and does not
+ // require any quoting whatsoever. Otherwise, this is similar to the
+ // standard CommandLineBuilder
+ class ProtocResponseFileBuilder
+ {
+ StringBuilder _data = new StringBuilder(1000);
+ public override string ToString() => _data.ToString();
+
+ // If 'value' is not empty, append '--name=value\n'.
+ public void AddSwitchMaybe(string name, string value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ _data.Append("--").Append(name).Append("=")
+ .Append(value).Append('\n');
+ }
+ }
+
+ // Add switch with the 'values' separated by commas, for options.
+ public void AddSwitchMaybe(string name, string[] values)
+ {
+ if (values?.Length > 0)
+ {
+ _data.Append("--").Append(name).Append("=")
+ .Append(string.Join(",", values)).Append('\n');
+ }
+ }
+
+ // Add a positional argument to the file data.
+ public void AddArg(string arg)
+ {
+ _data.Append(arg).Append('\n');
+ }
+ };
+
+ // Called by the base ToolTask to get response file contents.
+ protected override string GenerateResponseFileCommands()
+ {
+ var cmd = new ProtocResponseFileBuilder();
+ cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir));
+ cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions);
+ cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe);
+ cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir));
+ cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions);
+ if (ProtoPath != null)
+ {
+ foreach (string path in ProtoPath)
+ cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path));
+ }
+ cmd.AddSwitchMaybe("dependency_out", DependencyOut);
+ foreach (var proto in ProtoBuf)
+ {
+ cmd.AddArg(proto.ItemSpec);
+ }
+ return cmd.ToString();
+ }
+
+ // Protoc cannot digest trailing slashes in directory names,
+ // curiously under Linux, but not in Windows.
+ static string TrimEndSlash(string dir)
+ {
+ if (dir == null || dir.Length <= 1)
+ {
+ return dir;
+ }
+ string trim = dir.TrimEnd('/', '\\');
+ // Do not trim the root slash, drive letter possible.
+ if (trim.Length == 0)
+ {
+ // Slashes all the way down.
+ return dir.Substring(0, 1);
+ }
+ if (trim.Length == 2 && dir.Length > 2 && trim[1] == ':')
+ {
+ // We have a drive letter and root, e. g. 'C:\'
+ return dir.Substring(0, 3);
+ }
+ return trim;
+ }
+
+ // Called by the base class to log tool's command line.
+ //
+ // Protoc command file is peculiar, with one argument per line, separated
+ // by newlines. Unwrap it for log readability into a single line, and also
+ // quote arguments, lest it look weird and so it may be copied and pasted
+ // into shell. Since this is for logging only, correct enough is correct.
+ protected override void LogToolCommand(string cmd)
+ {
+ var printer = new StringBuilder(1024);
+
+ // Print 'str' slice into 'printer', wrapping in quotes if contains some
+ // interesting characters in file names, or if empty string. The list of
+ // characters requiring quoting is not by any means exhaustive; we are
+ // just striving to be nice, not guaranteeing to be nice.
+ var quotable = new[] { ' ', '!', '$', '&', '\'', '^' };
+ void PrintQuoting(string str, int start, int count)
+ {
+ bool wrap = count == 0 || str.IndexOfAny(quotable, start, count) >= 0;
+ if (wrap) printer.Append('"');
+ printer.Append(str, start, count);
+ if (wrap) printer.Append('"');
+ }
+
+ for (int ib = 0, ie; (ie = cmd.IndexOf('\n', ib)) >= 0; ib = ie + 1)
+ {
+ // First line only contains both the program name and the first switch.
+ // We can rely on at least the '--out_dir' switch being always present.
+ if (ib == 0)
+ {
+ int iep = cmd.IndexOf(" --");
+ if (iep > 0)
+ {
+ PrintQuoting(cmd, 0, iep);
+ ib = iep + 1;
+ }
+ }
+ printer.Append(' ');
+ if (cmd[ib] == '-')
+ {
+ // Print switch unquoted, including '=' if any.
+ int iarg = cmd.IndexOf('=', ib, ie - ib);
+ if (iarg < 0)
+ {
+ // Bare switch without a '='.
+ printer.Append(cmd, ib, ie - ib);
+ continue;
+ }
+ printer.Append(cmd, ib, iarg + 1 - ib);
+ ib = iarg + 1;
+ }
+ // A positional argument or switch value.
+ PrintQuoting(cmd, ib, ie - ib);
+ }
+
+ base.LogToolCommand(printer.ToString());
+ }
+
+ // Main task entry point.
+ public override bool Execute()
+ {
+ base.UseCommandProcessor = false;
+
+ bool ok = base.Execute();
+ if (!ok)
+ {
+ return false;
+ }
+
+ // Read dependency output file from the compiler to retrieve the
+ // definitive list of created files. Report the dependency file
+ // itself as having been written to.
+ if (DependencyOut != null)
+ {
+ string[] outputs = DepFileUtil.ReadDependencyOutputs(DependencyOut, Log);
+ if (HasLoggedErrors)
+ {
+ return false;
+ }
+
+ GeneratedFiles = new ITaskItem[outputs.Length];
+ for (int i = 0; i < outputs.Length; i++)
+ {
+ GeneratedFiles[i] = new TaskItem(outputs[i]);
+ }
+ AdditionalFileWrites = new ITaskItem[] { new TaskItem(DependencyOut) };
+ }
+
+ return true;
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs
new file mode 100644
index 0000000000..915be3421e
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs
@@ -0,0 +1,86 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Collections.Generic;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ public class ProtoCompilerOutputs : Task
+ {
+ /// <summary>
+ /// Code generator. Currently supported are "csharp", "cpp".
+ /// </summary>
+ [Required]
+ public string Generator { get; set; }
+
+ /// <summary>
+ /// All Proto files in the project. The task computes possible outputs
+ /// from these proto files, and returns them in the PossibleOutputs list.
+ /// Not all of these might be actually produced by protoc; this is dealt
+ /// with later in the ProtoCompile task which returns the list of
+ /// files actually produced by the compiler.
+ /// </summary>
+ [Required]
+ public ITaskItem[] ProtoBuf { get; set; }
+
+ /// <summary>
+ /// Output items per each potential output. We do not look at existing
+ /// cached dependency even if they exist, since file may be refactored,
+ /// affecting whether or not gRPC code file is generated from a given proto.
+ /// Instead, all potentially possible generated sources are collected.
+ /// It is a wise idea to generate empty files later for those potentials
+ /// that are not actually created by protoc, so the dependency checks
+ /// result in a minimal recompilation. The Protoc task can output the
+ /// list of files it actually produces, given right combination of its
+ /// properties.
+ /// Output items will have the Source metadata set on them:
+ /// <ItemName Include="MyProto.cs" Source="my_proto.proto" />
+ /// </summary>
+ [Output]
+ public ITaskItem[] PossibleOutputs { get; private set; }
+
+ public override bool Execute()
+ {
+ var generator = GeneratorServices.GetForLanguage(Generator, Log);
+ if (generator == null)
+ {
+ // Error already logged, just return.
+ return false;
+ }
+
+ // Get language-specific possible output. The generator expects certain
+ // metadata be set on the proto item.
+ var possible = new List<ITaskItem>();
+ foreach (var proto in ProtoBuf)
+ {
+ var outputs = generator.GetPossibleOutputs(proto);
+ foreach (string output in outputs)
+ {
+ var ti = new TaskItem(output);
+ ti.SetMetadata(Metadata.Source, proto.ItemSpec);
+ possible.Add(ti);
+ }
+ }
+ PossibleOutputs = possible.ToArray();
+
+ return !Log.HasLoggedErrors;
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools/ProtoReadDependencies.cs b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs
new file mode 100644
index 0000000000..963837e8b7
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs
@@ -0,0 +1,78 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using System.Collections.Generic;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ public class ProtoReadDependencies : Task
+ {
+ /// <summary>
+ /// The collection is used to collect possible additional dependencies
+ /// of proto files cached under ProtoDepDir.
+ /// </summary>
+ [Required]
+ public ITaskItem[] ProtoBuf { get; set; }
+
+ /// <summary>
+ /// Directory where protoc dependency files are cached.
+ /// </summary>
+ [Required]
+ public string ProtoDepDir { get; set; }
+
+ /// <summary>
+ /// Additional items that a proto file depends on. This list may include
+ /// extra dependencies; we do our best to include as few extra positives
+ /// as reasonable to avoid missing any. The collection item is the
+ /// dependency, and its Source metadatum is the dependent proto file, like
+ /// <ItemName Include="/usr/include/proto/wrapper.proto"
+ /// Source="my_proto.proto" />
+ /// </summary>
+ [Output]
+ public ITaskItem[] Dependencies { get; private set; }
+
+ public override bool Execute()
+ {
+ // Read dependency files, where available. There might be none,
+ // just use a best effort.
+ if (ProtoDepDir != null)
+ {
+ var dependencies = new List<ITaskItem>();
+ foreach (var proto in ProtoBuf)
+ {
+ string[] deps = DepFileUtil.ReadDependencyInputs(ProtoDepDir, proto.ItemSpec, Log);
+ foreach (string dep in deps)
+ {
+ var ti = new TaskItem(dep);
+ ti.SetMetadata(Metadata.Source, proto.ItemSpec);
+ dependencies.Add(ti);
+ }
+ }
+ Dependencies = dependencies.ToArray();
+ }
+ else
+ {
+ Dependencies = new ITaskItem[0];
+ }
+
+ return !Log.HasLoggedErrors;
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs
new file mode 100644
index 0000000000..aed8a66339
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs
@@ -0,0 +1,63 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC 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.
+
+#endregion
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ /// <summary>
+ /// A helper task to resolve actual OS type and bitness.
+ /// </summary>
+ public class ProtoToolsPlatform : Task
+ {
+ /// <summary>
+ /// Return one of 'linux', 'macosx' or 'windows'.
+ /// If the OS is unknown, the property is not set.
+ /// </summary>
+ [Output]
+ public string Os { get; set; }
+
+ /// <summary>
+ /// Return one of 'x64' or 'x86'.
+ /// If the CPU is unknown, the property is not set.
+ /// </summary>
+ [Output]
+ public string Cpu { get; set; }
+
+
+ public override bool Execute()
+ {
+ switch (Platform.Os)
+ {
+ case Platform.OsKind.Linux: Os = "linux"; break;
+ case Platform.OsKind.MacOsX: Os = "macosx"; break;
+ case Platform.OsKind.Windows: Os = "windows"; break;
+ default: Os = ""; break;
+ }
+
+ switch (Platform.Cpu)
+ {
+ case Platform.CpuKind.X86: Cpu = "x86"; break;
+ case Platform.CpuKind.X64: Cpu = "x64"; break;
+ default: Cpu = ""; break;
+ }
+ return true;
+ }
+ };
+}
diff --git a/src/csharp/Grpc.Tools/build/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/Grpc.Tools.props
new file mode 100644
index 0000000000..dbcd8bf494
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.props
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- Name of this file must match package ID. -->
+ <!-- Packages will be split later. -->
+ <Import Project="_grpc/_Grpc.Tools.props"/>
+ <Import Project="_protobuf/Google.Protobuf.Tools.props"/>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/Grpc.Tools.targets b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets
new file mode 100644
index 0000000000..c0a5b1e2c5
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+
+ <!-- Name of this file must match package ID. -->
+ <!-- Packages will be split later. -->
+ <Import Project="_grpc/_Grpc.Tools.targets"/>
+ <Import Project="_protobuf/Google.Protobuf.Tools.targets"/>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml
new file mode 100644
index 0000000000..54468eb5ef
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml
@@ -0,0 +1,30 @@
+<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
+ <Rule Name="ProtoBuf"
+ DisplayName="File Properties"
+ PageTemplate="generic"
+ Description="File Properties"
+ OverrideMode="Extend">
+ <Rule.DataSource>
+ <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf"
+ HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" />
+ </Rule.DataSource>
+
+ <Rule.Categories>
+ <Category Name="gRPC" DisplayName="gRPC" />
+ </Rule.Categories>
+
+ <EnumProperty Name="GrpcServices" DisplayName="gRPC Stub Classes"
+ Category="gRPC" Default="Both"
+ Description="Generate gRPC server and client stub classes.">
+ <EnumValue Name="Both" DisplayName="Client and Server" IsDefault="true" />
+ <EnumValue Name="Client" DisplayName="Client only" />
+ <EnumValue Name="Server" DisplayName="Server only" />
+ <EnumValue Name="None" DisplayName="Do not generate" />
+ <EnumProperty.DataSource>
+ <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext"
+ PersistenceStyle="Attribute" />
+ </EnumProperty.DataSource>
+ </EnumProperty>
+
+ </Rule>
+</ProjectSchemaDefinitions>
diff --git a/src/csharp/Grpc.Tools/build/_grpc/README b/src/csharp/Grpc.Tools/build/_grpc/README
new file mode 100644
index 0000000000..4a7204b9ff
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/README
@@ -0,0 +1,3 @@
+TODO(kkm): These file will go into Grpc.Tools/build after package split.
+ Remove leading underscores from file names; they are hiding the
+ files from some NuGet versions which pull them into project.
diff --git a/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props
new file mode 100644
index 0000000000..8ce07c48ab
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ </PropertyGroup>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets
new file mode 100644
index 0000000000..5f76c03ce5
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ <gRPC_PluginFileName Condition=" '$(gRPC_PluginFileName)' == '' and '$(Language)' == 'C#' ">grpc_csharp_plugin</gRPC_PluginFileName>
+ </PropertyGroup>
+
+ <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+ <!-- Extend property pages with gRPC properties. -->
+ <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Grpc.CSharp.xml">
+ <Context>File;BrowseObject</Context>
+ </PropertyPageSchema>
+ </ItemGroup>
+
+ <ItemDefinitionGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+ <ProtoBuf>
+ <GrpcServices Condition=" '%(ProtoBuf.GrpcServices)' == '' ">Both</GrpcServices>
+ </ProtoBuf>
+ </ItemDefinitionGroup>
+
+ <!-- This target is invoked in a C# project, or can be called in a customized project. -->
+ <Target Name="gRPC_ResolvePluginFullPath" AfterTargets="Protobuf_ResolvePlatform">
+ <PropertyGroup>
+ <!-- TODO(kkm): Do not use Protobuf_PackagedToolsPath, roll gRPC's own. -->
+ <!-- TODO(kkm): Do not package windows x64 builds (#13098). -->
+ <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' "
+ >$(Protobuf_PackagedToolsPath)\$(Protobuf_ToolsOs)_x86\$(gRPC_PluginFileName).exe</gRPC_PluginFullPath>
+ <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' "
+ >$(Protobuf_PackagedToolsPath)/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/$(gRPC_PluginFileName)</gRPC_PluginFullPath>
+ </PropertyGroup>
+ </Target>
+
+ <Target Name="_gRPC_PrepareCompileOptions" AfterTargets="Protobuf_PrepareCompileOptions">
+ <ItemGroup Condition=" '$(Language)' == 'C#' ">
+ <Protobuf_Compile Condition=" %(Protobuf_Compile.GrpcServices) != 'None' ">
+ <GrpcPluginExe Condition=" '%(Protobuf_Compile.GrpcPluginExe)' == '' ">$(gRPC_PluginFullPath)</GrpcPluginExe>
+ <GrpcOutputDir Condition=" '%(Protobuf_Compile.GrpcOutputDir)' == '' " >%(Protobuf_Compile.OutputDir)</GrpcOutputDir>
+ <_GrpcOutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._GrpcOutputOptions);internal_access</_GrpcOutputOptions>
+ </Protobuf_Compile>
+ <Protobuf_Compile Condition=" '%(Protobuf_Compile.GrpcServices)' == 'Client' ">
+ <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_server</_GrpcOutputOptions>
+ </Protobuf_Compile>
+ <Protobuf_Compile Condition=" '%(Protobuf_Compile.GrpcServices)' == 'Server' ">
+ <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_client</_GrpcOutputOptions>
+ </Protobuf_Compile>
+ </ItemGroup>
+ </Target>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props
new file mode 100644
index 0000000000..9f2d8bb4b5
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+
+ <!-- Revision number of this package conventions (as if "API" version). -->
+ <Protobuf_ToolingRevision>1</Protobuf_ToolingRevision>
+
+ <!-- TODO(kkm): Remove one "../" when separating packages. -->
+ <!-- TODO(kkm): Do not place non-tools under tools/, use build/native/bin/. -->
+ <Protobuf_PackagedToolsPath>$( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../../tools) )</Protobuf_PackagedToolsPath>
+ <Protobuf_StandardImportsPath>$( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../native/include) )</Protobuf_StandardImportsPath>
+ </PropertyGroup>
+
+ <!-- NET SDK projects only: include proto files by default. Other project
+ types are not setting or using $(EnableDefaultItems).
+ Note that MSBuild evaluates all ItemGroups and their conditions in the
+ final pass over the build script, so properties like EnableDefaultProtoBufItems
+ here can be changed later in the project. -->
+ <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' ">
+ <ProtoBuf Include="**/*.proto"
+ Condition=" '$(EnableDefaultItems)' == 'true' and '$(EnableDefaultProtoBufItems)' == 'true' " />
+ </ItemGroup>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets
new file mode 100644
index 0000000000..1d233d23a8
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets
@@ -0,0 +1,384 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+ <!-- We allow a non-C# generator be set by the user, but skip adding outputs to Compile in this case. -->
+ <Protobuf_Generator Condition=" '$(Protobuf_Generator)' == '' and '$(Language)' == 'C#' ">CSharp</Protobuf_Generator>
+ <!-- Configuration is passing the smoke test. -->
+ <Protobuf_ProjectSupported Condition=" '$(Protobuf_Generator)' != '' ">true</Protobuf_ProjectSupported>
+ <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' == 'Core' ">netstandard1.3\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
+ <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net45\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly>
+ </PropertyGroup>
+
+ <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoToolsPlatform" />
+ <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoCompilerOutputs" />
+ <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoReadDependencies" />
+ <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoCompile" />
+
+ <PropertyGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' ">
+ <Protobuf_IntermediatePath Condition=" '$(Protobuf_IntermediatePath)' == '' ">$(IntermediateOutputPath)</Protobuf_IntermediatePath>
+ <Protobuf_OutputPath Condition=" '$(Protobuf_OutputPath)' == '' ">$(Protobuf_IntermediatePath)</Protobuf_OutputPath>
+ <Protobuf_DepFilesPath Condition=" '$(Protobuf_DepFilesPath)' == '' ">$(Protobuf_IntermediatePath)</Protobuf_DepFilesPath>
+ </PropertyGroup>
+
+ <ItemDefinitionGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+ <ProtoBuf>
+ <Access Condition="'%(ProtoBuf.Access)' == '' ">Public</Access>
+ <ProtoCompile Condition="'%(ProtoBuf.ProtoCompile)' == '' ">True</ProtoCompile>
+ <ProtoRoot Condition="'%(ProtoBuf.ProtoRoot)' == '' " />
+ <CompileOutputs Condition="'%(ProtoBuf.CompileOutputs)' == ''">True</CompileOutputs>
+ <OutputDir Condition="'%(ProtoBuf.OutputDir)' == '' ">$(Protobuf_OutputPath)</OutputDir>
+ </ProtoBuf>
+ </ItemDefinitionGroup>
+
+ <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' ">
+ <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Protobuf.CSharp.xml">
+ <Context>File;BrowseObject</Context>
+ </PropertyPageSchema>
+ <AvailableItemName Include="ProtoBuf" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <!-- NET SDK: by default, do not include proto files in the directory.
+ Current Microsoft's recommendation is against globbing:
+ https://docs.microsoft.com/en-us/dotnet/core/tools/csproj#recommendation -->
+ <EnableDefaultProtoBufItems Condition=" '$(EnableDefaultProtoBufItems)' == '' ">false</EnableDefaultProtoBufItems>
+ </PropertyGroup>
+
+ <!-- Check configuration sanity before build. -->
+ <Target Name="_Protobuf_SanityCheck" BeforeTargets="PrepareForBuild">
+ <Error
+ Condition=" '$(Protobuf_ProjectSupported)' != 'true' "
+ Text="Google.Protobuf.Tools proto compilation is only supported by default in a C# project (extension .csproj)" />
+ </Target>
+
+ <!--================================================================================
+ Tool path resolution
+ =================================================================================-->
+
+ <!-- Extension point for plugin packages: use Protobuf_ToolsOs and Protobuf_ToolsCpu
+ to resolve executable. Either or both may be blank, however, if resolution
+ fails; do check them before using. -->
+ <Target Name="Protobuf_ResolvePlatform">
+ <ProtoToolsPlatform>
+ <Output TaskParameter="Os" PropertyName="_Protobuf_ToolsOs"/>
+ <Output TaskParameter="Cpu" PropertyName="_Protobuf_ToolsCpu"/>
+ </ProtoToolsPlatform>
+
+ <PropertyGroup>
+ <!-- First try environment variable. -->
+ <Protobuf_ToolsOs>$(PROTOBUF_TOOLS_OS)</Protobuf_ToolsOs>
+ <Protobuf_ToolsCpu>$(PROTOBUF_TOOLS_CPU)</Protobuf_ToolsCpu>
+ <Protobuf_ProtocFullPath>$(PROTOBUF_PROTOC)</Protobuf_ProtocFullPath>
+
+ <!-- Next try OS and CPU resolved by ProtoToolsPlatform. -->
+ <Protobuf_ToolsOs Condition=" '$(Protobuf_ToolsOs)' == '' ">$(_Protobuf_ToolsOs)</Protobuf_ToolsOs>
+ <Protobuf_ToolsCpu Condition=" '$(Protobuf_ToolsCpu)' == '' ">$(_Protobuf_ToolsCpu)</Protobuf_ToolsCpu>
+ <!-- TODO(kkm): Do not package windows x64 builds (#13098). -->
+ <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' "
+ >$(Protobuf_PackagedToolsPath)\$(Protobuf_ToolsOs)_x86\protoc.exe</Protobuf_ProtocFullPath>
+ <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' "
+ >$(Protobuf_PackagedToolsPath)/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/protoc</Protobuf_ProtocFullPath>
+ </PropertyGroup>
+
+ <Error Condition=" '$(DesignTimeBuild)' != 'true' and '$(PROTOBUF_PROTOC)' == ''
+ and ( '$(Protobuf_ToolsOs)' == '' or '$(Protobuf_ToolsCpu)' == '' ) "
+ Text="Google.Protobuf.Tools cannot determine host OS and CPU.&#10;Use environment variables PROTOBUF_TOOLS_OS={linux|macosx|windows} and PROTOBUF_TOOLS_CPU={x86|x64} to try the closest match to your system.&#10;You may also set PROTOBUF_PROTOC to specify full path to the host-provided compiler (v3.5+ is required)." />
+ </Target>
+
+ <!--================================================================================
+ Proto compilation
+ =================================================================================-->
+
+ <!-- Extension points. -->
+ <Target Name="Protobuf_BeforeCompile" />
+ <Target Name="Protobuf_AfterCompile" />
+
+ <!-- Main compile sequence. Certain steps are gated by the value $(DesignTimeBuild),
+ so the sequence is good for either design time or build time. -->
+ <Target Name="Protobuf_Compile"
+ Condition=" '@(ProtoBuf)' != '' "
+ DependsOnTargets=" Protobuf_BeforeCompile;
+ Protobuf_ResolvePlatform;
+ _Protobuf_SelectFiles;
+ Protobuf_PrepareCompile;
+ _Protobuf_AugmentLanguageCompile;
+ _Protobuf_CoreCompile;
+ Protobuf_ReconcileOutputs;
+ Protobuf_AfterCompile" />
+
+ <!-- Do proto compilation by default in a C# project. In other types, the user invoke
+ Protobuf_Compile directly where required. -->
+ <!-- TODO(kkm): Do shared compile in outer multitarget project? -->
+ <Target Name="_Protobuf_Compile_BeforeCsCompile"
+ BeforeTargets="BeforeCompile"
+ DependsOnTargets="Protobuf_Compile"
+ Condition=" '$(Language)' == 'C#' " />
+
+ <Target Name="_Protobuf_SelectFiles">
+ <!-- Guess .proto root for the files. Whenever the root is set for a file explicitly,
+ leave it as is. Otherwise, for files under the project directory, set the root
+ to "." for the project's directory, as it is the current when compiling; for the
+ files outside of project directory, use each .proto file's directory as the root. -->
+ <FindUnderPath Path="$(MSBuildProjectDirectory)"
+ Files="@(ProtoBuf->WithMetadataValue('ProtoRoot',''))">
+ <Output TaskParameter="InPath" ItemName="_Protobuf_NoRootInProject"/>
+ <Output TaskParameter="OutOfPath" ItemName="_Protobuf_NoRootElsewhere"/>
+ </FindUnderPath>
+ <ItemGroup>
+ <!-- Files with explicit metadata. -->
+ <Protobuf_Compile Include="@(ProtoBuf->HasMetadata('ProtoRoot'))" />
+ <!-- In-project files will have ProtoRoot='.'. -->
+ <Protobuf_Compile Include="@(_Protobuf_NoRootInProject)">
+ <ProtoRoot>.</ProtoRoot>
+ </Protobuf_Compile>
+ <!-- Out-of-project files will have respective ProtoRoot='%(RelativeDir)'. -->
+ <Protobuf_Compile Include="@(_Protobuf_NoRootElsewhere)">
+ <ProtoRoot>%(RelativeDir)</ProtoRoot>
+ </Protobuf_Compile>
+ <!-- Remove files not for compile. -->
+ <Protobuf_Compile Remove="@(Protobuf_Compile)" Condition=" !%(ProtoCompile) " />
+ <!-- Ensure invariant Source=%(Identity). -->
+ <Protobuf_Compile>
+ <Source>%(Identity)</Source>
+ </Protobuf_Compile>
+ </ItemGroup>
+ </Target>
+
+ <!-- Extension point for non-C# project. Protobuf_Generator should be supported
+ by the ProtoCompile task, but we skip inferring expected outputs. All proto
+ files will be always recompiled with a warning, unless you add expectations
+ to the Protobuf_ExpectedOutputs collection.
+
+ All inferred ExpectedOutputs will be added to code compile (C#) in a C# project
+ by default. This is controlled per-proto by the CompileOutputs metadata. -->
+ <Target Name="Protobuf_PrepareCompile" Condition=" '@(Protobuf_Compile)' != '' ">
+ <!-- Predict expected names. -->
+ <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' "
+ ProtoBuf="@(Protobuf_Compile)"
+ Generator="$(Protobuf_Generator)">
+ <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" />
+ </ProtoCompilerOutputs>
+ <!-- Read any dependency files from previous compiles. -->
+ <ProtoReadDependencies Condition=" '$(Protobuf_DepFilesPath)' != '' and '$(DesignTimeBuild)' != 'true' "
+ ProtoBuf="@(Protobuf_Compile)"
+ ProtoDepDir="$(Protobuf_DepFilesPath)" >
+ <Output TaskParameter="Dependencies" ItemName="Protobuf_Dependencies" />
+ </ProtoReadDependencies>
+ </Target>
+
+ <!-- Add all expected outputs, and only these, to language compile. -->
+ <Target Name="_Protobuf_AugmentLanguageCompile"
+ DependsOnTargets="_Protobuf_EnforceInvariants"
+ Condition=" '$(Language)' == 'C#' ">
+ <ItemGroup>
+ <_Protobuf_CodeCompile Include="@(Protobuf_ExpectedOutputs->Distinct())"
+ Condition=" '%(Source)' != '' and '@(Protobuf_Compile->WithMetadataValue('CompileOutputs', 'true'))' != '' " />
+ <Compile Include="@(_Protobuf_CodeCompile)" />
+ </ItemGroup>
+ </Target>
+
+ <!-- These invariants must be kept for compile up-to-date check to work. -->
+ <Target Name="_Protobuf_EnforceInvariants">
+ <!-- Enforce Source=Identity on proto files. The 'Source' metadata is used as a common
+ key to match dependencies/expected outputs in the lists for up-to-date checks. -->
+ <ItemGroup>
+ <Protobuf_Compile>
+ <Source>%(Identity)</Source>
+ </Protobuf_Compile>
+ </ItemGroup>
+
+ <!-- Remove possible output and dependency declarations that have no Source set, or those
+ not matching any proto marked for compilation. -->
+ <ItemGroup>
+ <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Protobuf_ExpectedOutputs.Source)' == '' " />
+ <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " />
+ <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Protobuf_Dependencies.Source)' == '' " />
+ <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " />
+ </ItemGroup>
+ </Target>
+
+ <!-- Gather files with and without known outputs, separately. -->
+ <Target Name="_Protobuf_GatherStaleFiles"
+ Condition=" '@(Protobuf_Compile)' != '' "
+ DependsOnTargets="_Protobuf_EnforceInvariants; _Protobuf_GatherStaleSimple; _Protobuf_GatherStaleBatched">
+ <ItemGroup>
+ <!-- Drop outputs from MSBuild inference (they won't have the '_Exec' metadata). -->
+ <_Protobuf_OutOfDateProto Remove="@(_Protobuf_OutOfDateProto->WithMetadataValue('_Exec',''))" />
+ </ItemGroup>
+ </Target>
+
+ <Target Name="_Protobuf_GatherStaleSimple">
+ <!-- Simple selection: always compile files that have no declared outputs (but warn below). -->
+ <ItemGroup>
+ <_Protobuf_OutOfDateProto Include="@(Protobuf_Compile)"
+ Condition = " '%(Source)' != '' and '@(Protobuf_ExpectedOutputs)' == '' ">
+ <_Exec>true</_Exec>
+ </_Protobuf_OutOfDateProto>
+ </ItemGroup>
+
+ <!-- You are seeing this warning because there was no Protobuf_ExpectedOutputs items with
+ their Source attribute pointing to the proto files listed in the warning. Such files
+ will be recompiled on every build, as there is nothing to run up-to-date check against.
+ Set Protobuf_NoOrphanWarning to 'true' to suppress if this is what you want. -->
+ <Warning Condition=" '@(_Protobuf_OutOfDateProto)' != '' and '$(Protobuf_NoOrphanWarning)' != 'true' "
+ Text="The following files have no known outputs, and will be always recompiled as if out-of-date:&#10;@(_Protobuf_Orphans->'&#10; %(Identity)', '')" />
+ </Target>
+
+ <Target Name="_Protobuf_GatherStaleBatched"
+ Inputs="@(Protobuf_Compile);%(Source);@(Protobuf_Dependencies);$(MSBuildAllProjects)"
+ Outputs="@(Protobuf_ExpectedOutputs)" >
+ <!-- The '_Exec' metadatum is set to distinguish really executed items from those MSBuild so
+ "helpfully" infers in a bucketed task. For the same reason, cannot use the intrinsic
+ ItemGroup task here. -->
+ <CreateItem Include="@(Protobuf_Compile)" AdditionalMetadata="_Exec=true">
+ <Output TaskParameter="Include" ItemName="_Protobuf_OutOfDateProto"/>
+ </CreateItem>
+ </Target>
+
+ <!-- Extension point: Plugins massage metadata into recognized metadata
+ values passed to the ProtoCompile task. -->
+ <Target Name="Protobuf_PrepareCompileOptions" Condition=" '@(Protobuf_Compile)' != '' ">
+ <ItemGroup>
+ <Protobuf_Compile>
+ <_OutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._OutputOptions);internal_access</_OutputOptions>
+ </Protobuf_Compile>
+ </ItemGroup>
+ </Target>
+
+ <Target Name="_Protobuf_CoreCompile"
+ Condition=" '$(DesignTimeBuild)' != 'true' "
+ DependsOnTargets="Protobuf_PrepareCompileOptions;_Protobuf_GatherStaleFiles">
+ <!-- Ensure output directories. -->
+ <MakeDir Directories="%(_Protobuf_OutOfDateProto.OutputDir)" />
+ <MakeDir Directories="%(_Protobuf_OutOfDateProto.GrpcOutputDir)" />
+ <MakeDir Directories="$(Protobuf_DepFilesPath)" />
+
+ <!-- Force output to the current directory if the user has set it to empty. -->
+ <ItemGroup>
+ <_Protobuf_OutOfDateProto>
+ <OutputDir Condition=" '%(OutputDir)' == '' ">.</OutputDir>
+ </_Protobuf_OutOfDateProto>
+ </ItemGroup>
+
+ <ProtoCompile Condition=" '@(_Protobuf_OutOfDateProto)' != '' "
+ ToolExe="$(Protobuf_ProtocFullPath)"
+ Generator="$(Protobuf_Generator)"
+ ProtoBuf="%(_Protobuf_OutOfDateProto.Source)"
+ ProtoPath="%(_Protobuf_OutOfDateProto.AdditionalImportDirs);$(Protobuf_StandardImportsPath);%(_Protobuf_OutOfDateProto.ProtoRoot)"
+ ProtoDepDir="$(Protobuf_DepFilesPath)"
+ OutputDir="%(_Protobuf_OutOfDateProto.OutputDir)"
+ OutputOptions="%(_Protobuf_OutOfDateProto._OutputOptions)"
+ GrpcPluginExe="%(_Protobuf_OutOfDateProto.GrpcPluginExe)"
+ GrpcOutputDir="%(_Protobuf_OutOfDateProto.GrpcOutputDir)"
+ GrpcOutputOptions="%(_Protobuf_OutOfDateProto._GrpcOutputOptions)"
+ >
+ <Output TaskParameter="GeneratedFiles" ItemName="_Protobuf_GeneratedFiles"/>
+ </ProtoCompile>
+
+ <!-- Compute files expected but not in fact produced by protoc. -->
+ <ItemGroup Condition=" '@(_Protobuf_OutOfDateProto)' != '' ">
+ <Protobuf_ExpectedNotGenerated Include="@(Protobuf_ExpectedOutputs)"
+ Condition=" '%(Source)' != '' and '@(_Protobuf_OutOfDateProto)' != '' " />
+ <Protobuf_ExpectedNotGenerated Remove="@(_Protobuf_GeneratedFiles)" />
+ </ItemGroup>
+ </Target>
+
+ <!-- Extension point. Plugins and/or unsupported projects may take special care of the
+ Protobuf_ExpectedNotGenerated list in BeforeTargets. We just silently create the
+ missing outputs so that out-of-date checks work (we do not add them to language
+ compile though). You can empty this collection in your Before targets to do nothing.
+ The target is not executed if the proto compiler is not executed. -->
+ <Target Name="Protobuf_ReconcileOutputs"
+ Condition=" '$(DesignTimeBuild)' != 'true' ">
+ <!-- Warn about unexpected/missing files outside object file directory only.
+ This should have happened because build was incorrectly customized. -->
+ <FindUnderPath Path="$(BaseIntermediateOutputPath)" Files="@(Protobuf_ExpectedNotGenerated)">
+ <Output TaskParameter="InPath" ItemName="_Protobuf_ExpectedNotGeneratedInTemp"/>
+ <Output TaskParameter="OutOfPath" ItemName="_Protobuf_ExpectedNotGeneratedElsewhere"/>
+ </FindUnderPath>
+
+ <!-- Prevent unnecessary recompilation by silently creating empty files. This probably
+ has happened because a proto file with an rpc service was processed by the gRPC
+ plugin, and the user did not set GrpcOutput to None. When we treat outputs as
+ transient, we can do it permissively. -->
+ <Touch Files="@(_Protobuf_ExpectedNotGeneratedInTemp)" AlwaysCreate="true" />
+
+ <!-- Also create empty files outside of the intermediate directory, if the user wants so. -->
+ <Touch Files="@(_Protobuf_ExpectedNotGeneratedElsewhere)" AlwaysCreate="true"
+ Condition=" '$(Protobuf_TouchMissingExpected)' == 'true' "/>
+
+ <!-- You are seeing this warning because there were some Protobuf_ExpectedOutputs items
+ (outside of the transient directory under obj/) not in fact produced by protoc. -->
+ <Warning Condition=" '@(_Protobuf_ExpectedNotGeneratedElsewhere)' != '' and $(Protobuf_NoWarnMissingExpected) != 'true' "
+ Text="Some expected protoc outputs were not generated.&#10;@(_Protobuf_ExpectedNotGeneratedElsewhere->'&#10; %(Identity)', '')" />
+ </Target>
+
+ <!--================================================================================
+ Proto cleanup
+ =================================================================================-->
+
+ <!-- We fully support cleanup only in a C# project. If extending the build for other
+ generators/plugins, then mostly roll your own. -->
+
+ <!-- Extension points. -->
+ <Target Name="Protobuf_BeforeClean" />
+ <Target Name="Protobuf_AfterClean" />
+
+ <!-- Main cleanup sequence. -->
+ <Target Name="Protobuf_Clean"
+ Condition=" '@(ProtoBuf)' != '' "
+ DependsOnTargets=" Protobuf_BeforeClean;
+ Protobuf_PrepareClean;
+ _Protobuf_CoreClean;
+ Protobuf_AfterClean" />
+
+ <!-- Do proto cleanup by default in a C# project. In other types, the user should
+ invoke Protobuf_Clean directly if required. -->
+ <Target Name="_Protobuf_Clean_AfterCsClean"
+ AfterTargets="CoreClean"
+ DependsOnTargets="Protobuf_Clean"
+ Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' " />
+
+ <!-- Extension point for non-C# project. ProtoCompilerOutputs is not invoked for
+ non-C# projects, since inferring protoc outputs is required, so this is a
+ no-op in other project types. In your extension target populate the
+ Protobuf_ExpectedOutputs with all possible output. An option is to include
+ all existing outputs using Include with a wildcard, if you know where to look.
+
+ Note this is like Protobuf_PrepareCompile, but uses @(Protobuf) regardless
+ of the Compile metadata, to remove all possible outputs. Plugins should err
+ on the side of overextending the Protobuf_ExpectedOutputs here.
+
+ All ExpectedOutputs will be removed. -->
+ <Target Name="Protobuf_PrepareClean" Condition=" '@(Protobuf)' != '' ">
+ <!-- Predict expected names. -->
+ <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' "
+ ProtoBuf="@(Protobuf)"
+ Generator="$(Protobuf_Generator)">
+ <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" />
+ </ProtoCompilerOutputs>
+ </Target>
+
+ <Target Name="_Protobuf_CoreClean">
+ <ItemGroup>
+ <_Protobuf_Protodep Include="$(Protobuf_DepFilesPath)*.protodep" />
+ </ItemGroup>
+ <Delete Files="@(Protobuf_ExpectedOutputs);@(_Protobuf_Protodep)" TreatErrorsAsWarnings="true" />
+ </Target>
+
+ <!--================================================================================
+ Design-time support
+ =================================================================================-->
+
+ <!-- Add all .proto files to the SourceFilesProjectOutputGroupOutput, so that:
+ * Visual Studio triggers a build when any of them changed;
+ * The Pack target includes .proto files into the source package. -->
+ <Target Name="_Protobuf_SourceFilesProjectOutputGroup"
+ BeforeTargets="SourceFilesProjectOutputGroup"
+ Condition=" '@(ProtoBuf)' != '' " >
+ <ItemGroup>
+ <SourceFilesProjectOutputGroupOutput Include="@(ProtoBuf->'%(FullPath)')" />
+ </ItemGroup>
+ </Target>
+</Project>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml
new file mode 100644
index 0000000000..2c41fbcbd0
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml
@@ -0,0 +1,99 @@
+<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
+ <FileExtension Name=".proto"
+ ContentType="ProtoFile" />
+
+ <ContentType Name="ProtoFile"
+ DisplayName="Protocol buffer definitions file"
+ ItemType="ProtoBuf" />
+
+ <ItemType Name="ProtoBuf"
+ DisplayName="Protobuf compiler" />
+
+ <Rule Name="ProtoBuf"
+ DisplayName="File Properties"
+ PageTemplate="generic"
+ Description="File Properties"
+ OverrideMode="Extend">
+ <Rule.DataSource>
+ <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf"
+ HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" />
+ </Rule.DataSource>
+
+ <Rule.Categories>
+ <Category Name="Advanced" DisplayName="Advanced" />
+ <Category Name="Protobuf" DisplayName="Protobuf" />
+ <Category Name="Misc" DisplayName="Misc" />
+ </Rule.Categories>
+
+ <DynamicEnumProperty Name="{}{ItemType}" DisplayName="Build Action" Category="Advanced"
+ Description="How the file relates to the build and deployment processes."
+ EnumProvider="ItemTypes" />
+
+ <StringProperty Name="Identity" Visible="false" ReadOnly="true">
+ <StringProperty.DataSource>
+ <DataSource Persistence="Intrinsic" ItemType="ProtoBuf"
+ PersistedName="Identity" SourceOfDefaultValue="AfterContext" />
+ </StringProperty.DataSource>
+ </StringProperty>
+
+ <StringProperty Name="FullPath"
+ DisplayName="Full Path"
+ ReadOnly="true"
+ Category="Misc"
+ Description="Location of the file.">
+ <StringProperty.DataSource>
+ <DataSource Persistence="Intrinsic" ItemType="ProtoBuf"
+ PersistedName="FullPath" SourceOfDefaultValue="AfterContext" />
+ </StringProperty.DataSource>
+ </StringProperty>
+
+ <StringProperty Name="FileNameAndExtension"
+ DisplayName="File Name"
+ ReadOnly="true"
+ Category="Misc"
+ Description="Name of the file or folder.">
+ <StringProperty.DataSource>
+ <DataSource Persistence="Intrinsic" ItemType="ProtoBuf"
+ PersistedName="FileNameAndExtension" SourceOfDefaultValue="AfterContext" />
+ </StringProperty.DataSource>
+ </StringProperty>
+
+ <BoolProperty Name="Visible" Visible="false" Default="true" />
+
+ <StringProperty Name="DependentUpon" Visible="false">
+ <StringProperty.Metadata>
+ <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" />
+ </StringProperty.Metadata>
+ </StringProperty>
+
+ <StringProperty Name="Link" Visible="false">
+ <StringProperty.DataSource>
+ <DataSource SourceOfDefaultValue="AfterContext" />
+ </StringProperty.DataSource>
+ <StringProperty.Metadata>
+ <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" />
+ </StringProperty.Metadata>
+ </StringProperty>
+
+ <EnumProperty Name="Access" DisplayName="Class Access"
+ Category="Protobuf"
+ Description="Public or internal access modifier on generated classes.">
+ <EnumValue Name="Public" DisplayName="Public" IsDefault="true" />
+ <EnumValue Name="Internal" DisplayName="Internal" />
+ <EnumProperty.DataSource>
+ <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext"
+ PersistenceStyle="Attribute" />
+ </EnumProperty.DataSource>
+ </EnumProperty>
+
+ <BoolProperty Name="ProtoCompile" DisplayName="Compile Protobuf"
+ Category="Protobuf" Default="true"
+ Description="Specifies if this file is compiled or only imported by other files.">
+ <BoolProperty.DataSource>
+ <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext"
+ PersistenceStyle="Attribute" />
+ </BoolProperty.DataSource>
+ </BoolProperty>
+
+ </Rule>
+</ProjectSchemaDefinitions>
diff --git a/src/csharp/Grpc.Tools/build/_protobuf/README b/src/csharp/Grpc.Tools/build/_protobuf/README
new file mode 100644
index 0000000000..e6e358a218
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/_protobuf/README
@@ -0,0 +1 @@
+TODO(kkm): These file will go into Google.Protobuf.Tools/build after package split.
diff --git a/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props
new file mode 100644
index 0000000000..f83c4d135a
--- /dev/null
+++ b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
+
+ <!-- Revision number of this package conventions (as if "API" version). -->
+ <Protobuf_ToolingRevision>1</Protobuf_ToolingRevision>
+
+ <!-- For a Visual Studio C++ native project we currently only resolve tools and import paths. -->
+ <!-- TODO(kkm): Do not place non-tools under tools/, use build/native/bin/. -->
+ <!-- TODO(kkm): Do not package windows x64 builds (#13098). -->
+ <Protobuf_ProtocFullPath>$(MSBuildThisFileDirectory)..\..\tools\windows_x86\protoc.exe</Protobuf_ProtocFullPath>
+ <Protobuf_StandardImportsPath>$(MSBuildThisFileDirectory)include\</Protobuf_StandardImportsPath>
+ <gRPC_PluginFileName>grpc_cpp_plugin</gRPC_PluginFileName>
+ <gRPC_PluginFullPath>$(MSBuildThisFileDirectory)..\..\tools\windows_x86\grpc_cpp_plugin.exe</gRPC_PluginFullPath>
+ </PropertyGroup>
+</Project>
diff --git a/src/csharp/Grpc.sln b/src/csharp/Grpc.sln
index d9a7b8d556..6c1b2e9998 100644
--- a/src/csharp/Grpc.sln
+++ b/src/csharp/Grpc.sln
@@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Reflection.Tests", "Gr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Microbenchmarks", "Grpc.Microbenchmarks\Grpc.Microbenchmarks.csproj", "{84C17746-4727-4290-8E8B-A380793DAE1E}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Tools", "Grpc.Tools\Grpc.Tools.csproj", "{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Tools.Tests", "Grpc.Tools.Tests\Grpc.Tools.Tests.csproj", "{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -117,6 +121,14 @@ Global
{84C17746-4727-4290-8E8B-A380793DAE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84C17746-4727-4290-8E8B-A380793DAE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84C17746-4727-4290-8E8B-A380793DAE1E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/csharp/README.md b/src/csharp/README.md
index d8aed94988..9a91035d06 100644
--- a/src/csharp/README.md
+++ b/src/csharp/README.md
@@ -87,6 +87,7 @@ $ python tools/run_tests/run_tests.py -l csharp -c dbg
DOCUMENTATION
-------------
+- [.NET Build Integration](BUILD-INTEGRATION.md)
- [API Reference][]
- [Helloworld Example][]
- [RouteGuide Tutorial][]
diff --git a/src/csharp/build_packages_dotnetcli.bat b/src/csharp/build_packages_dotnetcli.bat
index 8f38f0aa20..27688360e9 100755
--- a/src/csharp/build_packages_dotnetcli.bat
+++ b/src/csharp/build_packages_dotnetcli.bat
@@ -13,7 +13,7 @@
@rem limitations under the License.
@rem Current package versions
-set VERSION=1.15.0-dev
+set VERSION=1.17.0-dev
@rem Adjust the location of nuget.exe
set NUGET=C:\nuget\nuget.exe
@@ -39,10 +39,10 @@ xcopy /Y /I nativelibs\csharp_ext_windows_x64\grpc_csharp_ext.dll ..\..\cmake\bu
%DOTNET% pack --configuration Release Grpc.Auth --output ..\..\..\artifacts || goto :error
%DOTNET% pack --configuration Release Grpc.HealthCheck --output ..\..\..\artifacts || goto :error
%DOTNET% pack --configuration Release Grpc.Reflection --output ..\..\..\artifacts || goto :error
+%DOTNET% pack --configuration Release Grpc.Tools --output ..\..\..\artifacts || goto :error
%NUGET% pack Grpc.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts || goto :error
%NUGET% pack Grpc.Core.NativeDebug.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts
-%NUGET% pack Grpc.Tools.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts
@rem copy resulting nuget packages to artifacts directory
xcopy /Y /I *.nupkg ..\..\artifacts\ || goto :error
diff --git a/src/csharp/build_unitypackage.bat b/src/csharp/build_unitypackage.bat
index 9c53114b84..dd74de0491 100644
--- a/src/csharp/build_unitypackage.bat
+++ b/src/csharp/build_unitypackage.bat
@@ -13,7 +13,7 @@
@rem limitations under the License.
@rem Current package versions
-set VERSION=1.15.0-dev
+set VERSION=1.17.0-dev
@rem Adjust the location of nuget.exe
set NUGET=C:\nuget\nuget.exe
diff --git a/src/csharp/doc/README.md b/src/csharp/doc/README.md
index 46cce013a1..427f457eb6 100644
--- a/src/csharp/doc/README.md
+++ b/src/csharp/doc/README.md
@@ -1,9 +1,28 @@
DocFX-generated C# API Reference
--------------------------------
+## Generating docs manually (on Windows)
+
Install docfx based on instructions here: https://github.com/dotnet/docfx
```
# generate docfx documentation into ./html directory
$ docfx
```
+
+## Release process: script for regenerating the docs automatically
+
+After each gRPC C# release, the docs need to be regenerated
+and updated on the grpc.io site. The automated script will
+re-generate the docs (using dockerized docfx installation)
+and make everything ready for creating a PR to update the docs.
+
+```
+# 1. Run the script on Linux with docker installed
+$ ./generate_reference_docs.sh
+
+# 2. Enter the git repo with updated "gh-pages" branch
+$ cd grpc-gh-pages
+
+# 3. Review the changes and create a pull request
+```
diff --git a/src/csharp/doc/generate_reference_docs.sh b/src/csharp/doc/generate_reference_docs.sh
new file mode 100755
index 0000000000..c20d6c30bd
--- /dev/null
+++ b/src/csharp/doc/generate_reference_docs.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+# Copyright 2018 The gRPC 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.
+
+# Generates C# API docs using docfx inside docker.
+set -ex
+cd $(dirname $0)
+
+# cleanup temporary files
+rm -rf html obj grpc-gh-pages
+
+# generate into src/csharp/doc/html directory
+cd ..
+docker run --rm -v "$(pwd)":/work -w /work/doc --user "$(id -u):$(id -g)" -it tsgkadot/docker-docfx:latest docfx
+cd doc
+
+# prepare a clone of "gh-pages" branch where the generated docs are stored
+GITHUB_USER="${USER}"
+git clone -b gh-pages -o upstream git@github.com:grpc/grpc.git grpc-gh-pages
+cd grpc-gh-pages
+git remote add origin "git@github.com:${GITHUB_USER}/grpc.git"
+
+# replace old generated docs by the ones we just generated
+rm -r csharp
+cp -r ../html csharp
+
+echo "Done. Go to src/csharp/doc/grpc-gh-pages git repository and create a pull request to update the generated docs."
diff --git a/src/csharp/doc/integration.md-fig.1-classic.png b/src/csharp/doc/integration.md-fig.1-classic.png
new file mode 100644
index 0000000000..80c57261ad
--- /dev/null
+++ b/src/csharp/doc/integration.md-fig.1-classic.png
Binary files differ
diff --git a/src/csharp/doc/integration.md-fig.2-sdk.png b/src/csharp/doc/integration.md-fig.2-sdk.png
new file mode 100644
index 0000000000..6d653e4a58
--- /dev/null
+++ b/src/csharp/doc/integration.md-fig.2-sdk.png
Binary files differ
diff --git a/src/csharp/experimental/README.md b/src/csharp/experimental/README.md
index bd53cbcd35..64515075ce 100644
--- a/src/csharp/experimental/README.md
+++ b/src/csharp/experimental/README.md
@@ -22,10 +22,14 @@ gRPC C# now has experimental support for Unity. Please try using gRPC with
Unity and provide feedback!
How to test gRPC in a Unity project
+
1. Create a Unity project that targets .NET 4.x (Edit -> Project Settings -> Editor -> Scripting Runtime Version). gRPC uses APIs that are only available in .NET4.5+ so this is a requirement.
+
2. Download the latest development build of `grpc_unity_package.VERSION.zip` from
[daily builds](https://packages.grpc.io/)
+
3. Extract the `.zip` file in the `Assets` directory in your Unity project
+
4. Unity IDE will pick up all the bundled files and add them to project automatically.
You should be able to use gRPC and Protobuf in your scripts from now on.
diff --git a/src/csharp/ext/grpc_csharp_ext.c b/src/csharp/ext/grpc_csharp_ext.c
index 87a2516f8d..ed002ae1ff 100644
--- a/src/csharp/ext/grpc_csharp_ext.c
+++ b/src/csharp/ext/grpc_csharp_ext.c
@@ -964,7 +964,7 @@ GPR_EXPORT grpc_server_credentials* GPR_CALLTYPE
grpcsharp_ssl_server_credentials_create(
const char* pem_root_certs, const char** key_cert_pair_cert_chain_array,
const char** key_cert_pair_private_key_array, size_t num_key_cert_pairs,
- int force_client_auth) {
+ grpc_ssl_client_certificate_request_type client_request_type) {
size_t i;
grpc_server_credentials* creds;
grpc_ssl_pem_key_cert_pair* key_cert_pairs =
@@ -979,12 +979,9 @@ grpcsharp_ssl_server_credentials_create(
key_cert_pairs[i].private_key = key_cert_pair_private_key_array[i];
}
}
- creds = grpc_ssl_server_credentials_create_ex(
- pem_root_certs, key_cert_pairs, num_key_cert_pairs,
- force_client_auth
- ? GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY
- : GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE,
- NULL);
+ creds = grpc_ssl_server_credentials_create_ex(pem_root_certs, key_cert_pairs,
+ num_key_cert_pairs,
+ client_request_type, NULL);
gpr_free(key_cert_pairs);
return creds;
}
diff --git a/src/csharp/tests.json b/src/csharp/tests.json
index c2f243fe0a..760776f9e7 100644
--- a/src/csharp/tests.json
+++ b/src/csharp/tests.json
@@ -23,8 +23,10 @@
"Grpc.Core.Tests.ClientServerTest",
"Grpc.Core.Tests.CompressionTest",
"Grpc.Core.Tests.ContextPropagationTest",
+ "Grpc.Core.Tests.ContextualMarshallerTest",
"Grpc.Core.Tests.GrpcEnvironmentTest",
"Grpc.Core.Tests.HalfcloseTest",
+ "Grpc.Core.Tests.MarshallerTest",
"Grpc.Core.Tests.MarshallingErrorsTest",
"Grpc.Core.Tests.MetadataTest",
"Grpc.Core.Tests.PerformanceTest",
@@ -62,5 +64,15 @@
"Grpc.Reflection.Tests": [
"Grpc.Reflection.Tests.ReflectionClientServerTest",
"Grpc.Reflection.Tests.SymbolRegistryTest"
+ ],
+ "Grpc.Tools.Tests": [
+ "Grpc.Tools.Tests.CppGeneratorTest",
+ "Grpc.Tools.Tests.CSharpGeneratorTest",
+ "Grpc.Tools.Tests.DepFileUtilTest",
+ "Grpc.Tools.Tests.GeneratorTest",
+ "Grpc.Tools.Tests.ProtoCompileBasicTest",
+ "Grpc.Tools.Tests.ProtoCompileCommandLineGeneratorTest",
+ "Grpc.Tools.Tests.ProtoCompileCommandLinePrinterTest",
+ "Grpc.Tools.Tests.ProtoToolsPlatformTaskTest"
]
}