Skip to content

Commit e3fcf60

Browse files
Generalize startup hook application (#6347)
1 parent aaafb98 commit e3fcf60

File tree

9 files changed

+119
-146
lines changed

9 files changed

+119
-146
lines changed

src/Tools/dotnet-monitor/Exceptions/ExceptionsService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public ExceptionsService(
4343

4444
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
4545
{
46-
if (!_options.Value.GetEnabled() || !await _startupHookService.CheckHasStartupHookAsync(stoppingToken))
46+
if (!_options.Value.GetEnabled() || !await _startupHookService.Applied.WaitAsync(stoppingToken))
4747
{
4848
return;
4949
}

src/Tools/dotnet-monitor/HostingStartup/HostingStartupService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken)
5757
try
5858
{
5959
// Hosting startup requires the startup hook
60-
if (!await _startupHookService.CheckHasStartupHookAsync(cancellationToken))
60+
if (!await _startupHookService.Applied.WaitAsync(cancellationToken))
6161
{
6262
return;
6363
}

src/Tools/dotnet-monitor/LoggingExtensions.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -470,8 +470,8 @@ internal static class LoggingExtensions
470470
logLevel: LogLevel.Debug,
471471
formatString: Strings.LogFormatString_StartCollectArtifact);
472472

473-
private static readonly Action<ILogger, int, string, Exception> _startupHookInstructions =
474-
LoggerMessage.Define<int, string>(
473+
private static readonly Action<ILogger, int, string, string, Exception> _startupHookInstructions =
474+
LoggerMessage.Define<int, string, string>(
475475
eventId: LoggingEventIds.StartupHookInstructions.EventId(),
476476
logLevel: LogLevel.Warning,
477477
formatString: Strings.LogFormatString_StartupHookInstructions);
@@ -494,8 +494,8 @@ internal static class LoggingExtensions
494494
logLevel: LogLevel.Debug,
495495
formatString: Strings.LogFormatString_ProfilerRuntimeIdentifier);
496496

497-
private static readonly Action<ILogger, Exception> _startupHookApplyFailed =
498-
LoggerMessage.Define(
497+
private static readonly Action<ILogger, string, Exception> _startupHookApplyFailed =
498+
LoggerMessage.Define<string>(
499499
eventId: LoggingEventIds.StartupHookApplyFailed.EventId(),
500500
logLevel: LogLevel.Warning,
501501
formatString: Strings.LogFormatString_StartupHookApplyFailed);
@@ -919,9 +919,9 @@ public static void StartCollectArtifact(this ILogger logger, string artifactType
919919
_startCollectArtifact(logger, artifactType, null);
920920
}
921921

922-
public static void StartupHookInstructions(this ILogger logger, int processId, string startupHookLibraryPath)
922+
public static void StartupHookInstructions(this ILogger logger, int processId, string startupHookFileName, string startupHookLibraryPath)
923923
{
924-
_startupHookInstructions(logger, processId, startupHookLibraryPath, null);
924+
_startupHookInstructions(logger, processId, startupHookFileName, startupHookLibraryPath, null);
925925
}
926926

927927
public static void UnableToWatchForDisconnect(this ILogger logger, Exception exception)
@@ -939,9 +939,9 @@ public static void ProfilerRuntimeIdentifier(this ILogger logger, string runtime
939939
_profilerRuntimeIdentifier(logger, runtimeIdentifier, source, null);
940940
}
941941

942-
public static void StartupHookApplyFailed(this ILogger logger, Exception ex)
942+
public static void StartupHookApplyFailed(this ILogger logger, string startupHookFileName, Exception ex)
943943
{
944-
_startupHookApplyFailed(logger, ex);
944+
_startupHookApplyFailed(logger, startupHookFileName, ex);
945945
}
946946

947947
public static void EndpointInitializationFailed(this ILogger logger, int processId, Exception ex)

src/Tools/dotnet-monitor/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ public static IServiceCollection ConfigureHostingStartup(this IServiceCollection
367367

368368
public static IServiceCollection ConfigureStartupHook(this IServiceCollection services)
369369
{
370-
services.AddTransient<StartupHookValidator>();
370+
services.AddScoped<StartupHookApplicator>();
371371
services.AddScoped<StartupHookService>();
372372
services.AddScopedForwarder<IDiagnosticLifetimeService, StartupHookService>();
373373
return services;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Diagnostics.Monitoring.WebApi;
5+
using Microsoft.Diagnostics.NETCore.Client;
6+
using Microsoft.Diagnostics.Tools.Monitor.LibrarySharing;
7+
using Microsoft.Extensions.FileProviders;
8+
using Microsoft.Extensions.Logging;
9+
using System;
10+
using System.Collections.Generic;
11+
using System.IO;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
15+
namespace Microsoft.Diagnostics.Tools.Monitor.StartupHook
16+
{
17+
internal sealed class StartupHookApplicator
18+
{
19+
private readonly ILogger _logger;
20+
private readonly IEndpointInfo _endpointInfo;
21+
private readonly ISharedLibraryService _sharedLibraryService;
22+
23+
public StartupHookApplicator(
24+
ILogger<StartupHookApplicator> logger,
25+
IEndpointInfo endpointInfo,
26+
ISharedLibraryService sharedLibraryService)
27+
{
28+
_logger = logger;
29+
_endpointInfo = endpointInfo;
30+
_sharedLibraryService = sharedLibraryService;
31+
}
32+
33+
public async Task<bool> ApplyAsync(string tfm, string fileName, CancellationToken token)
34+
{
35+
IFileInfo fileInfo = await GetFileInfoAsync(tfm, fileName, token);
36+
if (!fileInfo.Exists)
37+
{
38+
// When the file doesn't exist fileInfo.Name will contain the full path of the missing file.
39+
_logger.StartupHookApplyFailed(fileName, new FileNotFoundException(null, fileInfo.Name));
40+
return false;
41+
}
42+
43+
DiagnosticsClient client = new(_endpointInfo.Endpoint);
44+
IDictionary<string, string> env = await client.GetProcessEnvironmentAsync(token);
45+
46+
if (IsEnvironmentConfiguredForStartupHook(fileInfo, env))
47+
{
48+
return true;
49+
}
50+
51+
52+
if (_endpointInfo.RuntimeVersion.Major < 8)
53+
{
54+
_logger.StartupHookInstructions(_endpointInfo.ProcessId, fileInfo.Name, fileInfo.PhysicalPath);
55+
return false;
56+
}
57+
58+
return await ApplyUsingDiagnosticClientAsync(fileInfo, _endpointInfo, client, token);
59+
}
60+
61+
private async Task<IFileInfo> GetFileInfoAsync(string tfm, string fileName, CancellationToken token)
62+
{
63+
IFileProviderFactory fileProviderFactory = await _sharedLibraryService.GetFactoryAsync(token);
64+
IFileProvider managedFileProvider = fileProviderFactory.CreateManaged(tfm);
65+
return managedFileProvider.GetFileInfo(fileName);
66+
}
67+
68+
private static bool IsEnvironmentConfiguredForStartupHook(IFileInfo fileInfo, IDictionary<string, string> env)
69+
=> env.TryGetValue(ToolIdentifiers.EnvironmentVariables.StartupHooks, out string startupHookPaths) &&
70+
startupHookPaths?.Contains(fileInfo.Name, StringComparison.OrdinalIgnoreCase) == true;
71+
72+
private async Task<bool> ApplyUsingDiagnosticClientAsync(IFileInfo fileInfo, IEndpointInfo endpointInfo, DiagnosticsClient client, CancellationToken token)
73+
{
74+
try
75+
{
76+
await client.ApplyStartupHookAsync(fileInfo.PhysicalPath, token);
77+
return true;
78+
}
79+
catch (Exception ex)
80+
{
81+
_logger.StartupHookApplyFailed(fileInfo.Name, ex);
82+
return false;
83+
}
84+
}
85+
}
86+
}

src/Tools/dotnet-monitor/StartupHook/StartupHookService.cs

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.Diagnostics.Monitoring.WebApi;
5-
using System;
65
using System.Threading;
76
using System.Threading.Tasks;
87

@@ -11,49 +10,40 @@ namespace Microsoft.Diagnostics.Tools.Monitor.StartupHook
1110
internal sealed class StartupHookService :
1211
IDiagnosticLifetimeService
1312
{
14-
private readonly IEndpointInfo _endpointInfo;
13+
// Intent is to ship a single TFM of the startup hook, which should be the lowest supported version.
14+
// If necessary, the startup hook should dynamically access APIs for higher version TFMs and handle
15+
// all exceptions appropriately.
16+
private const string Tfm = "net6.0";
17+
private const string FileName = "Microsoft.Diagnostics.Monitoring.StartupHook.dll";
18+
1519
private readonly IInProcessFeatures _inProcessFeatures;
16-
private readonly TaskCompletionSource<bool> _resultSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
17-
private readonly StartupHookValidator _startupHookValidator;
20+
private readonly StartupHookApplicator _startupHookApplicator;
21+
private readonly TaskCompletionSource<bool> _appliedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
22+
23+
public Task<bool> Applied => _appliedSource.Task;
1824

1925
public StartupHookService(
20-
IEndpointInfo endpointInfo,
2126
IInProcessFeatures inProcessFeatures,
22-
StartupHookValidator startupHookValidator)
27+
StartupHookApplicator startupHookApplicator)
2328
{
24-
_endpointInfo = endpointInfo ?? throw new ArgumentNullException(nameof(endpointInfo));
25-
_inProcessFeatures = inProcessFeatures ?? throw new ArgumentNullException(nameof(inProcessFeatures));
26-
_startupHookValidator = startupHookValidator ?? throw new ArgumentNullException(nameof(startupHookValidator));
29+
_inProcessFeatures = inProcessFeatures;
30+
_startupHookApplicator = startupHookApplicator;
2731
}
2832

2933
public async ValueTask StartAsync(CancellationToken cancellationToken)
3034
{
31-
if (_inProcessFeatures.IsStartupHookRequired)
35+
if (!_inProcessFeatures.IsStartupHookRequired)
3236
{
33-
if (await _startupHookValidator.CheckEnvironmentAsync(_endpointInfo, cancellationToken))
34-
{
35-
_resultSource.SetResult(true);
36-
return;
37-
}
38-
39-
if (await _startupHookValidator.ApplyStartupHook(_endpointInfo, cancellationToken))
40-
{
41-
_resultSource.SetResult(true);
42-
return;
43-
}
44-
45-
await _startupHookValidator.CheckEnvironmentAsync(_endpointInfo, cancellationToken, logInstructions: true);
37+
_appliedSource.TrySetResult(false);
38+
return;
4639
}
4740

48-
_resultSource.SetResult(false);
41+
_appliedSource.TrySetResult(await _startupHookApplicator.ApplyAsync(Tfm, FileName, cancellationToken));
4942
}
5043

5144
public ValueTask StopAsync(CancellationToken cancellationToken)
5245
{
5346
return ValueTask.CompletedTask;
5447
}
55-
56-
public Task<bool> CheckHasStartupHookAsync(CancellationToken cancellationToken)
57-
=> _resultSource.Task.WaitAsync(cancellationToken);
5848
}
5949
}

src/Tools/dotnet-monitor/StartupHook/StartupHookValidator.cs

Lines changed: 0 additions & 102 deletions
This file was deleted.

src/Tools/dotnet-monitor/Strings.Designer.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Tools/dotnet-monitor/Strings.resx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@
815815
<value>Started capturing {artifactType} artifact.</value>
816816
</data>
817817
<data name="LogFormatString_StartupHookInstructions" xml:space="preserve">
818-
<value>Exception-based features require that the DOTNET_STARTUP_HOOKS environment variable is set on the target process {processId} and must contain the path to the .NET Monitor startup hook library. The path to the library is "{path}".</value>
818+
<value>Certain in process features require that the DOTNET_STARTUP_HOOKS environment variable is set on the target process {processId} and must contain the path to the "{startupHookFileName}" startup hook library. The path to the library is "{path}".</value>
819819
</data>
820820
<data name="LogFormatString_UnableToApplyProfiler" xml:space="preserve">
821821
<value>Unable to apply profiler.</value>
@@ -916,8 +916,7 @@
916916
<value>Unable to fully remove endpoint for process {processId}.</value>
917917
</data>
918918
<data name="LogFormatString_StartupHookApplyFailed" xml:space="preserve">
919-
<value>Failed to apply the startup hook. Not all in process features will be available.</value>
920-
<comment>Gets a string similar to "Failed to apply the startup hook. Not all in process features will be available."</comment>
919+
<value>Failed to apply the startup hook "{startupHookFileName}". Not all in process features will be available.</value>
921920
</data>
922921
<data name="ErrorMessage_SharedFileDiffersFromSource" xml:space="preserve">
923922
<value>The shared file '{0}' is different from source file '{1}'.</value>
@@ -943,4 +942,4 @@
943942
<data name="ParameterCapturingNotAvailable_Reason_HotReload" xml:space="preserve">
944943
<value>The process has Hot Reload enabled.</value>
945944
</data>
946-
</root>
945+
</root>

0 commit comments

Comments
 (0)