From d64903049622ac6bb61e8f98518bdbcfc95ec4a4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 10 Aug 2023 23:14:30 +0800 Subject: [PATCH] Update OS version detection to get version directly from Windows --- src/Grpc.Net.Client/Internal/NtDll.cs | 69 +++++++++++++++++++ .../Internal/OperatingSystem.cs | 16 ++++- .../OperatingSystemTests.cs | 34 +++++++++ 3 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/Grpc.Net.Client/Internal/NtDll.cs create mode 100644 test/Grpc.Net.Client.Tests/OperatingSystemTests.cs diff --git a/src/Grpc.Net.Client/Internal/NtDll.cs b/src/Grpc.Net.Client/Internal/NtDll.cs new file mode 100644 index 000000000..0061decbe --- /dev/null +++ b/src/Grpc.Net.Client/Internal/NtDll.cs @@ -0,0 +1,69 @@ +#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; + +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 + { + /// + /// The operation completed successfully. + /// + 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 diff --git a/src/Grpc.Net.Client/Internal/OperatingSystem.cs b/src/Grpc.Net.Client/Internal/OperatingSystem.cs index fcc7a0753..4d2e45c73 100644 --- a/src/Grpc.Net.Client/Internal/OperatingSystem.cs +++ b/src/Grpc.Net.Client/Internal/OperatingSystem.cs @@ -39,13 +39,23 @@ internal sealed class OperatingSystem : IOperatingSystem 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; -#endif IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - OSVersion = Environment.OSVersion.Version; + 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 } } diff --git a/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs b/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs new file mode 100644 index 000000000..f2bca0885 --- /dev/null +++ b/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs @@ -0,0 +1,34 @@ +#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 NUnit.Framework; +using OperatingSystem = Grpc.Net.Client.Internal.OperatingSystem; + +namespace Grpc.Net.Client.Tests; + +public class OperatingSystemTests +{ +#if NET5_0_OR_GREATER + [Test] + public void OSVersion_ModernDotNet_MatchesEnvironment() + { + // Environment.OSVersion and OperatingSystem.OSVersion should match in .NET 5 and greater. + Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion); + } +#endif +}