Skip to content

Commit 966fe96

Browse files
committed
Synchronize watched process and reporter output printing
1 parent e1e7867 commit 966fe96

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;
@@ -139,6 +138,8 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger)
139138
{
140139
var testAsset = CopyTestAsset("WatchAppMultiProc", trigger);
141140

141+
var tfm = ToolsetInfo.CurrentTargetFramework;
142+
142143
var workingDirectory = testAsset.Path;
143144
var hostDir = Path.Combine(testAsset.Path, "Host");
144145
var hostProject = Path.Combine(hostDir, "Host.csproj");
@@ -216,18 +217,18 @@ async Task MakeValidDependencyChange()
216217
{
217218
var hasUpdateSourceA = w.CreateCompletionSource();
218219
var hasUpdateSourceB = w.CreateCompletionSource();
219-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
220+
w.Reporter.OnProcessOutput += line =>
220221
{
221222
if (line.Content.Contains("<Updated Lib>"))
222223
{
223-
if (projectPath == serviceProjectA)
224+
if (line.Content.StartsWith($"[A ({tfm})]"))
224225
{
225226
if (!hasUpdateSourceA.Task.IsCompleted)
226227
{
227228
hasUpdateSourceA.SetResult();
228229
}
229230
}
230-
else if (projectPath == serviceProjectB)
231+
else if (line.Content.StartsWith($"[B ({tfm})]"))
231232
{
232233
if (!hasUpdateSourceB.Task.IsCompleted)
233234
{
@@ -236,7 +237,7 @@ async Task MakeValidDependencyChange()
236237
}
237238
else
238239
{
239-
Assert.Fail("Only service projects should be updated");
240+
Assert.Fail($"Only service projects should be updated: '{line.Content}'");
240241
}
241242
}
242243
};
@@ -270,9 +271,9 @@ public static void Common()
270271
async Task MakeRudeEditChange()
271272
{
272273
var hasUpdateSource = w.CreateCompletionSource();
273-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
274+
w.Reporter.OnProcessOutput += line =>
274275
{
275-
if (projectPath == serviceProjectA && line.Content.Contains("Started A: 2"))
276+
if (line.Content.StartsWith($"[ServiceA ({tfm})]") && line.Content.Contains("Started A: 2"))
276277
{
277278
hasUpdateSource.SetResult();
278279
}
@@ -297,6 +298,7 @@ async Task MakeRudeEditChange()
297298
public async Task UpdateAppliedToNewProcesses(bool sharedOutput)
298299
{
299300
var testAsset = CopyTestAsset("WatchAppMultiProc", sharedOutput);
301+
var tfm = ToolsetInfo.CurrentTargetFramework;
300302

301303
if (sharedOutput)
302304
{
@@ -322,21 +324,21 @@ public async Task UpdateAppliedToNewProcesses(bool sharedOutput)
322324

323325
var hasUpdateA = new SemaphoreSlim(initialCount: 0);
324326
var hasUpdateB = new SemaphoreSlim(initialCount: 0);
325-
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
327+
w.Reporter.OnProcessOutput += line =>
326328
{
327329
if (line.Content.Contains("<Updated Lib>"))
328330
{
329-
if (projectPath == serviceProjectA)
331+
if (line.Content.StartsWith($"[A ({tfm})]"))
330332
{
331333
hasUpdateA.Release();
332334
}
333-
else if (projectPath == serviceProjectB)
335+
else if (line.Content.StartsWith($"[B ({tfm})]"))
334336
{
335337
hasUpdateB.Release();
336338
}
337339
else
338340
{
339-
Assert.Fail("Only service projects should be updated");
341+
Assert.Fail($"Only service projects should be updated: '{line.Content}'");
340342
}
341343
}
342344
};
@@ -395,6 +397,7 @@ public enum UpdateLocation
395397
public async Task HostRestart(UpdateLocation updateLocation)
396398
{
397399
var testAsset = CopyTestAsset("WatchAppMultiProc", updateLocation);
400+
var tfm = ToolsetInfo.CurrentTargetFramework;
398401

399402
var workingDirectory = testAsset.Path;
400403
var hostDir = Path.Combine(testAsset.Path, "Host");
@@ -411,17 +414,17 @@ public async Task HostRestart(UpdateLocation updateLocation)
411414
var restartRequested = w.Reporter.RegisterSemaphore(MessageDescriptor.RestartRequested);
412415

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

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)
@@ -329,9 +329,6 @@ public async Task ProjectReferences_Graph()
329329
MuxerPath: MuxerPath,
330330
WorkingDirectory: testDirectory);
331331

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

337334
var result = await filesetFactory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
@@ -367,7 +364,7 @@ public async Task ProjectReferences_Graph()
367364
"Collecting watch items from 'F'",
368365
"Collecting watch items from 'G'",
369366
],
370-
output.Where(l => l.Contains("Collecting watch items from")).Select(l => l.Trim()).Order());
367+
_reporter.Messages.Where(l => l.text.Contains("Collecting watch items from")).Select(l => l.text.Trim()).Order());
371368
}
372369

373370
[Fact]
@@ -390,17 +387,14 @@ public async Task MsbuildOutput()
390387
MuxerPath: MuxerPath,
391388
WorkingDirectory: Path.GetDirectoryName(project1Path)!);
392389

393-
var output = new List<string>();
394-
_reporter.OnProcessOutput += line => output.Add($"{(line.IsError ? "[stderr]" : "[stdout]")} {line.Content}");
395-
396390
var factory = new MSBuildFileSetFactory(project1Path, buildArguments: [], options, _reporter);
397391
var result = await factory.TryCreateAsync(requireProjectGraph: null, CancellationToken.None);
398392
Assert.Null(result);
399393

400-
// note: msbuild prints errors to stdout:
394+
// note: msbuild prints errors to stdout, we match the pattern and report as error:
401395
AssertEx.Equal(
402-
$"[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)",
403-
output.Single(l => l.Contains("error NU1201")));
396+
(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)"),
397+
_reporter.Messages.Single(l => l.text.Contains("error NU1201")));
404398
}
405399

406400
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)