Skip to content

Commit

Permalink
add more details into grpc-csharp user agent string
Browse files Browse the repository at this point in the history
  • Loading branch information
jtattermusch committed Apr 7, 2021
1 parent f604fd5 commit 168b1a8
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 6 deletions.
63 changes: 63 additions & 0 deletions src/csharp/Grpc.Core.Tests/Internal/UserAgentStringProviderTest.cs
@@ -0,0 +1,63 @@
#region Copyright notice and license

// Copyright 2021 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 System;
using Grpc.Core;
using Grpc.Core.Internal;
using Grpc.Core.Utils;
using NUnit.Framework;

namespace Grpc.Core.Internal.Tests
{
public class UserAgentStringProviderTest
{
[Test]
public void BasicTest()
{
Assert.AreEqual("grpc-csharp/1.0 (.NET Framework 4.6.1; CLR 1.2.3.4; net45; x64)",
new UserAgentStringProvider("1.0", ".NET Framework 4.6.1", "1.2.3.4", "net45", CommonPlatformDetection.CpuArchitecture.X64).GrpcCsharpUserAgentString);
Assert.AreEqual("grpc-csharp/1.0 (CLR 1.2.3.4; net45; x64)",
new UserAgentStringProvider("1.0", null, "1.2.3.4", "net45", CommonPlatformDetection.CpuArchitecture.X64).GrpcCsharpUserAgentString);
Assert.AreEqual("grpc-csharp/1.0 (.NET Framework 4.6.1; net45; x64)",
new UserAgentStringProvider("1.0", ".NET Framework 4.6.1", null, "net45", CommonPlatformDetection.CpuArchitecture.X64).GrpcCsharpUserAgentString);
Assert.AreEqual("grpc-csharp/1.0 (.NET Framework 4.6.1; CLR 1.2.3.4; x64)",
new UserAgentStringProvider("1.0", ".NET Framework 4.6.1", "1.2.3.4", null, CommonPlatformDetection.CpuArchitecture.X64).GrpcCsharpUserAgentString);
}

[Test]
public void ArchitectureTest()
{
Assert.AreEqual("grpc-csharp/1.0 (.NET Framework 4.6.1; CLR 1.2.3.4; net45; arm64)",
new UserAgentStringProvider("1.0", ".NET Framework 4.6.1", "1.2.3.4", "net45", CommonPlatformDetection.CpuArchitecture.Arm64).GrpcCsharpUserAgentString);

// unknown architecture
Assert.AreEqual("grpc-csharp/1.0 (.NET Framework 4.6.1; CLR 1.2.3.4; net45)",
new UserAgentStringProvider("1.0", ".NET Framework 4.6.1", "1.2.3.4", "net45", CommonPlatformDetection.CpuArchitecture.Unknown).GrpcCsharpUserAgentString);
}

[Test]
public void FrameworkDescriptionTest()
{
Assert.AreEqual("grpc-csharp/1.0 (Mono 6.12.0.93; x64)",
new UserAgentStringProvider("1.0", "Mono 6.12.0.93 (2020-02/620cf538206 Tue Aug 25 14:04:52 EDT 2020)", null, null, CommonPlatformDetection.CpuArchitecture.X64).GrpcCsharpUserAgentString);

Assert.AreEqual("grpc-csharp/1.0 (x64)",
new UserAgentStringProvider("1.0", "(some invalid framework description)", null, null, CommonPlatformDetection.CpuArchitecture.X64).GrpcCsharpUserAgentString);
}
}
}
9 changes: 5 additions & 4 deletions src/csharp/Grpc.Core.Tests/UserAgentStringTest.cs
Expand Up @@ -50,10 +50,10 @@ public void DefaultUserAgentString()
helper = new MockServiceHelper(Host);
helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
{
var userAgentString = context.RequestHeaders.First(m => (m.Key == "user-agent")).Value;
var userAgentString = context.RequestHeaders.GetValue("user-agent");
var parts = userAgentString.Split(new [] {' '}, 2);
Assert.AreEqual(string.Format("grpc-csharp/{0}", VersionInfo.CurrentVersion), parts[0]);
Assert.IsTrue(parts[1].StartsWith("grpc-c/"));
Assert.AreEqual($"grpc-csharp/{VersionInfo.CurrentVersion}", parts[0]);
Assert.That(parts[1], Does.Match(@"\(.*\) grpc-c/.*"));
return Task.FromResult("PASS");
});

