Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/Dotnet.Watch/Watch/Browser/BrowserLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ internal sealed class BrowserLauncher(ILogger logger, IProcessOutputReporter pro
private ImmutableHashSet<ProjectInstanceId> _browserLaunchAttempted = [];

/// <summary>
/// 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.
/// </summary>
public void InstallBrowserLaunchTrigger(
ProcessSpec processSpec,
public Action<OutputLine>? TryGetBrowserLaunchOutputObserver(
ProjectGraphNode projectNode,
ProjectOptions projectOptions,
AbstractBrowserRefreshServer? server,
Expand All @@ -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()))
Expand Down
21 changes: 21 additions & 0 deletions src/Dotnet.Watch/Watch/Process/ProcessSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,25 @@ internal sealed class ProcessSpec

public string GetArgumentsDisplay()
=> CommandLineUtilities.JoinArguments(Arguments ?? []);

/// <summary>
/// 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.
/// </summary>
public void RedirectOutput(Action<OutputLine>? 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;
}
}
}
16 changes: 4 additions & 12 deletions src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>();

// initialize with project settings:
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal static partial class WebServerProcessStateObserver
[GeneratedRegex(@"Login to the dashboard at (?<url>.*)\s*$", RegexOptions.Compiled)]
private static partial Regex GetAspireDashboardUrlRegex();

public static void Observe(ProjectGraphNode serverProject, ProcessSpec serverProcessSpec, Action<string> onServerListening)
public static Action<OutputLine> GetObserver(ProjectGraphNode serverProject, Action<string> 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
Expand All @@ -30,7 +30,7 @@ public static void Observe(ProjectGraphNode serverProject, ProcessSpec serverPro

var _notified = false;

serverProcessSpec.OnOutput += line =>
return line =>
{
if (_notified)
{
Expand Down
16 changes: 9 additions & 7 deletions src/Dotnet.Watch/dotnet-watch/Watch/DotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -68,15 +67,18 @@ public static async Task WatchAsync(DotNetWatchContext context, CancellationToke

browserRefreshServer?.ConfigureLaunchEnvironment(environmentBuilder, enableHotReload: false);

foreach (var (name, value) in environmentBuilder)
Action<OutputLine>? 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
Expand Down
Loading