diff --git a/.gitignore b/.gitignore index 3e759b75bf4..4d0f3d5af45 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.user *.userosscache *.sln.docstates +.vscode/solution-explorer # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/.vscode/launch.json b/.vscode/launch.json index ddbfc5fa664..571c85e3511 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,20 @@ "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart" }, + { + "name": "Jaeger Exporter Tests (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/test/OpenTelemetry.Exporter.Jaeger.Tests/bin/Debug/netcoreapp2.2/OpenTelemetry.Exporter.Jaeger.Tests.dll", + "args": [], + "cwd": "${workspaceFolder}/test/OpenTelemetry.Exporter.Jaeger.Tests", + // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window + "console": "internalConsole", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" + }, { "name": ".NET Core Attach", "type": "coreclr", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a914fef1305..2d4663d57e2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,12 +5,16 @@ "tasks": [ { "label": "build", - "command": "dotnet build", + "command": "dotnet", "type": "shell", "group": "build", "presentation": { "reveal": "silent" }, + "args": [ + "build", + "/property:GenerateFullPaths=true" + ], "problemMatcher": "$msCompile" } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcdd85aa11..89157c48a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ the release. 1. Modified the content of PrometheusExporterOptions from `Uri()` to `string`. 2. `HttpListener()` can support "+" as: hostname which listens on all ports. 3. Modified samples/TestPrometheus.cs to safely use the new implementation. + 4. Jaeger exporter implemented - Copy from [OpenCensus](http://github.com/census-instrumentation/opencensus-csharp) at diff --git a/Directory.Build.targets b/Directory.Build.targets index cff5023aa19..96d0176b091 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,6 +1,6 @@ - + All diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index b76e95f5904..ab2ac06e63f 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -80,6 +80,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggingTracer.Demo.ConsoleA EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggingTracer.Demo.AspNetCore", "samples\LoggingTracer\LoggingTracer.Demo.AspNetCore\LoggingTracer.Demo.AspNetCore.csproj", "{1EB74FCE-55C5-476A-8BB0-C46B77FE1070}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Exporter.Jaeger", "src\OpenTelemetry.Exporter.Jaeger\OpenTelemetry.Exporter.Jaeger.csproj", "{8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Exporter.Jaeger.Tests", "test\OpenTelemetry.Exporter.Jaeger.Tests\OpenTelemetry.Exporter.Jaeger.Tests.csproj", "{21E69213-72D5-453F-BD00-75EF36AC4965}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -170,12 +174,19 @@ Global {1EB74FCE-55C5-476A-8BB0-C46B77FE1070}.Debug|Any CPU.Build.0 = Debug|Any CPU {1EB74FCE-55C5-476A-8BB0-C46B77FE1070}.Release|Any CPU.ActiveCfg = Release|Any CPU {1EB74FCE-55C5-476A-8BB0-C46B77FE1070}.Release|Any CPU.Build.0 = Release|Any CPU + {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D47E3CF-9AE3-42FE-9084-FEB72D9AD769}.Release|Any CPU.Build.0 = Release|Any CPU + {21E69213-72D5-453F-BD00-75EF36AC4965}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21E69213-72D5-453F-BD00-75EF36AC4965}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21E69213-72D5-453F-BD00-75EF36AC4965}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21E69213-72D5-453F-BD00-75EF36AC4965}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} = {61188153-47FB-4567-AC9B-79B2435853EB} {F2F81E76-6A0E-466B-B673-EBBF1A9ED075} = {77C7929A-2EED-4AA6-8705-B5C443C8AA0F} {6EC9DEC9-086F-4F29-BF3F-5FC7253829D5} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} {D9C14CDA-5182-4DEA-8606-98FF1A388BD3} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} diff --git a/README.md b/README.md index bdf4fc21b85..13f3ad55a26 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,42 @@ Outgoing http calls to Redis made usign StackExchange.Redis library can be autom ## OpenTelemetry QuickStart: exporting data +### Using the Jaeger exporter + +The Jaeger exporter communicates to a Jaeger Agent through the compact thrift protocol on +the Compact Thrift API port. You can configure the Jaeger exporter by following the directions below: + +1. [Get Jaeger][jaeger-get-started]. +2. Configure the `JaegerExporter` + - `ServiceName`: The name of your application or service. + - `AgengHost`: Usually `localhost` since an agent should + usually be running on the same machine as your application or service. + - `AgentPort`: The compact thrift protocol port of the Jaeger Agent (default `6831`) + - `MaxPacketSize`: The maximum size of each UDP packet that gets sent to the agent. (default `65000`) +3. See the [sample][jaeger-sample] for an example of how to use the exporter. + +``` csharp +var exporter = new JaegerExporter( + new JaegerExporterOptions + { + ServiceName = "tracing-to-jaeger-service", + AgentHost = host, + AgentPort = port, + }, + Tracing.ExportComponent); + +exporter.Start(); + +var span = tracer + .SpanBuilder("incoming request") + .SetSampler(Samplers.AlwaysSample) + .StartSpan(); + +Thread.Sleep(TimeSpan.FromSeconds(1)); +span.End(); +``` + + ### Using Zipkin exporter Configure Zipkin exporter to see traces in Zipkin UI. @@ -316,6 +352,7 @@ deprecate it for 18 months before removing it, if possible. [up-for-grabs-issues]: https://github.com/open-telemetry/OpenTelemetry-dotnet/issues?q=is%3Aissue+is%3Aopen+label%3Aup-for-grabs [good-first-issues]: https://github.com/open-telemetry/OpenTelemetry-dotnet/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 [zipkin-get-started]: https://zipkin.io/pages/quickstart.html +[jaeger-get-started]: https://www.jaegertracing.io/docs/1.13/getting-started/ [ai-get-started]: https://docs.microsoft.com/azure/application-insights [stackdriver-trace-setup]: https://cloud.google.com/trace/docs/setup/ [stackdriver-monitoring-setup]: https://cloud.google.com/monitoring/api/enable-api @@ -326,6 +363,7 @@ deprecate it for 18 months before removing it, if possible. [ai-sample]: https://github.com/open-telemetry/opentelemetry-dotnet/blob/master/samples/Exporters/TestApplicationInsights.cs [stackdriver-sample]: https://github.com/open-telemetry/opentelemetry-dotnet/blob/master/samples/Exporters/TestStackdriver.cs [zipkin-sample]: https://github.com/open-telemetry/opentelemetry-dotnet/blob/master/samples/Exporters/TestZipkin.cs +[jaeger-sample]: https://github.com/open-telemetry/opentelemetry-dotnet/blob/master/samples/Exporters/TestJaeger.cs [prometheus-get-started]: https://prometheus.io/docs/introduction/first_steps/ [prometheus-sample]: https://github.com/open-telemetry/opentelemetry-dotnet/blob/master/samples/Exporters/TestPrometheus.cs diff --git a/lib/Directory.Build.props b/lib/Directory.Build.props new file mode 100644 index 00000000000..e3fa4661306 --- /dev/null +++ b/lib/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + True + + \ No newline at end of file diff --git a/lib/Directory.Build.targets b/lib/Directory.Build.targets new file mode 100644 index 00000000000..347a97078b1 --- /dev/null +++ b/lib/Directory.Build.targets @@ -0,0 +1,10 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers + + + \ No newline at end of file diff --git a/lib/Thrift/Collections/TCollections.cs b/lib/Thrift/Collections/TCollections.cs new file mode 100644 index 00000000000..147bfc7d31c --- /dev/null +++ b/lib/Thrift/Collections/TCollections.cs @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Collections; + +namespace Thrift.Collections +{ + // ReSharper disable once InconsistentNaming + public class TCollections + { + /// + /// This will return true if the two collections are value-wise the same. + /// If the collection contains a collection, the collections will be compared using this method. + /// + public static bool Equals(IEnumerable first, IEnumerable second) + { + if (first == null && second == null) + { + return true; + } + + if (first == null || second == null) + { + return false; + } + + var fiter = first.GetEnumerator(); + var siter = second.GetEnumerator(); + + var fnext = fiter.MoveNext(); + var snext = siter.MoveNext(); + + while (fnext && snext) + { + var fenum = fiter.Current as IEnumerable; + var senum = siter.Current as IEnumerable; + + if (fenum != null && senum != null) + { + if (!Equals(fenum, senum)) + { + return false; + } + } + else if (fenum == null ^ senum == null) + { + return false; + } + else if (!Equals(fiter.Current, siter.Current)) + { + return false; + } + + fnext = fiter.MoveNext(); + snext = siter.MoveNext(); + } + + return fnext == snext; + } + + /// + /// This returns a hashcode based on the value of the enumerable. + /// + public static int GetHashCode(IEnumerable enumerable) + { + if (enumerable == null) + { + return 0; + } + + var hashcode = 0; + + foreach (var obj in enumerable) + { + var enum2 = obj as IEnumerable; + var objHash = enum2 == null ? obj.GetHashCode() : GetHashCode(enum2); + + unchecked + { + hashcode = (hashcode*397) ^ (objHash); + } + } + + return hashcode; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Collections/THashSet.cs b/lib/Thrift/Collections/THashSet.cs new file mode 100644 index 00000000000..011f0a0d629 --- /dev/null +++ b/lib/Thrift/Collections/THashSet.cs @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Collections; +using System.Collections.Generic; + +namespace Thrift.Collections +{ + // ReSharper disable once InconsistentNaming + public class THashSet : ICollection + { + private readonly HashSet _set = new HashSet(); + + public int Count => _set.Count; + + public bool IsReadOnly => false; + + public void Add(T item) + { + _set.Add(item); + } + + public void Clear() + { + _set.Clear(); + } + + public bool Contains(T item) + { + return _set.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _set.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _set.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) _set).GetEnumerator(); + } + + public bool Remove(T item) + { + return _set.Remove(item); + } + } +} \ No newline at end of file diff --git a/lib/Thrift/ITAsyncProcessor.cs b/lib/Thrift/ITAsyncProcessor.cs new file mode 100644 index 00000000000..db8e40aefe8 --- /dev/null +++ b/lib/Thrift/ITAsyncProcessor.cs @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols; + +namespace Thrift +{ + public interface ITAsyncProcessor + { + Task ProcessAsync(TProtocol iprot, TProtocol oprot); + Task ProcessAsync(TProtocol iprot, TProtocol oprot, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/lib/Thrift/ITProcessorFactory.cs b/lib/Thrift/ITProcessorFactory.cs new file mode 100644 index 00000000000..5133e5c4801 --- /dev/null +++ b/lib/Thrift/ITProcessorFactory.cs @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using Thrift.Server; +using Thrift.Transports; + +namespace Thrift +{ + // ReSharper disable once InconsistentNaming + public interface ITProcessorFactory + { + ITAsyncProcessor GetAsyncProcessor(TClientTransport trans, TBaseServer baseServer = null); + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TField.cs b/lib/Thrift/Protocols/Entities/TField.cs new file mode 100644 index 00000000000..d311535e73c --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TField.cs @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public struct TField + { + public TField(string name, TType type, short id) + { + Name = name; + Type = type; + ID = id; + } + + public string Name { get; set; } + + public TType Type { get; set; } + + // ReSharper disable once InconsistentNaming - do not rename - it used for generation + public short ID { get; set; } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TList.cs b/lib/Thrift/Protocols/Entities/TList.cs new file mode 100644 index 00000000000..ce232207cba --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TList.cs @@ -0,0 +1,33 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public struct TList + { + public TList(TType elementType, int count) + { + ElementType = elementType; + Count = count; + } + + public TType ElementType { get; set; } + + public int Count { get; set; } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TMap.cs b/lib/Thrift/Protocols/Entities/TMap.cs new file mode 100644 index 00000000000..9195593db27 --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TMap.cs @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public struct TMap + { + public TMap(TType keyType, TType valueType, int count) + { + KeyType = keyType; + ValueType = valueType; + Count = count; + } + + public TType KeyType { get; set; } + + public TType ValueType { get; set; } + + public int Count { get; set; } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TMessage.cs b/lib/Thrift/Protocols/Entities/TMessage.cs new file mode 100644 index 00000000000..17f49298f77 --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TMessage.cs @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public struct TMessage + { + public TMessage(string name, TMessageType type, int seqid) + { + Name = name; + Type = type; + SeqID = seqid; + } + + public string Name { get; set; } + + public TMessageType Type { get; set; } + + // ReSharper disable once InconsistentNaming - do not rename - it used for generation + public int SeqID { get; set; } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TMessageType.cs b/lib/Thrift/Protocols/Entities/TMessageType.cs new file mode 100644 index 00000000000..d7b9a227595 --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TMessageType.cs @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public enum TMessageType + { + Call = 1, + Reply = 2, + Exception = 3, + Oneway = 4 + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TSet.cs b/lib/Thrift/Protocols/Entities/TSet.cs new file mode 100644 index 00000000000..a583b54a681 --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TSet.cs @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public struct TSet + { + public TSet(TType elementType, int count) + { + ElementType = elementType; + Count = count; + } + + public TSet(TList list) + : this(list.ElementType, list.Count) + { + } + + public TType ElementType { get; set; } + + public int Count { get; set; } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TStruct.cs b/lib/Thrift/Protocols/Entities/TStruct.cs new file mode 100644 index 00000000000..a28dcc3d001 --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TStruct.cs @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public struct TStruct + { + public TStruct(string name) + { + Name = name; + } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Entities/TType.cs b/lib/Thrift/Protocols/Entities/TType.cs new file mode 100644 index 00000000000..ebe781c9547 --- /dev/null +++ b/lib/Thrift/Protocols/Entities/TType.cs @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Entities +{ + // ReSharper disable once InconsistentNaming + public enum TType : byte + { + Stop = 0, + Void = 1, + Bool = 2, + Byte = 3, + Double = 4, + I16 = 6, + I32 = 8, + I64 = 10, + String = 11, + Struct = 12, + Map = 13, + Set = 14, + List = 15 + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/ITProtocolFactory.cs b/lib/Thrift/Protocols/ITProtocolFactory.cs new file mode 100644 index 00000000000..ecc5cc494d4 --- /dev/null +++ b/lib/Thrift/Protocols/ITProtocolFactory.cs @@ -0,0 +1,27 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using Thrift.Transports; + +namespace Thrift.Protocols +{ + // ReSharper disable once InconsistentNaming + public interface ITProtocolFactory + { + TProtocol GetProtocol(TClientTransport trans); + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/TAbstractBase.cs b/lib/Thrift/Protocols/TAbstractBase.cs new file mode 100644 index 00000000000..4e18681bfb9 --- /dev/null +++ b/lib/Thrift/Protocols/TAbstractBase.cs @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Protocols +{ + // ReSharper disable once InconsistentNaming + public interface TAbstractBase + { + Task WriteAsync(TProtocol tProtocol, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/TBase.cs b/lib/Thrift/Protocols/TBase.cs new file mode 100644 index 00000000000..014e1aee856 --- /dev/null +++ b/lib/Thrift/Protocols/TBase.cs @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Protocols +{ + // ReSharper disable once InconsistentNaming + public interface TBase : TAbstractBase + { + Task ReadAsync(TProtocol tProtocol, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/TBinaryProtocol.cs b/lib/Thrift/Protocols/TBinaryProtocol.cs new file mode 100644 index 00000000000..72bc3d2cdca --- /dev/null +++ b/lib/Thrift/Protocols/TBinaryProtocol.cs @@ -0,0 +1,613 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; +using Thrift.Transports; + +namespace Thrift.Protocols +{ + // ReSharper disable once InconsistentNaming + public class TBinaryProtocol : TProtocol + { + //TODO: Unit tests + //TODO: Localization + //TODO: pragma + + protected const uint VersionMask = 0xffff0000; + protected const uint Version1 = 0x80010000; + + protected bool StrictRead; + protected bool StrictWrite; + + public TBinaryProtocol(TClientTransport trans) + : this(trans, false, true) + { + } + + public TBinaryProtocol(TClientTransport trans, bool strictRead, bool strictWrite) + : base(trans) + { + StrictRead = strictRead; + StrictWrite = strictWrite; + } + + public override async Task WriteMessageBeginAsync(TMessage message, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (StrictWrite) + { + var version = Version1 | (uint) message.Type; + await WriteI32Async((int) version, cancellationToken).ConfigureAwait(false); + await WriteStringAsync(message.Name, cancellationToken).ConfigureAwait(false); + await WriteI32Async(message.SeqID, cancellationToken).ConfigureAwait(false); + } + else + { + await WriteStringAsync(message.Name, cancellationToken).ConfigureAwait(false); + await WriteByteAsync((sbyte) message.Type, cancellationToken).ConfigureAwait(false); + await WriteI32Async(message.SeqID, cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteMessageEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteStructBeginAsync(TStruct @struct, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteStructEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteFieldBeginAsync(TField field, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteByteAsync((sbyte) field.Type, cancellationToken).ConfigureAwait(false); + await WriteI16Async(field.ID, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteFieldStopAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteByteAsync((sbyte) TType.Stop, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMapBeginAsync(TMap map, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteByteAsync((sbyte) map.KeyType, cancellationToken).ConfigureAwait(false); + await WriteByteAsync((sbyte) map.ValueType, cancellationToken).ConfigureAwait(false); + await WriteI32Async(map.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMapEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteListBeginAsync(TList list, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteByteAsync((sbyte) list.ElementType, cancellationToken).ConfigureAwait(false); + await WriteI32Async(list.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteListEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteSetBeginAsync(TSet set, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteByteAsync((sbyte) set.ElementType, cancellationToken).ConfigureAwait(false); + await WriteI32Async(set.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteSetEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteBoolAsync(bool b, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteByteAsync(b ? (sbyte) 1 : (sbyte) 0, cancellationToken).ConfigureAwait(false); + } + + protected internal static byte[] CreateWriteByte(sbyte b) + { + var bout = new byte[1]; + + bout[0] = (byte) b; + + return bout; + } + + public override async Task WriteByteAsync(sbyte b, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var bout = CreateWriteByte(b); + await Trans.WriteAsync(bout, 0, 1, cancellationToken).ConfigureAwait(false); + } + + protected internal static byte[] CreateWriteI16(short s) + { + var i16Out = new byte[2]; + + i16Out[0] = (byte) (0xff & (s >> 8)); + i16Out[1] = (byte) (0xff & s); + + return i16Out; + } + + public override async Task WriteI16Async(short i16, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var i16Out = CreateWriteI16(i16); + await Trans.WriteAsync(i16Out, 0, 2, cancellationToken).ConfigureAwait(false); + } + + protected internal static byte[] CreateWriteI32(int i32) + { + var i32Out = new byte[4]; + + i32Out[0] = (byte) (0xff & (i32 >> 24)); + i32Out[1] = (byte) (0xff & (i32 >> 16)); + i32Out[2] = (byte) (0xff & (i32 >> 8)); + i32Out[3] = (byte) (0xff & i32); + + return i32Out; + } + + public override async Task WriteI32Async(int i32, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var i32Out = CreateWriteI32(i32); + await Trans.WriteAsync(i32Out, 0, 4, cancellationToken).ConfigureAwait(false); + } + + protected internal static byte[] CreateWriteI64(long i64) + { + var i64Out = new byte[8]; + + i64Out[0] = (byte) (0xff & (i64 >> 56)); + i64Out[1] = (byte) (0xff & (i64 >> 48)); + i64Out[2] = (byte) (0xff & (i64 >> 40)); + i64Out[3] = (byte) (0xff & (i64 >> 32)); + i64Out[4] = (byte) (0xff & (i64 >> 24)); + i64Out[5] = (byte) (0xff & (i64 >> 16)); + i64Out[6] = (byte) (0xff & (i64 >> 8)); + i64Out[7] = (byte) (0xff & i64); + + return i64Out; + } + + public override async Task WriteI64Async(long i64, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var i64Out = CreateWriteI64(i64); + await Trans.WriteAsync(i64Out, 0, 8, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteDoubleAsync(double d, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteI64Async(BitConverter.DoubleToInt64Bits(d), cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteBinaryAsync(byte[] bytes, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteI32Async(bytes.Length, cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadMessageBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var message = new TMessage(); + var size = await ReadI32Async(cancellationToken).ConfigureAwait(false); + if (size < 0) + { + var version = (uint) size & VersionMask; + if (version != Version1) + { + throw new TProtocolException(TProtocolException.BAD_VERSION, + $"Bad version in ReadMessageBegin: {version}"); + } + message.Type = (TMessageType) (size & 0x000000ff); + message.Name = await ReadStringAsync(cancellationToken).ConfigureAwait(false); + message.SeqID = await ReadI32Async(cancellationToken).ConfigureAwait(false); + } + else + { + if (StrictRead) + { + throw new TProtocolException(TProtocolException.BAD_VERSION, + "Missing version in ReadMessageBegin, old client?"); + } + message.Name = await ReadStringBodyAsync(size, cancellationToken).ConfigureAwait(false); + message.Type = (TMessageType) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + message.SeqID = await ReadI32Async(cancellationToken).ConfigureAwait(false); + } + return message; + } + + public override async Task ReadMessageEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadStructBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + //TODO: no read from internal transport? + return new TStruct(); + } + + public override async Task ReadStructEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadFieldBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var field = new TField + { + Type = (TType) await ReadByteAsync(cancellationToken) + }; + + if (field.Type != TType.Stop) + { + field.ID = await ReadI16Async(cancellationToken).ConfigureAwait(false); + } + + return field; + } + + public override async Task ReadFieldEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadMapBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var map = new TMap + { + KeyType = (TType) await ReadByteAsync(cancellationToken), + ValueType = (TType) await ReadByteAsync(cancellationToken), + Count = await ReadI32Async(cancellationToken) + }; + + return map; + } + + public override async Task ReadMapEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadListBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var list = new TList + { + ElementType = (TType) await ReadByteAsync(cancellationToken), + Count = await ReadI32Async(cancellationToken) + }; + + return list; + } + + public override async Task ReadListEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadSetBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var set = new TSet + { + ElementType = (TType) await ReadByteAsync(cancellationToken), + Count = await ReadI32Async(cancellationToken) + }; + + return set; + } + + public override async Task ReadSetEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadBoolAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + return (await ReadByteAsync(cancellationToken).ConfigureAwait(false)) == 1; + } + + public override async Task ReadByteAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var bin = new byte[1]; + await Trans.ReadAllAsync(bin, 0, 1, cancellationToken).ConfigureAwait(false); //TODO: why readall ? + return (sbyte) bin[0]; + } + + public override async Task ReadI16Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var i16In = new byte[2]; + await Trans.ReadAllAsync(i16In, 0, 2, cancellationToken).ConfigureAwait(false); + var result = (short) (((i16In[0] & 0xff) << 8) | i16In[1] & 0xff); + return result; + } + + public override async Task ReadI32Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var i32In = new byte[4]; + await Trans.ReadAllAsync(i32In, 0, 4, cancellationToken).ConfigureAwait(false); + + var result = + ((i32In[0] & 0xff) << 24) | + ((i32In[1] & 0xff) << 16) | + ((i32In[2] & 0xff) << 8) | + i32In[3] & 0xff; + + return result; + } + +#pragma warning disable 675 + + protected internal long CreateReadI64(byte[] buf) + { + var result = + ((long) (buf[0] & 0xff) << 56) | + ((long) (buf[1] & 0xff) << 48) | + ((long) (buf[2] & 0xff) << 40) | + ((long) (buf[3] & 0xff) << 32) | + ((long) (buf[4] & 0xff) << 24) | + ((long) (buf[5] & 0xff) << 16) | + ((long) (buf[6] & 0xff) << 8) | + buf[7] & 0xff; + + return result; + } + +#pragma warning restore 675 + + public override async Task ReadI64Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var i64In = new byte[8]; + await Trans.ReadAllAsync(i64In, 0, 8, cancellationToken).ConfigureAwait(false); + return CreateReadI64(i64In); + } + + public override async Task ReadDoubleAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var d = await ReadI64Async(cancellationToken).ConfigureAwait(false); + return BitConverter.Int64BitsToDouble(d); + } + + public override async Task ReadBinaryAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var size = await ReadI32Async(cancellationToken).ConfigureAwait(false); + var buf = new byte[size]; + await Trans.ReadAllAsync(buf, 0, size, cancellationToken).ConfigureAwait(false); + return buf; + } + + private async Task ReadStringBodyAsync(int size, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var buf = new byte[size]; + await Trans.ReadAllAsync(buf, 0, size, cancellationToken).ConfigureAwait(false); + return Encoding.UTF8.GetString(buf, 0, buf.Length); + } + + public class Factory : ITProtocolFactory + { + protected bool StrictRead; + protected bool StrictWrite; + + public Factory() + : this(false, true) + { + } + + public Factory(bool strictRead, bool strictWrite) + { + StrictRead = strictRead; + StrictWrite = strictWrite; + } + + public TProtocol GetProtocol(TClientTransport trans) + { + return new TBinaryProtocol(trans, StrictRead, StrictWrite); + } + } + } +} diff --git a/lib/Thrift/Protocols/TCompactProtocol.cs b/lib/Thrift/Protocols/TCompactProtocol.cs new file mode 100644 index 00000000000..a9101075403 --- /dev/null +++ b/lib/Thrift/Protocols/TCompactProtocol.cs @@ -0,0 +1,922 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; +using Thrift.Transports; + +namespace Thrift.Protocols +{ + //TODO: implementation of TProtocol + + // ReSharper disable once InconsistentNaming + public class TCompactProtocol : TProtocol + { + private const byte ProtocolId = 0x82; + private const byte Version = 1; + private const byte VersionMask = 0x1f; // 0001 1111 + private const byte TypeMask = 0xE0; // 1110 0000 + private const byte TypeBits = 0x07; // 0000 0111 + private const int TypeShiftAmount = 5; + private static readonly TStruct AnonymousStruct = new TStruct(string.Empty); + private static readonly TField Tstop = new TField(string.Empty, TType.Stop, 0); + + // ReSharper disable once InconsistentNaming + private static readonly byte[] TTypeToCompactType = new byte[16]; + + /// + /// Used to keep track of the last field for the current and previous structs, so we can do the delta stuff. + /// + private readonly Stack _lastField = new Stack(15); + + /// + /// If we encounter a boolean field begin, save the TField here so it can have the value incorporated. + /// + private TField? _booleanField; + + /// + /// If we Read a field header, and it's a boolean field, save the boolean value here so that ReadBool can use it. + /// + private bool? _boolValue; + + private short _lastFieldId; + + public TCompactProtocol(TClientTransport trans) + : base(trans) + { + TTypeToCompactType[(int) TType.Stop] = Types.Stop; + TTypeToCompactType[(int) TType.Bool] = Types.BooleanTrue; + TTypeToCompactType[(int) TType.Byte] = Types.Byte; + TTypeToCompactType[(int) TType.I16] = Types.I16; + TTypeToCompactType[(int) TType.I32] = Types.I32; + TTypeToCompactType[(int) TType.I64] = Types.I64; + TTypeToCompactType[(int) TType.Double] = Types.Double; + TTypeToCompactType[(int) TType.String] = Types.Binary; + TTypeToCompactType[(int) TType.List] = Types.List; + TTypeToCompactType[(int) TType.Set] = Types.Set; + TTypeToCompactType[(int) TType.Map] = Types.Map; + TTypeToCompactType[(int) TType.Struct] = Types.Struct; + } + + public void Reset() + { + _lastField.Clear(); + _lastFieldId = 0; + } + + public override async Task WriteMessageBeginAsync(TMessage message, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Trans.WriteAsync(new[] {ProtocolId}, cancellationToken).ConfigureAwait(false); + await + Trans.WriteAsync( + new[] {(byte) ((Version & VersionMask) | (((uint) message.Type << TypeShiftAmount) & TypeMask))}, + cancellationToken); + + var bufferTuple = CreateWriteVarInt32((uint) message.SeqID); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + + await WriteStringAsync(message.Name, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMessageEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Write a struct begin. This doesn't actually put anything on the wire. We + /// use it as an opportunity to put special placeholder markers on the field + /// stack so we can get the field id deltas correct. + /// + public override async Task WriteStructBeginAsync(TStruct @struct, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + _lastField.Push(_lastFieldId); + _lastFieldId = 0; + } + + public override async Task WriteStructEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + _lastFieldId = _lastField.Pop(); + } + + private async Task WriteFieldBeginInternalAsync(TField field, byte typeOverride, + CancellationToken cancellationToken) + { + // if there's a exType override, use that. + var typeToWrite = typeOverride == 0xFF ? GetCompactType(field.Type) : typeOverride; + + // check if we can use delta encoding for the field id + if ((field.ID > _lastFieldId) && (field.ID - _lastFieldId <= 15)) + { + var b = (byte) (((field.ID - _lastFieldId) << 4) | typeToWrite); + // Write them together + await Trans.WriteAsync(new[] {b}, cancellationToken).ConfigureAwait(false); + } + else + { + // Write them separate + await Trans.WriteAsync(new[] {typeToWrite}, cancellationToken).ConfigureAwait(false); + await WriteI16Async(field.ID, cancellationToken).ConfigureAwait(false); + } + + _lastFieldId = field.ID; + } + + public override async Task WriteFieldBeginAsync(TField field, CancellationToken cancellationToken) + { + if (field.Type == TType.Bool) + { + _booleanField = field; + } + else + { + await WriteFieldBeginInternalAsync(field, 0xFF, cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteFieldEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteFieldStopAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Trans.WriteAsync(new[] {Types.Stop}, cancellationToken).ConfigureAwait(false); + } + + protected async Task WriteCollectionBeginAsync(TType elemType, int size, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + /* + Abstract method for writing the start of lists and sets. List and sets on + the wire differ only by the exType indicator. + */ + + if (size <= 14) + { + await Trans.WriteAsync(new[] {(byte) ((size << 4) | GetCompactType(elemType))}, cancellationToken).ConfigureAwait(false); + } + else + { + await Trans.WriteAsync(new[] {(byte) (0xf0 | GetCompactType(elemType))}, cancellationToken).ConfigureAwait(false); + + var bufferTuple = CreateWriteVarInt32((uint) size); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteListBeginAsync(TList list, CancellationToken cancellationToken) + { + await WriteCollectionBeginAsync(list.ElementType, list.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteListEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteSetBeginAsync(TSet set, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await WriteCollectionBeginAsync(set.ElementType, set.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteSetEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteBoolAsync(bool b, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + /* + Write a boolean value. Potentially, this could be a boolean field, in + which case the field header info isn't written yet. If so, decide what the + right exType header is for the value and then Write the field header. + Otherwise, Write a single byte. + */ + + if (_booleanField != null) + { + // we haven't written the field header yet + await + WriteFieldBeginInternalAsync(_booleanField.Value, b ? Types.BooleanTrue : Types.BooleanFalse, + cancellationToken); + _booleanField = null; + } + else + { + // we're not part of a field, so just Write the value. + await Trans.WriteAsync(new[] {b ? Types.BooleanTrue : Types.BooleanFalse}, cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteByteAsync(sbyte b, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Trans.WriteAsync(new[] {(byte) b}, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI16Async(short i16, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var bufferTuple = CreateWriteVarInt32(IntToZigzag(i16)); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + } + + protected internal Tuple CreateWriteVarInt32(uint n) + { + // Write an i32 as a varint.Results in 1 - 5 bytes on the wire. + var i32Buf = new byte[5]; + var idx = 0; + + while (true) + { + if ((n & ~0x7F) == 0) + { + i32Buf[idx++] = (byte) n; + break; + } + + i32Buf[idx++] = (byte) ((n & 0x7F) | 0x80); + n >>= 7; + } + + return new Tuple(i32Buf, idx); + } + + public override async Task WriteI32Async(int i32, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var bufferTuple = CreateWriteVarInt32(IntToZigzag(i32)); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + } + + protected internal Tuple CreateWriteVarInt64(ulong n) + { + // Write an i64 as a varint. Results in 1-10 bytes on the wire. + var buf = new byte[10]; + var idx = 0; + + while (true) + { + if ((n & ~(ulong) 0x7FL) == 0) + { + buf[idx++] = (byte) n; + break; + } + buf[idx++] = (byte) ((n & 0x7F) | 0x80); + n >>= 7; + } + + return new Tuple(buf, idx); + } + + public override async Task WriteI64Async(long i64, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var bufferTuple = CreateWriteVarInt64(LongToZigzag(i64)); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteDoubleAsync(double d, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var data = new byte[8]; + FixedLongToBytes(BitConverter.DoubleToInt64Bits(d), data, 0); + await Trans.WriteAsync(data, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStringAsync(string str, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var bytes = Encoding.UTF8.GetBytes(str); + + var bufferTuple = CreateWriteVarInt32((uint) bytes.Length); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteBinaryAsync(byte[] bytes, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + var bufferTuple = CreateWriteVarInt32((uint) bytes.Length); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMapBeginAsync(TMap map, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (map.Count == 0) + { + await Trans.WriteAsync(new[] {(byte) 0}, cancellationToken).ConfigureAwait(false); + } + else + { + var bufferTuple = CreateWriteVarInt32((uint) map.Count); + await Trans.WriteAsync(bufferTuple.Item1, 0, bufferTuple.Item2, cancellationToken).ConfigureAwait(false); + await + Trans.WriteAsync( + new[] {(byte) ((GetCompactType(map.KeyType) << 4) | GetCompactType(map.ValueType))}, + cancellationToken); + } + } + + public override async Task WriteMapEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadMessageBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var protocolId = (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + if (protocolId != ProtocolId) + { + throw new TProtocolException($"Expected protocol id {ProtocolId:X} but got {protocolId:X}"); + } + + var versionAndType = (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + var version = (byte) (versionAndType & VersionMask); + + if (version != Version) + { + throw new TProtocolException($"Expected version {Version} but got {version}"); + } + + var type = (byte) ((versionAndType >> TypeShiftAmount) & TypeBits); + var seqid = (int) await ReadVarInt32Async(cancellationToken).ConfigureAwait(false); + var messageName = await ReadStringAsync(cancellationToken).ConfigureAwait(false); + + return new TMessage(messageName, (TMessageType) type, seqid); + } + + public override async Task ReadMessageEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadStructBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + // some magic is here ) + + _lastField.Push(_lastFieldId); + _lastFieldId = 0; + + return AnonymousStruct; + } + + public override async Task ReadStructEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + /* + Doesn't actually consume any wire data, just removes the last field for + this struct from the field stack. + */ + + // consume the last field we Read off the wire. + _lastFieldId = _lastField.Pop(); + } + + public override async Task ReadFieldBeginAsync(CancellationToken cancellationToken) + { + // Read a field header off the wire. + var type = (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + // if it's a stop, then we can return immediately, as the struct is over. + if (type == Types.Stop) + { + return Tstop; + } + + short fieldId; + // mask off the 4 MSB of the exType header. it could contain a field id delta. + var modifier = (short) ((type & 0xf0) >> 4); + if (modifier == 0) + { + fieldId = await ReadI16Async(cancellationToken).ConfigureAwait(false); + } + else + { + fieldId = (short) (_lastFieldId + modifier); + } + + var field = new TField(string.Empty, GetTType((byte) (type & 0x0f)), fieldId); + // if this happens to be a boolean field, the value is encoded in the exType + if (IsBoolType(type)) + { + _boolValue = (byte) (type & 0x0f) == Types.BooleanTrue; + } + + // push the new field onto the field stack so we can keep the deltas going. + _lastFieldId = field.ID; + return field; + } + + public override async Task ReadFieldEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadMapBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + /* + Read a map header off the wire. If the size is zero, skip Reading the key + and value exType. This means that 0-length maps will yield TMaps without the + "correct" types. + */ + + var size = (int) await ReadVarInt32Async(cancellationToken).ConfigureAwait(false); + var keyAndValueType = size == 0 ? (byte) 0 : (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + return new TMap(GetTType((byte) (keyAndValueType >> 4)), GetTType((byte) (keyAndValueType & 0xf)), size); + } + + public override async Task ReadMapEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadSetBeginAsync(CancellationToken cancellationToken) + { + /* + Read a set header off the wire. If the set size is 0-14, the size will + be packed into the element exType header. If it's a longer set, the 4 MSB + of the element exType header will be 0xF, and a varint will follow with the + true size. + */ + + return new TSet(await ReadListBeginAsync(cancellationToken).ConfigureAwait(false)); + } + + public override async Task ReadBoolAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + /* + Read a boolean off the wire. If this is a boolean field, the value should + already have been Read during ReadFieldBegin, so we'll just consume the + pre-stored value. Otherwise, Read a byte. + */ + + if (_boolValue != null) + { + var result = _boolValue.Value; + _boolValue = null; + return result; + } + + return (await ReadByteAsync(cancellationToken).ConfigureAwait(false)) == Types.BooleanTrue; + } + + public override async Task ReadByteAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + // Read a single byte off the wire. Nothing interesting here. + var buf = new byte[1]; + await Trans.ReadAllAsync(buf, 0, 1, cancellationToken).ConfigureAwait(false); + return (sbyte) buf[0]; + } + + public override async Task ReadI16Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + return (short) ZigzagToInt(await ReadVarInt32Async(cancellationToken).ConfigureAwait(false)); + } + + public override async Task ReadI32Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + return ZigzagToInt(await ReadVarInt32Async(cancellationToken).ConfigureAwait(false)); + } + + public override async Task ReadI64Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + return ZigzagToLong(await ReadVarInt64Async(cancellationToken).ConfigureAwait(false)); + } + + public override async Task ReadDoubleAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var longBits = new byte[8]; + await Trans.ReadAllAsync(longBits, 0, 8, cancellationToken).ConfigureAwait(false); + + return BitConverter.Int64BitsToDouble(BytesToLong(longBits)); + } + + public override async Task ReadStringAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + // Reads a byte[] (via ReadBinary), and then UTF-8 decodes it. + var length = (int) await ReadVarInt32Async(cancellationToken).ConfigureAwait(false); + + if (length == 0) + { + return string.Empty; + } + + var buf = new byte[length]; + await Trans.ReadAllAsync(buf, 0, length, cancellationToken).ConfigureAwait(false); + + return Encoding.UTF8.GetString(buf); + } + + public override async Task ReadBinaryAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + // Read a byte[] from the wire. + var length = (int) await ReadVarInt32Async(cancellationToken).ConfigureAwait(false); + if (length == 0) + { + return new byte[0]; + } + + var buf = new byte[length]; + await Trans.ReadAllAsync(buf, 0, length, cancellationToken).ConfigureAwait(false); + return buf; + } + + public override async Task ReadListBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + /* + Read a list header off the wire. If the list size is 0-14, the size will + be packed into the element exType header. If it's a longer list, the 4 MSB + of the element exType header will be 0xF, and a varint will follow with the + true size. + */ + + var sizeAndType = (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + var size = (sizeAndType >> 4) & 0x0f; + if (size == 15) + { + size = (int) await ReadVarInt32Async(cancellationToken).ConfigureAwait(false); + } + + var type = GetTType(sizeAndType); + return new TList(type, size); + } + + public override async Task ReadListEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadSetEndAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + private static byte GetCompactType(TType ttype) + { + // Given a TType value, find the appropriate TCompactProtocol.Types constant. + return TTypeToCompactType[(int) ttype]; + } + + + private async Task ReadVarInt32Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + /* + Read an i32 from the wire as a varint. The MSB of each byte is set + if there is another byte to follow. This can Read up to 5 bytes. + */ + + uint result = 0; + var shift = 0; + + while (true) + { + var b = (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + result |= (uint) (b & 0x7f) << shift; + if ((b & 0x80) != 0x80) + { + break; + } + shift += 7; + } + + return result; + } + + private async Task ReadVarInt64Async(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + /* + Read an i64 from the wire as a proper varint. The MSB of each byte is set + if there is another byte to follow. This can Read up to 10 bytes. + */ + + var shift = 0; + ulong result = 0; + while (true) + { + var b = (byte) await ReadByteAsync(cancellationToken).ConfigureAwait(false); + result |= (ulong) (b & 0x7f) << shift; + if ((b & 0x80) != 0x80) + { + break; + } + shift += 7; + } + + return result; + } + + private static int ZigzagToInt(uint n) + { + return (int) (n >> 1) ^ -(int) (n & 1); + } + + private static long ZigzagToLong(ulong n) + { + return (long) (n >> 1) ^ -(long) (n & 1); + } + + private static long BytesToLong(byte[] bytes) + { + /* + Note that it's important that the mask bytes are long literals, + otherwise they'll default to ints, and when you shift an int left 56 bits, + you just get a messed up int. + */ + + return + ((bytes[7] & 0xffL) << 56) | + ((bytes[6] & 0xffL) << 48) | + ((bytes[5] & 0xffL) << 40) | + ((bytes[4] & 0xffL) << 32) | + ((bytes[3] & 0xffL) << 24) | + ((bytes[2] & 0xffL) << 16) | + ((bytes[1] & 0xffL) << 8) | + (bytes[0] & 0xffL); + } + + private static bool IsBoolType(byte b) + { + var lowerNibble = b & 0x0f; + return (lowerNibble == Types.BooleanTrue) || (lowerNibble == Types.BooleanFalse); + } + + private static TType GetTType(byte type) + { + // Given a TCompactProtocol.Types constant, convert it to its corresponding TType value. + switch ((byte) (type & 0x0f)) + { + case Types.Stop: + return TType.Stop; + case Types.BooleanFalse: + case Types.BooleanTrue: + return TType.Bool; + case Types.Byte: + return TType.Byte; + case Types.I16: + return TType.I16; + case Types.I32: + return TType.I32; + case Types.I64: + return TType.I64; + case Types.Double: + return TType.Double; + case Types.Binary: + return TType.String; + case Types.List: + return TType.List; + case Types.Set: + return TType.Set; + case Types.Map: + return TType.Map; + case Types.Struct: + return TType.Struct; + default: + throw new TProtocolException($"Don't know what exType: {(byte) (type & 0x0f)}"); + } + } + + private static ulong LongToZigzag(long n) + { + // Convert l into a zigzag long. This allows negative numbers to be represented compactly as a varint + return (ulong) (n << 1) ^ (ulong) (n >> 63); + } + + private static uint IntToZigzag(int n) + { + // Convert n into a zigzag int. This allows negative numbers to be represented compactly as a varint + return (uint) (n << 1) ^ (uint) (n >> 31); + } + + private static void FixedLongToBytes(long n, byte[] buf, int off) + { + // Convert a long into little-endian bytes in buf starting at off and going until off+7. + buf[off + 0] = (byte) (n & 0xff); + buf[off + 1] = (byte) ((n >> 8) & 0xff); + buf[off + 2] = (byte) ((n >> 16) & 0xff); + buf[off + 3] = (byte) ((n >> 24) & 0xff); + buf[off + 4] = (byte) ((n >> 32) & 0xff); + buf[off + 5] = (byte) ((n >> 40) & 0xff); + buf[off + 6] = (byte) ((n >> 48) & 0xff); + buf[off + 7] = (byte) ((n >> 56) & 0xff); + } + + public class Factory : ITProtocolFactory + { + public TProtocol GetProtocol(TClientTransport trans) + { + return new TCompactProtocol(trans); + } + } + + /// + /// All of the on-wire exType codes. + /// + private static class Types + { + public const byte Stop = 0x00; + public const byte BooleanTrue = 0x01; + public const byte BooleanFalse = 0x02; + public const byte Byte = 0x03; + public const byte I16 = 0x04; + public const byte I32 = 0x05; + public const byte I64 = 0x06; + public const byte Double = 0x07; + public const byte Binary = 0x08; + public const byte List = 0x09; + public const byte Set = 0x0A; + public const byte Map = 0x0B; + public const byte Struct = 0x0C; + } + } +} diff --git a/lib/Thrift/Protocols/TJSONProtocol.cs b/lib/Thrift/Protocols/TJSONProtocol.cs new file mode 100644 index 00000000000..b2b5df2d9f2 --- /dev/null +++ b/lib/Thrift/Protocols/TJSONProtocol.cs @@ -0,0 +1,981 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; +using Thrift.Protocols.Utilities; +using Thrift.Transports; + +namespace Thrift.Protocols +{ + /// + /// JSON protocol implementation for thrift. + /// This is a full-featured protocol supporting Write and Read. + /// Please see the C++ class header for a detailed description of the + /// protocol's wire format. + /// Adapted from the Java version. + /// + // ReSharper disable once InconsistentNaming + public class TJsonProtocol : TProtocol + { + private const long Version = 1; + + // Temporary buffer used by several methods + private readonly byte[] _tempBuffer = new byte[4]; + + // Current context that we are in + protected JSONBaseContext Context; + + // Stack of nested contexts that we may be in + protected Stack ContextStack = new Stack(); + + // Reader that manages a 1-byte buffer + protected LookaheadReader Reader; + + // Default encoding + protected Encoding Utf8Encoding = Encoding.UTF8; + + /// + /// TJsonProtocol Constructor + /// + public TJsonProtocol(TClientTransport trans) + : base(trans) + { + Context = new JSONBaseContext(this); + Reader = new LookaheadReader(this); + } + + /// + /// Push a new JSON context onto the stack. + /// + protected void PushContext(JSONBaseContext c) + { + ContextStack.Push(Context); + Context = c; + } + + /// + /// Pop the last JSON context off the stack + /// + protected void PopContext() + { + Context = ContextStack.Pop(); + } + + /// + /// Read a byte that must match b[0]; otherwise an exception is thrown. + /// Marked protected to avoid synthetic accessor in JSONListContext.Read + /// and JSONPairContext.Read + /// + protected async Task ReadJsonSyntaxCharAsync(byte[] bytes, CancellationToken cancellationToken) + { + var ch = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (ch != bytes[0]) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, $"Unexpected character: {(char) ch}"); + } + } + + /// + /// Write the bytes in array buf as a JSON characters, escaping as needed + /// + private async Task WriteJsonStringAsync(byte[] bytes, CancellationToken cancellationToken) + { + await Context.WriteAsync(cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + + var len = bytes.Length; + for (var i = 0; i < len; i++) + { + if ((bytes[i] & 0x00FF) >= 0x30) + { + if (bytes[i] == TJSONProtocolConstants.Backslash[0]) + { + await Trans.WriteAsync(TJSONProtocolConstants.Backslash, cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(TJSONProtocolConstants.Backslash, cancellationToken).ConfigureAwait(false); + } + else + { + await Trans.WriteAsync(bytes.ToArray(), i, 1, cancellationToken).ConfigureAwait(false); + } + } + else + { + _tempBuffer[0] = TJSONProtocolConstants.JsonCharTable[bytes[i]]; + if (_tempBuffer[0] == 1) + { + await Trans.WriteAsync(bytes, i, 1, cancellationToken).ConfigureAwait(false); + } + else if (_tempBuffer[0] > 1) + { + await Trans.WriteAsync(TJSONProtocolConstants.Backslash, cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(_tempBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + } + else + { + await Trans.WriteAsync(TJSONProtocolConstants.EscSequences, cancellationToken).ConfigureAwait(false); + _tempBuffer[0] = TJSONProtocolHelper.ToHexChar((byte) (bytes[i] >> 4)); + _tempBuffer[1] = TJSONProtocolHelper.ToHexChar(bytes[i]); + await Trans.WriteAsync(_tempBuffer, 0, 2, cancellationToken).ConfigureAwait(false); + } + } + } + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + /// + /// Write out number as a JSON value. If the context dictates so, it will be + /// wrapped in quotes to output as a JSON string. + /// + private async Task WriteJsonIntegerAsync(long num, CancellationToken cancellationToken) + { + await Context.WriteAsync(cancellationToken).ConfigureAwait(false); + var str = num.ToString(); + + var escapeNum = Context.EscapeNumbers(); + if (escapeNum) + { + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + var bytes = Utf8Encoding.GetBytes(str); + await Trans.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + + if (escapeNum) + { + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Write out a double as a JSON value. If it is NaN or infinity or if the + /// context dictates escaping, Write out as JSON string. + /// + private async Task WriteJsonDoubleAsync(double num, CancellationToken cancellationToken) + { + await Context.WriteAsync(cancellationToken).ConfigureAwait(false); + var str = num.ToString("G17", CultureInfo.InvariantCulture); + var special = false; + + switch (str[0]) + { + case 'N': // NaN + case 'I': // Infinity + special = true; + break; + case '-': + if (str[1] == 'I') + { + // -Infinity + special = true; + } + break; + } + + var escapeNum = special || Context.EscapeNumbers(); + + if (escapeNum) + { + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + await Trans.WriteAsync(Utf8Encoding.GetBytes(str), cancellationToken).ConfigureAwait(false); + + if (escapeNum) + { + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Write out contents of byte array b as a JSON string with base-64 encoded + /// data + /// + private async Task WriteJsonBase64Async(byte[] bytes, CancellationToken cancellationToken) + { + await Context.WriteAsync(cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + + var len = bytes.Length; + var off = 0; + + while (len >= 3) + { + // Encode 3 bytes at a time + TBase64Helper.Encode(bytes, off, 3, _tempBuffer, 0); + await Trans.WriteAsync(_tempBuffer, 0, 4, cancellationToken).ConfigureAwait(false); + off += 3; + len -= 3; + } + + if (len > 0) + { + // Encode remainder + TBase64Helper.Encode(bytes, off, len, _tempBuffer, 0); + await Trans.WriteAsync(_tempBuffer, 0, len + 1, cancellationToken).ConfigureAwait(false); + } + + await Trans.WriteAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + private async Task WriteJsonObjectStartAsync(CancellationToken cancellationToken) + { + await Context.WriteAsync(cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(TJSONProtocolConstants.LeftBrace, cancellationToken).ConfigureAwait(false); + PushContext(new JSONPairContext(this)); + } + + private async Task WriteJsonObjectEndAsync(CancellationToken cancellationToken) + { + PopContext(); + await Trans.WriteAsync(TJSONProtocolConstants.RightBrace, cancellationToken).ConfigureAwait(false); + } + + private async Task WriteJsonArrayStartAsync(CancellationToken cancellationToken) + { + await Context.WriteAsync(cancellationToken).ConfigureAwait(false); + await Trans.WriteAsync(TJSONProtocolConstants.LeftBracket, cancellationToken).ConfigureAwait(false); + PushContext(new JSONListContext(this)); + } + + private async Task WriteJsonArrayEndAsync(CancellationToken cancellationToken) + { + PopContext(); + await Trans.WriteAsync(TJSONProtocolConstants.RightBracket, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMessageBeginAsync(TMessage message, CancellationToken cancellationToken) + { + await WriteJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + await WriteJsonIntegerAsync(Version, cancellationToken).ConfigureAwait(false); + + var b = Utf8Encoding.GetBytes(message.Name); + await WriteJsonStringAsync(b, cancellationToken).ConfigureAwait(false); + + await WriteJsonIntegerAsync((long) message.Type, cancellationToken).ConfigureAwait(false); + await WriteJsonIntegerAsync(message.SeqID, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMessageEndAsync(CancellationToken cancellationToken) + { + await WriteJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStructBeginAsync(TStruct @struct, CancellationToken cancellationToken) + { + await WriteJsonObjectStartAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStructEndAsync(CancellationToken cancellationToken) + { + await WriteJsonObjectEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldBeginAsync(TField field, CancellationToken cancellationToken) + { + await WriteJsonIntegerAsync(field.ID, cancellationToken).ConfigureAwait(false); + await WriteJsonObjectStartAsync(cancellationToken).ConfigureAwait(false); + await WriteJsonStringAsync(TJSONProtocolHelper.GetTypeNameForTypeId(field.Type), cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldEndAsync(CancellationToken cancellationToken) + { + await WriteJsonObjectEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldStopAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task WriteMapBeginAsync(TMap map, CancellationToken cancellationToken) + { + await WriteJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + await WriteJsonStringAsync(TJSONProtocolHelper.GetTypeNameForTypeId(map.KeyType), cancellationToken).ConfigureAwait(false); + await WriteJsonStringAsync(TJSONProtocolHelper.GetTypeNameForTypeId(map.ValueType), cancellationToken).ConfigureAwait(false); + await WriteJsonIntegerAsync(map.Count, cancellationToken).ConfigureAwait(false); + await WriteJsonObjectStartAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMapEndAsync(CancellationToken cancellationToken) + { + await WriteJsonObjectEndAsync(cancellationToken).ConfigureAwait(false); + await WriteJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteListBeginAsync(TList list, CancellationToken cancellationToken) + { + await WriteJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + await WriteJsonStringAsync(TJSONProtocolHelper.GetTypeNameForTypeId(list.ElementType), cancellationToken).ConfigureAwait(false); + await WriteJsonIntegerAsync(list.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteListEndAsync(CancellationToken cancellationToken) + { + await WriteJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteSetBeginAsync(TSet set, CancellationToken cancellationToken) + { + await WriteJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + await WriteJsonStringAsync(TJSONProtocolHelper.GetTypeNameForTypeId(set.ElementType), cancellationToken).ConfigureAwait(false); + await WriteJsonIntegerAsync(set.Count, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteSetEndAsync(CancellationToken cancellationToken) + { + await WriteJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteBoolAsync(bool b, CancellationToken cancellationToken) + { + await WriteJsonIntegerAsync(b ? 1 : 0, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteByteAsync(sbyte b, CancellationToken cancellationToken) + { + await WriteJsonIntegerAsync(b, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI16Async(short i16, CancellationToken cancellationToken) + { + await WriteJsonIntegerAsync(i16, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI32Async(int i32, CancellationToken cancellationToken) + { + await WriteJsonIntegerAsync(i32, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI64Async(long i64, CancellationToken cancellationToken) + { + await WriteJsonIntegerAsync(i64, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteDoubleAsync(double d, CancellationToken cancellationToken) + { + await WriteJsonDoubleAsync(d, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStringAsync(string s, CancellationToken cancellationToken) + { + var b = Utf8Encoding.GetBytes(s); + await WriteJsonStringAsync(b, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteBinaryAsync(byte[] bytes, CancellationToken cancellationToken) + { + await WriteJsonBase64Async(bytes, cancellationToken).ConfigureAwait(false); + } + + /// + /// Read in a JSON string, unescaping as appropriate.. Skip Reading from the + /// context if skipContext is true. + /// + private async Task ReadJsonStringAsync(bool skipContext, CancellationToken cancellationToken) + { + using (var buffer = new MemoryStream()) + { + var codeunits = new List(); + + + if (!skipContext) + { + await Context.ReadAsync(cancellationToken).ConfigureAwait(false); + } + + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + + while (true) + { + var ch = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (ch == TJSONProtocolConstants.Quote[0]) + { + break; + } + + // escaped? + if (ch != TJSONProtocolConstants.EscSequences[0]) + { + await buffer.WriteAsync(new[] {ch}, 0, 1, cancellationToken).ConfigureAwait(false); + continue; + } + + // distinguish between \uXXXX and \? + ch = await Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + if (ch != TJSONProtocolConstants.EscSequences[1]) // control chars like \n + { + var off = Array.IndexOf(TJSONProtocolConstants.EscapeChars, (char) ch); + if (off == -1) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, "Expected control char"); + } + ch = TJSONProtocolConstants.EscapeCharValues[off]; + await buffer.WriteAsync(new[] {ch}, 0, 1, cancellationToken).ConfigureAwait(false); + continue; + } + + // it's \uXXXX + await Trans.ReadAllAsync(_tempBuffer, 0, 4, cancellationToken).ConfigureAwait(false); + + var wch = (short) ((TJSONProtocolHelper.ToHexVal(_tempBuffer[0]) << 12) + + (TJSONProtocolHelper.ToHexVal(_tempBuffer[1]) << 8) + + (TJSONProtocolHelper.ToHexVal(_tempBuffer[2]) << 4) + + TJSONProtocolHelper.ToHexVal(_tempBuffer[3])); + + if (char.IsHighSurrogate((char) wch)) + { + if (codeunits.Count > 0) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, "Expected low surrogate char"); + } + codeunits.Add((char) wch); + } + else if (char.IsLowSurrogate((char) wch)) + { + if (codeunits.Count == 0) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, "Expected high surrogate char"); + } + + codeunits.Add((char) wch); + var tmp = Utf8Encoding.GetBytes(codeunits.ToArray()); + await buffer.WriteAsync(tmp, 0, tmp.Length, cancellationToken).ConfigureAwait(false); + codeunits.Clear(); + } + else + { + var tmp = Utf8Encoding.GetBytes(new[] {(char) wch}); + await buffer.WriteAsync(tmp, 0, tmp.Length, cancellationToken).ConfigureAwait(false); + } + } + + if (codeunits.Count > 0) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, "Expected low surrogate char"); + } + + return buffer.ToArray(); + } + } + + /// + /// Read in a sequence of characters that are all valid in JSON numbers. Does + /// not do a complete regex check to validate that this is actually a number. + /// + private async Task ReadJsonNumericCharsAsync(CancellationToken cancellationToken) + { + var strbld = new StringBuilder(); + while (true) + { + //TODO: workaround for primitive types with TJsonProtocol, think - how to rewrite into more easy form without exceptions + try + { + var ch = await Reader.PeekAsync(cancellationToken).ConfigureAwait(false); + if (!TJSONProtocolHelper.IsJsonNumeric(ch)) + { + break; + } + var c = (char)await Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + strbld.Append(c); + } + catch (TTransportException) + { + break; + } + } + return strbld.ToString(); + } + + /// + /// Read in a JSON number. If the context dictates, Read in enclosing quotes. + /// + private async Task ReadJsonIntegerAsync(CancellationToken cancellationToken) + { + await Context.ReadAsync(cancellationToken).ConfigureAwait(false); + if (Context.EscapeNumbers()) + { + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + var str = await ReadJsonNumericCharsAsync(cancellationToken).ConfigureAwait(false); + if (Context.EscapeNumbers()) + { + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + try + { + return long.Parse(str); + } + catch (FormatException) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, "Bad data encounted in numeric data"); + } + } + + /// + /// Read in a JSON double value. Throw if the value is not wrapped in quotes + /// when expected or if wrapped in quotes when not expected. + /// + private async Task ReadJsonDoubleAsync(CancellationToken cancellationToken) + { + await Context.ReadAsync(cancellationToken).ConfigureAwait(false); + if (await Reader.PeekAsync(cancellationToken) == TJSONProtocolConstants.Quote[0]) + { + var arr = await ReadJsonStringAsync(true, cancellationToken).ConfigureAwait(false); + var dub = double.Parse(Utf8Encoding.GetString(arr, 0, arr.Length), CultureInfo.InvariantCulture); + + if (!Context.EscapeNumbers() && !double.IsNaN(dub) && !double.IsInfinity(dub)) + { + // Throw exception -- we should not be in a string in this case + throw new TProtocolException(TProtocolException.INVALID_DATA, "Numeric data unexpectedly quoted"); + } + + return dub; + } + + if (Context.EscapeNumbers()) + { + // This will throw - we should have had a quote if escapeNum == true + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.Quote, cancellationToken).ConfigureAwait(false); + } + + try + { + return double.Parse(await ReadJsonNumericCharsAsync(cancellationToken).ConfigureAwait(false), CultureInfo.InvariantCulture); + } + catch (FormatException) + { + throw new TProtocolException(TProtocolException.INVALID_DATA, "Bad data encounted in numeric data"); + } + } + + /// + /// Read in a JSON string containing base-64 encoded data and decode it. + /// + private async Task ReadJsonBase64Async(CancellationToken cancellationToken) + { + var b = await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false); + var len = b.Length; + var off = 0; + var size = 0; + + // reduce len to ignore fill bytes + while ((len > 0) && (b[len - 1] == '=')) + { + --len; + } + + // read & decode full byte triplets = 4 source bytes + while (len > 4) + { + // Decode 4 bytes at a time + TBase64Helper.Decode(b, off, 4, b, size); // NB: decoded in place + off += 4; + len -= 4; + size += 3; + } + + // Don't decode if we hit the end or got a single leftover byte (invalid + // base64 but legal for skip of regular string exType) + if (len > 1) + { + // Decode remainder + TBase64Helper.Decode(b, off, len, b, size); // NB: decoded in place + size += len - 1; + } + + // Sadly we must copy the byte[] (any way around this?) + var result = new byte[size]; + Array.Copy(b, 0, result, 0, size); + return result; + } + + private async Task ReadJsonObjectStartAsync(CancellationToken cancellationToken) + { + await Context.ReadAsync(cancellationToken).ConfigureAwait(false); + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.LeftBrace, cancellationToken).ConfigureAwait(false); + PushContext(new JSONPairContext(this)); + } + + private async Task ReadJsonObjectEndAsync(CancellationToken cancellationToken) + { + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.RightBrace, cancellationToken).ConfigureAwait(false); + PopContext(); + } + + private async Task ReadJsonArrayStartAsync(CancellationToken cancellationToken) + { + await Context.ReadAsync(cancellationToken).ConfigureAwait(false); + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.LeftBracket, cancellationToken).ConfigureAwait(false); + PushContext(new JSONListContext(this)); + } + + private async Task ReadJsonArrayEndAsync(CancellationToken cancellationToken) + { + await ReadJsonSyntaxCharAsync(TJSONProtocolConstants.RightBracket, cancellationToken).ConfigureAwait(false); + PopContext(); + } + + public override async Task ReadMessageBeginAsync(CancellationToken cancellationToken) + { + var message = new TMessage(); + await ReadJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + if (await ReadJsonIntegerAsync(cancellationToken) != Version) + { + throw new TProtocolException(TProtocolException.BAD_VERSION, "Message contained bad version."); + } + + var buf = await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false); + message.Name = Utf8Encoding.GetString(buf, 0, buf.Length); + message.Type = (TMessageType) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + message.SeqID = (int) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + return message; + } + + public override async Task ReadMessageEndAsync(CancellationToken cancellationToken) + { + await ReadJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadStructBeginAsync(CancellationToken cancellationToken) + { + await ReadJsonObjectStartAsync(cancellationToken).ConfigureAwait(false); + return new TStruct(); + } + + public override async Task ReadStructEndAsync(CancellationToken cancellationToken) + { + await ReadJsonObjectEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadFieldBeginAsync(CancellationToken cancellationToken) + { + var field = new TField(); + var ch = await Reader.PeekAsync(cancellationToken).ConfigureAwait(false); + if (ch == TJSONProtocolConstants.RightBrace[0]) + { + field.Type = TType.Stop; + } + else + { + field.ID = (short) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + await ReadJsonObjectStartAsync(cancellationToken).ConfigureAwait(false); + field.Type = TJSONProtocolHelper.GetTypeIdForTypeName(await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false)); + } + return field; + } + + public override async Task ReadFieldEndAsync(CancellationToken cancellationToken) + { + await ReadJsonObjectEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadMapBeginAsync(CancellationToken cancellationToken) + { + var map = new TMap(); + await ReadJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + map.KeyType = TJSONProtocolHelper.GetTypeIdForTypeName(await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false)); + map.ValueType = TJSONProtocolHelper.GetTypeIdForTypeName(await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false)); + map.Count = (int) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + await ReadJsonObjectStartAsync(cancellationToken).ConfigureAwait(false); + return map; + } + + public override async Task ReadMapEndAsync(CancellationToken cancellationToken) + { + await ReadJsonObjectEndAsync(cancellationToken).ConfigureAwait(false); + await ReadJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadListBeginAsync(CancellationToken cancellationToken) + { + var list = new TList(); + await ReadJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + list.ElementType = TJSONProtocolHelper.GetTypeIdForTypeName(await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false)); + list.Count = (int) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + return list; + } + + public override async Task ReadListEndAsync(CancellationToken cancellationToken) + { + await ReadJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadSetBeginAsync(CancellationToken cancellationToken) + { + var set = new TSet(); + await ReadJsonArrayStartAsync(cancellationToken).ConfigureAwait(false); + set.ElementType = TJSONProtocolHelper.GetTypeIdForTypeName(await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false)); + set.Count = (int) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + return set; + } + + public override async Task ReadSetEndAsync(CancellationToken cancellationToken) + { + await ReadJsonArrayEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadBoolAsync(CancellationToken cancellationToken) + { + return (await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false)) != 0; + } + + public override async Task ReadByteAsync(CancellationToken cancellationToken) + { + return (sbyte) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadI16Async(CancellationToken cancellationToken) + { + return (short) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadI32Async(CancellationToken cancellationToken) + { + return (int) await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadI64Async(CancellationToken cancellationToken) + { + return await ReadJsonIntegerAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadDoubleAsync(CancellationToken cancellationToken) + { + return await ReadJsonDoubleAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadStringAsync(CancellationToken cancellationToken) + { + var buf = await ReadJsonStringAsync(false, cancellationToken).ConfigureAwait(false); + return Utf8Encoding.GetString(buf, 0, buf.Length); + } + + public override async Task ReadBinaryAsync(CancellationToken cancellationToken) + { + return await ReadJsonBase64Async(cancellationToken).ConfigureAwait(false); + } + + /// + /// Factory for JSON protocol objects + /// + public class Factory : ITProtocolFactory + { + public TProtocol GetProtocol(TClientTransport trans) + { + return new TJsonProtocol(trans); + } + } + + /// + /// Base class for tracking JSON contexts that may require + /// inserting/Reading additional JSON syntax characters + /// This base context does nothing. + /// + protected class JSONBaseContext + { + protected TJsonProtocol Proto; + + public JSONBaseContext(TJsonProtocol proto) + { + Proto = proto; + } + + public virtual async Task WriteAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public virtual async Task ReadAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public virtual bool EscapeNumbers() + { + return false; + } + } + + /// + /// Context for JSON lists. Will insert/Read commas before each item except + /// for the first one + /// + protected class JSONListContext : JSONBaseContext + { + private bool _first = true; + + public JSONListContext(TJsonProtocol protocol) + : base(protocol) + { + } + + public override async Task WriteAsync(CancellationToken cancellationToken) + { + if (_first) + { + _first = false; + } + else + { + await Proto.Trans.WriteAsync(TJSONProtocolConstants.Comma, cancellationToken).ConfigureAwait(false); + } + } + + public override async Task ReadAsync(CancellationToken cancellationToken) + { + if (_first) + { + _first = false; + } + else + { + await Proto.ReadJsonSyntaxCharAsync(TJSONProtocolConstants.Comma, cancellationToken).ConfigureAwait(false); + } + } + } + + /// + /// Context for JSON records. Will insert/Read colons before the value portion + /// of each record pair, and commas before each key except the first. In + /// addition, will indicate that numbers in the key position need to be + /// escaped in quotes (since JSON keys must be strings). + /// + // ReSharper disable once InconsistentNaming + protected class JSONPairContext : JSONBaseContext + { + private bool _colon = true; + + private bool _first = true; + + public JSONPairContext(TJsonProtocol proto) + : base(proto) + { + } + + public override async Task WriteAsync(CancellationToken cancellationToken) + { + if (_first) + { + _first = false; + _colon = true; + } + else + { + await Proto.Trans.WriteAsync(_colon ? TJSONProtocolConstants.Colon : TJSONProtocolConstants.Comma, cancellationToken).ConfigureAwait(false); + _colon = !_colon; + } + } + + public override async Task ReadAsync(CancellationToken cancellationToken) + { + if (_first) + { + _first = false; + _colon = true; + } + else + { + await Proto.ReadJsonSyntaxCharAsync(_colon ? TJSONProtocolConstants.Colon : TJSONProtocolConstants.Comma, cancellationToken).ConfigureAwait(false); + _colon = !_colon; + } + } + + public override bool EscapeNumbers() + { + return _colon; + } + } + + /// + /// Holds up to one byte from the transport + /// + protected class LookaheadReader + { + private readonly byte[] _data = new byte[1]; + + private bool _hasData; + protected TJsonProtocol Proto; + + public LookaheadReader(TJsonProtocol proto) + { + Proto = proto; + } + + /// + /// Return and consume the next byte to be Read, either taking it from the + /// data buffer if present or getting it from the transport otherwise. + /// + public async Task ReadAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (_hasData) + { + _hasData = false; + } + else + { + // find more easy way to avoid exception on reading primitive types + await Proto.Trans.ReadAllAsync(_data, 0, 1, cancellationToken).ConfigureAwait(false); + } + return _data[0]; + } + + /// + /// Return the next byte to be Read without consuming, filling the data + /// buffer if it has not been filled alReady. + /// + public async Task PeekAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (!_hasData) + { + // find more easy way to avoid exception on reading primitive types + await Proto.Trans.ReadAllAsync(_data, 0, 1, cancellationToken).ConfigureAwait(false); + } + _hasData = true; + return _data[0]; + } + } + } +} diff --git a/lib/Thrift/Protocols/TMultiplexedProtocol.cs b/lib/Thrift/Protocols/TMultiplexedProtocol.cs new file mode 100644 index 00000000000..38f1c6ca0d8 --- /dev/null +++ b/lib/Thrift/Protocols/TMultiplexedProtocol.cs @@ -0,0 +1,91 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; + +namespace Thrift.Protocols +{ + /** + * TMultiplexedProtocol is a protocol-independent concrete decorator that allows a Thrift + * client to communicate with a multiplexing Thrift server, by prepending the service name + * to the function name during function calls. + * + * NOTE: THIS IS NOT TO BE USED BY SERVERS. + * On the server, use TMultiplexedProcessor to handle requests from a multiplexing client. + * + * This example uses a single socket transport to invoke two services: + * + * TSocketClientTransport transport = new TSocketClientTransport("localhost", 9090); + * transport.open(); + * + * TBinaryProtocol protocol = new TBinaryProtocol(transport); + * + * TMultiplexedProtocol mp = new TMultiplexedProtocol(protocol, "Calculator"); + * Calculator.Client service = new Calculator.Client(mp); + * + * TMultiplexedProtocol mp2 = new TMultiplexedProtocol(protocol, "WeatherReport"); + * WeatherReport.Client service2 = new WeatherReport.Client(mp2); + * + * System.out.println(service.add(2,2)); + * System.out.println(service2.getTemperature()); + * + */ + + //TODO: implementation of TProtocol + + // ReSharper disable once InconsistentNaming + public class TMultiplexedProtocol : TProtocolDecorator + { + /** Used to delimit the service name from the function name */ + public const string Separator = ":"; + + private readonly string _serviceName; + + /** + * Wrap the specified protocol, allowing it to be used to communicate with a + * multiplexing server. The serviceName is required as it is + * prepended to the message header so that the multiplexing server can broker + * the function call to the proper service. + * + * Args: + * protocol Your communication protocol of choice, e.g. TBinaryProtocol + * serviceName The service name of the service communicating via this protocol. + */ + + public TMultiplexedProtocol(TProtocol protocol, string serviceName) + : base(protocol) + { + _serviceName = serviceName; + } + + public override async Task WriteMessageBeginAsync(TMessage message, CancellationToken cancellationToken) + { + switch (message.Type) + { + case TMessageType.Call: + case TMessageType.Oneway: + await base.WriteMessageBeginAsync(new TMessage($"{_serviceName}{Separator}{message.Name}", message.Type, message.SeqID), cancellationToken).ConfigureAwait(false); + break; + default: + await base.WriteMessageBeginAsync(message, cancellationToken).ConfigureAwait(false); + break; + } + } + } +} diff --git a/lib/Thrift/Protocols/TProtocol.cs b/lib/Thrift/Protocols/TProtocol.cs new file mode 100644 index 00000000000..5bda512f9a3 --- /dev/null +++ b/lib/Thrift/Protocols/TProtocol.cs @@ -0,0 +1,376 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; +using Thrift.Transports; + +namespace Thrift.Protocols +{ + // ReSharper disable once InconsistentNaming + public abstract class TProtocol : IDisposable + { + public const int DefaultRecursionDepth = 64; + private bool _isDisposed; + protected int RecursionDepth; + + protected TClientTransport Trans; + + protected TProtocol(TClientTransport trans) + { + Trans = trans; + RecursionLimit = DefaultRecursionDepth; + RecursionDepth = 0; + } + + public TClientTransport Transport => Trans; + + protected int RecursionLimit { get; set; } + + public void Dispose() + { + Dispose(true); + } + + public void IncrementRecursionDepth() + { + if (RecursionDepth < RecursionLimit) + { + ++RecursionDepth; + } + else + { + throw new TProtocolException(TProtocolException.DEPTH_LIMIT, "Depth limit exceeded"); + } + } + + public void DecrementRecursionDepth() + { + --RecursionDepth; + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + (Trans as IDisposable)?.Dispose(); + } + } + _isDisposed = true; + } + + public virtual async Task WriteMessageBeginAsync(TMessage message) + { + await WriteMessageBeginAsync(message, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteMessageBeginAsync(TMessage message, CancellationToken cancellationToken); + + public virtual async Task WriteMessageEndAsync() + { + await WriteMessageEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteMessageEndAsync(CancellationToken cancellationToken); + + public virtual async Task WriteStructBeginAsync(TStruct @struct) + { + await WriteStructBeginAsync(@struct, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteStructBeginAsync(TStruct @struct, CancellationToken cancellationToken); + + public virtual async Task WriteStructEndAsync() + { + await WriteStructEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteStructEndAsync(CancellationToken cancellationToken); + + public virtual async Task WriteFieldBeginAsync(TField field) + { + await WriteFieldBeginAsync(field, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteFieldBeginAsync(TField field, CancellationToken cancellationToken); + + public virtual async Task WriteFieldEndAsync() + { + await WriteFieldEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteFieldEndAsync(CancellationToken cancellationToken); + + public virtual async Task WriteFieldStopAsync() + { + await WriteFieldStopAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteFieldStopAsync(CancellationToken cancellationToken); + + public virtual async Task WriteMapBeginAsync(TMap map) + { + await WriteMapBeginAsync(map, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteMapBeginAsync(TMap map, CancellationToken cancellationToken); + + public virtual async Task WriteMapEndAsync() + { + await WriteMapEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteMapEndAsync(CancellationToken cancellationToken); + + public virtual async Task WriteListBeginAsync(TList list) + { + await WriteListBeginAsync(list, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteListBeginAsync(TList list, CancellationToken cancellationToken); + + public virtual async Task WriteListEndAsync() + { + await WriteListEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteListEndAsync(CancellationToken cancellationToken); + + public virtual async Task WriteSetBeginAsync(TSet set) + { + await WriteSetBeginAsync(set, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteSetBeginAsync(TSet set, CancellationToken cancellationToken); + + public virtual async Task WriteSetEndAsync() + { + await WriteSetEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteSetEndAsync(CancellationToken cancellationToken); + + public virtual async Task WriteBoolAsync(bool b) + { + await WriteBoolAsync(b, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteBoolAsync(bool b, CancellationToken cancellationToken); + + public virtual async Task WriteByteAsync(sbyte b) + { + await WriteByteAsync(b, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteByteAsync(sbyte b, CancellationToken cancellationToken); + + public virtual async Task WriteI16Async(short i16) + { + await WriteI16Async(i16, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteI16Async(short i16, CancellationToken cancellationToken); + + public virtual async Task WriteI32Async(int i32) + { + await WriteI32Async(i32, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteI32Async(int i32, CancellationToken cancellationToken); + + public virtual async Task WriteI64Async(long i64) + { + await WriteI64Async(i64, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteI64Async(long i64, CancellationToken cancellationToken); + + public virtual async Task WriteDoubleAsync(double d) + { + await WriteDoubleAsync(d, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteDoubleAsync(double d, CancellationToken cancellationToken); + + public virtual async Task WriteStringAsync(string s) + { + await WriteStringAsync(s, CancellationToken.None).ConfigureAwait(false); + } + + public virtual async Task WriteStringAsync(string s, CancellationToken cancellationToken) + { + var bytes = Encoding.UTF8.GetBytes(s); + await WriteBinaryAsync(bytes, cancellationToken).ConfigureAwait(false); + } + + public virtual async Task WriteBinaryAsync(byte[] bytes) + { + await WriteBinaryAsync(bytes, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteBinaryAsync(byte[] bytes, CancellationToken cancellationToken); + + public virtual async Task ReadMessageBeginAsync() + { + return await ReadMessageBeginAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadMessageBeginAsync(CancellationToken cancellationToken); + + public virtual async Task ReadMessageEndAsync() + { + await ReadMessageEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadMessageEndAsync(CancellationToken cancellationToken); + + public virtual async Task ReadStructBeginAsync() + { + return await ReadStructBeginAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadStructBeginAsync(CancellationToken cancellationToken); + + public virtual async Task ReadStructEndAsync() + { + await ReadStructEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadStructEndAsync(CancellationToken cancellationToken); + + public virtual async Task ReadFieldBeginAsync() + { + return await ReadFieldBeginAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadFieldBeginAsync(CancellationToken cancellationToken); + + public virtual async Task ReadFieldEndAsync() + { + await ReadFieldEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadFieldEndAsync(CancellationToken cancellationToken); + + public virtual async Task ReadMapBeginAsync() + { + return await ReadMapBeginAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadMapBeginAsync(CancellationToken cancellationToken); + + public virtual async Task ReadMapEndAsync() + { + await ReadMapEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadMapEndAsync(CancellationToken cancellationToken); + + public virtual async Task ReadListBeginAsync() + { + return await ReadListBeginAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadListBeginAsync(CancellationToken cancellationToken); + + public virtual async Task ReadListEndAsync() + { + await ReadListEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadListEndAsync(CancellationToken cancellationToken); + + public virtual async Task ReadSetBeginAsync() + { + return await ReadSetBeginAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadSetBeginAsync(CancellationToken cancellationToken); + + public virtual async Task ReadSetEndAsync() + { + await ReadSetEndAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadSetEndAsync(CancellationToken cancellationToken); + + public virtual async Task ReadBoolAsync() + { + return await ReadBoolAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadBoolAsync(CancellationToken cancellationToken); + + public virtual async Task ReadByteAsync() + { + return await ReadByteAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadByteAsync(CancellationToken cancellationToken); + + public virtual async Task ReadI16Async() + { + return await ReadI16Async(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadI16Async(CancellationToken cancellationToken); + + public virtual async Task ReadI32Async() + { + return await ReadI32Async(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadI32Async(CancellationToken cancellationToken); + + public virtual async Task ReadI64Async() + { + return await ReadI64Async(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadI64Async(CancellationToken cancellationToken); + + public virtual async Task ReadDoubleAsync() + { + return await ReadDoubleAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadDoubleAsync(CancellationToken cancellationToken); + + public virtual async Task ReadStringAsync() + { + return await ReadStringAsync(CancellationToken.None).ConfigureAwait(false); + } + + public virtual async Task ReadStringAsync(CancellationToken cancellationToken) + { + var buf = await ReadBinaryAsync(cancellationToken).ConfigureAwait(false); + return Encoding.UTF8.GetString(buf, 0, buf.Length); + } + + public virtual async Task ReadBinaryAsync() + { + return await ReadBinaryAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadBinaryAsync(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/TProtocolDecorator.cs b/lib/Thrift/Protocols/TProtocolDecorator.cs new file mode 100644 index 00000000000..b1c47a9200e --- /dev/null +++ b/lib/Thrift/Protocols/TProtocolDecorator.cs @@ -0,0 +1,247 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; + +namespace Thrift.Protocols +{ + // ReSharper disable once InconsistentNaming + /// + /// TProtocolDecorator forwards all requests to an enclosed TProtocol instance, + /// providing a way to author concise concrete decorator subclasses.While it has + /// no abstract methods, it is marked abstract as a reminder that by itself, + /// it does not modify the behaviour of the enclosed TProtocol. + /// + public abstract class TProtocolDecorator : TProtocol + { + private readonly TProtocol _wrappedProtocol; + + protected TProtocolDecorator(TProtocol protocol) + : base(protocol.Transport) + { + _wrappedProtocol = protocol ?? throw new ArgumentNullException(nameof(protocol)); + } + + public override async Task WriteMessageBeginAsync(TMessage message, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteMessageBeginAsync(message, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMessageEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteMessageEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStructBeginAsync(TStruct @struct, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteStructBeginAsync(@struct, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStructEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteStructEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldBeginAsync(TField field, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteFieldBeginAsync(field, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteFieldEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteFieldStopAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteFieldStopAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMapBeginAsync(TMap map, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteMapBeginAsync(map, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteMapEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteMapEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteListBeginAsync(TList list, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteListBeginAsync(list, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteListEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteListEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteSetBeginAsync(TSet set, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteSetBeginAsync(set, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteSetEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteSetEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteBoolAsync(bool b, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteBoolAsync(b, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteByteAsync(sbyte b, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteByteAsync(b, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI16Async(short i16, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteI16Async(i16, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI32Async(int i32, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteI32Async(i32, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteI64Async(long i64, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteI64Async(i64, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteDoubleAsync(double d, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteDoubleAsync(d, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteStringAsync(string s, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteStringAsync(s, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteBinaryAsync(byte[] bytes, CancellationToken cancellationToken) + { + await _wrappedProtocol.WriteBinaryAsync(bytes, cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadMessageBeginAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadMessageBeginAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadMessageEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.ReadMessageEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadStructBeginAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadStructBeginAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadStructEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.ReadStructEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadFieldBeginAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadFieldBeginAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadFieldEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.ReadFieldEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadMapBeginAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadMapBeginAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadMapEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.ReadMapEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadListBeginAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadListBeginAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadListEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.ReadListEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadSetBeginAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadSetBeginAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadSetEndAsync(CancellationToken cancellationToken) + { + await _wrappedProtocol.ReadSetEndAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadBoolAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadBoolAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadByteAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadByteAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadI16Async(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadI16Async(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadI32Async(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadI32Async(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadI64Async(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadI64Async(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadDoubleAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadDoubleAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadStringAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadStringAsync(cancellationToken).ConfigureAwait(false); + } + + public override async Task ReadBinaryAsync(CancellationToken cancellationToken) + { + return await _wrappedProtocol.ReadBinaryAsync(cancellationToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/TProtocolException.cs b/lib/Thrift/Protocols/TProtocolException.cs new file mode 100644 index 00000000000..8c67c3bfdf9 --- /dev/null +++ b/lib/Thrift/Protocols/TProtocolException.cs @@ -0,0 +1,59 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +// ReSharper disable InconsistentNaming +namespace Thrift.Protocols +{ + public class TProtocolException : TException + { + // do not rename public constants - they used in generated files + public const int UNKNOWN = 0; + public const int INVALID_DATA = 1; + public const int NEGATIVE_SIZE = 2; + public const int SIZE_LIMIT = 3; + public const int BAD_VERSION = 4; + public const int NOT_IMPLEMENTED = 5; + public const int DEPTH_LIMIT = 6; + + protected int Type = UNKNOWN; + + public TProtocolException() + { + } + + public TProtocolException(int type) + { + Type = type; + } + + public TProtocolException(int type, string message) + : base(message) + { + Type = type; + } + + public TProtocolException(string message) + : base(message) + { + } + + public int GetExceptionType() + { + return Type; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Utilities/TBase64Helper.cs b/lib/Thrift/Protocols/Utilities/TBase64Helper.cs new file mode 100644 index 00000000000..7eff5e181d1 --- /dev/null +++ b/lib/Thrift/Protocols/Utilities/TBase64Helper.cs @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; + +namespace Thrift.Protocols.Utilities +{ + // ReSharper disable once InconsistentNaming + internal static class TBase64Helper + { + //TODO: Constants + //TODO: Check for args + //TODO: Unitests + + internal const string EncodeTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + private static readonly int[] DecodeTable = + { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + }; + + internal static void Encode(byte[] src, int srcOff, int len, byte[] dst, int dstOff) + { + if (src == null) + { + throw new ArgumentNullException(nameof(src)); + } + + dst[dstOff] = (byte) EncodeTable[(src[srcOff] >> 2) & 0x3F]; + + if (len == 3) + { + dst[dstOff + 1] = (byte) EncodeTable[((src[srcOff] << 4) & 0x30) | ((src[srcOff + 1] >> 4) & 0x0F)]; + dst[dstOff + 2] = (byte) EncodeTable[((src[srcOff + 1] << 2) & 0x3C) | ((src[srcOff + 2] >> 6) & 0x03)]; + dst[dstOff + 3] = (byte) EncodeTable[src[srcOff + 2] & 0x3F]; + } + else if (len == 2) + { + dst[dstOff + 1] = (byte) EncodeTable[((src[srcOff] << 4) & 0x30) | ((src[srcOff + 1] >> 4) & 0x0F)]; + dst[dstOff + 2] = (byte) EncodeTable[(src[srcOff + 1] << 2) & 0x3C]; + } + else + { + // len == 1 + dst[dstOff + 1] = (byte) EncodeTable[(src[srcOff] << 4) & 0x30]; + } + } + + internal static void Decode(byte[] src, int srcOff, int len, byte[] dst, int dstOff) + { + if (src == null) + { + throw new ArgumentNullException(nameof(src)); + } + + dst[dstOff] = (byte) ((DecodeTable[src[srcOff] & 0x0FF] << 2) | (DecodeTable[src[srcOff + 1] & 0x0FF] >> 4)); + + if (len > 2) + { + dst[dstOff + 1] = + (byte) + (((DecodeTable[src[srcOff + 1] & 0x0FF] << 4) & 0xF0) | (DecodeTable[src[srcOff + 2] & 0x0FF] >> 2)); + if (len > 3) + { + dst[dstOff + 2] = + (byte) + (((DecodeTable[src[srcOff + 2] & 0x0FF] << 6) & 0xC0) | DecodeTable[src[srcOff + 3] & 0x0FF]); + } + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Utilities/TBase64Utils.cs b/lib/Thrift/Protocols/Utilities/TBase64Utils.cs new file mode 100644 index 00000000000..15fd45cbef9 --- /dev/null +++ b/lib/Thrift/Protocols/Utilities/TBase64Utils.cs @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; + +namespace Thrift.Protocols.Utilities +{ + // ReSharper disable once InconsistentNaming + internal static class TBase64Utils + { + //TODO: Constants + //TODO: Check for args + //TODO: Unitests + + internal const string EncodeTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + private static readonly int[] DecodeTable = + { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + }; + + internal static void Encode(byte[] src, int srcOff, int len, byte[] dst, int dstOff) + { + if (src == null) + { + throw new ArgumentNullException(nameof(src)); + } + + dst[dstOff] = (byte) EncodeTable[(src[srcOff] >> 2) & 0x3F]; + + if (len == 3) + { + dst[dstOff + 1] = (byte) EncodeTable[((src[srcOff] << 4) & 0x30) | ((src[srcOff + 1] >> 4) & 0x0F)]; + dst[dstOff + 2] = (byte) EncodeTable[((src[srcOff + 1] << 2) & 0x3C) | ((src[srcOff + 2] >> 6) & 0x03)]; + dst[dstOff + 3] = (byte) EncodeTable[src[srcOff + 2] & 0x3F]; + } + else if (len == 2) + { + dst[dstOff + 1] = (byte) EncodeTable[((src[srcOff] << 4) & 0x30) | ((src[srcOff + 1] >> 4) & 0x0F)]; + dst[dstOff + 2] = (byte) EncodeTable[(src[srcOff + 1] << 2) & 0x3C]; + } + else + { + // len == 1 + dst[dstOff + 1] = (byte) EncodeTable[(src[srcOff] << 4) & 0x30]; + } + } + + internal static void Decode(byte[] src, int srcOff, int len, byte[] dst, int dstOff) + { + if (src == null) + { + throw new ArgumentNullException(nameof(src)); + } + + dst[dstOff] = (byte) ((DecodeTable[src[srcOff] & 0x0FF] << 2) | (DecodeTable[src[srcOff + 1] & 0x0FF] >> 4)); + + if (len > 2) + { + dst[dstOff + 1] = + (byte) + (((DecodeTable[src[srcOff + 1] & 0x0FF] << 4) & 0xF0) | (DecodeTable[src[srcOff + 2] & 0x0FF] >> 2)); + if (len > 3) + { + dst[dstOff + 2] = + (byte) + (((DecodeTable[src[srcOff + 2] & 0x0FF] << 6) & 0xC0) | DecodeTable[src[srcOff + 3] & 0x0FF]); + } + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Utilities/TJsonProtocolConstants.cs b/lib/Thrift/Protocols/Utilities/TJsonProtocolConstants.cs new file mode 100644 index 00000000000..93eff785555 --- /dev/null +++ b/lib/Thrift/Protocols/Utilities/TJsonProtocolConstants.cs @@ -0,0 +1,61 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Protocols.Utilities +{ + // ReSharper disable once InconsistentNaming + public static class TJSONProtocolConstants + { + //TODO Check for performance for reusing ImmutableArray from System.Collections.Immutable (https://blogs.msdn.microsoft.com/dotnet/2013/06/24/please-welcome-immutablearrayt/) + // can be possible to get better performance and also better GC + + public static readonly byte[] Comma = {(byte) ','}; + public static readonly byte[] Colon = {(byte) ':'}; + public static readonly byte[] LeftBrace = {(byte) '{'}; + public static readonly byte[] RightBrace = {(byte) '}'}; + public static readonly byte[] LeftBracket = {(byte) '['}; + public static readonly byte[] RightBracket = {(byte) ']'}; + public static readonly byte[] Quote = {(byte) '"'}; + public static readonly byte[] Backslash = {(byte) '\\'}; + + public static readonly byte[] JsonCharTable = + { + 0, 0, 0, 0, 0, 0, 0, 0, (byte) 'b', (byte) 't', (byte) 'n', 0, (byte) 'f', (byte) 'r', 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, (byte) '"', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + }; + + public static readonly char[] EscapeChars = "\"\\/bfnrt".ToCharArray(); + public static readonly byte[] EscapeCharValues = {(byte) '"', (byte) '\\', (byte) '/', (byte) '\b', (byte) '\f', (byte) '\n', (byte) '\r', (byte) '\t'}; + public static readonly byte[] EscSequences = {(byte) '\\', (byte) 'u', (byte) '0', (byte) '0'}; + + public static class TypeNames + { + public static readonly byte[] NameBool = { (byte)'t', (byte)'f' }; + public static readonly byte[] NameByte = { (byte)'i', (byte)'8' }; + public static readonly byte[] NameI16 = { (byte)'i', (byte)'1', (byte)'6' }; + public static readonly byte[] NameI32 = { (byte)'i', (byte)'3', (byte)'2' }; + public static readonly byte[] NameI64 = { (byte)'i', (byte)'6', (byte)'4' }; + public static readonly byte[] NameDouble = { (byte)'d', (byte)'b', (byte)'l' }; + public static readonly byte[] NameStruct = { (byte)'r', (byte)'e', (byte)'c' }; + public static readonly byte[] NameString = { (byte)'s', (byte)'t', (byte)'r' }; + public static readonly byte[] NameMap = { (byte)'m', (byte)'a', (byte)'p' }; + public static readonly byte[] NameList = { (byte)'l', (byte)'s', (byte)'t' }; + public static readonly byte[] NameSet = { (byte)'s', (byte)'e', (byte)'t' }; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Utilities/TJsonProtocolHelper.cs b/lib/Thrift/Protocols/Utilities/TJsonProtocolHelper.cs new file mode 100644 index 00000000000..adc26a9af01 --- /dev/null +++ b/lib/Thrift/Protocols/Utilities/TJsonProtocolHelper.cs @@ -0,0 +1,176 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using Thrift.Protocols.Entities; + +namespace Thrift.Protocols.Utilities +{ + // ReSharper disable once InconsistentNaming + public static class TJSONProtocolHelper + { + public static byte[] GetTypeNameForTypeId(TType typeId) + { + switch (typeId) + { + case TType.Bool: + return TJSONProtocolConstants.TypeNames.NameBool; + case TType.Byte: + return TJSONProtocolConstants.TypeNames.NameByte; + case TType.I16: + return TJSONProtocolConstants.TypeNames.NameI16; + case TType.I32: + return TJSONProtocolConstants.TypeNames.NameI32; + case TType.I64: + return TJSONProtocolConstants.TypeNames.NameI64; + case TType.Double: + return TJSONProtocolConstants.TypeNames.NameDouble; + case TType.String: + return TJSONProtocolConstants.TypeNames.NameString; + case TType.Struct: + return TJSONProtocolConstants.TypeNames.NameStruct; + case TType.Map: + return TJSONProtocolConstants.TypeNames.NameMap; + case TType.Set: + return TJSONProtocolConstants.TypeNames.NameSet; + case TType.List: + return TJSONProtocolConstants.TypeNames.NameList; + default: + throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED, "Unrecognized exType"); + } + } + + public static TType GetTypeIdForTypeName(byte[] name) + { + var result = TType.Stop; + if (name.Length > 1) + { + switch (name[0]) + { + case (byte) 'd': + result = TType.Double; + break; + case (byte) 'i': + switch (name[1]) + { + case (byte) '8': + result = TType.Byte; + break; + case (byte) '1': + result = TType.I16; + break; + case (byte) '3': + result = TType.I32; + break; + case (byte) '6': + result = TType.I64; + break; + } + break; + case (byte) 'l': + result = TType.List; + break; + case (byte) 'm': + result = TType.Map; + break; + case (byte) 'r': + result = TType.Struct; + break; + case (byte) 's': + if (name[1] == (byte) 't') + { + result = TType.String; + } + else if (name[1] == (byte) 'e') + { + result = TType.Set; + } + break; + case (byte) 't': + result = TType.Bool; + break; + } + } + if (result == TType.Stop) + { + throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED, "Unrecognized exType"); + } + return result; + } + + /// + /// Return true if the given byte could be a valid part of a JSON number. + /// + public static bool IsJsonNumeric(byte b) + { + switch (b) + { + case (byte)'+': + case (byte)'-': + case (byte)'.': + case (byte)'0': + case (byte)'1': + case (byte)'2': + case (byte)'3': + case (byte)'4': + case (byte)'5': + case (byte)'6': + case (byte)'7': + case (byte)'8': + case (byte)'9': + case (byte)'E': + case (byte)'e': + return true; + default: + return false; + } + } + + /// + /// Convert a byte containing a hex char ('0'-'9' or 'a'-'f') into its + /// corresponding hex value + /// + public static byte ToHexVal(byte ch) + { + if (ch >= '0' && ch <= '9') + { + return (byte)((char)ch - '0'); + } + + if (ch >= 'a' && ch <= 'f') + { + ch += 10; + return (byte)((char)ch - 'a'); + } + + throw new TProtocolException(TProtocolException.INVALID_DATA, "Expected hex character"); + } + + /// + /// Convert a byte containing a hex value to its corresponding hex character + /// + public static byte ToHexChar(byte val) + { + val &= 0x0F; + if (val < 10) + { + return (byte)((char)val + '0'); + } + val -= 10; + return (byte)((char)val + 'a'); + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Protocols/Utilities/TProtocolUtil.cs b/lib/Thrift/Protocols/Utilities/TProtocolUtil.cs new file mode 100644 index 00000000000..ffc466c8825 --- /dev/null +++ b/lib/Thrift/Protocols/Utilities/TProtocolUtil.cs @@ -0,0 +1,110 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols.Entities; + +namespace Thrift.Protocols.Utilities +{ + // ReSharper disable once InconsistentNaming + public static class TProtocolUtil + { + public static async Task SkipAsync(TProtocol protocol, TType type, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + protocol.IncrementRecursionDepth(); + try + { + switch (type) + { + case TType.Bool: + await protocol.ReadBoolAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.Byte: + await protocol.ReadByteAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.I16: + await protocol.ReadI16Async(cancellationToken).ConfigureAwait(false); + break; + case TType.I32: + await protocol.ReadI32Async(cancellationToken).ConfigureAwait(false); + break; + case TType.I64: + await protocol.ReadI64Async(cancellationToken).ConfigureAwait(false); + break; + case TType.Double: + await protocol.ReadDoubleAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.String: + // Don't try to decode the string, just skip it. + await protocol.ReadBinaryAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.Struct: + await protocol.ReadStructBeginAsync(cancellationToken).ConfigureAwait(false); + while (true) + { + var field = await protocol.ReadFieldBeginAsync(cancellationToken).ConfigureAwait(false); + if (field.Type == TType.Stop) + { + break; + } + await SkipAsync(protocol, field.Type, cancellationToken).ConfigureAwait(false); + await protocol.ReadFieldEndAsync(cancellationToken).ConfigureAwait(false); + } + await protocol.ReadStructEndAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.Map: + var map = await protocol.ReadMapBeginAsync(cancellationToken).ConfigureAwait(false); + for (var i = 0; i < map.Count; i++) + { + await SkipAsync(protocol, map.KeyType, cancellationToken).ConfigureAwait(false); + await SkipAsync(protocol, map.ValueType, cancellationToken).ConfigureAwait(false); + } + await protocol.ReadMapEndAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.Set: + var set = await protocol.ReadSetBeginAsync(cancellationToken).ConfigureAwait(false); + for (var i = 0; i < set.Count; i++) + { + await SkipAsync(protocol, set.ElementType, cancellationToken).ConfigureAwait(false); + } + await protocol.ReadSetEndAsync(cancellationToken).ConfigureAwait(false); + break; + case TType.List: + var list = await protocol.ReadListBeginAsync(cancellationToken).ConfigureAwait(false); + for (var i = 0; i < list.Count; i++) + { + await SkipAsync(protocol, list.ElementType, cancellationToken).ConfigureAwait(false); + } + await protocol.ReadListEndAsync(cancellationToken).ConfigureAwait(false); + break; + default: + throw new TProtocolException(TProtocolException.INVALID_DATA, "Unknown data type " + type.ToString("d")); + } + } + finally + { + protocol.DecrementRecursionDepth(); + } + } + } +} diff --git a/lib/Thrift/README.md b/lib/Thrift/README.md new file mode 100644 index 00000000000..acd7ed2ceb9 --- /dev/null +++ b/lib/Thrift/README.md @@ -0,0 +1,12 @@ +# Thrift Protocol .Net Implementation + +This is the .net implementation of the Apache Thrift protocol. This code was forked from the [Jaeger Tracing C# Client Repository](https://github.com/jaegertracing/jaeger-client-csharp). + +Path: src/Thrift + +commitID: 0794ea71cb6e58f7bf0f0ef2c0c8ceceb1d8b6d9 + +The following changes were made to this fork: + +* ConfigureAwait(false) added to async calls to prevent deadlocks. +* THttpClientTransport uses WebRequestHandler() in .NET Framework 4.6 builds. \ No newline at end of file diff --git a/lib/Thrift/Server/TBaseServer.cs b/lib/Thrift/Server/TBaseServer.cs new file mode 100644 index 00000000000..f7af8174850 --- /dev/null +++ b/lib/Thrift/Server/TBaseServer.cs @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols; +using Thrift.Transports; + +namespace Thrift.Server +{ + // ReSharper disable once InconsistentNaming + public abstract class TBaseServer + { + protected ITProtocolFactory InputProtocolFactory; + protected TTransportFactory InputTransportFactory; + protected ITProcessorFactory ItProcessorFactory; + protected ITProtocolFactory OutputProtocolFactory; + protected TTransportFactory OutputTransportFactory; + + protected TServerEventHandler ServerEventHandler; + protected TServerTransport ServerTransport; + + protected TBaseServer(ITProcessorFactory itProcessorFactory, TServerTransport serverTransport, + TTransportFactory inputTransportFactory, TTransportFactory outputTransportFactory, + ITProtocolFactory inputProtocolFactory, ITProtocolFactory outputProtocolFactory) + { + ItProcessorFactory = itProcessorFactory ?? throw new ArgumentNullException(nameof(itProcessorFactory)); + ServerTransport = serverTransport; + InputTransportFactory = inputTransportFactory ?? throw new ArgumentNullException(nameof(inputTransportFactory)); + OutputTransportFactory = outputTransportFactory ?? throw new ArgumentNullException(nameof(outputTransportFactory)); + InputProtocolFactory = inputProtocolFactory ?? throw new ArgumentNullException(nameof(inputProtocolFactory)); + OutputProtocolFactory = outputProtocolFactory ?? throw new ArgumentNullException(nameof(outputProtocolFactory)); + } + + public void SetEventHandler(TServerEventHandler seh) + { + ServerEventHandler = seh; + } + + public TServerEventHandler GetEventHandler() + { + return ServerEventHandler; + } + + public abstract void Stop(); + + public virtual void Start() + { + // do nothing + } + + public virtual async Task ServeAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Server/TServerEventHandler.cs b/lib/Thrift/Server/TServerEventHandler.cs new file mode 100644 index 00000000000..733bb4bef7c --- /dev/null +++ b/lib/Thrift/Server/TServerEventHandler.cs @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols; +using Thrift.Transports; + +namespace Thrift.Server +{ + //TODO: replacement by event? + + /// + /// Interface implemented by server users to handle events from the server + /// + // ReSharper disable once InconsistentNaming + public interface TServerEventHandler + { + /// + /// Called before the server begins */ + /// + Task PreServeAsync(CancellationToken cancellationToken); + + /// + /// Called when a new client has connected and is about to being processing */ + /// + Task CreateContextAsync(TProtocol input, TProtocol output, CancellationToken cancellationToken); + + /// + /// Called when a client has finished request-handling to delete server context */ + /// + Task DeleteContextAsync(object serverContext, TProtocol input, TProtocol output, + CancellationToken cancellationToken); + + /// + /// Called when a client is about to call the processor */ + /// + Task ProcessContextAsync(object serverContext, TClientTransport transport, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/lib/Thrift/Settings.Stylecop b/lib/Thrift/Settings.Stylecop new file mode 100644 index 00000000000..e74fe5aed50 --- /dev/null +++ b/lib/Thrift/Settings.Stylecop @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/lib/Thrift/SingletonTProcessorFactory.cs b/lib/Thrift/SingletonTProcessorFactory.cs new file mode 100644 index 00000000000..c35123348ba --- /dev/null +++ b/lib/Thrift/SingletonTProcessorFactory.cs @@ -0,0 +1,38 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using Thrift.Server; +using Thrift.Transports; + +namespace Thrift +{ + // ReSharper disable once InconsistentNaming + public class SingletonTProcessorFactory : ITProcessorFactory + { + private readonly ITAsyncProcessor _tAsyncProcessor; + + public SingletonTProcessorFactory(ITAsyncProcessor tAsyncProcessor) + { + _tAsyncProcessor = tAsyncProcessor; + } + + public ITAsyncProcessor GetAsyncProcessor(TClientTransport trans, TBaseServer baseServer = null) + { + return _tAsyncProcessor; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/TApplicationException.cs b/lib/Thrift/TApplicationException.cs new file mode 100644 index 00000000000..7e96272239a --- /dev/null +++ b/lib/Thrift/TApplicationException.cs @@ -0,0 +1,150 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols; +using Thrift.Protocols.Entities; +using Thrift.Protocols.Utilities; + +namespace Thrift +{ + // ReSharper disable once InconsistentNaming + public class TApplicationException : TException + { + public enum ExceptionType + { + Unknown, + UnknownMethod, + InvalidMessageType, + WrongMethodName, + BadSequenceId, + MissingResult, + InternalError, + ProtocolError, + InvalidTransform, + InvalidProtocol, + UnsupportedClientType + } + + private const int MessageTypeFieldId = 1; + private const int ExTypeFieldId = 2; + + protected ExceptionType Type; + + public TApplicationException() + { + } + + public TApplicationException(ExceptionType type) + { + Type = type; + } + + public TApplicationException(ExceptionType type, string message) + : base(message) + { + Type = type; + } + + public static async Task ReadAsync(TProtocol inputProtocol, CancellationToken cancellationToken) + { + string message = null; + var type = ExceptionType.Unknown; + + await inputProtocol.ReadStructBeginAsync(cancellationToken).ConfigureAwait(false); + while (true) + { + var field = await inputProtocol.ReadFieldBeginAsync(cancellationToken).ConfigureAwait(false); + if (field.Type == TType.Stop) + { + break; + } + + switch (field.ID) + { + case MessageTypeFieldId: + if (field.Type == TType.String) + { + message = await inputProtocol.ReadStringAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await TProtocolUtil.SkipAsync(inputProtocol, field.Type, cancellationToken).ConfigureAwait(false); + } + break; + case ExTypeFieldId: + if (field.Type == TType.I32) + { + type = (ExceptionType) await inputProtocol.ReadI32Async(cancellationToken).ConfigureAwait(false); + } + else + { + await TProtocolUtil.SkipAsync(inputProtocol, field.Type, cancellationToken).ConfigureAwait(false); + } + break; + default: + await TProtocolUtil.SkipAsync(inputProtocol, field.Type, cancellationToken).ConfigureAwait(false); + break; + } + + await inputProtocol.ReadFieldEndAsync(cancellationToken).ConfigureAwait(false); + } + + await inputProtocol.ReadStructEndAsync(cancellationToken).ConfigureAwait(false); + + return new TApplicationException(type, message); + } + + public async Task WriteAsync(TProtocol outputProtocol, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + const string messageTypeFieldName = "message"; + const string exTypeFieldName = "exType"; + const string structApplicationExceptionName = "TApplicationException"; + + var struc = new TStruct(structApplicationExceptionName); + var field = new TField(); + + await outputProtocol.WriteStructBeginAsync(struc, cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(Message)) + { + field.Name = messageTypeFieldName; + field.Type = TType.String; + field.ID = MessageTypeFieldId; + await outputProtocol.WriteFieldBeginAsync(field, cancellationToken).ConfigureAwait(false); + await outputProtocol.WriteStringAsync(Message, cancellationToken).ConfigureAwait(false); + await outputProtocol.WriteFieldEndAsync(cancellationToken).ConfigureAwait(false); + } + + field.Name = exTypeFieldName; + field.Type = TType.I32; + field.ID = ExTypeFieldId; + + await outputProtocol.WriteFieldBeginAsync(field, cancellationToken).ConfigureAwait(false); + await outputProtocol.WriteI32Async((int) Type, cancellationToken).ConfigureAwait(false); + await outputProtocol.WriteFieldEndAsync(cancellationToken).ConfigureAwait(false); + await outputProtocol.WriteFieldStopAsync(cancellationToken).ConfigureAwait(false); + await outputProtocol.WriteStructEndAsync(cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/lib/Thrift/TBaseClient.cs b/lib/Thrift/TBaseClient.cs new file mode 100644 index 00000000000..5284a124ffe --- /dev/null +++ b/lib/Thrift/TBaseClient.cs @@ -0,0 +1,91 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols; + +namespace Thrift +{ + // ReSharper disable once InconsistentNaming + /// + /// TBaseClient. + /// Base client for generated clients. + /// Do not change this class without checking generated code (namings, etc.) + /// + public abstract class TBaseClient + { + private readonly TProtocol _inputProtocol; + private readonly TProtocol _outputProtocol; + private bool _isDisposed; + private int _seqId; + public readonly Guid ClientId = Guid.NewGuid(); + + protected TBaseClient(TProtocol inputProtocol, TProtocol outputProtocol) + { + _inputProtocol = inputProtocol ?? throw new ArgumentNullException(nameof(inputProtocol)); + _outputProtocol = outputProtocol ?? throw new ArgumentNullException(nameof(outputProtocol)); + } + + public TProtocol InputProtocol => _inputProtocol; + + public TProtocol OutputProtocol => _outputProtocol; + + public int SeqId + { + get { return ++_seqId; } + } + + public virtual async Task OpenTransportAsync() + { + await OpenTransportAsync(CancellationToken.None).ConfigureAwait(false); + } + + public virtual async Task OpenTransportAsync(CancellationToken cancellationToken) + { + if (!_inputProtocol.Transport.IsOpen) + { + await _inputProtocol.Transport.OpenAsync(cancellationToken).ConfigureAwait(false); + } + + if (!_inputProtocol.Transport.IsOpen) + { + await _outputProtocol.Transport.OpenAsync(cancellationToken).ConfigureAwait(false); + } + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _inputProtocol?.Dispose(); + _outputProtocol?.Dispose(); + } + } + + _isDisposed = true; + } + } +} diff --git a/lib/Thrift/TException.cs b/lib/Thrift/TException.cs new file mode 100644 index 00000000000..6aa588d7f34 --- /dev/null +++ b/lib/Thrift/TException.cs @@ -0,0 +1,34 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; + +namespace Thrift +{ + // ReSharper disable once InconsistentNaming + public class TException : Exception + { + public TException() + { + } + + public TException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/lib/Thrift/TMultiplexedProcessor.cs b/lib/Thrift/TMultiplexedProcessor.cs new file mode 100644 index 00000000000..d58d697929e --- /dev/null +++ b/lib/Thrift/TMultiplexedProcessor.cs @@ -0,0 +1,143 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Protocols; +using Thrift.Protocols.Entities; + +namespace Thrift +{ + // ReSharper disable once InconsistentNaming + public class TMultiplexedProcessor : ITAsyncProcessor + { + //TODO: Localization + + private readonly Dictionary _serviceProcessorMap = + new Dictionary(); + + public async Task ProcessAsync(TProtocol iprot, TProtocol oprot) + { + return await ProcessAsync(iprot, oprot, CancellationToken.None).ConfigureAwait(false); + } + + public async Task ProcessAsync(TProtocol iprot, TProtocol oprot, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + try + { + var message = await iprot.ReadMessageBeginAsync(cancellationToken).ConfigureAwait(false); + + if ((message.Type != TMessageType.Call) && (message.Type != TMessageType.Oneway)) + { + await FailAsync(oprot, message, TApplicationException.ExceptionType.InvalidMessageType, + "Message exType CALL or ONEWAY expected", cancellationToken); + return false; + } + + // Extract the service name + var index = message.Name.IndexOf(TMultiplexedProtocol.Separator, StringComparison.Ordinal); + if (index < 0) + { + await FailAsync(oprot, message, TApplicationException.ExceptionType.InvalidProtocol, + $"Service name not found in message name: {message.Name}. Did you forget to use a TMultiplexProtocol in your client?", + cancellationToken); + return false; + } + + // Create a new TMessage, something that can be consumed by any TProtocol + var serviceName = message.Name.Substring(0, index); + ITAsyncProcessor actualProcessor; + if (!_serviceProcessorMap.TryGetValue(serviceName, out actualProcessor)) + { + await FailAsync(oprot, message, TApplicationException.ExceptionType.InternalError, + $"Service name not found: {serviceName}. Did you forget to call RegisterProcessor()?", + cancellationToken); + return false; + } + + // Create a new TMessage, removing the service name + var newMessage = new TMessage( + message.Name.Substring(serviceName.Length + TMultiplexedProtocol.Separator.Length), + message.Type, + message.SeqID); + + // Dispatch processing to the stored processor + return + await + actualProcessor.ProcessAsync(new StoredMessageProtocol(iprot, newMessage), oprot, + cancellationToken); + } + catch (IOException) + { + return false; // similar to all other processors + } + } + + public void RegisterProcessor(string serviceName, ITAsyncProcessor processor) + { + if (_serviceProcessorMap.ContainsKey(serviceName)) + { + throw new InvalidOperationException( + $"Processor map already contains processor with name: '{serviceName}'"); + } + + _serviceProcessorMap.Add(serviceName, processor); + } + + private async Task FailAsync(TProtocol oprot, TMessage message, TApplicationException.ExceptionType extype, + string etxt, CancellationToken cancellationToken) + { + var appex = new TApplicationException(extype, etxt); + + var newMessage = new TMessage(message.Name, TMessageType.Exception, message.SeqID); + + await oprot.WriteMessageBeginAsync(newMessage, cancellationToken).ConfigureAwait(false); + await appex.WriteAsync(oprot, cancellationToken).ConfigureAwait(false); + await oprot.WriteMessageEndAsync(cancellationToken).ConfigureAwait(false); + await oprot.Transport.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + private class StoredMessageProtocol : TProtocolDecorator + { + readonly TMessage _msgBegin; + + public StoredMessageProtocol(TProtocol protocol, TMessage messageBegin) + : base(protocol) + { + _msgBegin = messageBegin; + } + + public override async Task ReadMessageBeginAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + return _msgBegin; + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Thrift.csproj b/lib/Thrift/Thrift.csproj new file mode 100644 index 00000000000..3dbd8203b50 --- /dev/null +++ b/lib/Thrift/Thrift.csproj @@ -0,0 +1,17 @@ + + + + net46;netstandard2.0 + netstandard2.0 + OpenTelemetry authors + https://opentelemetry.io/img/logos/opentelemetry-icon-color.png + https://OpenTelemetry.io + Tracing;OpenTelemetry;Management;Monitoring;Jaeger;distributed-tracing + + + + + + + + diff --git a/lib/Thrift/Transports/Client/TBufferedClientTransport.cs b/lib/Thrift/Transports/Client/TBufferedClientTransport.cs new file mode 100644 index 00000000000..58f08899c3a --- /dev/null +++ b/lib/Thrift/Transports/Client/TBufferedClientTransport.cs @@ -0,0 +1,206 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + // ReSharper disable once InconsistentNaming + public class TBufferedClientTransport : TClientTransport + { + private readonly int _bufSize; + private readonly MemoryStream _inputBuffer = new MemoryStream(0); + private readonly MemoryStream _outputBuffer = new MemoryStream(0); + private readonly TClientTransport _transport; + private bool _isDisposed; + + //TODO: should support only specified input transport? + public TBufferedClientTransport(TClientTransport transport, int bufSize = 1024) + { + if (bufSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufSize), "Buffer size must be a positive number."); + } + + _transport = transport ?? throw new ArgumentNullException(nameof(transport)); + _bufSize = bufSize; + } + + public TClientTransport UnderlyingTransport + { + get + { + CheckNotDisposed(); + + return _transport; + } + } + + public override bool IsOpen => !_isDisposed && _transport.IsOpen; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + CheckNotDisposed(); + + await _transport.OpenAsync(cancellationToken).ConfigureAwait(false); + } + + public override void Close() + { + CheckNotDisposed(); + + _transport.Close(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + //TODO: investigate how it should work correctly + CheckNotDisposed(); + + ValidateBufferArgs(buffer, offset, length); + + if (!IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + if (_inputBuffer.Capacity < _bufSize) + { + _inputBuffer.Capacity = _bufSize; + } + + var got = await _inputBuffer.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + if (got > 0) + { + return got; + } + + _inputBuffer.Seek(0, SeekOrigin.Begin); + _inputBuffer.SetLength(_inputBuffer.Capacity); + + ArraySegment bufSegment; + _inputBuffer.TryGetBuffer(out bufSegment); + + // investigate + var filled = await _transport.ReadAsync(bufSegment.Array, 0, (int) _inputBuffer.Length, cancellationToken).ConfigureAwait(false); + _inputBuffer.SetLength(filled); + + if (filled == 0) + { + return 0; + } + + return await ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + CheckNotDisposed(); + + ValidateBufferArgs(buffer, offset, length); + + if (!IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + // Relative offset from "off" argument + var writtenCount = 0; + if (_outputBuffer.Length > 0) + { + var capa = (int) (_outputBuffer.Capacity - _outputBuffer.Length); + var writeSize = capa <= length ? capa : length; + await _outputBuffer.WriteAsync(buffer, offset, writeSize, cancellationToken).ConfigureAwait(false); + + writtenCount += writeSize; + if (writeSize == capa) + { + //ArraySegment bufSegment; + //_outputBuffer.TryGetBuffer(out bufSegment); + var data = _outputBuffer.ToArray(); + //await _transport.WriteAsync(bufSegment.Array, cancellationToken).ConfigureAwait(false); + await _transport.WriteAsync(data, cancellationToken).ConfigureAwait(false); + _outputBuffer.SetLength(0); + } + } + + while (length - writtenCount >= _bufSize) + { + await _transport.WriteAsync(buffer, offset + writtenCount, _bufSize, cancellationToken).ConfigureAwait(false); + writtenCount += _bufSize; + } + + var remain = length - writtenCount; + if (remain > 0) + { + if (_outputBuffer.Capacity < _bufSize) + { + _outputBuffer.Capacity = _bufSize; + } + await _outputBuffer.WriteAsync(buffer, offset + writtenCount, remain, cancellationToken).ConfigureAwait(false); + } + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + CheckNotDisposed(); + + if (!IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + if (_outputBuffer.Length > 0) + { + //ArraySegment bufSegment; + var data = _outputBuffer.ToArray(); // TryGetBuffer(out bufSegment); + + await _transport.WriteAsync(data /*bufSegment.Array*/, cancellationToken).ConfigureAwait(false); + _outputBuffer.SetLength(0); + } + + await _transport.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + private void CheckNotDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(_transport)); + } + } + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _inputBuffer?.Dispose(); + _outputBuffer?.Dispose(); + _transport?.Dispose(); + } + } + _isDisposed = true; + } + } +} diff --git a/lib/Thrift/Transports/Client/TFramedClientTransport.cs b/lib/Thrift/Transports/Client/TFramedClientTransport.cs new file mode 100644 index 00000000000..85c43db06f8 --- /dev/null +++ b/lib/Thrift/Transports/Client/TFramedClientTransport.cs @@ -0,0 +1,201 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + //TODO: check for correct implementation + + // ReSharper disable once InconsistentNaming + public class TFramedClientTransport : TClientTransport + { + private const int HeaderSize = 4; + private readonly byte[] _headerBuf = new byte[HeaderSize]; + private readonly MemoryStream _readBuffer = new MemoryStream(1024); + private readonly TClientTransport _transport; + private readonly MemoryStream _writeBuffer = new MemoryStream(1024); + + private bool _isDisposed; + + public TFramedClientTransport(TClientTransport transport) + { + _transport = transport ?? throw new ArgumentNullException(nameof(transport)); + + InitWriteBuffer(); + } + + public override bool IsOpen => !_isDisposed && _transport.IsOpen; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + CheckNotDisposed(); + + await _transport.OpenAsync(cancellationToken).ConfigureAwait(false); + } + + public override void Close() + { + CheckNotDisposed(); + + _transport.Close(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + CheckNotDisposed(); + + ValidateBufferArgs(buffer, offset, length); + + if (!IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + var got = await _readBuffer.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + if (got > 0) + { + return got; + } + + // Read another frame of data + await ReadFrameAsync(cancellationToken).ConfigureAwait(false); + + return await _readBuffer.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + private async Task ReadFrameAsync(CancellationToken cancellationToken) + { + await _transport.ReadAllAsync(_headerBuf, 0, HeaderSize, cancellationToken).ConfigureAwait(false); + + var size = DecodeFrameSize(_headerBuf); + + _readBuffer.SetLength(size); + _readBuffer.Seek(0, SeekOrigin.Begin); + + ArraySegment bufSegment; + _readBuffer.TryGetBuffer(out bufSegment); + + var buff = bufSegment.Array; + + await _transport.ReadAllAsync(buff, 0, size, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + CheckNotDisposed(); + + ValidateBufferArgs(buffer, offset, length); + + if (!IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + if (_writeBuffer.Length + length > int.MaxValue) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + + await _writeBuffer.WriteAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + CheckNotDisposed(); + + if (!IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + //ArraySegment bufSegment; + //_writeBuffer.TryGetBuffer(out bufSegment); + //var buf = bufSegment.Array; + var buf = _writeBuffer.ToArray(); + + //var len = (int)_writeBuffer.Length; + var dataLen = (int) _writeBuffer.Length - HeaderSize; + if (dataLen < 0) + { + throw new InvalidOperationException(); // logic error actually + } + + // Inject message header into the reserved buffer space + EncodeFrameSize(dataLen, buf); + + // Send the entire message at once + await _transport.WriteAsync(buf, cancellationToken).ConfigureAwait(false); + + InitWriteBuffer(); + + await _transport.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + private void InitWriteBuffer() + { + // Reserve space for message header to be put right before sending it out + _writeBuffer.SetLength(HeaderSize); + _writeBuffer.Seek(0, SeekOrigin.End); + } + + private static void EncodeFrameSize(int frameSize, byte[] buf) + { + buf[0] = (byte) (0xff & (frameSize >> 24)); + buf[1] = (byte) (0xff & (frameSize >> 16)); + buf[2] = (byte) (0xff & (frameSize >> 8)); + buf[3] = (byte) (0xff & (frameSize)); + } + + private static int DecodeFrameSize(byte[] buf) + { + return + ((buf[0] & 0xff) << 24) | + ((buf[1] & 0xff) << 16) | + ((buf[2] & 0xff) << 8) | + (buf[3] & 0xff); + } + + + private void CheckNotDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException("TFramedClientTransport"); + } + } + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _readBuffer?.Dispose(); + _writeBuffer?.Dispose(); + _transport?.Dispose(); + } + } + _isDisposed = true; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Client/THttpClientTransport.cs b/lib/Thrift/Transports/Client/THttpClientTransport.cs new file mode 100644 index 00000000000..6ea3061c9e7 --- /dev/null +++ b/lib/Thrift/Transports/Client/THttpClientTransport.cs @@ -0,0 +1,235 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + // ReSharper disable once InconsistentNaming + public class THttpClientTransport : TClientTransport + { + private readonly X509Certificate[] _certificates; + private readonly Uri _uri; + + // Timeouts in milliseconds + private int _connectTimeout = 30000; + private HttpClient _httpClient; + private Stream _inputStream; + + private bool _isDisposed; + private MemoryStream _outputStream = new MemoryStream(); + private static readonly MediaTypeWithQualityHeaderValue ApacheThriftMediaType = new MediaTypeWithQualityHeaderValue("application/vnd.apache.thrift.binary"); + + public THttpClientTransport(Uri u, IDictionary customHeaders) + : this(u, Enumerable.Empty(), customHeaders) + { + } + + public THttpClientTransport(Uri u, IEnumerable certificates, + IDictionary customHeaders) + { + _uri = u; + _certificates = (certificates ?? Enumerable.Empty()).ToArray(); + CustomHeaders = customHeaders; + + // due to current bug with performance of Dispose in netcore https://github.com/dotnet/corefx/issues/8809 + // this can be switched to default way (create client->use->dispose per flush) later + _httpClient = CreateClient(); + } + + public IDictionary CustomHeaders { get; } + + public int ConnectTimeout + { + set { _connectTimeout = value; } + } + + public override bool IsOpen => true; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override void Close() + { + if (_inputStream != null) + { + _inputStream.Dispose(); + _inputStream = null; + } + + if (_outputStream != null) + { + _outputStream.Dispose(); + _outputStream = null; + } + + if (_httpClient != null) + { + _httpClient.Dispose(); + _httpClient = null; + } + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (_inputStream == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "No request has been sent"); + } + + try + { + var ret = await _inputStream.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + + if (ret == -1) + { + throw new TTransportException(TTransportException.ExceptionType.EndOfFile, "No more data available"); + } + + return ret; + } + catch (IOException iox) + { + throw new TTransportException(TTransportException.ExceptionType.Unknown, iox.ToString()); + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + await _outputStream.WriteAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + private HttpClient CreateClient() + { +#if NET46 + var handler = new WebRequestHandler(); +#else + var handler = new HttpClientHandler(); +#endif + if (_certificates.Length > 0) + { + handler.ClientCertificates.AddRange(_certificates); + } + + var httpClient = new HttpClient(handler); + + if (_connectTimeout > 0) + { + httpClient.Timeout = TimeSpan.FromSeconds(_connectTimeout); + } + + httpClient.DefaultRequestHeaders.Accept.Add(ApacheThriftMediaType); + httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("THttpClientTransport", "1.0.0")); + + if (CustomHeaders != null) + { + foreach (var item in CustomHeaders) + { + httpClient.DefaultRequestHeaders.Add(item.Key, item.Value); + } + } + + return httpClient; + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + try + { + try + { + if (_outputStream.CanSeek) + { + _outputStream.Seek(0, SeekOrigin.Begin); + } + + using (var outStream = new StreamContent(_outputStream)) + { + outStream.Headers.ContentType = ApacheThriftMediaType; + var msg = await _httpClient.PostAsync(_uri, outStream, cancellationToken).ConfigureAwait(false); + + msg.EnsureSuccessStatusCode(); + + if (_inputStream != null) + { + _inputStream.Dispose(); + _inputStream = null; + } + + _inputStream = await msg.Content.ReadAsStreamAsync().ConfigureAwait(false); + if (_inputStream.CanSeek) + { + _inputStream.Seek(0, SeekOrigin.Begin); + } + } + } + catch (IOException iox) + { + throw new TTransportException(TTransportException.ExceptionType.Unknown, iox.ToString()); + } + catch (HttpRequestException wx) + { + throw new TTransportException(TTransportException.ExceptionType.Unknown, + "Couldn't connect to server: " + wx); + } + } + finally + { + _outputStream = new MemoryStream(); + } + } + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _inputStream?.Dispose(); + _outputStream?.Dispose(); + _httpClient?.Dispose(); + } + } + _isDisposed = true; + } + } +} diff --git a/lib/Thrift/Transports/Client/TMemoryBufferClientTransport.cs b/lib/Thrift/Transports/Client/TMemoryBufferClientTransport.cs new file mode 100644 index 00000000000..92c6bc3ae68 --- /dev/null +++ b/lib/Thrift/Transports/Client/TMemoryBufferClientTransport.cs @@ -0,0 +1,97 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + // ReSharper disable once InconsistentNaming + public class TMemoryBufferClientTransport : TClientTransport + { + private readonly MemoryStream _byteStream; + private bool _isDisposed; + + public TMemoryBufferClientTransport() + { + _byteStream = new MemoryStream(); + } + + public TMemoryBufferClientTransport(byte[] buf) + { + _byteStream = new MemoryStream(buf); + } + + public override bool IsOpen => true; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override void Close() + { + // do nothing + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + return await _byteStream.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken) + { + await _byteStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + await _byteStream.WriteAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public byte[] GetBuffer() + { + return _byteStream.ToArray(); + } + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _byteStream?.Dispose(); + } + } + _isDisposed = true; + } + } +} diff --git a/lib/Thrift/Transports/Client/TNamedPipeClientTransport.cs b/lib/Thrift/Transports/Client/TNamedPipeClientTransport.cs new file mode 100644 index 00000000000..b137ae3687c --- /dev/null +++ b/lib/Thrift/Transports/Client/TNamedPipeClientTransport.cs @@ -0,0 +1,95 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + // ReSharper disable once InconsistentNaming + public class TNamedPipeClientTransport : TClientTransport + { + private NamedPipeClientStream _client; + + public TNamedPipeClientTransport(string pipe) : this(".", pipe) + { + } + + public TNamedPipeClientTransport(string server, string pipe) + { + var serverName = string.IsNullOrWhiteSpace(server) ? server : "."; + + _client = new NamedPipeClientStream(serverName, pipe, PipeDirection.InOut, PipeOptions.None); + } + + public override bool IsOpen => _client != null && _client.IsConnected; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.AlreadyOpen); + } + + await _client.ConnectAsync(cancellationToken).ConfigureAwait(false); + } + + public override void Close() + { + if (_client != null) + { + _client.Dispose(); + _client = null; + } + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + if (_client == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + return await _client.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + if (_client == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + await _client.WriteAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + protected override void Dispose(bool disposing) + { + _client.Dispose(); + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Client/TSocketClientTransport.cs b/lib/Thrift/Transports/Client/TSocketClientTransport.cs new file mode 100644 index 00000000000..806c9b61cc8 --- /dev/null +++ b/lib/Thrift/Transports/Client/TSocketClientTransport.cs @@ -0,0 +1,139 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + // ReSharper disable once InconsistentNaming + public class TSocketClientTransport : TStreamClientTransport + { + private bool _isDisposed; + + public TSocketClientTransport(TcpClient client) + { + TcpClient = client ?? throw new ArgumentNullException(nameof(client)); + + if (IsOpen) + { + InputStream = client.GetStream(); + OutputStream = client.GetStream(); + } + } + + public TSocketClientTransport(IPAddress host, int port) + : this(host, port, 0) + { + } + + public TSocketClientTransport(IPAddress host, int port, int timeout) + { + Host = host; + Port = port; + + TcpClient = new TcpClient(); + TcpClient.ReceiveTimeout = TcpClient.SendTimeout = timeout; + TcpClient.Client.NoDelay = true; + } + + public TcpClient TcpClient { get; private set; } + public IPAddress Host { get; } + public int Port { get; } + + public int Timeout + { + set + { + if (TcpClient != null) + { + TcpClient.ReceiveTimeout = TcpClient.SendTimeout = value; + } + } + } + + public override bool IsOpen + { + get + { + if (TcpClient == null) + { + return false; + } + + return TcpClient.Connected; + } + } + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.AlreadyOpen, "Socket already connected"); + } + + if (Port <= 0) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "Cannot open without port"); + } + + if (TcpClient == null) + { + throw new InvalidOperationException("Invalid or not initialized tcp client"); + } + + await TcpClient.ConnectAsync(Host, Port).ConfigureAwait(false); + + InputStream = TcpClient.GetStream(); + OutputStream = TcpClient.GetStream(); + } + + public override void Close() + { + base.Close(); + + if (TcpClient != null) + { + TcpClient.Dispose(); + TcpClient = null; + } + } + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + TcpClient?.Dispose(); + + base.Dispose(disposing); + } + } + _isDisposed = true; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Client/TStreamClientTransport.cs b/lib/Thrift/Transports/Client/TStreamClientTransport.cs new file mode 100644 index 00000000000..e1a6d2b6e5f --- /dev/null +++ b/lib/Thrift/Transports/Client/TStreamClientTransport.cs @@ -0,0 +1,110 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + // ReSharper disable once InconsistentNaming + public class TStreamClientTransport : TClientTransport + { + private bool _isDisposed; + + protected TStreamClientTransport() + { + } + + public TStreamClientTransport(Stream inputStream, Stream outputStream) + { + InputStream = inputStream; + OutputStream = outputStream; + } + + protected Stream OutputStream { get; set; } + + protected Stream InputStream { get; set; } + + public override bool IsOpen => true; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override void Close() + { + if (InputStream != null) + { + InputStream.Dispose(); + InputStream = null; + } + + if (OutputStream != null) + { + OutputStream.Dispose(); + OutputStream = null; + } + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + if (InputStream == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, + "Cannot read from null inputstream"); + } + + return await InputStream.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + if (OutputStream == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, + "Cannot read from null inputstream"); + } + + await OutputStream.WriteAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return OutputStream.FlushAsync(cancellationToken); + } + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + InputStream?.Dispose(); + OutputStream?.Dispose(); + } + } + _isDisposed = true; + } + } +} diff --git a/lib/Thrift/Transports/Client/TTlsSocketClientTransport.cs b/lib/Thrift/Transports/Client/TTlsSocketClientTransport.cs new file mode 100644 index 00000000000..f60ea588af0 --- /dev/null +++ b/lib/Thrift/Transports/Client/TTlsSocketClientTransport.cs @@ -0,0 +1,237 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Client +{ + //TODO: check for correct work + + // ReSharper disable once InconsistentNaming + public class TTlsSocketClientTransport : TStreamClientTransport + { + private readonly X509Certificate2 _certificate; + private readonly RemoteCertificateValidationCallback _certValidator; + private readonly IPAddress _host; + private readonly bool _isServer; + private readonly LocalCertificateSelectionCallback _localCertificateSelectionCallback; + private readonly int _port; + private readonly SslProtocols _sslProtocols; + private TcpClient _client; + private SslStream _secureStream; + private int _timeout; + + public TTlsSocketClientTransport(TcpClient client, X509Certificate2 certificate, bool isServer = false, + RemoteCertificateValidationCallback certValidator = null, + LocalCertificateSelectionCallback localCertificateSelectionCallback = null, + SslProtocols sslProtocols = SslProtocols.Tls12) + { + _client = client; + _certificate = certificate; + _certValidator = certValidator; + _localCertificateSelectionCallback = localCertificateSelectionCallback; + _sslProtocols = sslProtocols; + _isServer = isServer; + + if (isServer && certificate == null) + { + throw new ArgumentException("TTlsSocketClientTransport needs certificate to be used for server", + nameof(certificate)); + } + + if (IsOpen) + { + InputStream = client.GetStream(); + OutputStream = client.GetStream(); + } + } + + public TTlsSocketClientTransport(IPAddress host, int port, string certificatePath, + RemoteCertificateValidationCallback certValidator = null, + LocalCertificateSelectionCallback localCertificateSelectionCallback = null, + SslProtocols sslProtocols = SslProtocols.Tls12) + : this(host, port, 0, + new X509Certificate2(certificatePath), + certValidator, + localCertificateSelectionCallback, + sslProtocols) + { + } + + public TTlsSocketClientTransport(IPAddress host, int port, + X509Certificate2 certificate = null, + RemoteCertificateValidationCallback certValidator = null, + LocalCertificateSelectionCallback localCertificateSelectionCallback = null, + SslProtocols sslProtocols = SslProtocols.Tls12) + : this(host, port, 0, + certificate, + certValidator, + localCertificateSelectionCallback, + sslProtocols) + { + } + + public TTlsSocketClientTransport(IPAddress host, int port, int timeout, + X509Certificate2 certificate, + RemoteCertificateValidationCallback certValidator = null, + LocalCertificateSelectionCallback localCertificateSelectionCallback = null, + SslProtocols sslProtocols = SslProtocols.Tls12) + { + _host = host; + _port = port; + _timeout = timeout; + _certificate = certificate; + _certValidator = certValidator; + _localCertificateSelectionCallback = localCertificateSelectionCallback; + _sslProtocols = sslProtocols; + + InitSocket(); + } + + public int Timeout + { + set { _client.ReceiveTimeout = _client.SendTimeout = _timeout = value; } + } + + public TcpClient TcpClient => _client; + + public IPAddress Host => _host; + + public int Port => _port; + + public override bool IsOpen + { + get + { + if (_client == null) + { + return false; + } + + return _client.Connected; + } + } + + private void InitSocket() + { + _client = new TcpClient(); + _client.ReceiveTimeout = _client.SendTimeout = _timeout; + _client.Client.NoDelay = true; + } + + private bool DefaultCertificateValidator(object sender, X509Certificate certificate, X509Chain chain, + SslPolicyErrors sslValidationErrors) + { + return sslValidationErrors == SslPolicyErrors.None; + } + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (IsOpen) + { + throw new TTransportException(TTransportException.ExceptionType.AlreadyOpen, "Socket already connected"); + } + + if (_host == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "Cannot open null host"); + } + + if (_port <= 0) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "Cannot open without port"); + } + + if (_client == null) + { + InitSocket(); + } + + if (_client != null) + { + await _client.ConnectAsync(_host, _port).ConfigureAwait(false); + await SetupTlsAsync().ConfigureAwait(false); + } + } + + public async Task SetupTlsAsync() + { + var validator = _certValidator ?? DefaultCertificateValidator; + + if (_localCertificateSelectionCallback != null) + { + _secureStream = new SslStream(_client.GetStream(), false, validator, _localCertificateSelectionCallback); + } + else + { + _secureStream = new SslStream(_client.GetStream(), false, validator); + } + + try + { + if (_isServer) + { + // Server authentication + await + _secureStream.AuthenticateAsServerAsync(_certificate, _certValidator != null, _sslProtocols, + true); + } + else + { + // Client authentication + var certs = _certificate != null + ? new X509CertificateCollection {_certificate} + : new X509CertificateCollection(); + + var targetHost = _host.ToString(); + await _secureStream.AuthenticateAsClientAsync(targetHost, certs, _sslProtocols, true).ConfigureAwait(false); + } + } + catch (Exception) + { + Close(); + throw; + } + + InputStream = _secureStream; + OutputStream = _secureStream; + } + + public override void Close() + { + base.Close(); + if (_client != null) + { + _client.Dispose(); + _client = null; + } + + if (_secureStream != null) + { + _secureStream.Dispose(); + _secureStream = null; + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Server/TNamedPipeServerTransport.cs b/lib/Thrift/Transports/Server/TNamedPipeServerTransport.cs new file mode 100644 index 00000000000..9d3b9341af1 --- /dev/null +++ b/lib/Thrift/Transports/Server/TNamedPipeServerTransport.cs @@ -0,0 +1,191 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports.Server +{ + // ReSharper disable once InconsistentNaming + public class TNamedPipeServerTransport : TServerTransport + { + /// + /// This is the address of the Pipe on the localhost. + /// + private readonly string _pipeAddress; + + private bool _asyncMode = true; + private volatile bool _isPending = true; + + private NamedPipeServerStream _stream = null; + + public TNamedPipeServerTransport(string pipeAddress) + { + _pipeAddress = pipeAddress; + } + + public override void Listen() + { + // nothing to do here + } + + public override void Close() + { + if (_stream != null) + { + try + { + //TODO: check for disconection + _stream.Disconnect(); + _stream.Dispose(); + } + finally + { + _stream = null; + _isPending = false; + } + } + } + + public override bool IsClientPending() + { + return _isPending; + } + + private void EnsurePipeInstance() + { + if (_stream == null) + { + var direction = PipeDirection.InOut; + var maxconn = 254; + var mode = PipeTransmissionMode.Byte; + var options = _asyncMode ? PipeOptions.Asynchronous : PipeOptions.None; + var inbuf = 4096; + var outbuf = 4096; + // TODO: security + + try + { + _stream = new NamedPipeServerStream(_pipeAddress, direction, maxconn, mode, options, inbuf, outbuf); + } + catch (NotImplementedException) // Mono still does not support async, fallback to sync + { + if (_asyncMode) + { + options &= (~PipeOptions.Asynchronous); + _stream = new NamedPipeServerStream(_pipeAddress, direction, maxconn, mode, options, inbuf, + outbuf); + _asyncMode = false; + } + else + { + throw; + } + } + } + } + + protected override async Task AcceptImplementationAsync(CancellationToken cancellationToken) + { + try + { + EnsurePipeInstance(); + + await _stream.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); + + var trans = new ServerTransport(_stream); + _stream = null; // pass ownership to ServerTransport + + //_isPending = false; + + return trans; + } + catch (TTransportException) + { + Close(); + throw; + } + catch (Exception e) + { + Close(); + throw new TTransportException(TTransportException.ExceptionType.NotOpen, e.Message); + } + } + + private class ServerTransport : TClientTransport + { + private readonly NamedPipeServerStream _stream; + + public ServerTransport(NamedPipeServerStream stream) + { + _stream = stream; + } + + public override bool IsOpen => _stream != null && _stream.IsConnected; + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + public override void Close() + { + _stream?.Dispose(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + if (_stream == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + return await _stream.ReadAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + if (_stream == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen); + } + + await _stream.WriteAsync(buffer, offset, length, cancellationToken).ConfigureAwait(false); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + } + + protected override void Dispose(bool disposing) + { + _stream?.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Server/TServerFramedTransport.cs b/lib/Thrift/Transports/Server/TServerFramedTransport.cs new file mode 100644 index 00000000000..293a0e36582 --- /dev/null +++ b/lib/Thrift/Transports/Server/TServerFramedTransport.cs @@ -0,0 +1,150 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Transports.Client; + +namespace Thrift.Transports.Server +{ + // ReSharper disable once InconsistentNaming + public class TServerFramedTransport : TServerTransport + { + private readonly int _clientTimeout; + private readonly int _port; + private TcpListener _server; + + public TServerFramedTransport(TcpListener listener) + : this(listener, 0) + { + } + + public TServerFramedTransport(TcpListener listener, int clientTimeout) + { + _server = listener; + _clientTimeout = clientTimeout; + } + + public TServerFramedTransport(int port) + : this(port, 0) + { + } + + public TServerFramedTransport(int port, int clientTimeout) + { + _port = port; + _clientTimeout = clientTimeout; + try + { + // Make server socket + _server = new TcpListener(IPAddress.Any, _port); + _server.Server.NoDelay = true; + } + catch (Exception) + { + _server = null; + throw new TTransportException("Could not create ServerSocket on port " + port + "."); + } + } + + public override void Listen() + { + // Make sure not to block on accept + if (_server != null) + { + try + { + _server.Start(); + } + catch (SocketException sx) + { + throw new TTransportException("Could not accept on listening socket: " + sx.Message); + } + } + } + + public override bool IsClientPending() + { + return _server.Pending(); + } + + protected override async Task AcceptImplementationAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (_server == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "No underlying server socket."); + } + + try + { + TFramedClientTransport tSocketTransport = null; + var tcpClient = await _server.AcceptTcpClientAsync().ConfigureAwait(false); + + try + { + tSocketTransport = new TFramedClientTransport(new TSocketClientTransport(tcpClient) + { + Timeout = _clientTimeout + }); + + return tSocketTransport; + } + catch (Exception) + { + if (tSocketTransport != null) + { + tSocketTransport.Dispose(); + } + else // Otherwise, clean it up ourselves. + { + ((IDisposable) tcpClient).Dispose(); + } + + throw; + } + } + catch (Exception ex) + { + throw new TTransportException(ex.ToString()); + } + } + + public override void Close() + { + if (_server != null) + { + try + { + _server.Stop(); + } + catch (Exception ex) + { + throw new TTransportException("WARNING: Could not close server socket: " + ex); + } + _server = null; + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Server/TServerSocketTransport.cs b/lib/Thrift/Transports/Server/TServerSocketTransport.cs new file mode 100644 index 00000000000..b9d7b9eca71 --- /dev/null +++ b/lib/Thrift/Transports/Server/TServerSocketTransport.cs @@ -0,0 +1,174 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Transports.Client; + +namespace Thrift.Transports.Server +{ + // ReSharper disable once InconsistentNaming + public class TServerSocketTransport : TServerTransport + { + private readonly int _clientTimeout; + private readonly int _port; + private readonly bool _useBufferedSockets; + private readonly bool _useFramedTransport; + private TcpListener _server; + + public TServerSocketTransport(TcpListener listener) + : this(listener, 0) + { + } + + public TServerSocketTransport(TcpListener listener, int clientTimeout) + { + _server = listener; + _clientTimeout = clientTimeout; + } + + public TServerSocketTransport(int port) + : this(port, 0) + { + } + + public TServerSocketTransport(int port, int clientTimeout) + : this(port, clientTimeout, false) + { + } + + public TServerSocketTransport(int port, int clientTimeout, bool useBufferedSockets): + this(port, clientTimeout, useBufferedSockets, false) + { + } + + public TServerSocketTransport(int port, int clientTimeout, bool useBufferedSockets, bool useFramedTransport) + { + _port = port; + _clientTimeout = clientTimeout; + _useBufferedSockets = useBufferedSockets; + _useFramedTransport = useFramedTransport; + try + { + // Make server socket + _server = new TcpListener(IPAddress.Any, _port); + _server.Server.NoDelay = true; + } + catch (Exception) + { + _server = null; + throw new TTransportException("Could not create ServerSocket on port " + port + "."); + } + } + + public override void Listen() + { + // Make sure not to block on accept + if (_server != null) + { + try + { + _server.Start(); + } + catch (SocketException sx) + { + throw new TTransportException("Could not accept on listening socket: " + sx.Message); + } + } + } + + public override bool IsClientPending() + { + return _server.Pending(); + } + + protected override async Task AcceptImplementationAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (_server == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "No underlying server socket."); + } + + try + { + TClientTransport tSocketTransport = null; + var tcpClient = await _server.AcceptTcpClientAsync().ConfigureAwait(false); + + try + { + tSocketTransport = new TSocketClientTransport(tcpClient) + { + Timeout = _clientTimeout + }; + + if (_useBufferedSockets) + { + tSocketTransport = new TBufferedClientTransport(tSocketTransport); + } + + if (_useFramedTransport) + { + tSocketTransport = new TFramedClientTransport(tSocketTransport); + } + + return tSocketTransport; + } + catch (Exception) + { + if (tSocketTransport != null) + { + tSocketTransport.Dispose(); + } + else // Otherwise, clean it up ourselves. + { + ((IDisposable) tcpClient).Dispose(); + } + + throw; + } + } + catch (Exception ex) + { + throw new TTransportException(ex.ToString()); + } + } + + public override void Close() + { + if (_server != null) + { + try + { + _server.Stop(); + } + catch (Exception ex) + { + throw new TTransportException("WARNING: Could not close server socket: " + ex); + } + _server = null; + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/Server/TTlsServerSocketTransport.cs b/lib/Thrift/Transports/Server/TTlsServerSocketTransport.cs new file mode 100644 index 00000000000..87528a792d1 --- /dev/null +++ b/lib/Thrift/Transports/Server/TTlsServerSocketTransport.cs @@ -0,0 +1,177 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Thrift.Transports.Client; + +namespace Thrift.Transports.Server +{ + // ReSharper disable once InconsistentNaming + public class TTlsServerSocketTransport : TServerTransport + { + private readonly RemoteCertificateValidationCallback _clientCertValidator; + private readonly int _clientTimeout = 0; + private readonly LocalCertificateSelectionCallback _localCertificateSelectionCallback; + private readonly int _port; + private readonly X509Certificate2 _serverCertificate; + private readonly SslProtocols _sslProtocols; + private readonly bool _useBufferedSockets; + private readonly bool _useFramedTransport; + private TcpListener _server; + + public TTlsServerSocketTransport(int port, X509Certificate2 certificate) + : this(port, false, certificate) + { + } + + public TTlsServerSocketTransport( + int port, + bool useBufferedSockets, + X509Certificate2 certificate, + RemoteCertificateValidationCallback clientCertValidator = null, + LocalCertificateSelectionCallback localCertificateSelectionCallback = null, + SslProtocols sslProtocols = SslProtocols.Tls12) + : this(port, useBufferedSockets, false, certificate, + clientCertValidator, localCertificateSelectionCallback, sslProtocols) + { + } + + public TTlsServerSocketTransport( + int port, + bool useBufferedSockets, + bool useFramedTransport, + X509Certificate2 certificate, + RemoteCertificateValidationCallback clientCertValidator = null, + LocalCertificateSelectionCallback localCertificateSelectionCallback = null, + SslProtocols sslProtocols = SslProtocols.Tls12) + { + if (!certificate.HasPrivateKey) + { + throw new TTransportException(TTransportException.ExceptionType.Unknown, + "Your server-certificate needs to have a private key"); + } + + _port = port; + _serverCertificate = certificate; + _useBufferedSockets = useBufferedSockets; + _useFramedTransport = useFramedTransport; + _clientCertValidator = clientCertValidator; + _localCertificateSelectionCallback = localCertificateSelectionCallback; + _sslProtocols = sslProtocols; + + try + { + // Create server socket + _server = new TcpListener(IPAddress.Any, _port); + _server.Server.NoDelay = true; + } + catch (Exception) + { + _server = null; + throw new TTransportException($"Could not create ServerSocket on port {port}."); + } + } + + public override void Listen() + { + // Make sure accept is not blocking + if (_server != null) + { + try + { + _server.Start(); + } + catch (SocketException sx) + { + throw new TTransportException($"Could not accept on listening socket: {sx.Message}"); + } + } + } + + public override bool IsClientPending() + { + return _server.Pending(); + } + + protected override async Task AcceptImplementationAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + if (_server == null) + { + throw new TTransportException(TTransportException.ExceptionType.NotOpen, "No underlying server socket."); + } + + try + { + var client = await _server.AcceptTcpClientAsync().ConfigureAwait(false); + client.SendTimeout = client.ReceiveTimeout = _clientTimeout; + + //wrap the client in an SSL Socket passing in the SSL cert + var tTlsSocket = new TTlsSocketClientTransport(client, _serverCertificate, true, _clientCertValidator, + _localCertificateSelectionCallback, _sslProtocols); + + await tTlsSocket.SetupTlsAsync().ConfigureAwait(false); + + TClientTransport trans = tTlsSocket; + + if (_useBufferedSockets) + { + trans = new TBufferedClientTransport(trans); + } + + if (_useFramedTransport) + { + trans = new TFramedClientTransport(trans); + } + + return trans; + } + catch (Exception ex) + { + throw new TTransportException(ex.ToString()); + } + } + + public override void Close() + { + if (_server != null) + { + try + { + _server.Stop(); + } + catch (Exception ex) + { + throw new TTransportException($"WARNING: Could not close server socket: {ex}"); + } + + _server = null; + } + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/TClientTransport.cs b/lib/Thrift/Transports/TClientTransport.cs new file mode 100644 index 00000000000..4eebbcf8cdd --- /dev/null +++ b/lib/Thrift/Transports/TClientTransport.cs @@ -0,0 +1,179 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports +{ + //TODO: think about client info + // ReSharper disable once InconsistentNaming + public abstract class TClientTransport : IDisposable + { + //TODO: think how to avoid peek byte + private readonly byte[] _peekBuffer = new byte[1]; + private bool _hasPeekByte; + public abstract bool IsOpen { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async Task PeekAsync(CancellationToken cancellationToken) + { + //If we already have a byte read but not consumed, do nothing. + if (_hasPeekByte) + { + return true; + } + + //If transport closed we can't peek. + if (!IsOpen) + { + return false; + } + + //Try to read one byte. If succeeds we will need to store it for the next read. + try + { + var bytes = await ReadAsync(_peekBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + if (bytes == 0) + { + return false; + } + } + catch (IOException) + { + return false; + } + + _hasPeekByte = true; + return true; + } + + public virtual async Task OpenAsync() + { + await OpenAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task OpenAsync(CancellationToken cancellationToken); + + public abstract void Close(); + + protected static void ValidateBufferArgs(byte[] buffer, int offset, int length) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Buffer offset is smaller than zero."); + } + + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Buffer length is smaller than zero."); + } + + if (offset + length > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(buffer), "Not enough data."); + } + } + + public virtual async Task ReadAsync(byte[] buffer, int offset, int length) + { + return await ReadAsync(buffer, offset, length, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task ReadAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken); + + public virtual async Task ReadAllAsync(byte[] buffer, int offset, int length) + { + return await ReadAllAsync(buffer, offset, length, CancellationToken.None).ConfigureAwait(false); + } + + public virtual async Task ReadAllAsync(byte[] buffer, int offset, int length, + CancellationToken cancellationToken) + { + ValidateBufferArgs(buffer, offset, length); + + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var retrieved = 0; + + //If we previously peeked a byte, we need to use that first. + if (_hasPeekByte) + { + buffer[offset + retrieved++] = _peekBuffer[0]; + _hasPeekByte = false; + } + + while (retrieved < length) + { + if (cancellationToken.IsCancellationRequested) + { + return await Task.FromCanceled(cancellationToken).ConfigureAwait(false); + } + + var returnedCount = await ReadAsync(buffer, offset + retrieved, length - retrieved, cancellationToken).ConfigureAwait(false); + if (returnedCount <= 0) + { + throw new TTransportException(TTransportException.ExceptionType.EndOfFile, + "Cannot read, Remote side has closed"); + } + retrieved += returnedCount; + } + return retrieved; + } + + public virtual async Task WriteAsync(byte[] buffer) + { + await WriteAsync(buffer, CancellationToken.None).ConfigureAwait(false); + } + + public virtual async Task WriteAsync(byte[] buffer, CancellationToken cancellationToken) + { + await WriteAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + } + + public virtual async Task WriteAsync(byte[] buffer, int offset, int length) + { + await WriteAsync(buffer, offset, length, CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken); + + public virtual async Task FlushAsync() + { + await FlushAsync(CancellationToken.None).ConfigureAwait(false); + } + + public abstract Task FlushAsync(CancellationToken cancellationToken); + + protected abstract void Dispose(bool disposing); + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/TServerTransport.cs b/lib/Thrift/Transports/TServerTransport.cs new file mode 100644 index 00000000000..21516c33a5e --- /dev/null +++ b/lib/Thrift/Transports/TServerTransport.cs @@ -0,0 +1,54 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +using System.Threading; +using System.Threading.Tasks; + +namespace Thrift.Transports +{ + // ReSharper disable once InconsistentNaming + public abstract class TServerTransport + { + public abstract void Listen(); + public abstract void Close(); + public abstract bool IsClientPending(); + + protected virtual async Task AcceptImplementationAsync() + { + return await AcceptImplementationAsync(CancellationToken.None).ConfigureAwait(false); + } + + protected abstract Task AcceptImplementationAsync(CancellationToken cancellationToken); + + public async Task AcceptAsync() + { + return await AcceptAsync(CancellationToken.None).ConfigureAwait(false); + } + + public async Task AcceptAsync(CancellationToken cancellationToken) + { + var transport = await AcceptImplementationAsync(cancellationToken).ConfigureAwait(false); + + if (transport == null) + { + throw new TTransportException($"{nameof(AcceptImplementationAsync)} should not return null"); + } + + return transport; + } + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/TTransportException.cs b/lib/Thrift/Transports/TTransportException.cs new file mode 100644 index 00000000000..b7c42e33a87 --- /dev/null +++ b/lib/Thrift/Transports/TTransportException.cs @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Transports +{ + // ReSharper disable once InconsistentNaming + public class TTransportException : TException + { + public enum ExceptionType + { + Unknown, + NotOpen, + AlreadyOpen, + TimedOut, + EndOfFile, + Interrupted + } + + protected ExceptionType ExType; + + public TTransportException() + { + } + + public TTransportException(ExceptionType exType) + : this() + { + ExType = exType; + } + + public TTransportException(ExceptionType exType, string message) + : base(message) + { + ExType = exType; + } + + public TTransportException(string message) + : base(message) + { + } + + public ExceptionType Type => ExType; + } +} \ No newline at end of file diff --git a/lib/Thrift/Transports/TTransportFactory.cs b/lib/Thrift/Transports/TTransportFactory.cs new file mode 100644 index 00000000000..d505f45d4ab --- /dev/null +++ b/lib/Thrift/Transports/TTransportFactory.cs @@ -0,0 +1,35 @@ +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you 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. + +namespace Thrift.Transports +{ + /// + /// From Mark Slee & Aditya Agarwal of Facebook: + /// Factory class used to create wrapped instance of Transports. + /// This is used primarily in servers, which get Transports from + /// a ServerTransport and then may want to mutate them (i.e. create + /// a BufferedTransport from the underlying base transport). + /// + // ReSharper disable once InconsistentNaming + public class TTransportFactory + { + public virtual TClientTransport GetTransport(TClientTransport trans) + { + return trans; + } + } +} diff --git a/samples/Exporters/Exporters.csproj b/samples/Exporters/Exporters.csproj index 32b518a9608..7013bb5e792 100644 --- a/samples/Exporters/Exporters.csproj +++ b/samples/Exporters/Exporters.csproj @@ -23,6 +23,7 @@ + diff --git a/samples/Exporters/Program.cs b/samples/Exporters/Program.cs index b03c75bcbf9..1b7fb7b7d9d 100644 --- a/samples/Exporters/Program.cs +++ b/samples/Exporters/Program.cs @@ -35,8 +35,9 @@ public class Program /// Arguments from command line. public static void Main(string[] args) { - Parser.Default.ParseArguments(args) + Parser.Default.ParseArguments(args) .MapResult( + (JaegerOptions options) => TestJaeger.Run(options.Host, options.Port), (ZipkinOptions options) => TestZipkin.Run(options.Uri), (ApplicationInsightsOptions options) => TestApplicationInsights.Run(), (PrometheusOptions options) => TestPrometheus.Run(), @@ -57,6 +58,16 @@ internal class StackdriverOptions public string ProjectId { get; set; } } + [Verb("jaeger", HelpText = "Specify the options required to test Jaeger exporter")] + internal class JaegerOptions + { + [Option('h', "host", HelpText = "Please specify the host of the Jaeger Agent", Required = true)] + public string Host { get; set; } + + [Option('p', "port", HelpText = "Please specify the port of the Jaeger Agent", Required = true)] + public int Port { get; set; } + } + [Verb("zipkin", HelpText = "Specify the options required to test Zipkin exporter")] internal class ZipkinOptions { diff --git a/samples/Exporters/TestJaeger.cs b/samples/Exporters/TestJaeger.cs new file mode 100644 index 00000000000..941bd5b12e8 --- /dev/null +++ b/samples/Exporters/TestJaeger.cs @@ -0,0 +1,103 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace Samples +{ + using System; + using System.Collections.Generic; + using System.Threading; + using OpenTelemetry.Exporter.Jaeger; + using OpenTelemetry.Trace; + using OpenTelemetry.Trace.Config; + using OpenTelemetry.Trace.Sampler; + + internal class TestJaeger + { + internal static object Run(string host, int port) + { + // 1. Configure exporter to export traces to Jaeger + var exporter = new JaegerExporter( + new JaegerExporterOptions + { + ServiceName = "tracing-to-jaeger-service", + AgentHost = host, + AgentPort = port, + }, + Tracing.ExportComponent); + + exporter.Start(); + + // 2. Configure 100% sample rate for the purposes of the demo + ITraceConfig traceConfig = Tracing.TraceConfig; + ITraceParams currentConfig = traceConfig.ActiveTraceParams; + var newConfig = currentConfig.ToBuilder() + .SetSampler(Samplers.AlwaysSample) + .Build(); + traceConfig.UpdateActiveTraceParams(newConfig); + + // 3. Tracer is global singleton. You can register it via dependency injection if it exists + // but if not - you can use it as follows: + var tracer = Tracing.Tracer; + + // 4. Create a scoped span. It will end automatically when using statement ends + using (tracer.WithSpan(tracer.SpanBuilder("Main").StartSpan())) + { + tracer.CurrentSpan.SetAttribute("custom-attribute", 55); + Console.WriteLine("About to do a busy work"); + for (int i = 0; i < 10; i++) + { + DoWork(i); + } + } + + // 5. Gracefully shutdown the exporter so it'll flush queued traces to Zipkin. + Tracing.ExportComponent.SpanExporter.Dispose(); + + return null; + } + + private static void DoWork(int i) + { + // 6. Get the global singleton Tracer object + var tracer = Tracing.Tracer; + + // 7. Start another span. If another span was already started, it'll use that span as the parent span. + // In this example, the main method already started a span, so that'll be the parent span, and this will be + // a child span. + using (tracer.WithSpan(tracer.SpanBuilder("DoWork").StartSpan())) + { + // Simulate some work. + var span = tracer.CurrentSpan; + + try + { + Console.WriteLine("Doing busy work"); + Thread.Sleep(1000); + } + catch (ArgumentOutOfRangeException e) + { + // 6. Set status upon error + span.Status = Status.Internal.WithDescription(e.ToString()); + } + + // 7. Annotate our span to capture metadata about our operation + var attributes = new Dictionary(); + attributes.Add("use", "demo"); + span.AddEvent("Invoking DoWork", attributes); + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs new file mode 100644 index 00000000000..83d7b3b47dc --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs @@ -0,0 +1,103 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + using Thrift.Protocols.Utilities; + + public class Batch : TAbstractBase + { + public Batch() + { + } + + public Batch(Process process, IEnumerable spans) + : this() + { + this.Process = process; + this.Spans = spans ?? Enumerable.Empty(); + } + + public Process Process { get; set; } + + public IEnumerable Spans { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + try + { + var struc = new TStruct("Batch"); + + await oprot.WriteStructBeginAsync(struc, cancellationToken); + + var field = new TField + { + Name = "process", + Type = TType.Struct, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await this.Process.WriteAsync(oprot, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "spans"; + field.Type = TType.List; + field.ID = 2; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + { + await oprot.WriteListBeginAsync(new TList(TType.Struct, this.Spans.Count()), cancellationToken); + foreach (JaegerSpan s in this.Spans) + { + await s.WriteAsync(oprot, cancellationToken); + } + + await oprot.WriteListEndAsync(cancellationToken); + } + + await oprot.WriteFieldEndAsync(cancellationToken); + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + public override string ToString() + { + var sb = new StringBuilder("Batch("); + sb.Append(", Process: "); + sb.Append(this.Process?.ToString() ?? ""); + sb.Append(", Spans: "); + sb.Append(this.Spans); + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs new file mode 100644 index 00000000000..aa794caca3b --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs @@ -0,0 +1,78 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class EmitBatchArgs : TAbstractBase + { + public EmitBatchArgs() + { + } + + public Batch Batch { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + try + { + var struc = new TStruct("emitBatch_args"); + await oprot.WriteStructBeginAsync(struc, cancellationToken); + if (this.Batch is Batch batch) + { + var field = new TField + { + Name = "batch", + Type = TType.Struct, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await batch.WriteAsync(oprot, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + } + + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + public override string ToString() + { + var sb = new StringBuilder("emitBatch_args("); + + if (this.Batch is Batch batch) + { + sb.Append("Batch: "); + sb.Append(batch?.ToString() ?? ""); + } + + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerUdpBatcher.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerUdpBatcher.cs new file mode 100644 index 00000000000..fea9cbdcace --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerUdpBatcher.cs @@ -0,0 +1,31 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + public interface IJaegerUdpBatcher : IDisposable + { + Task AppendAsync(JaegerSpan span, CancellationToken cancellationToken); + + Task CloseAsync(CancellationToken cancellationToken); + + Task FlushAsync(CancellationToken cancellationToken); + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerUdpClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerUdpClient.cs new file mode 100644 index 00000000000..b1a28617d8e --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerUdpClient.cs @@ -0,0 +1,35 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Net; + using System.Threading.Tasks; + + public interface IJaegerUdpClient : IDisposable + { + bool Connected { get; } + + EndPoint RemoteEndPoint { get; } + + void Connect(string host, int port); + + void Close(); + + Task SendAsync(byte[] datagram, int bytes); + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/InMemoryTransport.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/InMemoryTransport.cs new file mode 100644 index 00000000000..330dae04df5 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/InMemoryTransport.cs @@ -0,0 +1,90 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Transports; + + public class InMemoryTransport : TClientTransport + { + private readonly MemoryStream byteStream; + private bool isDisposed; + + public InMemoryTransport() + { + this.byteStream = new MemoryStream(); + } + + public override bool IsOpen => true; + + public override Task OpenAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + public override void Close() + { + // do nothing + } + + public override Task ReadAsync( + byte[] buffer, + int offset, + int length, + CancellationToken cancellationToken) => this.byteStream.ReadAsync(buffer, offset, length, cancellationToken); + + public override Task WriteAsync(byte[] buffer, CancellationToken cancellationToken) => this.byteStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); + + public override Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) => this.byteStream.WriteAsync(buffer, offset, length, cancellationToken); + + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + public byte[] GetBuffer() => this.byteStream.ToArray(); + + public void Reset() => this.byteStream.SetLength(0); + + // IDisposable + protected override void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.byteStream?.Dispose(); + } + } + + this.isDisposed = true; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Int128.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Int128.cs new file mode 100644 index 00000000000..99b5b827f42 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Int128.cs @@ -0,0 +1,53 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Diagnostics; + + public struct Int128 + { + public static Int128 Empty = default; + + private const int SpanIdBytes = 8; + private const int TraceIdBytes = 16; + + public Int128(ActivitySpanId spanId) + { + var bytes = new byte[SpanIdBytes]; + + spanId.CopyTo(bytes); + + this.High = 0; + this.Low = BitConverter.ToInt64(bytes, 0); + } + + public Int128(ActivityTraceId traceId) + { + var bytes = new byte[TraceIdBytes]; + + traceId.CopyTo(bytes); + + this.High = BitConverter.ToInt64(bytes, 0); + this.Low = BitConverter.ToInt64(bytes, 8); + } + + public long High { get; set; } + + public long Low { get; set; } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs new file mode 100644 index 00000000000..854244e4f90 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerConversionExtensions.cs @@ -0,0 +1,145 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using OpenTelemetry.Trace; + using OpenTelemetry.Trace.Export; + + public static class JaegerConversionExtensions + { + private const int DaysPerYear = 365; + + // Number of days in 4 years + private const int DaysPer4Years = (DaysPerYear * 4) + 1; // 1461 + + // Number of days in 100 years + private const int DaysPer100Years = (DaysPer4Years * 25) - 1; // 36524 + + // Number of days in 400 years + private const int DaysPer400Years = (DaysPer100Years * 4) + 1; // 146097 + + // Number of days from 1/1/0001 to 12/31/1969 + private const int DaysTo1970 = (DaysPer400Years * 4) + (DaysPer100Years * 3) + (DaysPer4Years * 17) + DaysPerYear; // 719,162 + + private const long UnixEpochTicks = DaysTo1970 * TimeSpan.TicksPerDay; + private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; + private const long UnixEpochMicroseconds = UnixEpochTicks / TicksPerMicrosecond; // 62,135,596,800,000,000 + + public static JaegerSpan ToJaegerSpan(this SpanData span) + { + IEnumerable jaegerTags = null; + + if (span?.Attributes?.AttributeMap is IEnumerable> attributeMap) + { + jaegerTags = attributeMap.Select(a => a.ToJaegerTag()).AsEnumerable(); + } + + IEnumerable jaegerLogs = null; + + if (span?.Events?.Events != null) + { + jaegerLogs = span.Events.Events.Select(e => e.ToJaegerLog()).AsEnumerable(); + } + + IEnumerable refs = null; + + if (span?.Links?.Links != null) + { + refs = span.Links.Links.Select(l => l.ToJaegerSpanRef()).Where(l => l != null).AsEnumerable(); + } + + var parentSpanId = new Int128(span.ParentSpanId); + + var traceId = span?.Context?.TraceId == null ? Int128.Empty : new Int128(span.Context.TraceId); + var spanId = span?.Context?.SpanId == null ? Int128.Empty : new Int128(span.Context.SpanId); + + return new JaegerSpan + { + TraceIdHigh = traceId.High, + TraceIdLow = traceId.Low, + SpanId = spanId.Low, + ParentSpanId = parentSpanId.Low, + OperationName = span.Name, + References = refs, + Flags = (span.Context.TraceOptions & ActivityTraceFlags.Recorded) > 0 ? 0x1 : 0, + StartTime = ToEpochMicroseconds(span.StartTimestamp), + Duration = ToEpochMicroseconds(span.EndTimestamp) - ToEpochMicroseconds(span.StartTimestamp), + JaegerTags = jaegerTags, + Logs = jaegerLogs, + }; + } + + public static JaegerTag ToJaegerTag(this KeyValuePair attribute) + { + switch (attribute.Value) + { + case string s: + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.STRING, VStr = s }; + case int i: + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.LONG, VLong = Convert.ToInt64(i) }; + case long l: + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.LONG, VLong = l }; + case float f: + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.DOUBLE, VDouble = Convert.ToDouble(f) }; + case double d: + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.DOUBLE, VDouble = d }; + case bool b: + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.BOOL, VBool = b }; + } + + return new JaegerTag { Key = attribute.Key, VType = JaegerTagType.STRING, VStr = attribute.Value.ToString() }; + } + + public static JaegerLog ToJaegerLog(this ITimedEvent timedEvent) + { + var tags = timedEvent.Event.Attributes.Select(a => a.ToJaegerTag()).ToList(); + tags.Add(new JaegerTag { Key = "description", VType = JaegerTagType.STRING, VStr = timedEvent.Event.Name }); + + return new JaegerLog + { + Timestamp = timedEvent.Timestamp.ToEpochMicroseconds(), + Fields = tags, + }; + } + + public static JaegerSpanRef ToJaegerSpanRef(this ILink link) + { + var traceId = link?.Context?.TraceId == null ? Int128.Empty : new Int128(link.Context.TraceId); + var spanId = link?.Context?.SpanId == null ? Int128.Empty : new Int128(link.Context.SpanId); + + return new JaegerSpanRef + { + TraceIdHigh = traceId.High, + TraceIdLow = traceId.Low, + SpanId = spanId.Low, + RefType = JaegerSpanRefType.CHILD_OF, + }; + } + + public static long ToEpochMicroseconds(this DateTime timestamp) + { + // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid + // the last digit being off by one for dates that result in negative Unix times + long microseconds = timestamp.Ticks / TicksPerMicrosecond; + return microseconds - UnixEpochMicroseconds; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs new file mode 100644 index 00000000000..c31906dbc27 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs @@ -0,0 +1,28 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + + public class JaegerExporterException : Exception + { + public JaegerExporterException(string message, Exception originalException) + : base(message, originalException) + { + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs new file mode 100644 index 00000000000..0ddef52eac2 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerLog.cs @@ -0,0 +1,101 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class JaegerLog : TAbstractBase + { + public JaegerLog() + { + } + + public JaegerLog(long timestamp, IEnumerable fields) + : this() + { + this.Timestamp = timestamp; + this.Fields = fields ?? Enumerable.Empty(); + } + + public long Timestamp { get; set; } + + public IEnumerable Fields { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + try + { + var struc = new TStruct("Log"); + await oprot.WriteStructBeginAsync(struc, cancellationToken); + + var field = new TField + { + Name = "timestamp", + Type = TType.I64, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.Timestamp, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "fields"; + field.Type = TType.List; + field.ID = 2; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + { + await oprot.WriteListBeginAsync(new TList(TType.Struct, this.Fields.Count()), cancellationToken); + + foreach (JaegerTag jt in this.Fields) + { + await jt.WriteAsync(oprot, cancellationToken); + } + + await oprot.WriteListEndAsync(cancellationToken); + } + + await oprot.WriteFieldEndAsync(cancellationToken); + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + public override string ToString() + { + var sb = new StringBuilder("Log("); + sb.Append(", Timestamp: "); + sb.Append(this.Timestamp); + sb.Append(", Fields: "); + sb.Append(this.Fields); + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs new file mode 100644 index 00000000000..c11f1443f4d --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpan.cs @@ -0,0 +1,255 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class JaegerSpan : TAbstractBase + { + public JaegerSpan() + { + } + + public JaegerSpan(long traceIdLow, long traceIdHigh, long spanId, long parentSpanId, string operationName, int flags, long startTime, long duration) + : this() + { + this.TraceIdLow = traceIdLow; + this.TraceIdHigh = traceIdHigh; + this.SpanId = spanId; + this.ParentSpanId = parentSpanId; + this.OperationName = operationName; + this.Flags = flags; + this.StartTime = startTime; + this.Duration = duration; + } + + public long TraceIdLow { get; set; } + + public long TraceIdHigh { get; set; } + + public long SpanId { get; set; } + + public long ParentSpanId { get; set; } + + public string OperationName { get; set; } + + public IEnumerable References { get; set; } + + public int Flags { get; set; } + + public long StartTime { get; set; } + + public long Duration { get; set; } + + public IEnumerable JaegerTags { get; set; } + + public IEnumerable Logs { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + try + { + var struc = new TStruct("Span"); + await oprot.WriteStructBeginAsync(struc, cancellationToken); + + var field = new TField + { + Name = "traceIdLow", + Type = TType.I64, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.TraceIdLow, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "traceIdHigh"; + field.Type = TType.I64; + field.ID = 2; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.TraceIdHigh, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "spanId"; + field.Type = TType.I64; + field.ID = 3; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.SpanId, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "parentSpanId"; + field.Type = TType.I64; + field.ID = 4; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.ParentSpanId, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "operationName"; + field.Type = TType.String; + field.ID = 5; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteStringAsync(this.OperationName, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + if (this.References is IEnumerable references) + { + field.Name = "references"; + field.Type = TType.List; + field.ID = 6; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + { + await oprot.WriteListBeginAsync(new TList(TType.Struct, references.Count()), cancellationToken); + + foreach (JaegerSpanRef sr in references) + { + await sr.WriteAsync(oprot, cancellationToken); + } + + await oprot.WriteListEndAsync(cancellationToken); + } + + await oprot.WriteFieldEndAsync(cancellationToken); + } + + field.Name = "flags"; + field.Type = TType.I32; + field.ID = 7; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI32Async(this.Flags, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "startTime"; + field.Type = TType.I64; + field.ID = 8; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.StartTime, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "duration"; + field.Type = TType.I64; + field.ID = 9; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.Duration, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + if (this.JaegerTags is IEnumerable jaegerTags) + { + field.Name = "JaegerTags"; + field.Type = TType.List; + field.ID = 10; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + { + await oprot.WriteListBeginAsync(new TList(TType.Struct, jaegerTags.Count()), cancellationToken); + + foreach (JaegerTag jt in jaegerTags) + { + await jt.WriteAsync(oprot, cancellationToken); + } + + await oprot.WriteListEndAsync(cancellationToken); + } + + await oprot.WriteFieldEndAsync(cancellationToken); + } + + if (this.Logs is IEnumerable logs) + { + field.Name = "logs"; + field.Type = TType.List; + field.ID = 11; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + { + await oprot.WriteListBeginAsync(new TList(TType.Struct, logs.Count()), cancellationToken); + + foreach (JaegerLog jl in logs) + { + await jl.WriteAsync(oprot, cancellationToken); + } + + await oprot.WriteListEndAsync(cancellationToken); + } + + await oprot.WriteFieldEndAsync(cancellationToken); + } + + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + public override string ToString() + { + var sb = new StringBuilder("Span("); + sb.Append(", TraceIdLow: "); + sb.Append(this.TraceIdLow); + sb.Append(", TraceIdHigh: "); + sb.Append(this.TraceIdHigh); + sb.Append(", SpanId: "); + sb.Append(this.SpanId); + sb.Append(", ParentSpanId: "); + sb.Append(this.ParentSpanId); + sb.Append(", OperationName: "); + sb.Append(this.OperationName); + if (this.References is IEnumerable jaegerSpanRef) + { + sb.Append(", References: "); + sb.Append(jaegerSpanRef); + } + + sb.Append(", Flags: "); + sb.Append(this.Flags); + sb.Append(", StartTime: "); + sb.Append(this.StartTime); + sb.Append(", Duration: "); + sb.Append(this.Duration); + if (this.JaegerTags is IEnumerable tags) + { + sb.Append(", JaegerTags: "); + sb.Append(tags); + } + + if (this.Logs is IEnumerable logs) + { + sb.Append(", Logs: "); + sb.Append(logs); + } + + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRef.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRef.cs new file mode 100644 index 00000000000..d9824380630 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRef.cs @@ -0,0 +1,119 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class JaegerSpanRef : TAbstractBase + { + public JaegerSpanRef() + { + } + + public JaegerSpanRef(JaegerSpanRefType refType, long traceIdLow, long traceIdHigh, long spanId) + : this() + { + this.RefType = refType; + this.TraceIdLow = traceIdLow; + this.TraceIdHigh = traceIdHigh; + this.SpanId = spanId; + } + + public JaegerSpanRefType RefType { get; set; } + + public long TraceIdLow { get; set; } + + public long TraceIdHigh { get; set; } + + public long SpanId { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + try + { + var struc = new TStruct("SpanRef"); + await oprot.WriteStructBeginAsync(struc, cancellationToken); + + var field = new TField + { + Name = "refType", + Type = TType.I32, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI32Async((int)this.RefType, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "traceIdLow"; + field.Type = TType.I64; + field.ID = 2; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.TraceIdLow, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "traceIdHigh"; + field.Type = TType.I64; + field.ID = 3; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.TraceIdHigh, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "spanId"; + field.Type = TType.I64; + field.ID = 4; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.SpanId, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + /// + /// + /// + /// A string representation of the object. + public override string ToString() + { + var sb = new StringBuilder("SpanRef("); + sb.Append(", RefType: "); + sb.Append(this.RefType); + sb.Append(", TraceIdLow: "); + sb.Append(this.TraceIdLow); + sb.Append(", TraceIdHigh: "); + sb.Append(this.TraceIdHigh); + sb.Append(", SpanId: "); + sb.Append(this.SpanId); + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs new file mode 100644 index 00000000000..4b49dfef5de --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs @@ -0,0 +1,36 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + + /// + /// Represents the different types of Jaeger Spans. + /// + public enum JaegerSpanRefType + { + /// + /// A child span + /// + CHILD_OF = 0, + + /// + /// A sibling span + /// + FOLLOWS_FROM = 1, + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs new file mode 100644 index 00000000000..71853ebe3ef --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs @@ -0,0 +1,180 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class JaegerTag : TAbstractBase + { + public JaegerTag() + { + } + + public JaegerTag(string key, JaegerTagType vType) + : this() + { + this.Key = key; + this.VType = vType; + } + + public string Key { get; set; } + + public JaegerTagType VType { get; set; } + + public string VStr { get; set; } + + public double? VDouble { get; set; } + + public bool? VBool { get; set; } + + public long? VLong { get; set; } + + public byte[] VBinary { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + try + { + var struc = new TStruct("Tag"); + await oprot.WriteStructBeginAsync(struc, cancellationToken); + + var field = new TField + { + Name = "key", + Type = TType.String, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteStringAsync(this.Key, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + field.Name = "vType"; + field.Type = TType.I32; + field.ID = 2; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI32Async((int)this.VType, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + if (this.VStr != null) + { + field.Name = "vStr"; + field.Type = TType.String; + field.ID = 3; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteStringAsync(this.VStr, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + } + + if (this.VDouble.HasValue) + { + field.Name = "vDouble"; + field.Type = TType.Double; + field.ID = 4; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteDoubleAsync(this.VDouble.Value, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + } + + if (this.VBool.HasValue) + { + field.Name = "vBool"; + field.Type = TType.Bool; + field.ID = 5; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteBoolAsync(this.VBool.Value, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + } + + if (this.VLong.HasValue) + { + field.Name = "vLong"; + field.Type = TType.I64; + field.ID = 6; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteI64Async(this.VLong.Value, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + } + + if (this.VBinary != null) + { + field.Name = "vBinary"; + field.Type = TType.String; + field.ID = 7; + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteBinaryAsync(this.VBinary, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + } + + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + public override string ToString() + { + var sb = new StringBuilder("Tag("); + sb.Append(", Key: "); + sb.Append(this.Key); + sb.Append(", VType: "); + sb.Append(this.VType); + if (this.VStr != null) + { + sb.Append(", VStr: "); + sb.Append(this.VStr); + } + + if (this.VDouble.HasValue) + { + sb.Append(", VDouble: "); + sb.Append(this.VDouble); + } + + if (this.VBool.HasValue) + { + sb.Append(", VBool: "); + sb.Append(this.VBool); + } + + if (this.VLong.HasValue) + { + sb.Append(", VLong: "); + sb.Append(this.VLong); + } + + if (this.VBinary != null) + { + sb.Append(", VBinary: "); + sb.Append(this.VBinary); + } + + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs new file mode 100644 index 00000000000..868a41cc461 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs @@ -0,0 +1,51 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + + /// + /// Indicates the data type of a Jaeger tag. + /// + public enum JaegerTagType + { + /// + /// Tag contains a string + /// + STRING = 0, + + /// + /// Tag contains a double + /// + DOUBLE = 1, + + /// + /// Tag contains a boolean + /// + BOOL = 2, + + /// + /// Tag contains a long + /// + LONG = 3, + + /// + /// Tag contains binary data + /// + BINARY = 4, + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClient.cs new file mode 100644 index 00000000000..061b5368232 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClient.cs @@ -0,0 +1,50 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Thrift; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class JaegerThriftClient : TBaseClient, IDisposable + { + public JaegerThriftClient(TProtocol protocol) + : this(protocol, protocol) + { + } + + public JaegerThriftClient(TProtocol inputProtocol, TProtocol outputProtocol) + : base(inputProtocol, outputProtocol) + { + } + + public async Task EmitBatchAsync(Batch batch, CancellationToken cancellationToken) + { + await this.OutputProtocol.WriteMessageBeginAsync(new TMessage("emitBatch", TMessageType.Oneway, this.SeqId), cancellationToken); + + var args = new EmitBatchArgs(); + args.Batch = batch; + + await args.WriteAsync(this.OutputProtocol, cancellationToken); + await this.OutputProtocol.WriteMessageEndAsync(cancellationToken); + await this.OutputProtocol.Transport.FlushAsync(cancellationToken); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClientTransport.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClientTransport.cs new file mode 100644 index 00000000000..4e97b346021 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClientTransport.cs @@ -0,0 +1,113 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.IO; + using System.Net.Sockets; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Transports; + + public class JaegerThriftClientTransport : TClientTransport + { + private readonly IJaegerUdpClient udpClient; + private readonly MemoryStream byteStream; + private bool isDisposed = false; + + public JaegerThriftClientTransport(string host, int port) + : this(host, port, new MemoryStream(), new JaegerUdpClient()) + { + } + + public JaegerThriftClientTransport(string host, int port, MemoryStream stream, IJaegerUdpClient client) + { + this.byteStream = stream; + this.udpClient = client; + this.udpClient.Connect(host, port); + } + + public override bool IsOpen => this.udpClient.Connected; + + public override void Close() + { + this.udpClient.Close(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + var bytes = this.byteStream.ToArray(); + + if (bytes.Length == 0) + { + return Task.CompletedTask; + } + + this.byteStream.SetLength(0); + + try + { + return this.udpClient.SendAsync(bytes, bytes.Length); + } + catch (SocketException se) + { + throw new TTransportException(TTransportException.ExceptionType.Unknown, $"Cannot flush because of socket exception. UDP Packet size was {bytes.Length}. Exception message: {se.Message}"); + } + catch (Exception e) + { + throw new TTransportException(TTransportException.ExceptionType.Unknown, $"Cannot flush closed transport. {e.Message}"); + } + } + + public override Task OpenAsync(CancellationToken cancellationToken) + { + // Do nothing + return Task.CompletedTask; + } + + public override Task ReadAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override Task WriteAsync(byte[] buffer, CancellationToken cancellationToken) + { + return this.WriteAsync(buffer, 0, buffer.Length, cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int length, CancellationToken cancellationToken) + { + return this.byteStream.WriteAsync(buffer, offset, length, cancellationToken); + } + + public override string ToString() + { + return $"{nameof(JaegerThriftClientTransport)}(Client={this.udpClient.RemoteEndPoint})"; + } + + protected override void Dispose(bool disposing) + { + if (!this.isDisposed && disposing) + { + this.byteStream?.Dispose(); + this.udpClient?.Dispose(); + } + + this.isDisposed = true; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTraceExporterHandler.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTraceExporterHandler.cs new file mode 100644 index 00000000000..5f1e64739c0 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTraceExporterHandler.cs @@ -0,0 +1,75 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Sockets; + using System.Threading; + using System.Threading.Tasks; + using OpenTelemetry.Trace; + using OpenTelemetry.Trace.Export; + using Thrift.Protocols; + + public class JaegerTraceExporterHandler : IHandler, IDisposable + { + private readonly IJaegerUdpBatcher jaegerAgentUdpBatcher; + private bool disposedValue = false; // To detect redundant dispose calls + + public JaegerTraceExporterHandler(JaegerExporterOptions options) + : this(new JaegerUdpBatcher(options)) + { + } + + public JaegerTraceExporterHandler(IJaegerUdpBatcher jaegerAgentUdpBatcher) + { + this.jaegerAgentUdpBatcher = jaegerAgentUdpBatcher; + } + + public async Task ExportAsync(IEnumerable spanDataList) + { + var jaegerspans = spanDataList.Select(sdl => sdl.ToJaegerSpan()); + + foreach (var s in jaegerspans) + { + await this.jaegerAgentUdpBatcher.AppendAsync(s, CancellationToken.None); + } + + await this.jaegerAgentUdpBatcher.FlushAsync(CancellationToken.None); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing). + this.Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.jaegerAgentUdpBatcher.Dispose(); + } + + this.disposedValue = true; + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpBatcher.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpBatcher.cs new file mode 100644 index 00000000000..8827909fb10 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpBatcher.cs @@ -0,0 +1,156 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + + public class JaegerUdpBatcher : IJaegerUdpBatcher + { + private const int DefaultMaxPacketSize = 65000; + private readonly int? maxPacketSize; + private readonly ITProtocolFactory protocolFactory; + private readonly JaegerThriftClientTransport clientTransport; + private readonly JaegerThriftClient thriftClient; + private readonly Process process; + private readonly int processByteSize; + private readonly List currentBatch = new List(); + + private int batchByteSize; + + private bool disposedValue = false; // To detect redundant calls + + public JaegerUdpBatcher(JaegerExporterOptions options) + { + this.maxPacketSize = options.MaxPacketSize == 0 ? DefaultMaxPacketSize : options.MaxPacketSize; + this.protocolFactory = new TCompactProtocol.Factory(); + this.clientTransport = new JaegerThriftClientTransport(options.AgentHost, options.AgentPort.Value); + this.thriftClient = new JaegerThriftClient(this.protocolFactory.GetProtocol(this.clientTransport)); + this.process = new Process(options.ServiceName, options.ProcessTags); + this.processByteSize = this.GetSize(this.process); + this.batchByteSize = this.processByteSize; + } + + public async Task AppendAsync(JaegerSpan span, CancellationToken cancellationToken) + { + int spanSize = this.GetSize(span); + + if (spanSize > this.maxPacketSize) + { + throw new JaegerExporterException($"ThriftSender received a span that was too large, size = {spanSize}, max = {this.maxPacketSize}", null); + } + + this.batchByteSize += spanSize; + if (this.batchByteSize <= this.maxPacketSize) + { + this.currentBatch.Add(span); + + if (this.batchByteSize < this.maxPacketSize) + { + return 0; + } + + return await this.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + int n; + + try + { + n = await this.FlushAsync(cancellationToken).ConfigureAwait(false); + } + catch (JaegerExporterException ex) + { + // +1 for the span not submitted in the buffer above + throw new JaegerExporterException(ex.Message, ex); + } + + this.currentBatch.Add(span); + this.batchByteSize = this.processByteSize + spanSize; + return n; + } + + public async Task FlushAsync(CancellationToken cancellationToken) + { + int n = this.currentBatch.Count; + + if (n == 0) + { + return 0; + } + + try + { + await this.SendAsync(this.process, this.currentBatch, cancellationToken).ConfigureAwait(false); + } + finally + { + this.currentBatch.Clear(); + this.batchByteSize = this.processByteSize; + } + + return n; + } + + public virtual Task CloseAsync(CancellationToken cancellationToken) => this.FlushAsync(cancellationToken); + + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing). + this.Dispose(true); + } + + protected async Task SendAsync(Process process, List spans, CancellationToken cancellationToken) + { + try + { + var batch = new Batch(process, spans); + await this.thriftClient.EmitBatchAsync(batch, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new JaegerExporterException($"Could not send {spans.Count} spans", ex); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.thriftClient.Dispose(); + this.clientTransport.Dispose(); + } + + this.disposedValue = true; + } + } + + private int GetSize(TAbstractBase thriftBase) + { + using (var memoryTransport = new InMemoryTransport()) + { + thriftBase.WriteAsync(this.protocolFactory.GetProtocol(memoryTransport), CancellationToken.None).GetAwaiter().GetResult(); + return memoryTransport.GetBuffer().Length; + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs new file mode 100644 index 00000000000..146d7f32a98 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs @@ -0,0 +1,45 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Net; + using System.Net.Sockets; + using System.Threading.Tasks; + + public class JaegerUdpClient : IJaegerUdpClient + { + private readonly UdpClient client; + + public JaegerUdpClient() + { + this.client = new UdpClient(); + } + + public bool Connected => this.client.Client.Connected; + + public EndPoint RemoteEndPoint => this.client.Client.RemoteEndPoint; + + public void Close() => this.client.Close(); + + public void Connect(string host, int port) => this.client.Connect(host, port); + + public Task SendAsync(byte[] datagram, int bytes) => this.client.SendAsync(datagram, bytes); + + public void Dispose() => this.client.Dispose(); + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs new file mode 100644 index 00000000000..b08c84d83ca --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs @@ -0,0 +1,116 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Thrift.Protocols; + using Thrift.Protocols.Entities; + + public class Process : TAbstractBase + { + public Process() + { + } + + public Process(string serviceName, IDictionary processTags) + : this() + { + this.ServiceName = serviceName; + + if (processTags != null) + { + this.Tags = new List(); + this.Tags.AddRange(processTags.Select(pt => pt.ToJaegerTag())); + } + } + + public string ServiceName { get; set; } + + public List Tags { get; set; } + + public async Task WriteAsync(TProtocol oprot, CancellationToken cancellationToken) + { + oprot.IncrementRecursionDepth(); + + try + { + var struc = new TStruct("Process"); + await oprot.WriteStructBeginAsync(struc, cancellationToken); + + var field = new TField + { + Name = "serviceName", + Type = TType.String, + ID = 1, + }; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + await oprot.WriteStringAsync(this.ServiceName, cancellationToken); + await oprot.WriteFieldEndAsync(cancellationToken); + + if (this.Tags != null) + { + field.Name = "tags"; + field.Type = TType.List; + field.ID = 2; + + await oprot.WriteFieldBeginAsync(field, cancellationToken); + { + await oprot.WriteListBeginAsync(new TList(TType.Struct, this.Tags.Count), cancellationToken); + + foreach (JaegerTag jt in this.Tags) + { + await jt.WriteAsync(oprot, cancellationToken); + } + + await oprot.WriteListEndAsync(cancellationToken); + } + + await oprot.WriteFieldEndAsync(cancellationToken); + } + + await oprot.WriteFieldStopAsync(cancellationToken); + await oprot.WriteStructEndAsync(cancellationToken); + } + finally + { + oprot.DecrementRecursionDepth(); + } + } + + public override string ToString() + { + var sb = new StringBuilder("Process("); + sb.Append(", ServiceName: "); + sb.Append(this.ServiceName); + + if (this.Tags != null) + { + sb.Append(", Tags: "); + sb.Append(this.Tags); + } + + sb.Append(")"); + return sb.ToString(); + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs new file mode 100644 index 00000000000..60f200f26a4 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs @@ -0,0 +1,128 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger +{ + using System; + using OpenTelemetry.Exporter.Jaeger.Implementation; + using OpenTelemetry.Trace.Export; + + public class JaegerExporter : IDisposable + { + public const string DefaultAgentUdpHost = "localhost"; + public const int DefaultAgentUdpCompactPort = 6831; + public const int DefaultMaxPacketSize = 65000; + + private const string ExporterName = "JaegerTraceExporter"; + + private readonly object @lock = new object(); + private readonly JaegerExporterOptions options; + private readonly IExportComponent exportComponent; + + private volatile bool isInitialized = false; + private JaegerTraceExporterHandler handler; + private bool disposedValue = false; // To detect redundant dispose calls + + public JaegerExporter(JaegerExporterOptions options, IExportComponent exportComponent) + { + this.ValidateOptions(options); + this.InitializeOptions(options); + + this.options = options; + this.exportComponent = exportComponent; + } + + public void Start() + { + lock (this.@lock) + { + if (this.isInitialized) + { + return; + } + + if (this.exportComponent != null) + { + this.handler = new JaegerTraceExporterHandler(this.options); + this.exportComponent.SpanExporter.RegisterHandler(ExporterName, this.handler); + } + } + } + + public void Stop() + { + if (!this.isInitialized) + { + return; + } + + lock (this.@lock) + { + if (this.exportComponent != null) + { + this.exportComponent.SpanExporter.UnregisterHandler(ExporterName); + } + } + + this.isInitialized = false; + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing). + this.Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.handler.Dispose(); + } + + this.disposedValue = true; + } + } + + private void ValidateOptions(JaegerExporterOptions options) + { + if (string.IsNullOrWhiteSpace(options.ServiceName)) + { + throw new ArgumentException("ServiceName", "Service Name is required."); + } + } + + private void InitializeOptions(JaegerExporterOptions options) + { + if (string.IsNullOrWhiteSpace(options.AgentHost)) + { + options.AgentHost = DefaultAgentUdpHost; + } + + if (!options.AgentPort.HasValue) + { + options.AgentPort = DefaultAgentUdpCompactPort; + } + + if (!options.MaxPacketSize.HasValue) + { + options.MaxPacketSize = DefaultMaxPacketSize; + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs new file mode 100644 index 00000000000..fef770a198f --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs @@ -0,0 +1,34 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger +{ + using System; + using System.Collections.Generic; + + public class JaegerExporterOptions + { + public string ServiceName { get; set; } + + public string AgentHost { get; set; } + + public int? AgentPort { get; set; } + + public int? MaxPacketSize { get; set; } + + public Dictionary ProcessTags { get; set; } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj new file mode 100644 index 00000000000..879d3410513 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj @@ -0,0 +1,16 @@ + + + + net46;netstandard2.0 + netstandard2.0 + OpenTelemetry authors + https://opentelemetry.io/img/logos/opentelemetry-icon-color.png + https://OpenTelemetry.io + Tracing;OpenTelemetry;Management;Monitoring;Jaeger;distributed-tracing + + + + + + + diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs new file mode 100644 index 00000000000..7c1ecb959b5 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerSpanConverterTest.cs @@ -0,0 +1,539 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Tests.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using OpenTelemetry.Exporter.Jaeger.Implementation; + using OpenTelemetry.Resources; + using OpenTelemetry.Trace; + using OpenTelemetry.Trace.Export; + using Xunit; + + public class JaegerSpanConverterTest + { + private const long MillisPerSecond = 1000L; + private const long NanosPerMillisecond = 1000 * 1000; + private const long NanosPerSecond = NanosPerMillisecond * MillisPerSecond; + + public JaegerSpanConverterTest() + { + } + + [Fact] + public void JaegerSpanConverterTest_ConvertSpanToJaegerSpan_AllPropertiesSet() + { + var startTimestamp = DateTime.Now; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.Now; + + var traceId = ActivityTraceId.CreateRandom(); + var traceIdAsInt = new Int128(traceId); + var spanId = ActivitySpanId.CreateRandom(); + var spanIdAsInt = new Int128(spanId); + var parentSpanId = ActivitySpanId.CreateRandom(); + var attributes = Attributes.Create(new Dictionary{ + { "stringKey", "value"}, + { "longKey", 1L}, + { "longKey2", 1 }, + { "doubleKey", 1D}, + { "doubleKey2", 1F}, + { "boolKey", true}, + }, 0); + var events = TimedEvents.Create(new List> + { + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event1", + new Dictionary + { + { "key", "value" }, + } + ) + ), + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event2", + new Dictionary + { + { "key", "value" }, + } + ) + ), + }, 0); + + var linkedSpanId = ActivitySpanId.CreateRandom(); + + var link = Link.FromSpanContext(SpanContext.Create( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty)); + + var linkTraceIdAsInt = new Int128(link.Context.TraceId); + var linkSpanIdAsInt = new Int128(link.Context.SpanId); + + var links = LinkList.Create(new List { link }, 0); + + var spanData = SpanData.Create( + SpanContext.Create( + traceId, + spanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty + ), + parentSpanId, + Resource.Empty, + "Name", + startTimestamp, + attributes, + events, + links, + null, + Status.Ok, + SpanKind.Client, + endTimestamp + ); + + var jaegerSpan = spanData.ToJaegerSpan(); + + Assert.Equal("Name", jaegerSpan.OperationName); + Assert.Equal(2, jaegerSpan.Logs.Count()); + + Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); + Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); + Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); + Assert.Equal(new Int128(parentSpanId).Low, jaegerSpan.ParentSpanId); + + Assert.Equal(links.Links.Count(), jaegerSpan.References.Count()); + var references = jaegerSpan.References.ToArray(); + var jaegerRef = references[0]; + Assert.Equal(linkTraceIdAsInt.High, jaegerRef.TraceIdHigh); + Assert.Equal(linkTraceIdAsInt.Low, jaegerRef.TraceIdLow); + Assert.Equal(linkSpanIdAsInt.Low, jaegerRef.SpanId); + + Assert.Equal(0x1, jaegerSpan.Flags); + + Assert.Equal(startTimestamp.ToEpochMicroseconds(), jaegerSpan.StartTime); + Assert.Equal(endTimestamp.ToEpochMicroseconds() - startTimestamp.ToEpochMicroseconds(), jaegerSpan.Duration); + + var tags = jaegerSpan.JaegerTags.ToArray(); + var tag = tags[0]; + Assert.Equal(JaegerTagType.STRING, tag.VType); + Assert.Equal("stringKey", tag.Key); + Assert.Equal("value", tag.VStr); + tag = tags[1]; + Assert.Equal(JaegerTagType.LONG, tag.VType); + Assert.Equal("longKey", tag.Key); + Assert.Equal(1, tag.VLong); + tag = tags[2]; + Assert.Equal(JaegerTagType.LONG, tag.VType); + Assert.Equal("longKey2", tag.Key); + Assert.Equal(1, tag.VLong); + tag = tags[3]; + Assert.Equal(JaegerTagType.DOUBLE, tag.VType); + Assert.Equal("doubleKey", tag.Key); + Assert.Equal(1, tag.VDouble); + tag = tags[4]; + Assert.Equal(JaegerTagType.DOUBLE, tag.VType); + Assert.Equal("doubleKey2", tag.Key); + Assert.Equal(1, tag.VDouble); + tag = tags[5]; + Assert.Equal(JaegerTagType.BOOL, tag.VType); + Assert.Equal("boolKey", tag.Key); + Assert.Equal(true, tag.VBool); + + var logs = jaegerSpan.Logs.ToArray(); + var jaegerLog = logs[0]; + Assert.Equal(events.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); + Assert.Equal(jaegerLog.Fields.Count(), 2); + var eventFields = jaegerLog.Fields.ToArray(); + var eventField = eventFields[0]; + Assert.Equal("key", eventField.Key); + Assert.Equal("value", eventField.VStr); + eventField = eventFields[1]; + Assert.Equal("description", eventField.Key); + Assert.Equal("Event1", eventField.VStr); + + jaegerLog = logs[1]; + Assert.Equal(events.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); + Assert.Equal(jaegerLog.Fields.Count(), 2); + eventFields = jaegerLog.Fields.ToArray(); + eventField = eventFields[0]; + Assert.Equal("key", eventField.Key); + Assert.Equal("value", eventField.VStr); + eventField = eventFields[1]; + Assert.Equal("description", eventField.Key); + Assert.Equal("Event2", eventField.VStr); + } + + [Fact] + public void JaegerSpanConverterTest_ConvertSpanToJaegerSpan_NoAttributes() + { + var startTimestamp = DateTime.Now; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.Now; + + var traceId = ActivityTraceId.CreateRandom(); + var traceIdAsInt = new Int128(traceId); + var spanId = ActivitySpanId.CreateRandom(); + var spanIdAsInt = new Int128(spanId); + var parentSpanId = ActivitySpanId.CreateRandom(); + var events = TimedEvents.Create(new List> + { + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event1", + new Dictionary + { + { "key", "value" }, + } + ) + ), + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event2", + new Dictionary + { + { "key", "value" }, + } + ) + ), + }, 0); + + var linkedSpanId = ActivitySpanId.CreateRandom(); + + var link = Link.FromSpanContext(SpanContext.Create( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty)); + + var linkTraceIdAsInt = new Int128(link.Context.TraceId); + var linkSpanIdAsInt = new Int128(link.Context.SpanId); + + var links = LinkList.Create(new List { link }, 0); + + var spanData = SpanData.Create( + SpanContext.Create( + traceId, + spanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty + ), + parentSpanId, + Resource.Empty, + "Name", + startTimestamp, + null, + events, + links, + null, + Status.Ok, + SpanKind.Client, + endTimestamp + ); + + var jaegerSpan = spanData.ToJaegerSpan(); + + Assert.Equal("Name", jaegerSpan.OperationName); + Assert.Equal(2, jaegerSpan.Logs.Count()); + + Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); + Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); + Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); + Assert.Equal(new Int128(parentSpanId).Low, jaegerSpan.ParentSpanId); + + Assert.Equal(links.Links.Count(), jaegerSpan.References.Count()); + var references = jaegerSpan.References.ToArray(); + var jaegerRef = references[0]; + Assert.Equal(linkTraceIdAsInt.High, jaegerRef.TraceIdHigh); + Assert.Equal(linkTraceIdAsInt.Low, jaegerRef.TraceIdLow); + Assert.Equal(linkSpanIdAsInt.Low, jaegerRef.SpanId); + + Assert.Equal(0x1, jaegerSpan.Flags); + + Assert.Equal(startTimestamp.ToEpochMicroseconds(), jaegerSpan.StartTime); + Assert.Equal(endTimestamp.ToEpochMicroseconds() - startTimestamp.ToEpochMicroseconds(), jaegerSpan.Duration); + + Assert.Empty(jaegerSpan.JaegerTags); + + var logs = jaegerSpan.Logs.ToArray(); + var jaegerLog = logs[0]; + Assert.Equal(events.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); + Assert.Equal(jaegerLog.Fields.Count(), 2); + var eventFields = jaegerLog.Fields.ToArray(); + var eventField = eventFields[0]; + Assert.Equal("key", eventField.Key); + Assert.Equal("value", eventField.VStr); + eventField = eventFields[1]; + Assert.Equal("description", eventField.Key); + Assert.Equal("Event1", eventField.VStr); + + jaegerLog = logs[1]; + Assert.Equal(events.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); + Assert.Equal(jaegerLog.Fields.Count(), 2); + eventFields = jaegerLog.Fields.ToArray(); + eventField = eventFields[0]; + Assert.Equal("key", eventField.Key); + Assert.Equal("value", eventField.VStr); + eventField = eventFields[1]; + Assert.Equal("description", eventField.Key); + Assert.Equal("Event2", eventField.VStr); + } + + [Fact] + public void JaegerSpanConverterTest_ConvertSpanToJaegerSpan_NoEvents() + { + var startTimestamp = DateTime.Now; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.Now; + + var traceId = ActivityTraceId.CreateRandom(); + var traceIdAsInt = new Int128(traceId); + var spanId = ActivitySpanId.CreateRandom(); + var spanIdAsInt = new Int128(spanId); + var parentSpanId = ActivitySpanId.CreateRandom(); + var attributes = Attributes.Create(new Dictionary{ + { "stringKey", "value"}, + { "longKey", 1L}, + { "longKey2", 1 }, + { "doubleKey", 1D}, + { "doubleKey2", 1F}, + { "boolKey", true}, + }, 0); + + var linkedSpanId = ActivitySpanId.CreateRandom(); + + var link = Link.FromSpanContext(SpanContext.Create( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty)); + + var linkTraceIdAsInt = new Int128(link.Context.TraceId); + var linkSpanIdAsInt = new Int128(link.Context.SpanId); + + var links = LinkList.Create(new List { link }, 0); + + var spanData = SpanData.Create( + SpanContext.Create( + traceId, + spanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty + ), + parentSpanId, + Resource.Empty, + "Name", + startTimestamp, + attributes, + null, + links, + null, + Status.Ok, + SpanKind.Client, + endTimestamp + ); + + var jaegerSpan = spanData.ToJaegerSpan(); + + Assert.Equal("Name", jaegerSpan.OperationName); + Assert.Empty(jaegerSpan.Logs); + + Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); + Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); + Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); + Assert.Equal(new Int128(parentSpanId).Low, jaegerSpan.ParentSpanId); + + Assert.Equal(links.Links.Count(), jaegerSpan.References.Count()); + var references = jaegerSpan.References.ToArray(); + var jaegerRef = references[0]; + Assert.Equal(linkTraceIdAsInt.High, jaegerRef.TraceIdHigh); + Assert.Equal(linkTraceIdAsInt.Low, jaegerRef.TraceIdLow); + Assert.Equal(linkSpanIdAsInt.Low, jaegerRef.SpanId); + + Assert.Equal(0x1, jaegerSpan.Flags); + + Assert.Equal(startTimestamp.ToEpochMicroseconds(), jaegerSpan.StartTime); + Assert.Equal(endTimestamp.ToEpochMicroseconds() - startTimestamp.ToEpochMicroseconds(), jaegerSpan.Duration); + + var tags = jaegerSpan.JaegerTags.ToArray(); + var tag = tags[0]; + Assert.Equal(JaegerTagType.STRING, tag.VType); + Assert.Equal("stringKey", tag.Key); + Assert.Equal("value", tag.VStr); + tag = tags[1]; + Assert.Equal(JaegerTagType.LONG, tag.VType); + Assert.Equal("longKey", tag.Key); + Assert.Equal(1, tag.VLong); + tag = tags[2]; + Assert.Equal(JaegerTagType.LONG, tag.VType); + Assert.Equal("longKey2", tag.Key); + Assert.Equal(1, tag.VLong); + tag = tags[3]; + Assert.Equal(JaegerTagType.DOUBLE, tag.VType); + Assert.Equal("doubleKey", tag.Key); + Assert.Equal(1, tag.VDouble); + tag = tags[4]; + Assert.Equal(JaegerTagType.DOUBLE, tag.VType); + Assert.Equal("doubleKey2", tag.Key); + Assert.Equal(1, tag.VDouble); + tag = tags[5]; + Assert.Equal(JaegerTagType.BOOL, tag.VType); + Assert.Equal("boolKey", tag.Key); + Assert.Equal(true, tag.VBool); + } + + [Fact] + public void JaegerSpanConverterTest_ConvertSpanToJaegerSpan_NoLinks() + { + var startTimestamp = DateTime.Now; + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = DateTime.Now; + + var traceId = ActivityTraceId.CreateRandom(); + var traceIdAsInt = new Int128(traceId); + var spanId = ActivitySpanId.CreateRandom(); + var spanIdAsInt = new Int128(spanId); + var parentSpanId = ActivitySpanId.CreateRandom(); + var attributes = Attributes.Create(new Dictionary{ + { "stringKey", "value"}, + { "longKey", 1L}, + { "longKey2", 1 }, + { "doubleKey", 1D}, + { "doubleKey2", 1F}, + { "boolKey", true}, + }, 0); + var events = TimedEvents.Create(new List> + { + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event1", + new Dictionary + { + { "key", "value" }, + } + ) + ), + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event2", + new Dictionary + { + { "key", "value" }, + } + ) + ), + }, 0); + + var spanData = SpanData.Create( + SpanContext.Create( + traceId, + spanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty + ), + parentSpanId, + Resource.Empty, + "Name", + startTimestamp, + attributes, + events, + null, + null, + Status.Ok, + SpanKind.Client, + endTimestamp + ); + + var jaegerSpan = spanData.ToJaegerSpan(); + + Assert.Equal("Name", jaegerSpan.OperationName); + Assert.Equal(2, jaegerSpan.Logs.Count()); + + Assert.Equal(traceIdAsInt.High, jaegerSpan.TraceIdHigh); + Assert.Equal(traceIdAsInt.Low, jaegerSpan.TraceIdLow); + Assert.Equal(spanIdAsInt.Low, jaegerSpan.SpanId); + Assert.Equal(new Int128(parentSpanId).Low, jaegerSpan.ParentSpanId); + + Assert.Empty(jaegerSpan.References); + + Assert.Equal(0x1, jaegerSpan.Flags); + + Assert.Equal(startTimestamp.ToEpochMicroseconds(), jaegerSpan.StartTime); + Assert.Equal(endTimestamp.ToEpochMicroseconds() - startTimestamp.ToEpochMicroseconds(), jaegerSpan.Duration); + + var tags = jaegerSpan.JaegerTags.ToArray(); + var tag = tags[0]; + Assert.Equal(JaegerTagType.STRING, tag.VType); + Assert.Equal("stringKey", tag.Key); + Assert.Equal("value", tag.VStr); + tag = tags[1]; + Assert.Equal(JaegerTagType.LONG, tag.VType); + Assert.Equal("longKey", tag.Key); + Assert.Equal(1, tag.VLong); + tag = tags[2]; + Assert.Equal(JaegerTagType.LONG, tag.VType); + Assert.Equal("longKey2", tag.Key); + Assert.Equal(1, tag.VLong); + tag = tags[3]; + Assert.Equal(JaegerTagType.DOUBLE, tag.VType); + Assert.Equal("doubleKey", tag.Key); + Assert.Equal(1, tag.VDouble); + tag = tags[4]; + Assert.Equal(JaegerTagType.DOUBLE, tag.VType); + Assert.Equal("doubleKey2", tag.Key); + Assert.Equal(1, tag.VDouble); + tag = tags[5]; + Assert.Equal(JaegerTagType.BOOL, tag.VType); + Assert.Equal("boolKey", tag.Key); + Assert.Equal(true, tag.VBool); + + var logs = jaegerSpan.Logs.ToArray(); + var jaegerLog = logs[0]; + Assert.Equal(events.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); + Assert.Equal(2, jaegerLog.Fields.Count()); + var eventFields = jaegerLog.Fields.ToArray(); + var eventField = eventFields[0]; + Assert.Equal("key", eventField.Key); + Assert.Equal("value", eventField.VStr); + eventField = eventFields[1]; + Assert.Equal("description", eventField.Key); + Assert.Equal("Event1", eventField.VStr); + + jaegerLog = logs[1]; + Assert.Equal(events.Events.First().Timestamp.ToEpochMicroseconds(), jaegerLog.Timestamp); + Assert.Equal(jaegerLog.Fields.Count(), 2); + eventFields = jaegerLog.Fields.ToArray(); + eventField = eventFields[0]; + Assert.Equal("key", eventField.Key); + Assert.Equal("value", eventField.VStr); + eventField = eventFields[1]; + Assert.Equal("description", eventField.Key); + Assert.Equal("Event2", eventField.VStr); + } + } +} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerThriftIntegrationTest.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerThriftIntegrationTest.cs new file mode 100644 index 00000000000..947affc9c64 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerThriftIntegrationTest.cs @@ -0,0 +1,132 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Tests.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using OpenTelemetry.Exporter.Jaeger.Implementation; + using OpenTelemetry.Resources; + using OpenTelemetry.Trace; + using OpenTelemetry.Trace.Export; + using Thrift.Protocols; + using Xunit; + using Process = Jaeger.Implementation.Process; + + public class JaegerThriftIntegrationTest + { + [Fact] + public async void JaegerThriftIntegrationTest_TAbstractBaseGeneratesConsistentThriftPayload() + { + var validJaegerThriftPayload = Convert.FromBase64String("goEBCWVtaXRCYXRjaBwcGAx0ZXN0IHByb2Nlc3MZHBgQdGVzdF9wcm9jZXNzX3RhZxUAGAp0ZXN0X3ZhbHVlAAAZHBbdlZjkk/C0+ZoBFtCr96fz8ZbpnQEW1KXb/ciQz6OBARaY3JDShc6hzrIBGAROYW1lGRwVABbdlZjkk/C0+ZoBFtCr96fz8ZbpnQEWkKas4ZbKtZyDAQAVAhaAgLPexpa/BRaAnJw5GWwYCXN0cmluZ0tleRUAGAV2YWx1ZQAYB2xvbmdLZXkVBkYCABgIbG9uZ0tleTIVBkYCABgJZG91YmxlS2V5FQInAAAAAAAA8D8AGApkb3VibGVLZXkyFQInAAAAAAAA8D8AGAdib29sS2V5FQQxABksFoCAs97Glr8FGSwYA2tleRUAGAV2YWx1ZQAYC2Rlc2NyaXB0aW9uFQAYBkV2ZW50MQAAFoCAs97Glr8FGSwYA2tleRUAGAV2YWx1ZQAYC2Rlc2NyaXB0aW9uFQAYBkV2ZW50MgAAAAAA"); + using (var memoryTransport = new InMemoryTransport()) + { + var protocolFactory = new TCompactProtocol.Factory(); + var thriftClient = new JaegerThriftClient(protocolFactory.GetProtocol(memoryTransport)); + var spanData = CreateTestSpan(); + var span = spanData.ToJaegerSpan(); + var process = new Process("test process", new Dictionary { { "test_process_tag", "test_value" } }); + var batch = new Batch(process, new List { span }); + + await thriftClient.EmitBatchAsync(batch, CancellationToken.None); + + var buff = memoryTransport.GetBuffer(); + + Assert.Equal(validJaegerThriftPayload, buff); + } + } + + + private SpanData CreateTestSpan() + { + var startTimestamp = new DateTime(2019, 1, 1); + var endTimestamp = startTimestamp.AddSeconds(60); + var eventTimestamp = new DateTime(2019, 1, 1); + + var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); + var traceIdAsInt = new Int128(traceId); + var spanId = ActivitySpanId.CreateFromString("6a69db47429ea340".AsSpan()); + var spanIdAsInt = new Int128(spanId); + var parentSpanId = ActivitySpanId.CreateFromBytes(new byte []{ 12, 23, 34, 45, 56, 67, 78, 89 }); + var attributes = Attributes.Create(new Dictionary{ + { "stringKey", "value"}, + { "longKey", 1L}, + { "longKey2", 1 }, + { "doubleKey", 1D}, + { "doubleKey2", 1F}, + { "boolKey", true}, + }, 0); + var events = TimedEvents.Create(new List> + { + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event1", + new Dictionary + { + { "key", "value" }, + } + ) + ), + TimedEvent.Create( + eventTimestamp, + Event.Create( + "Event2", + new Dictionary + { + { "key", "value" }, + } + ) + ), + }, 0); + + var linkedSpanId = ActivitySpanId.CreateFromString("888915b6286b9c41".AsSpan()); + + var link = Link.FromSpanContext(SpanContext.Create( + traceId, + linkedSpanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty)); + + var linkTraceIdAsInt = new Int128(link.Context.TraceId); + var linkSpanIdAsInt = new Int128(link.Context.SpanId); + + var links = LinkList.Create(new List { link }, 0); + + return SpanData.Create( + SpanContext.Create( + traceId, + spanId, + ActivityTraceFlags.Recorded, + Tracestate.Empty + ), + parentSpanId, + Resource.Empty, + "Name", + startTimestamp, + attributes, + events, + links, + null, + Status.Ok, + SpanKind.Client, + endTimestamp + ); + } + } +} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/ThriftUdpClientTransportTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/ThriftUdpClientTransportTests.cs new file mode 100644 index 00000000000..8c84f808f6a --- /dev/null +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/ThriftUdpClientTransportTests.cs @@ -0,0 +1,167 @@ +// +// Copyright 2018, OpenTelemetry 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. +// + +namespace OpenTelemetry.Exporter.Jaeger.Tests.Implementation +{ + using System; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Moq; + using OpenTelemetry.Exporter.Jaeger.Implementation; + using Thrift.Transports; + using Xunit; + + public class ThriftUdpClientTransportTests: IDisposable + { + private MemoryStream testingMemoryStream = new MemoryStream(); + private readonly Mock mockClient = new Mock(); + + public void Dispose() + { + this.testingMemoryStream?.Dispose(); + } + + [Fact] + public void Constructor_ShouldConnectClient() + { + var host = "host, yo"; + var port = 4528; + + new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + + this.mockClient.Verify(t => t.Connect(host, port), Times.Once); + } + + [Fact] + public void Close_ShouldCloseClient() + { + var host = "host, yo"; + var port = 4528; + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + transport.Close(); + + this.mockClient.Verify(t => t.Close(), Times.Once); + } + + [Fact] + public async Task ReadAsync_ShouldResultInNotImplementedException() + { + var host = "host, yo"; + var port = 4528; + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + var newBuffer = new byte[8]; + + await Assert.ThrowsAsync(async () => await transport.ReadAsync(newBuffer, 0, 7, CancellationToken.None)); + } + + [Fact] + public async Task WriteAsync_ShouldWriteToMemoryStream() + { + var host = "host, yo"; + var port = 4528; + var writeBuffer = new byte[] { 0x20, 0x10, 0x40, 0x30, 0x18, 0x14, 0x10, 0x28 }; + var readBuffer = new byte[8]; + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + + await transport.WriteAsync(writeBuffer, CancellationToken.None); + this.testingMemoryStream.Seek(0, SeekOrigin.Begin); + var size = await this.testingMemoryStream.ReadAsync(readBuffer, 0, 8, CancellationToken.None); + + Assert.Equal(8, size); + Assert.Equal(writeBuffer, readBuffer); + } + + [Fact] + public void FlushAsync_ShouldReturnWhenNothingIsInTheStream() + { + var host = "host, yo"; + var port = 4528; + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + var tInfo = transport.FlushAsync(); + + Assert.True(tInfo.IsCompleted); + this.mockClient.Verify(t => t.SendAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void FlushAsync_ShouldSendStreamBytes() + { + var host = "host, yo"; + var port = 4528; + var streamBytes = new byte[] { 0x20, 0x10, 0x40, 0x30, 0x18, 0x14, 0x10, 0x28 }; + this.testingMemoryStream = new MemoryStream(streamBytes); + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + var tInfo = transport.FlushAsync(); + + Assert.True(tInfo.IsCompleted); + this.mockClient.Verify(t => t.SendAsync(It.IsAny(), 8), Times.Once); + } + + [Fact] + public async Task FlushAsync_ShouldThrowWhenClientDoes() + { + var host = "host, yo"; + var port = 4528; + var streamBytes = new byte[] { 0x20, 0x10, 0x40, 0x30, 0x18, 0x14, 0x10, 0x28 }; + this.testingMemoryStream = new MemoryStream(streamBytes); + + //this.mockClient.Setup(t => t.SendAsync(It.IsAny(), It.IsAny())).Throws("message, yo"); + this.mockClient.Setup(t => t.SendAsync(It.IsAny(), It.IsAny())).Throws(new Exception("message, yo")); + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + var ex = await Assert.ThrowsAsync(() => transport.FlushAsync()); + + Assert.Equal("Cannot flush closed transport. message, yo", ex.Message); + } + + [Fact] + public void Dispose_ShouldCloseClientAndDisposeMemoryStream() + { + var host = "host, yo"; + var port = 4528; + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + transport.Dispose(); + + this.mockClient.Verify(t => t.Dispose(), Times.Once); + Assert.False(this.testingMemoryStream.CanRead); + Assert.False(this.testingMemoryStream.CanSeek); + Assert.False(this.testingMemoryStream.CanWrite); + } + + [Fact] + public void Dispose_ShouldNotTryToDisposeResourcesMoreThanOnce() + { + var host = "host, yo"; + var port = 4528; + + var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); + transport.Dispose(); + transport.Dispose(); + + this.mockClient.Verify(t => t.Dispose(), Times.Once); + Assert.False(this.testingMemoryStream.CanRead); + Assert.False(this.testingMemoryStream.CanSeek); + Assert.False(this.testingMemoryStream.CanWrite); + } + } +} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj b/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj new file mode 100644 index 00000000000..0f995164e4b --- /dev/null +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj @@ -0,0 +1,31 @@ + + + Unit test project for Stackdriver Exporter for OpenTelemetry + net46;netcoreapp2.1 + netcoreapp2.1 + false + + + + + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/xunit.runner.json b/test/OpenTelemetry.Exporter.Jaeger.Tests/xunit.runner.json new file mode 100644 index 00000000000..9fbc9011580 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "maxParallelThreads": 1, + "parallelizeTestCollections": false +} \ No newline at end of file