diff --git a/src/Grpc.Net.Client/Internal/NtDll.cs b/src/Grpc.Net.Client/Internal/NtDll.cs new file mode 100644 index 000000000..2abdbf5ad --- /dev/null +++ b/src/Grpc.Net.Client/Internal/NtDll.cs @@ -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; + +/// +/// Types for calling RtlGetVersion. See https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html +/// +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..c91c3b6e1 --- /dev/null +++ b/test/Grpc.Net.Client.Tests/OperatingSystemTests.cs @@ -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); + } +}