Skip to content

Commit

Permalink
Bail out if we're in a no-dynamic-code scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewlock committed Nov 27, 2024
1 parent 2ac2b32 commit c2c6227
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 4 deletions.
14 changes: 14 additions & 0 deletions tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ static Startup()

try
{
#if NETCOREAPP
// Check if we're in some sort of AOT scenario
// Equivalent to checking RuntimeFeature.IsDynamicCodeSupported (added in .NET 8)
// https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeFeature.NonNativeAot.cs
var dynamicCodeSupported = AppContext.TryGetSwitch("System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported", out bool isDynamicCodeSupported) ? isDynamicCodeSupported : true;
if (!dynamicCodeSupported)
{
// we require dynamic code so we should just bail out ASAP.
// This doesn't tell us for sure (the switch is only available on .NET 8+) but it's a minimum requirement
StartupLogger.Log("Dynamic code is not supported (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported context switch is false). Automatic instrumentation will be disabled");
return;
}
#endif

ManagedProfilerDirectory = ResolveManagedProfilerDirectory();
if (ManagedProfilerDirectory is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,67 @@ public async Task OnSupportedFrameworkInSsi_CallsForwarderWithExpectedTelemetry(
}
#endif

// The dynamic context switch/bail out is only available in .NET 8+
#if NET8_0_OR_GREATER
[SkippableFact]
[Trait("RunOnWindows", "True")]
public async Task WhenDynamicCodeIsEnabled_InstrumentsApp()
{
var dotnetRuntimeArgs = CreateRuntimeConfigWithDynamicCodeEnabled(true);

using var agent = EnvironmentHelper.GetMockAgent(useTelemetry: true);
using var processResult = await RunSampleAndWaitForExit(agent, arguments: "traces 1", dotnetRuntimeArgs: dotnetRuntimeArgs);
agent.Spans.Should().NotBeEmpty();
agent.Telemetry.Should().NotBeEmpty();
}

[SkippableFact]
[Trait("RunOnWindows", "True")]
public async Task WhenDynamicCodeIsDisabled_DoesNotInstrument()
{
var dotnetRuntimeArgs = CreateRuntimeConfigWithDynamicCodeEnabled(false);

using var agent = EnvironmentHelper.GetMockAgent(useTelemetry: true);
using var processResult = await RunSampleAndWaitForExit(agent, arguments: "traces 1", dotnetRuntimeArgs: dotnetRuntimeArgs);
agent.Spans.Should().BeEmpty();
agent.Telemetry.Should().BeEmpty();
}

private string CreateRuntimeConfigWithDynamicCodeEnabled(bool enabled)
{
// Set to false when PublishAot is set _even if the app is not published with AOT_
var name = "System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported";
var value = enabled ? "true" : "false";

// copy the app runtime config to a separate folder before modifying it
var fileName = "Samples.Console.runtimeconfig.json";
var sourceFile = Path.Combine(Path.GetDirectoryName(EnvironmentHelper.GetSampleApplicationPath())!, fileName);
var destDir = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
var destFile = Path.Combine(destDir, fileName);
Directory.CreateDirectory(destDir);

Output.WriteLine("Reading contents of " + sourceFile);
var contents = File.ReadAllText(sourceFile);

// hacky replacement to add an extra property, but meh, we can expand/fix it later if
// we need to support more values or support the value already existing
var replacement = $$"""
"configProperties": {
"{{name}}": {{value}},
""";
var fixedContents = contents.Replace(""" "configProperties": {""", replacement);

Output.WriteLine("Writing new contents to" + destFile);
File.WriteAllText(destFile, fixedContents);

// return the path to the variable in the format needed to be passed to the dotnet exe
// when running the program. Don't ask me why you need to use dotnet exec...
// it's weird, but here we are
var dotnetRuntimeArgs = $"exec --runtimeconfig \"{destFile}\"";
return dotnetRuntimeArgs;
}
#endif

/// <summary>
/// Should only be called _directly_ by running test, so that the testName is populated correctly
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public async Task<ProcessResult> RunDotnetTestSampleAndWaitForExit(MockTracerAge
return new ProcessResult(process, standardOutput, standardError, exitCode);
}

public async Task<Process> StartSample(MockTracerAgent agent, string arguments, string packageVersion, int aspNetCorePort, string framework = "", bool? enableSecurity = null, string externalRulesFile = null, bool usePublishWithRID = false)
public async Task<Process> StartSample(MockTracerAgent agent, string arguments, string packageVersion, int aspNetCorePort, string framework = "", bool? enableSecurity = null, string externalRulesFile = null, bool usePublishWithRID = false, string dotnetRuntimeArgs = null)
{
// get path to sample app that the profiler will attach to
var sampleAppPath = EnvironmentHelper.GetSampleApplicationPath(packageVersion, framework, usePublishWithRID);
Expand All @@ -167,9 +167,20 @@ public async Task<Process> StartSample(MockTracerAgent agent, string arguments,
throw new Exception($"application not found: {sampleAppPath}");
}

var runtimeArgs = string.Empty;
if (!string.IsNullOrEmpty(dotnetRuntimeArgs))
{
if (!EnvironmentHelper.IsCoreClr() || usePublishWithRID)
{
throw new Exception($"Cannot use {nameof(dotnetRuntimeArgs)} with .NET Framework or when publishing with RID");
}

runtimeArgs = $"{dotnetRuntimeArgs} ";
}

Output.WriteLine($"Starting Application: {sampleAppPath}");
var executable = EnvironmentHelper.IsCoreClr() && !usePublishWithRID ? EnvironmentHelper.GetSampleExecutionSource() : sampleAppPath;
var args = EnvironmentHelper.IsCoreClr() && !usePublishWithRID ? $"{sampleAppPath} {arguments ?? string.Empty}" : arguments;
var args = EnvironmentHelper.IsCoreClr() && !usePublishWithRID ? $"{runtimeArgs}{sampleAppPath} {arguments ?? string.Empty}" : arguments;

var process = await ProfilerHelper.StartProcessWithProfiler(
executable,
Expand All @@ -187,9 +198,9 @@ public async Task<Process> StartSample(MockTracerAgent agent, string arguments,
return process;
}

public async Task<ProcessResult> RunSampleAndWaitForExit(MockTracerAgent agent, string arguments = null, string packageVersion = "", string framework = "", int aspNetCorePort = 5000, bool usePublishWithRID = false)
public async Task<ProcessResult> RunSampleAndWaitForExit(MockTracerAgent agent, string arguments = null, string packageVersion = "", string framework = "", int aspNetCorePort = 5000, bool usePublishWithRID = false, string dotnetRuntimeArgs = null)
{
var process = await StartSample(agent, arguments, packageVersion, aspNetCorePort: aspNetCorePort, framework: framework, usePublishWithRID: usePublishWithRID);
var process = await StartSample(agent, arguments, packageVersion, aspNetCorePort: aspNetCorePort, framework: framework, usePublishWithRID: usePublishWithRID, dotnetRuntimeArgs: dotnetRuntimeArgs);
using var helper = new ProcessHelper(process);

return WaitForProcessResult(helper);
Expand Down

0 comments on commit c2c6227

Please sign in to comment.