Skip to content

Commit a8f7349

Browse files
committed
Only enable ASP.NET debug logging if logging level is Trace
1 parent cec531e commit a8f7349

File tree

7 files changed

+240
-5
lines changed

7 files changed

+240
-5
lines changed

src/BuiltInTools/HotReloadClient/Logging/LogEvents.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ public static void Log(this ILogger logger, LogEvent logEvent, params object[] a
2525
public static readonly LogEvent HotReloadSucceeded = Create(LogLevel.Information, "Hot reload succeeded.");
2626
public static readonly LogEvent RefreshingBrowser = Create(LogLevel.Debug, "Refreshing browser.");
2727
public static readonly LogEvent ReloadingBrowser = Create(LogLevel.Debug, "Reloading browser.");
28+
public static readonly LogEvent SendingWaitMessage = Create(LogLevel.Debug, "Sending wait message.");
2829
public static readonly LogEvent NoBrowserConnected = Create(LogLevel.Debug, "No browser is connected.");
2930
public static readonly LogEvent FailedToReceiveResponseFromConnectedBrowser = Create(LogLevel.Debug, "Failed to receive response from a connected browser.");
3031
public static readonly LogEvent UpdatingDiagnostics = Create(LogLevel.Debug, "Updating diagnostics.");
32+
public static readonly LogEvent UpdatingStaticAssets = Create(LogLevel.Debug, "Updating diagnostics.");
3133
public static readonly LogEvent SendingStaticAssetUpdateRequest = Create(LogLevel.Debug, "Sending static asset update request to connected browsers: '{0}'.");
3234
public static readonly LogEvent RefreshServerRunningAt = Create(LogLevel.Debug, "Refresh server running at {0}.");
33-
public static readonly LogEvent ConnectedToRefreshServer = Create(LogLevel.Debug, "Connected to refresh server.");
3435
}

src/BuiltInTools/HotReloadClient/Web/AbstractBrowserRefreshServer.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public void ConfigureLaunchEnvironment(IDictionary<string, string> builder, bool
101101
builder[MiddlewareEnvironmentVariables.DotNetModifiableAssemblies] = "debug";
102102
}
103103

104-
if (logger.IsEnabled(LogLevel.Debug))
104+
if (logger.IsEnabled(LogLevel.Trace))
105105
{
106106
// enable debug logging from middleware:
107107
builder[MiddlewareEnvironmentVariables.LoggingLevel] = "Debug";
@@ -163,7 +163,7 @@ public async Task WaitForClientConnectionAsync(CancellationToken cancellationTok
163163
}, progressCancellationSource.Token);
164164

165165
// Work around lack of Task.WaitAsync(cancellationToken) on .NET Framework:
166-
cancellationToken.Register(() => _browserConnected.TrySetCanceled());
166+
cancellationToken.Register(() => _browserConnected.SetCanceled());
167167

168168
try
169169
{
@@ -237,8 +237,12 @@ public ValueTask SendReloadMessageAsync(CancellationToken cancellationToken)
237237
}
238238

239239
public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken)
240-
=> SendAsync(s_waitMessage, cancellationToken);
240+
{
241+
logger.Log(LogEvents.SendingWaitMessage);
242+
return SendAsync(s_waitMessage, cancellationToken);
243+
}
241244

