From 9fbae695e49a495a23f645c468c272619f68ec65 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Wed, 7 Feb 2024 11:37:12 +0000 Subject: [PATCH] feat: Detect grpc-dotnet support on Windows under .NET Framework This will enable more users to use grpc-dotnet instead of Grpc.Core. Comprehensive testing is impractical, but this has been tested under Windows 10 (uses Grpc.Core) and Windows 11 (uses Grpc.Net.Client). Fixes #712. Note that we'll need to revisit code if a later version of Windows Server 2022 fully supports grpc-dotnet. --- Google.Api.Gax.Grpc/GrpcAdapter.cs | 19 +--- Google.Api.Gax.Grpc/GrpcWindowsDetection.cs | 100 ++++++++++++++++++++ 2 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 Google.Api.Gax.Grpc/GrpcWindowsDetection.cs diff --git a/Google.Api.Gax.Grpc/GrpcAdapter.cs b/Google.Api.Gax.Grpc/GrpcAdapter.cs index 089f5c59..582cf75d 100644 --- a/Google.Api.Gax.Grpc/GrpcAdapter.cs +++ b/Google.Api.Gax.Grpc/GrpcAdapter.cs @@ -128,29 +128,12 @@ private static GrpcAdapter DetectDefaultGrpcTransportAdapterPreferringGrpcNetCli } // Test 2, only for .NET Framework: check what version of Windows we're using. #if NET462_OR_GREATER - if (!PlatformFullySupportsGrpc()) + if (!GrpcWindowsDetection.PlatformFullySupportsGrpc()) { return GrpcCoreAdapter.Instance; } #endif return GrpcNetClientAdapter.Default; - - -#if NET462_OR_GREATER - // gRPC support on .NET Framework is complex. - // - Some Windows platforms don't support it at all, in which case an exception is thrown - // in GrpcChannel.ForAddress, which is handled above. - // - Other platforms support it partially - unary and server-streaming, but not - // bidi- or client-streaming. - // - Other platforms support it fully. - // - // While in theory we could tailor the default instance for a given service to whether - // streaming is required or not, that would lead to a very inconsistent experience. - // At some point, we wish to check the Windows version details and use that to determine - // which adapter to use, but for now we'll always use Grpc.Core. - // Once https://github.com/grpc/grpc-dotnet/issues/2242 is fixed, we can use that code. - static bool PlatformFullySupportsGrpc() => false; -#endif } } } diff --git a/Google.Api.Gax.Grpc/GrpcWindowsDetection.cs b/Google.Api.Gax.Grpc/GrpcWindowsDetection.cs new file mode 100644 index 00000000..1a9e1033 --- /dev/null +++ b/Google.Api.Gax.Grpc/GrpcWindowsDetection.cs @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Google LLC + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file or at + * https://developers.google.com/open-source/licenses/bsd + */ + +#if NET462_OR_GREATER + +using System.Runtime.InteropServices; +using System; + +internal static class GrpcWindowsDetection +{ + // gRPC support on .NET Framework is complex. + // - Some Windows platforms don't support it at all, in which case an exception is thrown + // in GrpcChannel.ForAddress, which is handled above. + // - Other platforms support it partially - unary and server-streaming, but not + // bidi- or client-streaming. + // - Other platforms support it fully. + // + // While in theory we could tailor the default instance for a given service to whether + // streaming is required or not, that would lead to a very inconsistent experience. + // We attempt to detect which version of Windows we're on, erring on the side of caution. + // Code based on https://gist.github.com/tonydnewell/6a663b7258ce3599e6bdae5c4291b2a6 + internal static bool PlatformFullySupportsGrpc() + { + // If we're running on Mono or similar, all bets are off. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + // 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 + + DetectWindowsVersion(out var windowsVersion, out var windowsServer); + + const int Windows11BuildVersion = 22000; + + // Windows 11 has full support. + // Currently, Windows Server (even 2022) doesn't fully support gRPC under .NET Framework. + // When there's a version that does, we can update this check. + return windowsVersion.Build >= Windows11BuildVersion; + } + + /// + /// Types for calling RtlGetVersion. See https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html + /// +#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo); +#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time + + private static void DetectWindowsVersion(out Version version, out bool isWindowsServer) + { + // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa + const byte VER_NT_SERVER = 3; + + var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf() }; + + if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS) + { + throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}."); + } + + version = new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0); + isWindowsServer = osVersionInfo.ProductType == VER_NT_SERVER; + } + + private enum NTSTATUS : uint + { + /// + /// The operation completed successfully. + /// + STATUS_SUCCESS = 0x00000000 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private 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