Expand All @@ -71,9 +71,10 @@ public void ApplicationUserAgentString()
channelOptions: new[] { new ChannelOption(ChannelOptions.PrimaryUserAgentString, "XYZ") });
helper.UnaryHandler = new UnaryServerMethod<string, string>((request, context) =>
{
var userAgentString = context.RequestHeaders.First(m => (m.Key == "user-agent")).Value;
var userAgentString = context.RequestHeaders.GetValue("user-agent");
var parts = userAgentString.Split(new[] { ' ' }, 3);
Assert.AreEqual("XYZ", parts[0]);
Assert.AreEqual($"grpc-csharp/{VersionInfo.CurrentVersion}", parts[1]);
return Task.FromResult("PASS");
});

Expand Down
3 changes: 1 addition & 2 deletions src/csharp/Grpc.Core/Channel.cs
Expand Up @@ -318,8 +318,7 @@ private static void EnsureUserAgentChannelOption(Dictionary<string, ChannelOptio
userAgentString = option.StringValue + " ";
};

// TODO(jtattermusch): it would be useful to also provide .NET/mono version.
userAgentString += string.Format("grpc-csharp/{0}", VersionInfo.CurrentVersion);
userAgentString += UserAgentStringProvider.DefaultInstance.GrpcCsharpUserAgentString;

options[ChannelOptions.PrimaryUserAgentString] = new ChannelOption(key, userAgentString);
}
Expand Down
66 changes: 66 additions & 0 deletions src/csharp/Grpc.Core/Internal/PlatformApis.cs
Expand Up @@ -47,6 +47,8 @@ internal static class PlatformApis
static readonly bool isMono;
static readonly bool isNet5OrHigher;
static readonly bool isNetCore;
static readonly string frameworkDescription;
static readonly string clrVersion;
static readonly string unityApplicationPlatform;
static readonly bool isXamarin;
static readonly bool isXamarinIOS;
Expand All @@ -72,6 +74,8 @@ static PlatformApis()
isNet5OrHigher = false;
isNetCore = false;
#endif
frameworkDescription = TryGetFrameworkDescription();
clrVersion = TryGetClrVersion();

// Detect mono runtime
isMono = Type.GetType("Mono.Runtime") != null;
Expand Down Expand Up @@ -124,6 +128,18 @@ static PlatformApis()
/// </summary>
public static bool IsNet5OrHigher => isNet5OrHigher;

/// <summary>
/// Contains <c>RuntimeInformation.FrameworkDescription</c> if the property is available on current TFM.
/// <c>null</c> otherwise.
/// </summary>
public static string FrameworkDescription => frameworkDescription;

/// <summary>
/// Contains the version of common language runtime obtained from <c>Environment.Version</c>
/// if the property is available on current TFM. <c>null</c> otherwise.
/// </summary>
public static string ClrVersion => clrVersion;

/// <summary>
/// true if running on .NET Core (CoreCLR) or NET 5+, false otherwise.
/// </summary>
Expand Down Expand Up @@ -183,5 +199,55 @@ static string TryGetUnityApplicationPlatform()
return null;
}
}

/// <summary>
/// Returns description of the framework this process is running on.
/// Value is based on <c>RuntimeInformation.FrameworkDescription</c>.
/// </summary>
static string TryGetFrameworkDescription()
{
#if NETSTANDARD
return RuntimeInformation.FrameworkDescription;
#else
// on full .NET framework we are targeting net45, and the property is only available starting from .NET Framework 4.7.1+
// try obtaining the value by reflection since we might be running on a newer framework even though we're targeting
// an older one.
var runtimeInformationClass = Type.GetType("System.Runtime.InteropServices.RuntimeInformation");
var frameworkDescriptionProperty = runtimeInformationClass?.GetTypeInfo().GetProperty("FrameworkDescription", BindingFlags.Static | BindingFlags.Public);
return frameworkDescriptionProperty?.GetValue(null)?.ToString();
#endif
}

/// <summary>
/// Returns version of the common language runtime this process is running on.
/// Value is based on <c>Environment.Version</c>.
/// </summary>
static string TryGetClrVersion()
{
#if NETSTANDARD1_5
return null;
#else
return Environment.Version.ToString();
#endif
}

