Skip to content
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

Merged
merged 1 commit into from
Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 12 additions & 20 deletions src/Stripe.net/Infrastructure/Public/SystemNetHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ namespace Stripe
/// </summary>
public class SystemNetHttpClient : IHttpClient
{
private const string StripeNetTargetFramework =
#if NETSTANDARD2_0
"netstandard2.0"
#elif NET45
"net45"
#else
"unknown"
#endif
;

private static readonly Lazy<System.Net.Http.HttpClient> LazyDefaultHttpClient
= new Lazy<System.Net.Http.HttpClient>(BuildDefaultSystemNetHttpClient);

Expand Down Expand Up @@ -204,29 +214,11 @@ private string BuildStripeClientUserAgentString()
{ "bindings_version", StripeConfiguration.StripeNetVersion },
{ "lang", ".net" },
{ "publisher", "stripe" },
{ "lang_version", RuntimeInformation.GetLanguageVersion() },
{ "lang_version", RuntimeInformation.GetRuntimeVersion() },
Copy link
Contributor

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?

Copy link
Contributor Author

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.

{ "os_version", RuntimeInformation.GetOSVersion() },
{ "stripe_net_target_framework", StripeNetTargetFramework },
};

#if NET45
string monoVersion = RuntimeInformation.GetMonoVersion();
if (!string.IsNullOrEmpty(monoVersion))
{
values.Add("mono_version", monoVersion);
}
#endif

var stripeNetTargetFramework =
#if NET45
"net45"
#elif NETSTANDARD2_0
"netstandard2.0"
#else
"unknown"
#endif
;
values.Add("stripe_net_target_framework", stripeNetTargetFramework);

if (this.appInfo != null)
{
values.Add("application", this.appInfo);
Expand Down
269 changes: 240 additions & 29 deletions src/Stripe.net/Infrastructure/RuntimeInformation.cs
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully grasp why we do this instead of sending servicingVersion or sanitizing it ourselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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());
}
}
3 changes: 2 additions & 1 deletion src/Stripe.net/Stripe.net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="Stylecop.Analyzers" Version="1.1.118">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
Expand All @@ -39,6 +39,7 @@
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
Expand Down