Skip to content

Commit

Permalink
Enable parameter capturing in no suspend mode. (#6778)
Browse files Browse the repository at this point in the history
* Enable parameter capturing in no suspend mode.

* Change the suspension mode in the configureApp callback

* Update ScenarioRunner.SingleTarget to support starting the app before the tool and add a test to verify the feature doesn't work if the startup hook is manually configured.

* docs update
  • Loading branch information
clguiman authored Jun 6, 2024
1 parent 1b1cb31 commit a2c9bfa
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 48 deletions.
5 changes: 2 additions & 3 deletions documentation/api/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,10 @@ Content-Type: application/x-ndjson

## Additional Requirements

- The target application must use ASP.NET Core.
- The target application cannot have [Hot Reload](https://learn.microsoft.com/visualstudio/debugger/hot-reload) enabled.
- `dotnet-monitor` must be set to `Listen` mode, and the target application must start suspended. See [diagnostic port configuration](../configuration/diagnostic-port-configuration.md) for information on how to do this.
- `dotnet-monitor` must be set to `Listen` mode. See [diagnostic port configuration](../configuration/diagnostic-port-configuration.md) for information on how to do this.
- If the target application is using .NET 7 then the dotnet-monitor startup hook must be manually configured and the target application must start suspended. In .NET 8+ this is not a requirement.
- This feature relies on a [ICorProfilerCallback](https://docs.microsoft.com/dotnet/framework/unmanaged-api/profiling/icorprofilercallback-interface) implementation. If the target application is already using an `ICorProfiler` that isn't notify-only, this feature will not be available.
- If a target application is using .NET 7 then the `dotnet-monitor` startup hook must be configured. This is automatically done in .NET 8+.

## Additional Notes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ public ParameterCapturingTests(ITestOutputHelper outputHelper, ServiceProviderFi
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public async Task UnresolvableMethodsFailsOperation(Architecture targetArchitecture)
{
await RunTestCaseCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, async (appRunner, apiClient) =>
await RunTestCaseCore(
TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp,
targetArchitecture,
shouldSuspendTargetApp: true,
enableStartupHook: true,
async (appRunner, apiClient) =>
{
int processId = await appRunner.ProcessIdTask;

Expand All @@ -78,40 +83,52 @@ await RunTestCaseCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp
[Theory]
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public Task CapturesParametersInNonAspNetApps(Architecture targetArchitecture) =>
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.NonAspNetApp, targetArchitecture, CapturedParameterFormat.JsonSequence);
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.NonAspNetApp, targetArchitecture, CapturedParameterFormat.JsonSequence, shouldSuspendTargetApp: true, enableStartupHook: true);

[Theory]
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public Task CapturesParametersAndOutputJsonSequence(Architecture targetArchitecture) =>
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, CapturedParameterFormat.JsonSequence);
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, CapturedParameterFormat.JsonSequence, shouldSuspendTargetApp: true, enableStartupHook: true);

[Theory]
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public Task CapturesParametersAndOutputNewlineDelimitedJson(Architecture targetArchitecture) =>
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, CapturedParameterFormat.NewlineDelimitedJson);
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, CapturedParameterFormat.NewlineDelimitedJson, shouldSuspendTargetApp: true, enableStartupHook: true);

#else // NET7_0_OR_GREATER
#if NET8_0_OR_GREATER
[Theory]
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public async Task Net6AppFailsOperation(Architecture targetArchitecture)
{
await RunTestCaseCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, async (appRunner, apiClient) =>
{
int processId = await appRunner.ProcessIdTask;
public Task CapturesParametersNoSuspend(Architecture targetArchitecture) =>
CapturesParametersCore(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, CapturedParameterFormat.JsonSequence, shouldSuspendTargetApp: false, enableStartupHook: false);

CaptureParametersConfiguration config = GetValidConfiguration();
#endif // NET8_0_OR_GREATER

ValidationProblemDetailsException validationException = await Assert.ThrowsAsync<ValidationProblemDetailsException>(() => apiClient.CaptureParametersAsync(processId, Timeout.InfiniteTimeSpan, config));
Assert.Equal(HttpStatusCode.BadRequest, validationException.StatusCode);
[Theory]
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public Task AppWithStartupHookFailsInNoSuspend(Architecture targetArchitecture) =>
ValidateBadRequestFailure(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, shouldSuspendTargetApp: false, enableStartupHook: true);

#else // NET7_0_OR_GREATER
[Theory]
[MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))]
public Task Net6AppFailsOperation(Architecture targetArchitecture) =>
ValidateBadRequestFailure(TestAppScenarios.ParameterCapturing.SubScenarios.AspNetApp, targetArchitecture, shouldSuspendTargetApp: true, enableStartupHook: true);

