forked from dotnet/sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBrowserLauncher.cs
More file actions
141 lines (121 loc) · 5.51 KB
/
BrowserLauncher.cs
File metadata and controls
141 lines (121 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// 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>
/// Retruns 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();
}
}