diff --git a/src/Dotnet.Watch/Watch/Browser/BrowserLauncher.cs b/src/Dotnet.Watch/Watch/Browser/BrowserLauncher.cs index 714b0cdd7a0d..d2f2bf19c60c 100644 --- a/src/Dotnet.Watch/Watch/Browser/BrowserLauncher.cs +++ b/src/Dotnet.Watch/Watch/Browser/BrowserLauncher.cs @@ -16,10 +16,9 @@ internal sealed class BrowserLauncher(ILogger logger, IProcessOutputReporter pro private ImmutableHashSet _browserLaunchAttempted = []; /// - /// Installs browser launch/reload trigger. + /// Returns an output observing action that triggers the launch of the browser, or null if the browser should not be launched. /// - public void InstallBrowserLaunchTrigger( - ProcessSpec processSpec, + public Action? TryGetBrowserLaunchOutputObserver( ProjectGraphNode projectNode, ProjectOptions projectOptions, AbstractBrowserRefreshServer? server, @@ -32,10 +31,10 @@ public void InstallBrowserLaunchTrigger( logger.LogError("Test requires browser to launch"); } - return; + return null; } - WebServerProcessStateObserver.Observe(projectNode, processSpec, url => + return WebServerProcessStateObserver.GetObserver(projectNode, url => { if (projectOptions.IsMainProject && ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, key) => set.Add(key), projectNode.ProjectInstance.GetId())) diff --git a/src/Dotnet.Watch/Watch/Process/ProcessSpec.cs b/src/Dotnet.Watch/Watch/Process/ProcessSpec.cs index d3e9badc6f35..5e1df816075f 100644 --- a/src/Dotnet.Watch/Watch/Process/ProcessSpec.cs +++ b/src/Dotnet.Watch/Watch/Process/ProcessSpec.cs @@ -24,4 +24,25 @@ internal sealed class ProcessSpec public string GetArgumentsDisplay() => CommandLineUtilities.JoinArguments(Arguments ?? []); + + /// + /// Stream output lines to the process output reporter when + /// - output observer is installed so that the output is also streamd to the console; + /// - testing to synchonize the output of the process with the logger output, so that the printed lines don't interleave; + /// unless the caller has already redirected the output (e.g. for Aspire child processes). + /// + /// Do not redirect output otherwise as it disables the ability of the process to use Console APIs. + /// + public void RedirectOutput(Action? outputObserver, IProcessOutputReporter outputReporter, EnvironmentOptions environmentOptions, string projectDisplayName) + { + if (environmentOptions.RunningAsTest || outputObserver != null) + { + OnOutput ??= line => + { + outputReporter.ReportOutput(outputReporter.PrefixProcessOutput ? line with { Content = $"[{projectDisplayName}] {line.Content}" } : line); + }; + + OnOutput += outputObserver; + } + } } diff --git a/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs b/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs index 4297eb189910..8b4b1707fefc 100644 --- a/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs +++ b/src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs @@ -60,15 +60,6 @@ public CompilationHandler CompilationHandler OnExit = onExit, }; - // Stream output lines to the process output reporter. - // The reporter synchronizes the output of the process with the logger output, - // so that the printed lines don't interleave. - // Only send the output to the reporter if no custom output handler was provided (e.g. for Aspire child processes). - processSpec.OnOutput ??= line => - { - context.ProcessOutputReporter.ReportOutput(context.ProcessOutputReporter.PrefixProcessOutput ? line with { Content = $"[{projectDisplayName}] {line.Content}" } : line); - }; - var environmentBuilder = new Dictionary(); // initialize with project settings: @@ -91,9 +82,10 @@ public CompilationHandler CompilationHandler processSpec.Arguments = GetProcessArguments(projectOptions, environmentBuilder); - // Attach trigger to the process that detects when the web server reports to the output that it's listening. - // Launches browser on the URL found in the process output for root projects. - context.BrowserLauncher.InstallBrowserLaunchTrigger(processSpec, projectNode, projectOptions, clients.BrowserRefreshServer, cancellationToken); + // Observes main project process output and launches browser when the URL is found in the output. + var outputObserver = context.BrowserLauncher.TryGetBrowserLaunchOutputObserver(projectNode, projectOptions, clients.BrowserRefreshServer, cancellationToken); + + processSpec.RedirectOutput(outputObserver, context.ProcessOutputReporter, context.EnvironmentOptions, projectDisplayName); return await compilationHandler.TrackRunningProjectAsync( projectNode, diff --git a/src/Dotnet.Watch/Watch/Process/WebServerProcessStateObserver.cs b/src/Dotnet.Watch/Watch/Process/WebServerProcessStateObserver.cs index 684937ae8db5..cae85ea28728 100644 --- a/src/Dotnet.Watch/Watch/Process/WebServerProcessStateObserver.cs +++ b/src/Dotnet.Watch/Watch/Process/WebServerProcessStateObserver.cs @@ -21,7 +21,7 @@ internal static partial class WebServerProcessStateObserver [GeneratedRegex(@"Login to the dashboard at (?.*)\s*$", RegexOptions.Compiled)] private static partial Regex GetAspireDashboardUrlRegex(); - public static void Observe(ProjectGraphNode serverProject, ProcessSpec serverProcessSpec, Action onServerListening) + public static Action GetObserver(ProjectGraphNode serverProject, Action onServerListening) { // Workaround for Aspire dashboard launching: scan for "Login to the dashboard at " prefix in the output and use the URL. // TODO: https://github.com/dotnet/sdk/issues/9038 @@ -30,7 +30,7 @@ public static void Observe(ProjectGraphNode serverProject, ProcessSpec serverPro var _notified = false; - serverProcessSpec.OnOutput += line => + return line => { if (_notified) { diff --git a/src/Dotnet.Watch/dotnet-watch/Watch/DotNetWatcher.cs b/src/Dotnet.Watch/dotnet-watch/Watch/DotNetWatcher.cs index 8a5acd6c0e27..e6efef90f8e8 100644 --- a/src/Dotnet.Watch/dotnet-watch/Watch/DotNetWatcher.cs +++ b/src/Dotnet.Watch/dotnet-watch/Watch/DotNetWatcher.cs @@ -58,8 +58,7 @@ public static async Task WatchAsync(DotNetWatchContext context, CancellationToke { [EnvironmentVariables.Names.DotnetWatch] = "1", [EnvironmentVariables.Names.DotnetWatchIteration] = (iteration + 1).ToString(CultureInfo.InvariantCulture), - }, - OnOutput = line => context.ProcessOutputReporter.ReportOutput(line) + } }; var browserRefreshServer = projectRootNode != null && HotReloadAppModel.InferFromProject(context, projectRootNode) is WebApplicationAppModel webAppModel @@ -68,15 +67,18 @@ public static async Task WatchAsync(DotNetWatchContext context, CancellationToke browserRefreshServer?.ConfigureLaunchEnvironment(environmentBuilder, enableHotReload: false); - foreach (var (name, value) in environmentBuilder) + Action? outputObserver = null; + if (projectRootNode != null) { - processSpec.EnvironmentVariables.Add(name, value); + Debug.Assert(context.MainProjectOptions != null); + outputObserver = context.BrowserLauncher.TryGetBrowserLaunchOutputObserver(projectRootNode, context.MainProjectOptions, browserRefreshServer, shutdownCancellationToken); } - if (projectRootNode != null) + processSpec.RedirectOutput(outputObserver, context.ProcessOutputReporter, context.EnvironmentOptions, projectRootNode?.GetDisplayName() ?? ""); + + foreach (var (name, value) in environmentBuilder) { - Debug.Assert(context.MainProjectOptions != null); - context.BrowserLauncher.InstallBrowserLaunchTrigger(processSpec, projectRootNode, context.MainProjectOptions, browserRefreshServer, shutdownCancellationToken); + processSpec.EnvironmentVariables.Add(name, value); } // Reset for next run