| File: Browser\BrowserLauncher.cs | Web Access |
| Project: src\sdk\src\Dotnet.Watch\Watch\Microsoft.DotNet.HotReload.Watch.csproj (Microsoft.DotNet.HotReload.Watch) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Build.Graph; using Microsoft.DotNet.HotReload; using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch; internal sealed class BrowserLauncher(ILogger logger, IProcessOutputReporter processOutputReporter, EnvironmentOptions environmentOptions) { // interlocked private ImmutableHashSet<ProjectInstanceId> _browserLaunchAttempted = []; /// <summary> /// Returns an output observing action that triggers the launch of the browser, or null if the browser should not be launched. /// </summary> public Action<OutputLine>? TryGetBrowserLaunchOutputObserver( ProjectGraphNode projectNode, ProjectOptions projectOptions, AbstractBrowserRefreshServer? server, CancellationToken cancellationToken) { if (!CanLaunchBrowser(projectOptions, out var launchProfile)) { if (environmentOptions.TestFlags.HasFlag(TestFlags.MockBrowser)) { logger.LogError("Test requires browser to launch"); } return null; } return WebServerProcessStateObserver.GetObserver(projectNode, url => { if (projectOptions.IsMainProject && ImmutableInterlocked.Update(ref _browserLaunchAttempted, static (set, key) => set.Add(key), projectNode.ProjectInstance.GetId())) { // first build iteration of a root project: var launchUrl = GetLaunchUrl(launchProfile.LaunchUrl, url); LaunchBrowser(launchUrl, server); } else if (server != null) { // Subsequent iterations (project has been rebuilt and relaunched). // Use refresh server to reload the browser, if available. _ = server.SendReloadMessageAsync(cancellationToken).AsTask(); } }); } public static string GetLaunchUrl(string? profileLaunchUrl, string outputLaunchUrl) => string.IsNullOrWhiteSpace(profileLaunchUrl) ? outputLaunchUrl : Uri.TryCreate(profileLaunchUrl, UriKind.Absolute, out _) ? profileLaunchUrl : Uri.TryCreate(outputLaunchUrl, UriKind.Absolute, out var launchUri) ? new Uri(launchUri, profileLaunchUrl).ToString() : outputLaunchUrl; private void LaunchBrowser(string launchUrl, AbstractBrowserRefreshServer? server) { var (fileName, arg, useShellExecute) = environmentOptions.BrowserPath is { } browserPath ? (browserPath, launchUrl, false) : (launchUrl, null, true); if (arg != null) { logger.Log(MessageDescriptor.LaunchingBrowserWithUrl, (fileName, arg)); } else { logger.Log(MessageDescriptor.LaunchingBrowser, fileName); } if (environmentOptions.TestFlags != TestFlags.None && environmentOptions.BrowserPath == null) { if (environmentOptions.TestFlags.HasFlag(TestFlags.MockBrowser)) { Debug.Assert(server != null); server.EmulateClientConnected(); } return; } // dotnet-watch, by default, relies on URL file association to launch browsers. On Windows and MacOS, this works fairly well // where URLs are associated with the default browser. On Linux, this is a bit murky. // From emperical observation, it's noted that failing to launch a browser results in either Process.Start returning a null-value // or for the process to have immediately exited. // We can use this to provide a helpful message. var processSpec = new ProcessSpec() { Executable = fileName, Arguments = arg != null ? [arg] : [], UseShellExecute = useShellExecute, OnOutput = environmentOptions.TestFlags.HasFlag(TestFlags.RedirectBrowserOutput) ? processOutputReporter.ReportOutput : null, }; using var browserProcess = ProcessRunner.TryStartProcess(processSpec, logger); if (browserProcess is null or { HasExited: true }) { logger.LogWarning("Unable to launch the browser. Url '{Url}'.", launchUrl); } } private bool CanLaunchBrowser(ProjectOptions projectOptions, [NotNullWhen(true)] out LaunchSettingsProfile? launchProfile) { launchProfile = null; if (environmentOptions.SuppressLaunchBrowser) { return false; } if (!projectOptions.IsCodeExecutionCommand) { logger.LogDebug("Command '{Command}' does not support launching browsers.", projectOptions.Command); return false; } launchProfile = GetLaunchProfile(projectOptions); if (launchProfile is not { LaunchBrowser: true }) { logger.LogDebug("launchSettings does not allow launching browsers."); return false; } logger.Log(MessageDescriptor.ConfiguredToLaunchBrowser); return true; } private LaunchSettingsProfile GetLaunchProfile(ProjectOptions projectOptions) { var profile = projectOptions.LaunchProfileName.HasValue ? LaunchSettingsProfile.ReadLaunchProfile(projectOptions.Representation, projectOptions.LaunchProfileName.Value, logger) : null; return profile ?? new(); } }