await appRunner.SendCommandAsync(TestAppScenarios.ParameterCapturing.Commands.Continue);
});
}
#endif // NET7_0_OR_GREATER

private async Task CapturesParametersCore(string subScenarioName,Architecture targetArchitecture, CapturedParameterFormat format)
private async Task CapturesParametersCore(
string subScenarioName,
Architecture targetArchitecture,
CapturedParameterFormat format,
bool shouldSuspendTargetApp,
bool enableStartupHook)
{
await RunTestCaseCore(subScenarioName, targetArchitecture, async (appRunner, apiClient) =>
await RunTestCaseCore(
subScenarioName,
targetArchitecture,
shouldSuspendTargetApp: shouldSuspendTargetApp,
enableStartupHook: enableStartupHook,
async (appRunner, apiClient) =>
{
int processId = await appRunner.ProcessIdTask;

Expand Down Expand Up @@ -143,7 +160,36 @@ await RunTestCaseCore(subScenarioName, targetArchitecture, async (appRunner, api
});
}

private async Task RunTestCaseCore(string subScenarioName, Architecture targetArchitecture, Func<AppRunner, ApiClient, Task> appValidate)
private async Task ValidateBadRequestFailure(
string subScenarioName,
Architecture targetArchitecture,
bool shouldSuspendTargetApp,
bool enableStartupHook)
{
await RunTestCaseCore(
subScenarioName,
targetArchitecture,
shouldSuspendTargetApp: shouldSuspendTargetApp,
enableStartupHook: enableStartupHook,
async (appRunner, apiClient) =>
{
int processId = await appRunner.ProcessIdTask;

CaptureParametersConfiguration config = GetValidConfiguration();

ValidationProblemDetailsException validationException = await Assert.ThrowsAsync<ValidationProblemDetailsException>(() => apiClient.CaptureParametersAsync(processId, Timeout.InfiniteTimeSpan, config));
Assert.Equal(HttpStatusCode.BadRequest, validationException.StatusCode);

await appRunner.SendCommandAsync(TestAppScenarios.ParameterCapturing.Commands.Continue);
});
}

private async Task RunTestCaseCore(
string subScenarioName,
Architecture targetArchitecture,
bool shouldSuspendTargetApp,
bool enableStartupHook,
Func<AppRunner, ApiClient, Task> appValidate)
{
await ScenarioRunner.SingleTarget(
_outputHelper,
Expand All @@ -153,8 +199,9 @@ await ScenarioRunner.SingleTarget(
appValidate: appValidate,
configureApp: runner =>
{
runner.EnableMonitorStartupHook = true;
runner.EnableMonitorStartupHook = enableStartupHook;
runner.Architecture = targetArchitecture;
runner.DiagnosticPortSuspend = shouldSuspendTargetApp;
},
configureTool: (toolRunner) =>
{
Expand All @@ -166,7 +213,8 @@ await ScenarioRunner.SingleTarget(
toolRunner.WriteKeyPerValueConfiguration(new RootOptions().AddFileSystemEgress(FileProviderName, _tempDirectory.FullName));
},
profilerLogLevel: LogLevel.Trace,
subScenarioName: subScenarioName);
subScenarioName: subScenarioName,
startAppBeforeTool: !shouldSuspendTargetApp);
}

private static CaptureParametersConfiguration GetValidConfiguration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public static async Task SingleTarget(
Action<MonitorCollectRunner> configureTool = null,
bool disableHttpEgress = false,
LogLevel profilerLogLevel = LogLevel.Error,
string subScenarioName = null)
string subScenarioName = null,
bool startAppBeforeTool = false)
{
DiagnosticPortHelper.Generate(
mode,
Expand All @@ -43,37 +44,66 @@ public static async Task SingleTarget(

configureTool?.Invoke(toolRunner);

await toolRunner.StartAsync();

using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(httpClientFactory);
ApiClient apiClient = new(outputHelper, httpClient);

await using AppRunner appRunner = new(outputHelper, Assembly.GetExecutingAssembly());
if (profilerLogLevel != LogLevel.None)
if (!startAppBeforeTool)
{
appRunner.ProfilerLogLevel = profilerLogLevel.ToString("G");
await toolRunner.StartAsync();
}
appRunner.ConnectionMode = appConnectionMode;
appRunner.DiagnosticPortPath = diagnosticPortPath;
appRunner.ScenarioName = scenarioName;
appRunner.SubScenarioName = subScenarioName;

configureApp?.Invoke(appRunner);
#nullable enable
HttpClient? httpClient = null;
ApiClient? apiClient = null;

await appRunner.ExecuteAsync(async () =>
try
{
// Wait for the process to be discovered.
int processId = await appRunner.ProcessIdTask;
_ = await apiClient.GetProcessWithRetryAsync(outputHelper, pid: processId);
if (!startAppBeforeTool)
{
httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(httpClientFactory);
apiClient = new(outputHelper, httpClient);
}

await using AppRunner appRunner = new(outputHelper, Assembly.GetExecutingAssembly());
if (profilerLogLevel != LogLevel.None)
{
appRunner.ProfilerLogLevel = profilerLogLevel.ToString("G");
}
appRunner.ConnectionMode = appConnectionMode;
appRunner.DiagnosticPortPath = diagnosticPortPath;
appRunner.ScenarioName = scenarioName;
appRunner.SubScenarioName = subScenarioName;

configureApp?.Invoke(appRunner);

await appRunner.ExecuteAsync(async () =>
{
if (startAppBeforeTool)
{
await toolRunner.StartAsync();
httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(httpClientFactory);
apiClient = new(outputHelper, httpClient);
}

await appValidate(appRunner, apiClient);
});
Assert.Equal(0, appRunner.ExitCode);
// Wait for the process to be discovered.
int processId = await appRunner.ProcessIdTask;
_ = await apiClient.GetProcessWithRetryAsync(outputHelper, pid: processId);

if (null != postAppValidate)
Assert.NotNull(apiClient);

await appValidate(appRunner, apiClient);
});
Assert.Equal(0, appRunner.ExitCode);

Assert.NotNull(apiClient);

if (null != postAppValidate)
{
await postAppValidate(apiClient, await appRunner.ProcessIdTask);
}
}
finally
{
await postAppValidate(apiClient, await appRunner.ProcessIdTask);
httpClient?.Dispose();
}
#nullable disable
}
}
}
6 changes: 5 additions & 1 deletion src/Tools/dotnet-monitor/Commands/CollectCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,15 @@ private static IHostBuilder Configure(this IHostBuilder builder, StartupAuthenti
services.AddSingleton<ProfilerChannel>();
services.ConfigureCollectionRules();
services.ConfigureLibrarySharing();
/*
* ConfigureInProcessFeatures needs to be called before ConfigureProfiler
* because the profiler needs to have access to environment variables set by in process features.
*/
services.ConfigureInProcessFeatures(context.Configuration);
services.ConfigureProfiler();
services.ConfigureStartupHook();
services.ConfigureExceptions();
services.ConfigureStartupLoggers(authConfigurator);
services.ConfigureInProcessFeatures(context.Configuration);
services.AddSingleton<IInProcessFeatures, InProcessFeatures>();
services.AddSingleton<IDumpOperationFactory, DumpOperationFactory>();
services.AddSingleton<ILogsOperationFactory, LogsOperationFactory>();
Expand Down

0 comments on commit a2c9bfa

Please sign in to comment.