/// <summary>
/// Returns the TFM of the Grpc.Core assembly.
/// </summary>
public static string GetGrpcCoreTargetFrameworkMoniker()
{
#if NETSTANDARD1_5
return "netstandard1.5";
#elif NETSTANDARD2_0
return "netstandard2.0";
#elif NET45
return "net45";
#else
// The TFM is determined at compile time.
// The is intentionally no "default" return clause here so that
// if the set of TFMs we build for changes and this method is not updated accordingly,
// it will result in compilation error.
#endif
}
}
}
114 changes: 114 additions & 0 deletions src/csharp/Grpc.Core/Internal/UserAgentStringProvider.cs
@@ -0,0 +1,114 @@
#region Copyright notice and license

// Copyright 2021 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 System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Grpc.Core.Internal
{
/// <summary>
/// Helps constructing the grpc-csharp component of the user agent string.
/// </summary>
internal class UserAgentStringProvider
{
static readonly UserAgentStringProvider defaultInstance;
readonly string userAgentString;

static UserAgentStringProvider()
{
defaultInstance = new UserAgentStringProvider(VersionInfo.CurrentVersion, PlatformApis.FrameworkDescription, PlatformApis.ClrVersion, PlatformApis.GetGrpcCoreTargetFrameworkMoniker(), PlatformApis.ProcessArchitecture);
}

public static UserAgentStringProvider DefaultInstance => defaultInstance;

public string GrpcCsharpUserAgentString => userAgentString;

public UserAgentStringProvider(string grpcCsharpVersion, string frameworkDescription, string clrVersion, string tfm, CommonPlatformDetection.CpuArchitecture arch)
{
var detailComponents = new List<string>();

string sanitizedFrameworkDescription = SanitizeFrameworkDescription(frameworkDescription);
if (sanitizedFrameworkDescription != null)
{
detailComponents.Add(sanitizedFrameworkDescription);
}

if (clrVersion != null)
{
detailComponents.Add($"CLR {clrVersion}");
}

if (tfm != null)
{
detailComponents.Add(tfm);
}

string architectureString = TryGetArchitectureString(arch);
if (architectureString != null)
{
detailComponents.Add(architectureString);
}

// TODO(jtattermusch): consider adding details about running under unity / xamarin etc.
var details = string.Join("; ", detailComponents);
userAgentString = $"grpc-csharp/{grpcCsharpVersion} ({details})";
}

static string TryGetArchitectureString(CommonPlatformDetection.CpuArchitecture arch)
{
switch (arch)
{
case CommonPlatformDetection.CpuArchitecture.X86:
return "x86";
case CommonPlatformDetection.CpuArchitecture.X64:
return "x64";
case CommonPlatformDetection.CpuArchitecture.Arm64:
return "arm64";
default:
return null;
}
}

static string SanitizeFrameworkDescription(string frameworkDescription)
{
if (frameworkDescription == null)
{
return null;
}

// Some platforms return more details in the FrameworkDescription string than we want.
// e.g. on mono, we will get something like "Mono 6.12.0.93 (2020-02/620cf538206 Tue Aug 25 14:04:52 EDT 2020)"
// For user agent string, we only want basic info on framework name and its version.
var parts = new List<string>(frameworkDescription.Split(' '));

int i = 0;
for (; i < parts.Count; i++)
{
var part = parts[i];
if (!Regex.IsMatch(part, @"^[-.,+@A-Za-z0-9]*$"))
{
// stop once we find first part that's not framework name or version
break;
}
}

var result = string.Join(" ", parts.GetRange(0, i));
return !string.IsNullOrEmpty(result) ? result : null;
}
}
}
1 change: 1 addition & 0 deletions src/csharp/tests.json
Expand Up @@ -17,6 +17,7 @@
"Grpc.Core.Internal.Tests.SliceBufferSafeHandleTest",
"Grpc.Core.Internal.Tests.SliceTest",
"Grpc.Core.Internal.Tests.TimespecTest",
"Grpc.Core.Internal.Tests.UserAgentStringProviderTest",
"Grpc.Core.Internal.Tests.WellKnownStringsTest",
"Grpc.Core.Tests.AppDomainUnloadTest",
"Grpc.Core.Tests.AuthContextTest",
Expand Down

0 comments on commit 168b1a8

Please sign in to comment.