-
Notifications
You must be signed in to change notification settings - Fork 572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Get accurate runtime version #1936
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,264 @@ | ||
namespace Stripe.Infrastructure | ||
{ | ||
#if NET45 | ||
using System; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Reflection; | ||
using Microsoft.Win32; | ||
#endif | ||
using System.Runtime.Versioning; | ||
using static System.Runtime.InteropServices.RuntimeInformation; | ||
|
||
/// <summary> | ||
/// This class is used to gather information about the runtime environment. This is actually a | ||
/// non-trivial task. The code below is largely borrowed from the | ||
/// <a href="https://github.com/dotnet/BenchmarkDotNet">BenchmarkDotNet</a> project. | ||
/// </summary> | ||
internal static class RuntimeInformation | ||
{ | ||
public static string GetLanguageVersion() | ||
internal const string Unknown = "?"; | ||
|
||
internal static bool IsMono { get; } = Type.GetType("Mono.Runtime") != null; | ||
|
||
internal static bool IsFullFramework => FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase); | ||
|
||
internal static bool IsNetCore => FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(typeof(object).Assembly.Location); | ||
|
||
/// <summary> | ||
/// "The north star for CoreRT is to be a flavor of .NET Core" -> CoreRT reports .NET Core everywhere. | ||
/// </summary> | ||
internal static bool IsCoreRT | ||
=> FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase) | ||
&& string.IsNullOrEmpty(typeof(object).Assembly.Location); // but it's merged to a single .exe and .Location returns null here ;) | ||
|
||
internal static bool IsRunningInContainer => string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true"); | ||
|
||
/// <summary>Returns a string that describes the operating system on which the app is running.</summary> | ||
/// <returns>A string that describes the operating system on which the app is running.</returns> | ||
public static string GetOSVersion() | ||
{ | ||
#if NET45 | ||
return ".NET Framework 4.5+"; | ||
#else | ||
return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; | ||
#endif | ||
return OSDescription; | ||
} | ||
|
||
public static string GetOSVersion() | ||
/// <summary>Returns a string that indicates the name of the .NET installation on which an app is running.</summary> | ||
/// <returns>A string that indicates the name of the .NET installation on which an app is running.</returns> | ||
public static string GetRuntimeVersion() | ||
{ | ||
if (IsMono) | ||
{ | ||
return GetMonoVersion(); | ||
} | ||
else if (IsFullFramework) | ||
{ | ||
return GetFullFrameworkVersion(); | ||
} | ||
else if (IsNetCore) | ||
{ | ||
return GetNetCoreVersion(); | ||
} | ||
else if (IsCoreRT) | ||
{ | ||
return FrameworkDescription.Replace("Core ", "CoreRT "); | ||
} | ||
|
||
return Unknown; | ||
} | ||
|
||
internal static string GetMonoVersion() | ||
{ | ||
var monoRuntimeType = Type.GetType("Mono.Runtime"); | ||
var monoDisplayName = monoRuntimeType?.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); | ||
if (monoDisplayName != null) | ||
{ | ||
string version = monoDisplayName.Invoke(null, null)?.ToString(); | ||
if (version != null) | ||
{ | ||
int bracket1 = version.IndexOf('('), bracket2 = version.IndexOf(')'); | ||
if (bracket1 != -1 && bracket2 != -1) | ||
{ | ||
string comment = version.Substring(bracket1 + 1, bracket2 - bracket1 - 1); | ||
var commentParts = comment.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); | ||
if (commentParts.Length > 2) | ||
{ | ||
version = version.Substring(0, bracket1) + "(" + commentParts[0] + " " + commentParts[1] + ")"; | ||
} | ||
} | ||
} | ||
|
||
return "Mono " + version; | ||
} | ||
|
||
return Unknown; | ||
} | ||
|
||
internal static string GetFullFrameworkVersion() | ||
{ | ||
#if NET45 | ||
return Environment.OSVersion.ToString(); | ||
#else | ||
return System.Runtime.InteropServices.RuntimeInformation.OSDescription; | ||
#endif | ||
var fullName = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; // sth like .NET Framework 4.7.3324.0 | ||
var servicingVersion = new string(fullName.SkipWhile(c => !char.IsDigit(c)).ToArray()); | ||
var releaseVersion = MapToReleaseVersion(servicingVersion); | ||
|
||
return $".NET Framework {releaseVersion}"; | ||
} | ||
|
||
#if NET45 | ||
public static string GetMonoVersion() | ||
internal static string MapToReleaseVersion(string servicingVersion) | ||
{ | ||
Type monoRuntimeType = typeof(object).Assembly.GetType("Mono.Runtime"); | ||
// the following code assumes that .NET 4.5 is the oldest supported version | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't fully grasp why we do this instead of sending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "servicing version" includes patch/build numbers that are not particularly relevant, so this method (adapted from here) sanitizes the servicing version to an actual release version (https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/versions-and-dependencies#version-information). |
||
if (string.Compare(servicingVersion, "4.5.1") < 0) | ||
{ | ||
return "4.5"; | ||
} | ||
|
||
if (string.Compare(servicingVersion, "4.5.2") < 0) | ||
{ | ||
return "4.5.1"; | ||
} | ||
|
||
if (string.Compare(servicingVersion, "4.6") < 0) | ||
{ | ||
return "4.5.2"; | ||
} | ||
|
||
if (string.Compare(servicingVersion, "4.6.1") < 0) | ||
{ | ||
return "4.6"; | ||
} | ||
|
||
if (string.Compare(servicingVersion, "4.6.2") < 0) | ||
{ | ||
return "4.6.1"; | ||
} | ||
|
||
if (string.Compare(servicingVersion, "4.7") < 0) | ||
{ | ||
return "4.6.2"; | ||
} | ||
|
||
if (monoRuntimeType != null) | ||
if (string.Compare(servicingVersion, "4.7.1") < 0) | ||
{ | ||
MethodInfo getDisplayNameMethod = monoRuntimeType.GetMethod( | ||
"GetDisplayName", | ||
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.ExactBinding, | ||
null, | ||
Type.EmptyTypes, | ||
null); | ||
return "4.7"; | ||
} | ||
|
||
if (getDisplayNameMethod != null) | ||
if (string.Compare(servicingVersion, "4.7.2") < 0) | ||
{ | ||
return "4.7.1"; | ||
} | ||
|
||
if (string.Compare(servicingVersion, "4.8") < 0) | ||
{ | ||
return "4.7.2"; | ||
} | ||
|
||
return "4.8"; // most probably the last major release of Full .NET Framework | ||
} | ||
|
||
internal static string GetNetCoreVersion() | ||
{ | ||
string runtimeVersion = TryGetCoreRuntimeVersion(out var version) ? version.ToString() : "?"; | ||
|
||
return $".NET Core {runtimeVersion}"; | ||
} | ||
|
||
internal static bool TryGetCoreRuntimeVersion(out Version version) | ||
{ | ||
// we can't just use System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription | ||
// because it can be null and it reports versions like 4.6.* for .NET Core 2.* | ||
string runtimeDirectory = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); | ||
if (TryGetVersionFromRuntimeDirectory(runtimeDirectory, out version)) | ||
{ | ||
return true; | ||
} | ||
|
||
// systemPrivateCoreLib.Product*Part properties return 0 so we have to implement some ugly parsing... | ||
var systemPrivateCoreLib = FileVersionInfo.GetVersionInfo(typeof(object).Assembly.Location); | ||
if (TryGetVersionFromProductInfo(systemPrivateCoreLib.ProductVersion, systemPrivateCoreLib.ProductName, out version)) | ||
{ | ||
return true; | ||
} | ||
|
||
string frameworkName = Assembly.GetEntryAssembly()?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkName; | ||
if (TryGetVersionFromFrameworkName(frameworkName, out version)) | ||
{ | ||
return true; | ||
} | ||
|
||
if (IsRunningInContainer) | ||
{ | ||
return Version.TryParse(Environment.GetEnvironmentVariable("DOTNET_VERSION"), out version) | ||
|| Version.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_VERSION"), out version); | ||
} | ||
|
||
version = null; | ||
return false; | ||
} | ||
|
||
// sample input: | ||
// for dotnet run: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.12\ | ||
// for dotnet publish: C:\Users\adsitnik\source\repos\ConsoleApp25\ConsoleApp25\bin\Release\netcoreapp2.0\win-x64\publish\ | ||
internal static bool TryGetVersionFromRuntimeDirectory(string runtimeDirectory, out Version version) | ||
{ | ||
if (!string.IsNullOrEmpty(runtimeDirectory) && Version.TryParse(GetParsableVersionPart(new DirectoryInfo(runtimeDirectory).Name), out version)) | ||
{ | ||
return true; | ||
} | ||
|
||
version = null; | ||
return false; | ||
} | ||
|
||
// sample input: | ||
// 2.0: 4.6.26614.01 @BuiltBy: dlab14-DDVSOWINAGE018 @Commit: a536e7eec55c538c94639cefe295aa672996bf9b, Microsoft .NET Framework | ||
// 2.1: 4.6.27817.01 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.1 @SrcCode: https://github.com/dotnet/coreclr/tree/6f78fbb3f964b4f407a2efb713a186384a167e5c, Microsoft .NET Framework | ||
// 2.2: 4.6.27817.03 @BuiltBy: dlab14-DDVSOWINAGE101 @Branch: release/2.2 @SrcCode: https://github.com/dotnet/coreclr/tree/ce1d090d33b400a25620c0145046471495067cc7, Microsoft .NET Framework | ||
// 3.0: 3.0.0-preview8.19379.2+ac25be694a5385a6a1496db40de932df0689b742, Microsoft .NET Core | ||
// 5.0: 5.0.0-alpha1.19413.7+0ecefa44c9d66adb8a997d5778dc6c246ad393a7, Microsoft .NET Core | ||
internal static bool TryGetVersionFromProductInfo(string productVersion, string productName, out Version version) | ||
{ | ||
if (!string.IsNullOrEmpty(productVersion) && !string.IsNullOrEmpty(productName)) | ||
{ | ||
if (productName.IndexOf(".NET Core", StringComparison.OrdinalIgnoreCase) >= 0) | ||
{ | ||
return (string)getDisplayNameMethod.Invoke(null, null); | ||
string parsableVersion = GetParsableVersionPart(productVersion); | ||
if (Version.TryParse(productVersion, out version) || Version.TryParse(parsableVersion, out version)) | ||
{ | ||
return true; | ||
} | ||
} | ||
|
||
// yes, .NET Core 2.X has a product name == .NET Framework... | ||
if (productName.IndexOf(".NET Framework", StringComparison.OrdinalIgnoreCase) >= 0) | ||
{ | ||
const string releaseVersionPrefix = "release/"; | ||
int releaseVersionIndex = productVersion.IndexOf(releaseVersionPrefix); | ||
if (releaseVersionIndex > 0) | ||
{ | ||
string releaseVersion = GetParsableVersionPart(productVersion.Substring(releaseVersionIndex + releaseVersionPrefix.Length)); | ||
|
||
return Version.TryParse(releaseVersion, out version); | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
version = null; | ||
return false; | ||
} | ||
#endif | ||
|
||
// sample input: | ||
// .NETCoreApp,Version=v2.0 | ||
// .NETCoreApp,Version=v2.1 | ||
internal static bool TryGetVersionFromFrameworkName(string frameworkName, out Version version) | ||
{ | ||
const string versionPrefix = ".NETCoreApp,Version=v"; | ||
if (!string.IsNullOrEmpty(frameworkName) && frameworkName.StartsWith(versionPrefix)) | ||
{ | ||
string frameworkVersion = GetParsableVersionPart(frameworkName.Substring(versionPrefix.Length)); | ||
|
||
return Version.TryParse(frameworkVersion, out version); | ||
} | ||
|
||
version = null; | ||
return false; | ||
} | ||
|
||
// Version.TryParse does not handle thing like 3.0.0-WORD | ||
private static string GetParsableVersionPart(string fullVersionName) => new string(fullVersionName.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is using the same property the right decision here? Won't it skew our results and make it harder to inspect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current value is more or less useless (it tells us the proportion of Framework vs. Core users, but no useful version numbers). I think we can start sending meaningful values and update our internal metrics to only look at the
lang_version
field when the bindings version is >= 35.0.