Skip to content

Commit

Permalink
Synchronize watched process and reporter output printing
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Jan 20, 2025
1 parent e1e7867 commit 879b716
Show file tree
Hide file tree
Showing 9 changed files with 45 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch;

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

public bool EnableProcessOutputReporting
=> false;

public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
=> throw new InvalidOperationException();

public void ReportProcessOutput(OutputLine line)
=> throw new InvalidOperationException();
=> underlyingReporter.ReportProcessOutput(line);

public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
=> underlyingReporter.Report(descriptor, _prefix + prefix, args);
Expand Down
17 changes: 7 additions & 10 deletions src/BuiltInTools/dotnet-watch/Internal/ConsoleReporter.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch
{
/// <summary>
Expand All @@ -15,16 +13,15 @@ internal sealed class ConsoleReporter(IConsole console, bool verbose, bool quiet
public bool IsQuiet { get; } = quiet;
public bool SuppressEmojis { get; } = suppressEmojis;

private readonly object _writeLock = new();

public bool EnableProcessOutputReporting
=> false;
private readonly Lock _writeLock = new();

public void ReportProcessOutput(OutputLine line)
=> throw new InvalidOperationException();

public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
=> throw new InvalidOperationException();
{
lock (_writeLock)
{
(line.IsError ? console.Error : console.Out).WriteLine(line.Content);
}
}

private void WriteLine(TextWriter writer, string message, ConsoleColor? color, string emoji)
{
Expand Down
12 changes: 9 additions & 3 deletions src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,19 @@ public bool IsVerbose
=> false;

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

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

void Report(MessageDescriptor descriptor, params object?[] args)
=> Report(descriptor, prefix: "", args);
Expand Down
17 changes: 5 additions & 12 deletions src/BuiltInTools/dotnet-watch/Internal/NullReporter.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch
{
/// <summary>
Expand All @@ -11,20 +9,15 @@ namespace Microsoft.DotNet.Watch
/// </summary>
internal sealed class NullReporter : IReporter
{
private NullReporter()
{ }

public static IReporter Singleton { get; } = new NullReporter();

public bool EnableProcessOutputReporting
=> false;
private NullReporter()
{
}

public void ReportProcessOutput(OutputLine line)
=> throw new InvalidOperationException();

public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
=> throw new InvalidOperationException();

{
}

public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
{
Expand Down
11 changes: 5 additions & 6 deletions src/BuiltInTools/dotnet-watch/Internal/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ public static async Task<int> RunAsync(ProcessSpec processSpec, IReporter report

var onOutput = processSpec.OnOutput;

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

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

Expand Down Expand Up @@ -186,7 +185,7 @@ private static Process CreateProcess(ProcessSpec processSpec, Action<OutputLine>
FileName = processSpec.Executable,
UseShellExecute = false,
WorkingDirectory = processSpec.WorkingDirectory,
RedirectStandardOutput = onOutput != null,
RedirectStandardOutput = onOutput != null,
RedirectStandardError = onOutput != null,
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ internal sealed class ProjectSpecificReporter(ProjectGraphNode node, IReporter u
public bool IsVerbose
=> underlyingReporter.IsVerbose;

public bool EnableProcessOutputReporting
=> underlyingReporter.EnableProcessOutputReporting;

public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
=> underlyingReporter.ReportProcessOutput(project, line);

public void ReportProcessOutput(OutputLine line)
=> ReportProcessOutput(node, line);
=> underlyingReporter.ReportProcessOutput(
underlyingReporter.PrefixProcessOutput ? line with { Content = $"[{_projectDisplayName}] {line.Content}" } : line);

public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
=> underlyingReporter.Report(descriptor, $"[{_projectDisplayName}] {prefix}", args);
Expand Down
24 changes: 14 additions & 10 deletions test/dotnet-watch.Tests/HotReload/RuntimeProcessLauncherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public async Task UpdateAndRudeEdit(TriggerEvent trigger)
{
var testAsset = CopyTestAsset("WatchAppMultiProc", trigger);

var tfm = ToolsetInfo.CurrentTargetFramework;

var workingDirectory = testAsset.Path;
var hostDir = Path.Combine(testAsset.Path, "Host");
var hostProject = Path.Combine(hostDir, "Host.csproj");
Expand Down Expand Up @@ -216,18 +218,18 @@ async Task MakeValidDependencyChange()
{
var hasUpdateSourceA = w.CreateCompletionSource();
var hasUpdateSourceB = w.CreateCompletionSource();
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
w.Reporter.OnProcessOutput += line =>
{
if (line.Content.Contains("<Updated Lib>"))
{
if (projectPath == serviceProjectA)
if (line.Content.StartsWith($"[ServiceA ({tfm})]"))
{
if (!hasUpdateSourceA.Task.IsCompleted)
{
hasUpdateSourceA.SetResult();
}
}
else if (projectPath == serviceProjectB)
else if (line.Content.StartsWith($"[ServiceB ({tfm})]"))
{
if (!hasUpdateSourceB.Task.IsCompleted)
{
Expand Down Expand Up @@ -270,9 +272,9 @@ public static void Common()
async Task MakeRudeEditChange()
{
var hasUpdateSource = w.CreateCompletionSource();
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
w.Reporter.OnProcessOutput += line =>
{
if (projectPath == serviceProjectA && line.Content.Contains("Started A: 2"))
if (line.Content.StartsWith($"[ServiceA ({tfm})]") && line.Content.Contains("Started A: 2"))
{
hasUpdateSource.SetResult();
}
Expand All @@ -297,6 +299,7 @@ async Task MakeRudeEditChange()
public async Task UpdateAppliedToNewProcesses(bool sharedOutput)
{
var testAsset = CopyTestAsset("WatchAppMultiProc", sharedOutput);
var tfm = ToolsetInfo.CurrentTargetFramework;

if (sharedOutput)
{
Expand All @@ -322,15 +325,15 @@ public async Task UpdateAppliedToNewProcesses(bool sharedOutput)

var hasUpdateA = new SemaphoreSlim(initialCount: 0);
var hasUpdateB = new SemaphoreSlim(initialCount: 0);
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
w.Reporter.OnProcessOutput += line =>
{
if (line.Content.Contains("<Updated Lib>"))
{
if (projectPath == serviceProjectA)
if (line.Content.StartsWith($"[ServiceA ({tfm})]"))
{
hasUpdateA.Release();
}
else if (projectPath == serviceProjectB)
else if (line.Content.StartsWith($"[ServiceB ({tfm})]"))
{
hasUpdateB.Release();
}
Expand Down Expand Up @@ -395,6 +398,7 @@ public enum UpdateLocation
public async Task HostRestart(UpdateLocation updateLocation)
{
var testAsset = CopyTestAsset("WatchAppMultiProc", updateLocation);
var tfm = ToolsetInfo.CurrentTargetFramework;

var workingDirectory = testAsset.Path;
var hostDir = Path.Combine(testAsset.Path, "Host");
Expand All @@ -411,11 +415,11 @@ public async Task HostRestart(UpdateLocation updateLocation)
var restartRequested = w.Reporter.RegisterSemaphore(MessageDescriptor.RestartRequested);

var hasUpdate = new SemaphoreSlim(initialCount: 0);
w.Reporter.OnProjectProcessOutput += (projectPath, line) =>
w.Reporter.OnProcessOutput += line =>
{
if (line.Content.Contains("<Updated>"))
{
if (projectPath == hostProject)
if (line.Content.StartsWith($"[Host ({tfm})]"))
{
hasUpdate.Release();
}
Expand Down
10 changes: 2 additions & 8 deletions test/dotnet-watch.Tests/Utilities/MockReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,15 @@

#nullable enable

using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch.UnitTests;

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

public bool EnableProcessOutputReporting => false;

public void ReportProcessOutput(OutputLine line)
=> throw new InvalidOperationException();

public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
=> throw new InvalidOperationException();
{
}

public void Report(MessageDescriptor descriptor, string prefix, object?[] args)
{
Expand Down
11 changes: 0 additions & 11 deletions test/dotnet-watch.Tests/Utilities/TestReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
#nullable enable

using System.Diagnostics;
using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch.UnitTests
{
Expand All @@ -19,7 +18,6 @@ public bool EnableProcessOutputReporting
public bool IsVerbose
=> true;

public event Action<string, OutputLine>? OnProjectProcessOutput;
public event Action<OutputLine>? OnProcessOutput;

public void ReportProcessOutput(OutputLine line)
Expand All @@ -28,16 +26,7 @@ public void ReportProcessOutput(OutputLine line)
ProcessOutput.Add(line.Content);

OnProcessOutput?.Invoke(line);
}

public void ReportProcessOutput(ProjectGraphNode project, OutputLine line)
{
var content = $"[{project.GetDisplayName()}]: {line.Content}";

WriteTestOutput(content);
ProcessOutput.Add(content);

OnProjectProcessOutput?.Invoke(project.ProjectInstance.FullPath, line);
}

public SemaphoreSlim RegisterSemaphore(MessageDescriptor descriptor)
Expand Down

0 comments on commit 879b716

Please sign in to comment.