Skip to content

Commit 1e95b9c

Browse files
committed
Update OS version detection to get version directly from Windows (grpc#2239)
1 parent ac7a60a commit 1e95b9c

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
#if !NET5_0_OR_GREATER
20+
21+
using System.Runtime.InteropServices;
22+
23+
namespace Grpc.Net.Client.Internal;
24+
25+
/// <summary>
26+
/// Types for calling RtlGetVersion. See https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
27+
/// </summary>
28+
internal static class NtDll
29+
{
30+
[DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)]
31+
internal static extern NTSTATUS RtlGetVersion(ref OSVERSIONINFOEX versionInfo);
32+
33+
internal static Version DetectWindowsVersion()
34+
{
35+
var osVersionInfo = new OSVERSIONINFOEX { OSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)) };
36+
37+
if (RtlGetVersion(ref osVersionInfo) != NTSTATUS.STATUS_SUCCESS)
38+
{
39+
throw new InvalidOperationException($"Failed to call internal {nameof(RtlGetVersion)}.");
40+
}
41+
42+
return new Version(osVersionInfo.MajorVersion, osVersionInfo.MinorVersion, osVersionInfo.BuildNumber, 0);
43+
}
44+
45+
internal enum NTSTATUS : uint
46+
{
47+
/// <summary>
48+
/// The operation completed successfully.
49+
/// </summary>
50+
STATUS_SUCCESS = 0x00000000
51+
}
52+
53+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
54+
internal struct OSVERSIONINFOEX
55+
{
56+
// The OSVersionInfoSize field must be set to Marshal.SizeOf(typeof(OSVERSIONINFOEX))
57+
public int OSVersionInfoSize;
58+
public int MajorVersion;
59+
public int MinorVersion;
60+
public int BuildNumber;
61+
public int PlatformId;
62+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
63+
public string CSDVersion;
64+
public ushort ServicePackMajor;
65+
public ushort ServicePackMinor;
66+
public short SuiteMask;
67+
public byte ProductType;
68+
public byte Reserved;
69+
}
70+
}
71+
72+
#endif

src/Grpc.Net.Client/Internal/OperatingSystem.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,23 @@ internal sealed class OperatingSystem : IOperatingSystem
3939

4040
private OperatingSystem()
4141
{
42-
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
4342
#if NET5_0_OR_GREATER
4443
IsAndroid = System.OperatingSystem.IsAndroid();
44+
IsWindows = System.OperatingSystem.IsWindows();
45+
IsBrowser = System.OperatingSystem.IsBrowser();
46+
OSVersion = Environment.OSVersion.Version;
4547
#else
4648
IsAndroid = false;
47-
#endif
4849
IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
49-
OSVersion = Environment.OSVersion.Version;
50+
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
51+
52+
// Older versions of .NET report an OSVersion.Version based on Windows compatibility settings.
53+
// For example, if an app running on Windows 11 is configured to be "compatible" with Windows 10
54+
// then the version returned is always Windows 10.
55+
//
56+
// Get correct Windows version directly from Windows by calling RtlGetVersion.
57+
// https://www.pinvoke.net/default.aspx/ntdll/RtlGetVersion.html
58+
OSVersion = IsWindows ? NtDll.DetectWindowsVersion() : Environment.OSVersion.Version;
59+
#endif
5060
}
5161
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using Grpc.Net.Client.Internal;
20+
using NUnit.Framework;
21+
using OperatingSystem = Grpc.Net.Client.Internal.OperatingSystem;
22+
23+
namespace Grpc.Net.Client.Tests;
24+
25+
public class OperatingSystemTests
26+
{
27+
#if !NET5_0_OR_GREATER
28+
[Test]
29+
[Platform("Win", Reason = "Only runs on Windows where ntdll.dll is present.")]
30+
public void DetectWindowsVersion_Windows_MatchesEnvironment()
31+
{
32+
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
33+
Assert.AreEqual(Environment.OSVersion.Version, NtDll.DetectWindowsVersion());
34+
}
35+
#endif
36+
37+
[Test]
38+
public void OSVersion_ModernDotNet_MatchesEnvironment()
39+
{
40+
// It is safe to compare Environment.OSVersion.Version on netfx because tests have no compatibilty setting.
41+
Assert.AreEqual(Environment.OSVersion.Version, OperatingSystem.Instance.OSVersion);
42+
}
43+
}

0 commit comments

Comments
 (0)