Skip to content

Commit 3cd7c65

Browse files
authored
Synchronize watched process and reporter output printing (#46141)
1 parent d82927f commit 3cd7c65

File tree

10 files changed

+60
-96
lines changed

10 files changed

+60
-96
lines changed

src/BuiltInTools/dotnet-watch/Internal/BrowserSpecificReporter.cs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.Build.Graph;
5-
64
namespace Microsoft.DotNet.Watch;
75

86
internal sealed class BrowserSpecificReporter(int browserId, IReporter underlyingReporter) : IReporter
@@ -12,14 +10,8 @@ internal sealed class BrowserSpecificReporter(int browserId, IReporter underlyin
1210
public bool IsVerbose
1311
=> underlyingReporter.IsVerbose;
1412

15-
public bool EnableProcessOutputReporting
16-
=> false;
17-
18-
public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
19-
=> throw new InvalidOperationException();
20-
2113
public void ReportProcessOutput(OutputLine line)
22-
=> throw new InvalidOperationException();
14+
=> underlyingReporter.ReportProcessOutput(line);
2315

2416
public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
2517
=> underlyingReporter.Report(descriptor, _prefix + prefix, args);

src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.Build.Graph;
5-
64
namespace Microsoft.DotNet.Watch
75
{
86
/// <summary>
@@ -15,16 +13,15 @@ internal sealed class ConsoleReporter(IConsole console, bool verbose, bool quiet
1513
public bool IsQuiet { get; } = quiet;
1614
public bool SuppressEmojis { get; } = suppressEmojis;
1715

18-
private readonly object _writeLock = new();
19-
20-
public bool EnableProcessOutputReporting
21-
=> false;
16+
private readonly Lock _writeLock = new();
2217

2318
public void ReportProcessOutput(OutputLine line)
24-
=> throw new InvalidOperationException();
25-
26-
public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
27-
=> throw new InvalidOperationException();
19+
{
20+
lock (_writeLock)
21+
{
22+
(line.IsError ? console.Error : console.Out).WriteLine(line.Content);
23+
}
24+
}
2825

2926
private void WriteLine(TextWriter writer, string message, ConsoleColor? color, string emoji)
3027
{

src/BuiltInTools/dotnet-watch/Internal/IReporter.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,19 @@ public bool IsVerbose
8686
=> false;
8787

8888
/// <summary>
89-
/// True to call <see cref="ReportProcessOutput"/> when launched process writes to standard output.
89+
/// If true, the output of the process will be prefixed with the project display name.
9090
/// Used for testing.
9191
/// </summary>
92-
bool EnableProcessOutputReporting { get; }
92+
public bool PrefixProcessOutput
93+
=> false;
9394

95+
/// <summary>
96+
/// Reports the output of a process that is being watched.
97+
/// </summary>
98+
/// <remarks>
99+
/// Not used to report output of dotnet-build processed launched by dotnet-watch to build or evaluate projects.
100+
/// </remarks>
94101
void ReportProcessOutput(OutputLine line);
95-
void ReportProcessOutput(ProjectGraphNode project, OutputLine line);
96102

97103
void Report(MessageDescriptor descriptor, params object?[] args)
98104
=> Report(descriptor, prefix: "", args);

src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.Build.Graph;
5-
64
namespace Microsoft.DotNet.Watch
75
{
86
/// <summary>
@@ -11,20 +9,15 @@ namespace Microsoft.DotNet.Watch
119
/// </summary>
1210
internal sealed class NullReporter : IReporter
1311
{
14-
private NullReporter()
15-
{ }
16-
1712
public static IReporter Singleton { get; } = new NullReporter();
1813

19-
public bool EnableProcessOutputReporting
20-
=> false;
14+
private NullReporter()
15+
{
16+
}
2117

2218
public void ReportProcessOutput(OutputLine line)
23-
=> throw new InvalidOperationException();
24-
25-
public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
26-
=> throw new InvalidOperationException();
27-
19+
{
20+
}
2821

2922
public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
3023
{

src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,10 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report
3131

3232
var onOutput = processSpec.OnOutput;
3333

34-
// allow tests to watch for application output:
35-
if (reporter.EnableProcessOutputReporting)
36-
{
37-
onOutput += line => reporter.ReportProcessOutput(line);
38-
}
34+
// If output isn't already redirected (build invocation) we redirect it to the reporter.
35+
// The reporter synchronizes the output of the process with the reporter output,
36+
// so that the printed lines don't interleave.
37+
onOutput ??= line => reporter.ReportProcessOutput(line);
3938

4039
using var process = CreateProcess(processSpec, onOutput, state, reporter);
4140

@@ -186,7 +185,7 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
186185
FileName = processSpec.Executable,
187186
UseShellExecute = false,
188187
WorkingDirectory = processSpec.WorkingDirectory,
189-
RedirectStandardOutput = onOutput != null,
188+
RedirectStandardOutput = onOutput != null,
190189
RedirectStandardError = onOutput != null,
191190
}
192191
};

src/BuiltInTools/dotnet-watch/Internal/ProjectSpecificReporter.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,9 @@ internal sealed class ProjectSpecificReporter(ProjectGraphNode node, IReporter u
1212
public bool IsVerbose
1313
=> underlyingReporter.IsVerbose;
1414

15-
public bool EnableProcessOutputReporting
16-
=> underlyingReporter.EnableProcessOutputReporting;
17-
18-
public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
19-
=> underlyingReporter.ReportProcessOutput(project, line);
20-
2115
public void ReportProcessOutput(OutputLine line)
22-
=> ReportProcessOutput(node, line);
16+
=> underlyingReporter.ReportProcessOutput(
17+
underlyingReporter.PrefixProcessOutput ? line with { Content = $"[{_projectDisplayName}] {line.Content}" } : line);
2318

2419
public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
2520
=> underlyingReporter.Report(descriptor, $"[{_projectDisplayName}] {prefix}", args);

test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
#nullable enable
55

6-
using System.Collections.Immutable;
76
using System.Runtime.CompilerServices;
87

98
namespace Microsoft.DotNet.Watch.UnitTests;
@@ -142,6 +141,8 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger)
142141
{
143142
var testAsset = CopyTestAsset("WatchAppMultiProc", trigger);
144143

144+
var tfm = ToolsetInfo.CurrentTargetFramework;
145+
145146
var workingDirectory = testAsset.Path;
146147
var hostDir = Path.Combine(testAsset.Path, "Host");
147148
var hostProject = Path.Combine(hostDir, "Host.csproj");
@@ -219,18 +220,18 @@ async Task MakeValidDependencyChange()
219220
{
220221
var hasUpdateSourceA = w.CreateCompletionSource();
221222
var hasUpdateSourceB = w.CreateCompletionSource();
222-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
223+
w.Reporter.OnProcessOutput += line =>
223224
{
224225
if (line.Content.Contains("<Updated Lib>"))
225226
{
226-
if (projectPath == serviceProjectA)
227+
if (line.Content.StartsWith($"[A ({tfm})]"))
227228
{
228229
if (!hasUpdateSourceA.Task.IsCompleted)
229230
{
230231
hasUpdateSourceA.SetResult();
231232
}
232233
}
233-
else if (projectPath == serviceProjectB)
234+
else if (line.Content.StartsWith($"[B ({tfm})]"))
234235
{
235236
if (!hasUpdateSourceB.Task.IsCompleted)
236237
{
@@ -239,7 +240,7 @@ async Task MakeValidDependencyChange()
239240
}
240241
else
241242
{
242-
Assert.Fail("Only service projects should be updated");
243+
Assert.Fail($"Only service projects should be updated: '{line.Content}'");
243244
}
244245
}
245246
};
@@ -273,9 +274,9 @@ public static void Common()
273274
async Task MakeRudeEditChange()
274275
{
275276
var hasUpdateSource = w.CreateCompletionSource();
276-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
277+
w.Reporter.OnProcessOutput += line =>
277278
{
278-
if (projectPath == serviceProjectA && line.Content.Contains("Started A: 2"))
279+
if (line.Content.StartsWith($"[A ({tfm})]") && line.Content.Contains("Started A: 2"))
279280
{
280281
hasUpdateSource.SetResult();
281282
}
@@ -300,6 +301,7 @@ async Task MakeRudeEditChange()
300301
public async Task UpdateAppliedToNewProcesses(bool sharedOutput)
301302
{
302303
var testAsset = CopyTestAsset("WatchAppMultiProc", sharedOutput);
304+
var tfm = ToolsetInfo.CurrentTargetFramework;
303305

304306
if (sharedOutput)
305307
{
@@ -325,21 +327,21 @@ public async Task UpdateAppliedToNewProcesses(bool sharedOutput)
325327

326328
var hasUpdateA = new SemaphoreSlim(initialCount: 0);
327329
var hasUpdateB = new SemaphoreSlim(initialCount: 0);
328-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
330+
w.Reporter.OnProcessOutput += line =>
329331
{
330332
if (line.Content.Contains("<Updated Lib>"))
331333
{
332-
if (projectPath == serviceProjectA)
334+
if (line.Content.StartsWith($"[A ({tfm})]"))
333335
{
334336
hasUpdateA.Release();
335337
}
336-
else if (projectPath == serviceProjectB)
338+
else if (line.Content.StartsWith($"[B ({tfm})]"))
337339
{
338340
hasUpdateB.Release();
339341
}
340342
else
341343
{
342-
Assert.Fail("Only service projects should be updated");
344+
Assert.Fail($"Only service projects should be updated: '{line.Content}'");
343345
}
344346
}
345347
};
@@ -398,6 +400,7 @@ public enum UpdateLocation
398400
public async Task HostRestart(UpdateLocation updateLocation)
399401
{
400402
var testAsset = CopyTestAsset("WatchAppMultiProc", updateLocation);
403+
var tfm = ToolsetInfo.CurrentTargetFramework;
401404

402405
var workingDirectory = testAsset.Path;
403406
var hostDir = Path.Combine(testAsset.Path, "Host");
@@ -414,17 +417,17 @@ public async Task HostRestart(UpdateLocation updateLocation)
414417
var restartRequested = w.Reporter.RegisterSemaphore(MessageDescriptor.RestartRequested);
415418

416419
var hasUpdate = new SemaphoreSlim(initialCount: 0);
417-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
420+
w.Reporter.OnProcessOutput += line =>
418421
{
419422
if (line.Content.Contains("<Updated>"))
420423
{
421-
if (projectPath == hostProject)
424+
if (line.Content.StartsWith($"[Host ({tfm})]"))
422425
{
423426
hasUpdate.Release();
424427
}
425428
else
426429
{
427-
Assert.Fail("Only service projects should be updated");
430+
Assert.Fail($"Only service projects should be updated: '{line.Content}'");
428431
}
429432
}
430433
};

test/dotnet-watch.Tests/MsBuildFileSetFactoryTest.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class MsBuildFileSetFactoryTest(ITestOutputHelper output)
1010
private readonly TestReporter _reporter = new(output);
1111
private readonly TestAssetsManager _testAssets = new(output);
1212

13-
private string MuxerPath
13+
private static string MuxerPath
1414
=> TestContext.Current.ToolsetUnderTest.DotNetHostPath;
1515

1616
private static string InspectPath(string path, string rootDir)
@@ -327,9 +327,6 @@ public async Task ProjectReferences_Graph()
327327

328328
var options = TestOptions.GetEnvironmentOptions(workingDirectory: testDirectory, muxerPath: MuxerPath);
329329

330-
var output = new List<string>();
331-
_reporter.OnProcessOutput += line => output.Add(line.Content);
332-
333330
var filesetFactory = new MSBuildFileSetFactory(projectA, buildArguments: ["/p:_DotNetWatchTraceOutput=true"], options, _reporter);
334331

335332
var result = await filesetFactory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
@@ -365,7 +362,7 @@ public async Task ProjectReferences_Graph()
365362
"Collecting watch items from 'F'",
366363
"Collecting watch items from 'G'",
367364
],
368-
output.Where(l => l.Contains("Collecting watch items from")).Select(l => l.Trim()).Order());
365+
_reporter.Messages.Where(l => l.text.Contains("Collecting watch items from")).Select(l => l.text.Trim()).Order());
369366
}
370367

371368
[Fact]
@@ -386,17 +383,14 @@ public async Task MsbuildOutput()
386383

387384
var options = TestOptions.GetEnvironmentOptions(workingDirectory: Path.GetDirectoryName(project1Path)!, muxerPath: MuxerPath);
388385

389-
var output = new List<string>();
390-
_reporter.OnProcessOutput += line => output.Add($"{(line.IsError ? "[stderr]" : "[stdout]")} {line.Content}");
391-
392386
var factory = new MSBuildFileSetFactory(project1Path, buildArguments: [], options, _reporter);
393387
var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
394388
Assert.Null(result);
395389

396-
// note: msbuild prints errors to stdout:
390+
// note: msbuild prints errors to stdout, we match the pattern and report as error:
397391
AssertEx.Equal(
398-
$"[stdout] {project1Path} : error NU1201: Project Project2 is not compatible with net462 (.NETFramework,Version=v4.6.2). Project Project2 supports: netstandard2.1 (.NETStandard,Version=v2.1)",
399-
output.Single(l => l.Contains("error NU1201")));
392+
(MessageSeverity.Error, $"{project1Path} : error NU1201: Project Project2 is not compatible with net462 (.NETFramework,Version=v4.6.2). Project Project2 supports: netstandard2.1 (.NETStandard,Version=v2.1)"),
393+
_reporter.Messages.Single(l => l.text.Contains("error NU1201")));
400394
}
401395

402396
private Task<EvaluationResult> Evaluate(TestAsset projectPath)

test/dotnet-watch.Tests/Utilities/MockReporter.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,15 @@
33

44
#nullable enable
55

6-
using Microsoft.Build.Graph;
7-
86
namespace Microsoft.DotNet.Watch.UnitTests;
97

108
internal class MockReporter : IReporter
119
{
1210
public readonly List<string> Messages = [];
1311

14-
public bool EnableProcessOutputReporting => false;
15-
1612
public void ReportProcessOutput(OutputLine line)
17-
=> throw new InvalidOperationException();
18-
19-
public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
20-
=> throw new InvalidOperationException();
13+
{
14+
}
2115

2216
public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
2317
{

0 commit comments

Comments
 (0)