diff --git a/src/Grpc.AspNetCore.Server/Internal/HttpContextServerCallContext.cs b/src/Grpc.AspNetCore.Server/Internal/HttpContextServerCallContext.cs index 1e0cde734..efd8a1d1d 100644 --- a/src/Grpc.AspNetCore.Server/Internal/HttpContextServerCallContext.cs +++ b/src/Grpc.AspNetCore.Server/Internal/HttpContextServerCallContext.cs @@ -27,6 +27,8 @@ namespace Grpc.AspNetCore.Server.Internal; +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(HttpContextServerCallContextDebugView))] internal sealed partial class HttpContextServerCallContext : ServerCallContext, IServerCallContextFeature { private static readonly AuthContext UnauthenticatedContext = new AuthContext(null, new Dictionary>()); @@ -572,4 +574,30 @@ internal void ValidateAcceptEncodingContainsResponseEncoding() GrpcServerLog.EncodingNotInAcceptEncoding(Logger, resolvedResponseGrpcEncoding); } } + + private string DebuggerToString() => $"Host = {Host}, Method = {Method}"; + + private sealed class HttpContextServerCallContextDebugView + { + private readonly HttpContextServerCallContext _context; + + public HttpContextServerCallContextDebugView(HttpContextServerCallContext context) + { + _context = context; + } + + public AuthContext AuthContext => _context.AuthContext; + public CancellationToken CancellationToken => _context.CancellationToken; + public DateTime Deadline => _context.Deadline; + public string Host => _context.Host; + public string Method => _context.Method; + public string Peer => _context.Peer; + public Metadata RequestHeaders => _context.RequestHeaders; + public Metadata ResponseTrailers => _context.ResponseTrailers; + public Status Status => _context.Status; + public IDictionary UserState => _context.UserState; + public WriteOptions? WriteOptions => _context.WriteOptions; + + public HttpContext HttpContext => _context.HttpContext; + } } diff --git a/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamReader.cs b/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamReader.cs index 5d6701153..8c548b6a2 100644 --- a/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamReader.cs +++ b/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamReader.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -16,16 +16,20 @@ #endregion +using System.Diagnostics; using Grpc.Core; using Grpc.Shared; namespace Grpc.AspNetCore.Server.Internal; +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(HttpContextStreamReader<>.HttpContextStreamReaderDebugView))] internal class HttpContextStreamReader : IAsyncStreamReader where TRequest : class { private readonly HttpContextServerCallContext _serverCallContext; private readonly Func _deserializer; private bool _completed; + private long _readCount; public HttpContextStreamReader(HttpContextServerCallContext serverCallContext, Func deserializer) { @@ -75,6 +79,7 @@ private bool ProcessPayload(TRequest? request) } Current = request; + Interlocked.Increment(ref _readCount); return true; } @@ -82,4 +87,20 @@ public void Complete() { _completed = true; } + + private string DebuggerToString() => $"ReadCount = {_readCount}, CallCompleted = {(_completed ? "true" : "false")}"; + + private sealed class HttpContextStreamReaderDebugView + { + private readonly HttpContextStreamReader _reader; + + public HttpContextStreamReaderDebugView(HttpContextStreamReader reader) + { + _reader = reader; + } + + public bool ReaderCompleted => _reader._completed; + public long ReadCount => _reader._readCount; + public TRequest Current => _reader.Current; + } } diff --git a/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs b/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs index 05719a91d..18ba714a8 100644 --- a/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs +++ b/src/Grpc.AspNetCore.Server/Internal/HttpContextStreamWriter.cs @@ -16,10 +16,13 @@ #endregion +using System.Diagnostics; using Grpc.Core; namespace Grpc.AspNetCore.Server.Internal; +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(HttpContextStreamWriter<>.HttpContextStreamWriterDebugView))] internal class HttpContextStreamWriter : IServerStreamWriter where TResponse : class { @@ -28,6 +31,7 @@ internal class HttpContextStreamWriter : IServerStreamWriter serializer) { @@ -93,6 +97,7 @@ private async Task WriteCoreAsync(TResponse message, CancellationToken cancellat } await _writeTask; + Interlocked.Increment(ref _writeCount); } finally { @@ -117,4 +122,21 @@ private bool IsWriteInProgressUnsynchronized return writeTask != null && !writeTask.IsCompleted; } } + + private string DebuggerToString() => $"WriteCount = {_writeCount}, CallCompleted = {(_completed ? "true" : "false")}"; + + private sealed class HttpContextStreamWriterDebugView + { + private readonly HttpContextStreamWriter _writer; + + public HttpContextStreamWriterDebugView(HttpContextStreamWriter writer) + { + _writer = writer; + } + + public bool WriterCompleted => _writer._completed; + public bool IsWriteInProgress => _writer.IsWriteInProgressUnsynchronized; + public long WriteCount => _writer._writeCount; + public WriteOptions? WriteOptions => _writer.WriteOptions; + } } diff --git a/src/Grpc.Core.Api/AsyncCallState.cs b/src/Grpc.Core.Api/AsyncCallState.cs index 62d3f72ed..390380373 100644 --- a/src/Grpc.Core.Api/AsyncCallState.cs +++ b/src/Grpc.Core.Api/AsyncCallState.cs @@ -16,7 +16,6 @@ #endregion - using System; using System.Threading.Tasks; diff --git a/src/Grpc.Core.Api/AsyncClientStreamingCall.cs b/src/Grpc.Core.Api/AsyncClientStreamingCall.cs index 7a38e7e80..df3577d36 100644 --- a/src/Grpc.Core.Api/AsyncClientStreamingCall.cs +++ b/src/Grpc.Core.Api/AsyncClientStreamingCall.cs @@ -17,8 +17,10 @@ #endregion using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Grpc.Core.Internal; namespace Grpc.Core; @@ -27,6 +29,8 @@ namespace Grpc.Core; /// /// Request message type for this call. /// Response message type for this call. +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(AsyncClientStreamingCall<,>.AsyncClientStreamingCallDebugView))] public sealed class AsyncClientStreamingCall : IDisposable { readonly IClientStreamWriter requestStream; @@ -164,4 +168,23 @@ public void Dispose() { callState.Dispose(); } + + private string DebuggerToString() => CallDebuggerHelpers.DebuggerToString(callState); + + private sealed class AsyncClientStreamingCallDebugView + { + private readonly AsyncClientStreamingCall _call; + + public AsyncClientStreamingCallDebugView(AsyncClientStreamingCall call) + { + _call = call; + } + + public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; + public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); + public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.GetAwaiter().GetResult() : null; + public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); + public IClientStreamWriter RequestStream => _call.RequestStream; + public TResponse? Response => _call.ResponseAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseAsync.Result : default; + } } diff --git a/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs b/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs index 8e7c042aa..e5fd0c8bc 100644 --- a/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs +++ b/src/Grpc.Core.Api/AsyncDuplexStreamingCall.cs @@ -17,7 +17,9 @@ #endregion using System; +using System.Diagnostics; using System.Threading.Tasks; +using Grpc.Core.Internal; namespace Grpc.Core; @@ -26,6 +28,8 @@ namespace Grpc.Core; /// /// Request message type for this call. /// Response message type for this call. +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(AsyncDuplexStreamingCall<,>.AsyncDuplexStreamingCallDebugView))] public sealed class AsyncDuplexStreamingCall : IDisposable { readonly IClientStreamWriter requestStream; @@ -141,4 +145,23 @@ public void Dispose() { callState.Dispose(); } + + private string DebuggerToString() => CallDebuggerHelpers.DebuggerToString(callState); + + private sealed class AsyncDuplexStreamingCallDebugView + { + private readonly AsyncDuplexStreamingCall _call; + + public AsyncDuplexStreamingCallDebugView(AsyncDuplexStreamingCall call) + { + _call = call; + } + + public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; + public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); + public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.Result : null; + public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); + public IAsyncStreamReader ResponseStream => _call.ResponseStream; + public IClientStreamWriter RequestStream => _call.RequestStream; + } } diff --git a/src/Grpc.Core.Api/AsyncServerStreamingCall.cs b/src/Grpc.Core.Api/AsyncServerStreamingCall.cs index c1cffdb2a..036ff3378 100644 --- a/src/Grpc.Core.Api/AsyncServerStreamingCall.cs +++ b/src/Grpc.Core.Api/AsyncServerStreamingCall.cs @@ -17,7 +17,9 @@ #endregion using System; +using System.Diagnostics; using System.Threading.Tasks; +using Grpc.Core.Internal; namespace Grpc.Core; @@ -25,6 +27,8 @@ namespace Grpc.Core; /// Return type for server streaming calls. /// /// Response message type for this call. +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(AsyncServerStreamingCall<>.AsyncServerStreamingCallDebugView))] public sealed class AsyncServerStreamingCall : IDisposable { readonly IAsyncStreamReader responseStream; @@ -122,4 +126,22 @@ public void Dispose() { callState.Dispose(); } + + private string DebuggerToString() => CallDebuggerHelpers.DebuggerToString(callState); + + private sealed class AsyncServerStreamingCallDebugView + { + private readonly AsyncServerStreamingCall _call; + + public AsyncServerStreamingCallDebugView(AsyncServerStreamingCall call) + { + _call = call; + } + + public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; + public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); + public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.Result : null; + public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); + public IAsyncStreamReader ResponseStream => _call.ResponseStream; + } } diff --git a/src/Grpc.Core.Api/AsyncUnaryCall.cs b/src/Grpc.Core.Api/AsyncUnaryCall.cs index eacae40df..2962694c8 100644 --- a/src/Grpc.Core.Api/AsyncUnaryCall.cs +++ b/src/Grpc.Core.Api/AsyncUnaryCall.cs @@ -17,8 +17,10 @@ #endregion using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Grpc.Core.Internal; namespace Grpc.Core; @@ -26,12 +28,13 @@ namespace Grpc.Core; /// Return type for single request - single response call. /// /// Response message type for this call. +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(AsyncUnaryCall<>.AsyncUnaryCallDebugView))] public sealed class AsyncUnaryCall : IDisposable { readonly Task responseAsync; readonly AsyncCallState callState; - /// /// Creates a new AsyncUnaryCall object with the specified properties. /// @@ -146,4 +149,22 @@ public void Dispose() { callState.Dispose(); } + + private string DebuggerToString() => CallDebuggerHelpers.DebuggerToString(callState); + + private sealed class AsyncUnaryCallDebugView + { + private readonly AsyncUnaryCall _call; + + public AsyncUnaryCallDebugView(AsyncUnaryCall call) + { + _call = call; + } + + public bool IsComplete => CallDebuggerHelpers.GetStatus(_call.callState) != null; + public Status? Status => CallDebuggerHelpers.GetStatus(_call.callState); + public Metadata? ResponseHeaders => _call.ResponseHeadersAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseHeadersAsync.Result : null; + public Metadata? Trailers => CallDebuggerHelpers.GetTrailers(_call.callState); + public TResponse? Response => _call.ResponseAsync.Status == TaskStatus.RanToCompletion ? _call.ResponseAsync.Result : default; + } } diff --git a/src/Grpc.Core.Api/AuthContext.cs b/src/Grpc.Core.Api/AuthContext.cs index d2a51a758..f296bfafb 100644 --- a/src/Grpc.Core.Api/AuthContext.cs +++ b/src/Grpc.Core.Api/AuthContext.cs @@ -17,6 +17,7 @@ #endregion using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Grpc.Core.Utils; @@ -28,6 +29,7 @@ namespace Grpc.Core; /// Using any other call/context properties for authentication purposes is wrong and inherently unsafe. /// Note: experimental API that can change or be removed without any prior notice. /// +[DebuggerDisplay("IsPeerAuthenticated = {IsPeerAuthenticated}")] public class AuthContext { private readonly string? peerIdentityPropertyName; diff --git a/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs b/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs new file mode 100644 index 000000000..cd9322972 --- /dev/null +++ b/src/Grpc.Core.Api/Internal/CallDebuggerHelpers.cs @@ -0,0 +1,63 @@ +#region Copyright notice and license + +// Copyright 2015-2016 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; + +namespace Grpc.Core.Internal; + +internal static class CallDebuggerHelpers +{ + public static string DebuggerToString(AsyncCallState callState) + { + var status = GetStatus(callState); + var debugText = $"IsComplete = {((status != null) ? "true" : "false")}"; + if (status != null) + { + debugText += $", Status = {status}"; + } + return debugText; + } + + public static Status? GetStatus(AsyncCallState callState) + { + // This is the only public API to get this value and there is no way to check if it's available. + // The overhead of throwing an error in the background is acceptable because this is only called while debugging. + try + { + return callState.GetStatus(); + } + catch (InvalidOperationException) + { + return null; + } + } + + public static Metadata? GetTrailers(AsyncCallState callState) + { + // This is the only public API to get this value and there is no way to check if it's available. + // The overhead of throwing an error in the background is acceptable because this is only called while debugging. + try + { + return callState.GetTrailers(); + } + catch (InvalidOperationException) + { + return null; + } + } +} diff --git a/src/Grpc.Core.Api/Metadata.cs b/src/Grpc.Core.Api/Metadata.cs index bb40e1954..523928dae 100644 --- a/src/Grpc.Core.Api/Metadata.cs +++ b/src/Grpc.Core.Api/Metadata.cs @@ -17,11 +17,14 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.Globalization; +using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using Grpc.Core.Api.Utils; - using Grpc.Core.Utils; namespace Grpc.Core; @@ -35,6 +38,8 @@ namespace Grpc.Core; /// Response trailersare sent by the server at the end of a remote call along with resulting call status. /// /// +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(MetadataDebugView))] public sealed class Metadata : IList { /// @@ -499,4 +504,27 @@ private static bool HasBinaryHeaderSuffix(string key) return false; } } + + private string DebuggerToString() + { + var debugText = $"Count = {Count}"; + if (IsReadOnly) + { + debugText += $", IsReadOnly = true"; + } + return debugText; + } + + private sealed class MetadataDebugView + { + private readonly Metadata _metadata; + + public MetadataDebugView(Metadata metadata) + { + _metadata = metadata; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Entry[] Items => _metadata.ToArray(); + } } diff --git a/src/Grpc.Net.Client/GrpcChannel.cs b/src/Grpc.Net.Client/GrpcChannel.cs index 7fb93471c..a73a45e53 100644 --- a/src/Grpc.Net.Client/GrpcChannel.cs +++ b/src/Grpc.Net.Client/GrpcChannel.cs @@ -17,6 +17,7 @@ #endregion using System.Collections.Concurrent; +using System.Diagnostics; using Grpc.Core; #if SUPPORT_LOAD_BALANCING using Grpc.Net.Client.Balancer; @@ -93,6 +94,7 @@ public sealed class GrpcChannel : ChannelBase, IDisposable internal ISystemClock Clock = SystemClock.Instance; internal IOperatingSystem OperatingSystem; internal IRandomGenerator RandomGenerator; + internal IDebugger Debugger; internal bool DisableClientDeadline; internal long MaxTimerDueTime = uint.MaxValue - 1; // Max System.Threading.Timer due time @@ -113,6 +115,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr LoggerFactory = channelOptions.LoggerFactory ?? channelOptions.ResolveService(NullLoggerFactory.Instance); OperatingSystem = channelOptions.ResolveService(Internal.OperatingSystem.Instance); RandomGenerator = channelOptions.ResolveService(new RandomGenerator()); + Debugger = channelOptions.ResolveService(new CachedDebugger()); Logger = LoggerFactory.CreateLogger(); #if SUPPORT_LOAD_BALANCING diff --git a/src/Grpc.Net.Client/Internal/CachedDebugger.cs b/src/Grpc.Net.Client/Internal/CachedDebugger.cs new file mode 100644 index 000000000..9605c3e32 --- /dev/null +++ b/src/Grpc.Net.Client/Internal/CachedDebugger.cs @@ -0,0 +1,32 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Diagnostics; + +namespace Grpc.Net.Client.Internal; + +internal sealed class CachedDebugger : IDebugger +{ + public bool IsAttached { get; } + + public CachedDebugger() + { + // Don't want to check Debugger.IsAttached with every call. Cache the result when the channel is created. + IsAttached = Debugger.IsAttached; + } +} diff --git a/src/Grpc.Net.Client/Internal/ClientStreamWriterBase.cs b/src/Grpc.Net.Client/Internal/ClientStreamWriterBase.cs index 43f7d6e97..a70049eac 100644 --- a/src/Grpc.Net.Client/Internal/ClientStreamWriterBase.cs +++ b/src/Grpc.Net.Client/Internal/ClientStreamWriterBase.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -16,7 +16,6 @@ #endregion -using System.Diagnostics; using Grpc.Core; using Microsoft.Extensions.Logging; using Log = Grpc.Net.Client.Internal.ClientStreamWriterBaseLog; @@ -71,8 +70,6 @@ protected bool IsWriteInProgressUnsynchronized { get { - Debug.Assert(Monitor.IsEntered(WriteLock)); - var writeTask = WriteTask; return writeTask != null && !writeTask.IsCompleted; } diff --git a/src/Grpc.Net.Client/Internal/HttpClientCallInvoker.cs b/src/Grpc.Net.Client/Internal/HttpClientCallInvoker.cs index 72b67e40b..695ba3fe7 100644 --- a/src/Grpc.Net.Client/Internal/HttpClientCallInvoker.cs +++ b/src/Grpc.Net.Client/Internal/HttpClientCallInvoker.cs @@ -16,6 +16,8 @@ #endregion +using System.Diagnostics; +using System.Runtime.CompilerServices; using Grpc.Core; using Grpc.Net.Client.Internal.Retry; @@ -42,6 +44,8 @@ public override AsyncClientStreamingCall AsyncClientStreami var call = CreateRootGrpcCall(Channel, method, options); call.StartClientStreaming(); + PrepareForDebugging(call); + return new AsyncClientStreamingCall( requestStream: call.ClientStreamWriter!, responseAsync: call.GetResponseAsync(), @@ -62,6 +66,8 @@ public override AsyncDuplexStreamingCall AsyncDuplexStreami var call = CreateRootGrpcCall(Channel, method, options); call.StartDuplexStreaming(); + PrepareForDebugging(call); + return new AsyncDuplexStreamingCall( requestStream: call.ClientStreamWriter!, responseStream: call.ClientStreamReader!, @@ -81,6 +87,8 @@ public override AsyncServerStreamingCall AsyncServerStreamingCall(Channel, method, options); call.StartServerStreaming(request); + PrepareForDebugging(call); + return new AsyncServerStreamingCall( responseStream: call.ClientStreamReader!, responseHeadersAsync: Callbacks.GetResponseHeadersAsync, @@ -98,6 +106,8 @@ public override AsyncUnaryCall AsyncUnaryCall(Me var call = CreateRootGrpcCall(Channel, method, options); call.StartUnary(request); + PrepareForDebugging(call); + return new AsyncUnaryCall( responseAsync: call.GetResponseAsync(), responseHeadersAsync: Callbacks.GetResponseHeadersAsync, @@ -142,6 +152,26 @@ private static IGrpcCall CreateRootGrpcCall(IGrpcCall call) + where TRequest : class + where TResponse : class + { + // By default, the debugger can't access a property that runs across threads. + // See https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.notifyofcrossthreaddependency + // + // The ResponseHeadersAsync task is lazy and is only started if accessed. Trying to initiate the lazy task from + // the debugger isn't allowed and the debugger requires you to opt-in to run it. Not a good experience. + // + // If the debugger is attached then we don't care about performance saving of making ResponseHeadersAsync lazy. + // Instead, start the ResponseHeadersAsync task with the call. This is in regular app execution so there is no problem + // doing it here. Now the response headers are automatically available when debugging. + if (Channel.Debugger.IsAttached) + { + // Start the ResponseHeadersAsync task. Response isn't important here. + _ = call.GetResponseHeadersAsync(); + } + } + public static GrpcCall CreateGrpcCall( GrpcChannel channel, Method method, diff --git a/src/Grpc.Net.Client/Internal/HttpContentClientStreamReader.cs b/src/Grpc.Net.Client/Internal/HttpContentClientStreamReader.cs index ab7e5655e..e703de01a 100644 --- a/src/Grpc.Net.Client/Internal/HttpContentClientStreamReader.cs +++ b/src/Grpc.Net.Client/Internal/HttpContentClientStreamReader.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -16,6 +16,7 @@ #endregion +using System.Diagnostics; using System.Runtime.ExceptionServices; using Grpc.Core; using Grpc.Shared; @@ -24,6 +25,8 @@ namespace Grpc.Net.Client.Internal; +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(HttpContentClientStreamReader<,>.HttpContentClientStreamReaderDebugView))] internal class HttpContentClientStreamReader : IAsyncStreamReader where TRequest : class where TResponse : class @@ -41,6 +44,7 @@ internal class HttpContentClientStreamReader : IAsyncStream private string? _grpcEncoding; private Stream? _responseStream; private Task? _moveNextTask; + private long _readCount; public HttpContentClientStreamReader(GrpcCall call) { @@ -178,6 +182,7 @@ private async Task MoveNextCore(CancellationToken cancellationToken) GrpcEventSource.Log.MessageReceived(); } Current = readMessage!; + Interlocked.Increment(ref _readCount); return true; } catch (OperationCanceledException ex) @@ -249,6 +254,23 @@ private bool IsMoveNextInProgressUnsynchronized return moveNextTask != null && !moveNextTask.IsCompleted; } } + + private string DebuggerToString() => $"ReadCount = {_readCount}, CallCompleted = {(_call.CallTask.IsCompletedSuccessfully() ? "true" : "false")}"; + + private sealed class HttpContentClientStreamReaderDebugView + { + private readonly HttpContentClientStreamReader _reader; + + public HttpContentClientStreamReaderDebugView(HttpContentClientStreamReader reader) + { + _reader = reader; + } + + public bool CallCompleted => _reader._call.CallTask.IsCompletedSuccessfully(); + public long ReadCount => _reader._readCount; + public bool IsMoveNextInProgress => _reader.IsMoveNextInProgressUnsynchronized; + public TResponse Current => _reader.Current; + } } internal static class HttpContentClientStreamReaderLog diff --git a/src/Grpc.Net.Client/Internal/HttpContentClientStreamWriter.cs b/src/Grpc.Net.Client/Internal/HttpContentClientStreamWriter.cs index c7cdf0845..68ea45e25 100644 --- a/src/Grpc.Net.Client/Internal/HttpContentClientStreamWriter.cs +++ b/src/Grpc.Net.Client/Internal/HttpContentClientStreamWriter.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -16,6 +16,7 @@ #endregion +using System.Diagnostics; using System.Runtime.ExceptionServices; using Grpc.Core; using Grpc.Shared; @@ -27,6 +28,8 @@ namespace Grpc.Net.Client.Internal; +[DebuggerDisplay("{DebuggerToString(),nq}")] +[DebuggerTypeProxy(typeof(HttpContentClientStreamWriter<,>.HttpContentClientStreamWriterDebugView))] internal class HttpContentClientStreamWriter : ClientStreamWriterBase where TRequest : class where TResponse : class @@ -36,6 +39,7 @@ internal class HttpContentClientStreamWriter : ClientStream private readonly GrpcCall _call; private bool _completeCalled; + private long _writeCount; public TaskCompletionSource WriteStreamTcs { get; } public TaskCompletionSource CompleteTcs { get; } @@ -98,6 +102,7 @@ public override async Task WriteCoreAsync(TRequest message, CancellationToken ca try { await WriteAsync(WriteMessageToStream, message, cancellationToken).ConfigureAwait(false); + Interlocked.Increment(ref _writeCount); } finally { @@ -189,4 +194,22 @@ public async Task WriteAsyncCore(Func, Str ExceptionDispatchInfo.Capture(resolvedCanceledException).Throw(); } } + + private string DebuggerToString() => $"WriteCount = {_writeCount}, CallCompleted = {(_call.CallTask.IsCompletedSuccessfully() ? "true" : "false")}"; + + private sealed class HttpContentClientStreamWriterDebugView + { + private readonly HttpContentClientStreamWriter _writer; + + public HttpContentClientStreamWriterDebugView(HttpContentClientStreamWriter writer) + { + _writer = writer; + } + + public bool CallCompleted => _writer._call.CallTask.IsCompletedSuccessfully(); + public bool WriterCompleted => _writer._completeCalled; + public bool IsWriteInProgress => _writer.IsWriteInProgressUnsynchronized; + public long WriteCount => _writer._writeCount; + public WriteOptions? WriteOptions => _writer.WriteOptions; + } } diff --git a/src/Grpc.Net.Client/Internal/IDebugger.cs b/src/Grpc.Net.Client/Internal/IDebugger.cs new file mode 100644 index 000000000..944b6ab48 --- /dev/null +++ b/src/Grpc.Net.Client/Internal/IDebugger.cs @@ -0,0 +1,24 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +namespace Grpc.Net.Client.Internal; + +internal interface IDebugger +{ + bool IsAttached { get; } +} diff --git a/src/Grpc.Net.Client/Internal/SystemClock.cs b/src/Grpc.Net.Client/Internal/SystemClock.cs index ab3c9b702..06345e296 100644 --- a/src/Grpc.Net.Client/Internal/SystemClock.cs +++ b/src/Grpc.Net.Client/Internal/SystemClock.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2019 The gRPC Authors // @@ -16,7 +16,6 @@ #endregion - namespace Grpc.Net.Client.Internal; internal sealed class SystemClock : ISystemClock diff --git a/test/FunctionalTests/Client/StreamingTests.cs b/test/FunctionalTests/Client/StreamingTests.cs index 8f6e731e4..da1668fe0 100644 --- a/test/FunctionalTests/Client/StreamingTests.cs +++ b/test/FunctionalTests/Client/StreamingTests.cs @@ -580,7 +580,6 @@ public async Task ServerStreaming_WriteAfterMethodCancelled_Error(bool writeBefo return true; } - return false; }); @@ -727,7 +726,6 @@ public async Task ClientStreaming_ReadAfterMethodCancelled_Error(bool readBefore return true; } - return false; });