Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,26 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
{
Log.AddressPathUnused(Logger, Address.OriginalString);
}

// Grpc.Net.Client + .NET Framework + WinHttpHandler requires features in WinHTTP, shipped in Windows, to work correctly.
// This scenario is supported in these versions of Windows or later:
// -Windows Server 2022 has partial support.
// -Unary and server streaming methods are supported.
// -Client and bidi streaming methods aren't supported.
// -Windows 11 has full support.
//
// GrpcChannel validates the Windows version is WinServer2022 or later. Win11 version number is greater than WinServer2022.
// Note that this doesn't block using unsupported client and bidi streaming methods on WinServer2022.
const int WinServer2022BuildVersion = 20348;
if (HttpHandlerType == HttpHandlerType.WinHttpHandler &&
OperatingSystem.IsWindows &&
OperatingSystem.OSVersion.Build < WinServer2022BuildVersion)
{
throw new InvalidOperationException("The channel configuration isn't valid on this operating system. " +
"The channel is configured to use WinHttpHandler and the current version of Windows " +
"doesn't support HTTP/2 features required by gRPC. Windows Server 2022 or Windows 11 or later is required. " +
"For more information, see https://aka.ms/aspnet/grpc/netframework.");
}
}

private void ResolveCredentials(GrpcChannelOptions channelOptions, out bool isSecure, out List<CallCredentials>? callCredentials)
Expand Down
72 changes: 72 additions & 0 deletions src/Grpc.Net.Client/Internal/NtDll.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#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

#if !NET5_0_OR_GREATER

using System.Runtime.InteropServices;

namespace Grpc.Net.Client.Internal;

/// <summary>
/// Types for calling RtlGetVersion. See https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
/// </summary>
internal static class NtDll
{
[DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo);

internal static Version DetectWindowsVersion()
{
var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };

if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS)
{
throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}.");
}

return new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
}

internal enum NTSTATUS : uint
{
/// <summary>
/// The operation completed successfully.
/// </summary>
STATUS_SUCCESS = 0x00000000
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct OSVERSIONINFOEX
{
// The OSVersionInfoSize field must be set to Marshal.SizeOf(typeof(OSVERSIONINFOEX))
public int OSVersionInfoSize;
public int MajorVersion;
public int MinorVersion;
public int BuildNumber;
public int PlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string CSDVersion;
public ushort ServicePackMajor;
public ushort ServicePackMinor;
public short SuiteMask;
public byte ProductType;
public byte Reserved;
}
}

#endif
20 changes: 18 additions & 2 deletions src/Grpc.Net.Client/Internal/OperatingSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand All @@ -24,6 +24,8 @@ internal interface IOperatingSystem
{
bool IsBrowser { get; }
bool IsAndroid { get; }
bool IsWindows { get; }
Version OSVersion { get; }
}

internal sealed class OperatingSystem : IOperatingSystem
Expand All @@ -32,14 +34,28 @@ internal sealed class OperatingSystem : IOperatingSystem

public bool IsBrowser { get; }
public bool IsAndroid { get; }
public bool IsWindows { get; }
public Version OSVersion { get; }

private OperatingSystem()
{
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
#if NET5_0_OR_GREATER
IsAndroid = System.OperatingSystem.IsAndroid();
IsWindows = System.OperatingSystem.IsWindows();
IsBrowser = System.OperatingSystem.IsBrowser();
OSVersion = Environment.OSVersion.Version;
#else
IsAndroid = false;
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));

// Older versions of .NET report an OSVersion.Version based on Windows compatibility settings.
// For example, if an app running on Windows 11 is configured to be "compatible" with Windows 10
// then the version returned is always Windows 10.
//
// Get correct Windows version directly from Windows by calling RtlGetVersion.
// https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
OSVersion = IsWindows ? NtDll.DetectWindowsVersion() : Environment.OSVersion.Version;
#endif
}
}
4 changes: 3 additions & 1 deletion test/Grpc.Net.Client.Tests/GetStatusTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -214,6 +214,8 @@ private class TestOperatingSystem : IOperatingSystem
{
public bool IsBrowser { get; set; }
public bool IsAndroid { get; set; }
public bool IsWindows { get; set; }
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
}

[Test]
Expand Down
60 changes: 60 additions & 0 deletions test/Grpc.Net.Client.Tests/GrpcChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,66 @@ private class TestOperatingSystem : IOperatingSystem
{
public bool IsBrowser { get; set; }
public bool IsAndroid { get; set; }
public bool IsWindows { get; set; }
public Version OSVersion { get; set; } = new Version(1, 2, 3, 4);
}

[Test]
public void WinHttpHandler_UnsupportedWindows_Throw()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IOperatingSystem>(new TestOperatingSystem
{
IsWindows = true,
OSVersion = new Version(1, 2, 3, 4)
});

#pragma warning disable CS0436 // Just need to have a type called WinHttpHandler to activate new behavior.
var winHttpHandler = new WinHttpHandler(new TestHttpMessageHandler());
#pragma warning restore CS0436

// Act
var ex = Assert.Throws<InvalidOperationException>(() =>
{
GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
{
HttpHandler = winHttpHandler,
ServiceProvider = services.BuildServiceProvider()
});
});

// Assert
Assert.AreEqual(ex!.Message, "The channel configuration isn't valid on this operating system. " +
"The channel is configured to use WinHttpHandler and the current version of Windows " +
"doesn't support HTTP/2 features required by gRPC. Windows Server 2022 or Windows 11 or later is required. " +
"For more information, see https://aka.ms/aspnet/grpc/netframework.");
}

[Test]
public void WinHttpHandler_SupportedWindows_Success()
{
// Arrange
var services = new ServiceCollection();
services.AddSingleton<IOperatingSystem>(new TestOperatingSystem
{
IsWindows = true,
OSVersion = Version.Parse("10.0.20348.169")
});

#pragma warning disable CS0436 // Just need to have a type called WinHttpHandler to activate new behavior.
var winHttpHandler = new WinHttpHandler(new TestHttpMessageHandler());
#pragma warning restore CS0436

// Act
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
{
HttpHandler = winHttpHandler,
ServiceProvider = services.BuildServiceProvider()
});

// Assert
Assert.AreEqual(HttpHandlerType.WinHttpHandler, channel.HttpHandlerType);
}

#if SUPPORT_LOAD_BALANCING
Expand Down
43 changes: 43 additions & 0 deletions test/Grpc.Net.Client.Tests/OperatingSystemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#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 Grpc.Net.Client.Internal;
using NUnit.Framework;
using OperatingSystem = Grpc.Net.Client.Internal.OperatingSystem;

namespace Grpc.Net.Client.Tests;

public class OperatingSystemTests
{
#if !NET5_0_OR_GREATER
[Test]
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
public void DetectWindowsVersion_Windows_MatchesEnvironment()
{
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
Assert.AreEqual(Environment.OSVersion.Version, NtDll.DetectWindowsVersion());
}
#endif

[Test]
public void OSVersion_ModernDotNet_MatchesEnvironment()
{
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion);
}
}