245+
// obsolete: to be removed
242246
public ValueTask SendPingMessageAsync(CancellationToken cancellationToken)
243247
=> SendAsync(s_pingMessage, cancellationToken);
244248

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 System.Globalization;
5+
using Microsoft.DotNet.HotReload;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.DotNet.Watch;
9+
10+
internal delegate ValueTask ProcessExitAction(int processId, int? exitCode);
11+
12+
internal sealed class ProjectLauncher(
13+
DotNetWatchContext context,
14+
ProjectNodeMap projectMap,
15+
CompilationHandler compilationHandler,
16+
int iteration)
17+
{
18+
public int Iteration = iteration;
19+
20+
public ILogger Logger
21+
=> context.Logger;
22+
23+
public ILoggerFactory LoggerFactory
24+
=> context.LoggerFactory;
25+
26+
public EnvironmentOptions EnvironmentOptions
27+
=> context.EnvironmentOptions;
28+
29+
public async ValueTask<RunningProject?> TryLaunchProcessAsync(
30+
ProjectOptions projectOptions,
31+
CancellationTokenSource processTerminationSource,
32+
Action<OutputLine>? onOutput,
33+
RestartOperation restartOperation,
34+
CancellationToken cancellationToken)
35+
{
36+
var projectNode = projectMap.TryGetProjectNode(projectOptions.ProjectPath, projectOptions.TargetFramework);
37+
if (projectNode == null)
38+
{
39+
// error already reported
40+
return null;
41+
}
42+
43+
if (!projectNode.IsNetCoreApp(Versions.Version6_0))
44+
{
45+
Logger.LogError($"Hot Reload based watching is only supported in .NET 6.0 or newer apps. Use --no-hot-reload switch or update the project's launchSettings.json to disable this feature.");
46+
return null;
47+
}
48+
49+
var appModel = HotReloadAppModel.InferFromProject(context, projectNode);
50+
51+
// create loggers that include project name in messages:
52+
var projectDisplayName = projectNode.GetDisplayName();
53+
var clientLogger = context.LoggerFactory.CreateLogger(HotReloadDotNetWatcher.ClientLogComponentName, projectDisplayName);
54+
var agentLogger = context.LoggerFactory.CreateLogger(HotReloadDotNetWatcher.AgentLogComponentName, projectDisplayName);
55+
56+
var clients = await appModel.TryCreateClientsAsync(clientLogger, agentLogger, cancellationToken);
57+
if (clients == null)
58+
{
59+
// error already reported
60+
return null;
61+
}
62+
63+
var processSpec = new ProcessSpec
64+
{
65+
Executable = EnvironmentOptions.MuxerPath,
66+
IsUserApplication = true,
67+
WorkingDirectory = projectOptions.WorkingDirectory,
68+
OnOutput = onOutput,
69+
};
70+
71+
// Stream output lines to the process output reporter.
72+
// The reporter synchronizes the output of the process with the logger output,
73+
// so that the printed lines don't interleave.
74+
processSpec.OnOutput += line =>
75+
{
76+
context.ProcessOutputReporter.ReportOutput(context.ProcessOutputReporter.PrefixProcessOutput ? line with { Content = $"[{projectDisplayName}] {line.Content}" } : line);
77+
};
78+
79+
var environmentBuilder = new Dictionary<string, string>();
80+
81+
// initialize with project settings:
82+
foreach (var (name, value) in projectOptions.LaunchEnvironmentVariables)
83+
{
84+
environmentBuilder[name] = value;
85+
}
86+
87+
// override any project settings:
88+
environmentBuilder[EnvironmentVariables.Names.DotnetWatch] = "1";
89+
environmentBuilder[EnvironmentVariables.Names.DotnetWatchIteration] = (Iteration + 1).ToString(CultureInfo.InvariantCulture);
90+
91+
if (Logger.IsEnabled(LogLevel.Trace))
92+
{
93+
environmentBuilder[EnvironmentVariables.Names.HotReloadDeltaClientLogMessages] =
94+
(EnvironmentOptions.SuppressEmojis ? Emoji.Default : Emoji.Agent).GetLogMessagePrefix() + $"[{projectDisplayName}]";
95+
}
96+
97+
clients.ConfigureLaunchEnvironment(environmentBuilder);
98+
99+
processSpec.Arguments = GetProcessArguments(projectOptions, environmentBuilder);
100+
101+
// Attach trigger to the process that detects when the web server reports to the output that it's listening.
102+
// Launches browser on the URL found in the process output for root projects.
103+
context.BrowserLauncher.InstallBrowserLaunchTrigger(processSpec, projectNode, projectOptions, clients.BrowserRefreshServer, cancellationToken);
104+
105+
return await compilationHandler.TrackRunningProjectAsync(
106+
projectNode,
107+
projectOptions,
108+
clients,
109+
processSpec,
110+
restartOperation,
111+
processTerminationSource,
112+
cancellationToken);
113+
}
114+
115+
private static IReadOnlyList<string> GetProcessArguments(ProjectOptions projectOptions, IDictionary<string, string> environmentBuilder)
116+
{
117+
var arguments = new List<string>()
118+
{
119+
projectOptions.Command,
120+
"--no-build"
121+
};
122+
123+
foreach (var (name, value) in environmentBuilder)
124+
{
125+
arguments.Add("-e");
126+
arguments.Add($"{name}={value}");
127+
}
128+
129+
arguments.AddRange(projectOptions.CommandArguments);
130+
return arguments;
131+
}
132+
133+
public ValueTask<int> TerminateProcessAsync(RunningProject project, CancellationToken cancellationToken)
134+
=> compilationHandler.TerminateNonRootProcessAsync(project, cancellationToken);
135+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.DotNet.HotReload;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Microsoft.DotNet.Watch.UnitTests;
8+
9+
public class BrowserRefreshServerTests
10+
{
11+
class TestListener : IDisposable
12+
{
13+
public void Dispose()
14+
{
15+
}
16+
}
17+
18+
[Theory]
19+
[CombinatorialData]
20+
public async Task ConfigureLaunchEnvironmentAsync(LogLevel logLevel, bool enableHotReload)
21+
{
22+
var middlewarePath = Path.GetTempPath();
23+
var middlewareFileName = Path.GetFileNameWithoutExtension(middlewarePath);
24+
25+
var server = new TestBrowserRefreshServer(middlewarePath)
26+
{
27+
CreateAndStartHostImpl = () => new WebServerHost(new TestListener(), ["http://test.endpoint"], virtualDirectory: "/test/virt/dir")
28+
};
29+
30+
((TestLogger)server.Logger).IsEnabledImpl = level => level == logLevel;
31+
32+
await server.StartAsync(CancellationToken.None);
33+
34+
var envBuilder = new Dictionary<string, string>();
35+
server.ConfigureLaunchEnvironment(envBuilder, enableHotReload);
36+
37+
Assert.True(envBuilder.Remove("ASPNETCORE_AUTO_RELOAD_WS_KEY"));
38+
39+
var expected = new List<string>()
40+
{
41+
"ASPNETCORE_AUTO_RELOAD_VDIR=/test/virt/dir",
42+
"ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT=http://test.endpoint",
43+
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=" + middlewareFileName,
44+
"DOTNET_STARTUP_HOOKS=" + middlewarePath,
45+
};
46+
47+
if (enableHotReload)
48+
{
49+
expected.Add("DOTNET_MODIFIABLE_ASSEMBLIES=debug");
50+
}
51+
52+
if (logLevel == LogLevel.Trace)
53+
{
54+
expected.Add("Logging__LogLevel__Microsoft.AspNetCore.Watch=Debug");
55+
}
56+
57+
AssertEx.SequenceEqual(expected.Order(), envBuilder.OrderBy(e => e.Key).Select(e => $"{e.Key}={e.Value}"));
58+
}
59+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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.DotNet.HotReload;
5+
6+
namespace Microsoft.DotNet.Watch.UnitTests;
7+
8+
internal class TestBrowserRefreshServer(string middlewareAssemblyPath)
9+
: AbstractBrowserRefreshServer(middlewareAssemblyPath, new TestLogger(), new TestLoggerFactory())
10+
{
11+
public Func<WebServerHost>? CreateAndStartHostImpl;
12+
13+
protected override ValueTask<WebServerHost> CreateAndStartHostAsync(CancellationToken cancellationToken)
14+
=> ValueTask.FromResult((CreateAndStartHostImpl ?? throw new NotImplementedException())());
15+
16+
protected override bool SuppressTimeouts => true;
17+
}

test/dotnet-watch.Tests/TestUtilities/TestLogger.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ internal class TestLogger(ITestOutputHelper? output = null) : ILogger
1111
public readonly Lock Guard = new();
1212
private readonly List<string> _messages = [];
1313

14+
public Func<LogLevel, bool>? IsEnabledImpl;
15+
1416
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
1517
{
1618
var message = $"[{logLevel}] {formatter(state, exception)}";
@@ -36,5 +38,5 @@ public ImmutableArray<string> GetAndClearMessages()
3638
where TState : notnull => throw new NotImplementedException();
3739

3840
public bool IsEnabled(LogLevel logLevel)
39-
=> true;
41+
=> IsEnabledImpl?.Invoke(logLevel) ?? true;
4042
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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.Extensions.Logging;
5+
6+
namespace Microsoft.DotNet.Watch.UnitTests;
7+
8+
internal class TestLoggerFactory(ITestOutputHelper? output = null) : ILoggerFactory
9+
{
10+
public Func<string, ILogger>? CreateLoggerImpl;
11+
12+
public ILogger CreateLogger(string categoryName)
13+
=> CreateLoggerImpl?.Invoke(categoryName) ?? new TestLogger(output);
14+
15+
public void AddProvider(ILoggerProvider provider) {}
16+
public void Dispose() { }
17+
}

0 commit comments

Comments
 (0)