Skip to content

Commit 1a1ab56

Browse files
authored
[blazor][wasm][debug]Press alt-shift-d and open firefox debug tab attached to the blazor app (#46132)
* Press alt-shift-d and open firefox debug tab attached to the blazor app * remove debugger.launch. * removing unrelated changes * Removing unnecessary changes on chrome debugging. * addressing @mkArtakMSFT comments * Addressing @mkArtakMSFT comments. * Addressing Steve comments, adding a console.warning message and remove the beautiful message, removed the Newtonsoft from the send message, todo: remove the Newtonsoft from receive message. * Completely removing newtonsoft usage as asked by steve. * Change warning message.
1 parent 7369057 commit 1a1ab56

File tree

5 files changed

+261
-11
lines changed

5 files changed

+261
-11
lines changed

src/Components/Web.JS/src/Platform/Mono/MonoDebugger.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ let hasReferencedPdbs = false;
1515
let debugBuild = false;
1616

1717
export function hasDebuggingEnabled(): boolean {
18-
return (hasReferencedPdbs || debugBuild) && currentBrowserIsChromeOrEdge;
18+
return (hasReferencedPdbs || debugBuild) && (currentBrowserIsChromeOrEdge || navigator.userAgent.includes('Firefox'));
1919
}
2020

2121
export function attachDebuggerHotkey(resourceLoader: WebAssemblyResourceLoader): void {
@@ -33,6 +33,8 @@ export function attachDebuggerHotkey(resourceLoader: WebAssemblyResourceLoader):
3333
if (evt.shiftKey && (evt.metaKey || evt.altKey) && evt.code === 'KeyD') {
3434
if (!debugBuild && !hasReferencedPdbs) {
3535
console.error('Cannot start debugging, because the application was not compiled with debugging enabled.');
36+
} else if (navigator.userAgent.includes('Firefox')) {
37+
launchFirefoxDebugger();
3638
} else if (!currentBrowserIsChromeOrEdge) {
3739
console.error('Currently, only Microsoft Edge (80+), Google Chrome, or Chromium, are supported for debugging.');
3840
} else {
@@ -42,6 +44,13 @@ export function attachDebuggerHotkey(resourceLoader: WebAssemblyResourceLoader):
4244
});
4345
}
4446

47+
async function launchFirefoxDebugger() {
48+
const response = await fetch(`_framework/debug?url=${encodeURIComponent(location.href)}&isFirefox=true`);
49+
if (response.status !== 200) {
50+
console.warn(await response.text());
51+
}
52+
}
53+
4554
function launchDebugger() {
4655
// The noopener flag is essential, because otherwise Chrome tracks the association with the
4756
// parent tab, and then when the parent tab pauses in the debugger, the child tab does so

src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,27 @@ internal static class DebugProxyLauncher
1818
private static Task<string>? LaunchedDebugProxyUrl;
1919
private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$", RegexOptions.None, TimeSpan.FromSeconds(10));
2020
private static readonly Regex ApplicationStartedRegex = new Regex(@"^\s*Application started\. Press Ctrl\+C to shut down\.$", RegexOptions.None, TimeSpan.FromSeconds(10));
21+
private static readonly Regex NowListeningFirefoxRegex = new Regex(@"^\s*Debug proxy for firefox now listening on tcp://(?<url>.*)\. And expecting firefox at port 6000\.$", RegexOptions.None, TimeSpan.FromSeconds(10));
2122
private static readonly string[] MessageSuppressionPrefixes = new[]
2223
{
2324
"Hosting environment:",
2425
"Content root path:",
2526
"Now listening on:",
2627
"Application started. Press Ctrl+C to shut down.",
28+
"Debug proxy for firefox now",
2729
};
2830

29-
public static Task<string> EnsureLaunchedAndGetUrl(IServiceProvider serviceProvider, string devToolsHost)
31+
public static Task<string> EnsureLaunchedAndGetUrl(IServiceProvider serviceProvider, string devToolsHost, bool isFirefox)
3032
{
3133
lock (LaunchLock)
3234
{
33-
LaunchedDebugProxyUrl ??= LaunchAndGetUrl(serviceProvider, devToolsHost);
35+
LaunchedDebugProxyUrl ??= LaunchAndGetUrl(serviceProvider, devToolsHost, isFirefox);
3436

3537
return LaunchedDebugProxyUrl;
3638
}
3739
}
3840

39-
private static async Task<string> LaunchAndGetUrl(IServiceProvider serviceProvider, string devToolsHost)
41+
private static async Task<string> LaunchAndGetUrl(IServiceProvider serviceProvider, string devToolsHost, bool isFirefox)
4042
{
4143
var tcs = new TaskCompletionSource<string>();
4244

@@ -48,7 +50,7 @@ private static async Task<string> LaunchAndGetUrl(IServiceProvider serviceProvid
4850
var processStartInfo = new ProcessStartInfo
4951
{
5052
FileName = muxerPath,
51-
Arguments = $"exec \"{executablePath}\" --OwnerPid {ownerPid} --DevToolsUrl {devToolsHost}",
53+
Arguments = $"exec \"{executablePath}\" --OwnerPid {ownerPid} --DevToolsUrl {devToolsHost} --IsFirefoxDebugging {isFirefox} --FirefoxProxyPort 6001",
5254
UseShellExecute = false,
5355
RedirectStandardOutput = true,
5456
RedirectStandardError = true,
@@ -63,7 +65,7 @@ private static async Task<string> LaunchAndGetUrl(IServiceProvider serviceProvid
6365
else
6466
{
6567
PassThroughConsoleOutput(debugProxyProcess);
66-
CompleteTaskWhenServerIsReady(debugProxyProcess, tcs);
68+
CompleteTaskWhenServerIsReady(debugProxyProcess, isFirefox, tcs);
6769

6870
new CancellationTokenSource(DebugProxyLaunchTimeout).Token.Register(() =>
6971
{
@@ -136,7 +138,7 @@ private static void PassThroughConsoleOutput(Process process)
136138
};
137139
}
138140

139-
private static void CompleteTaskWhenServerIsReady(Process aspNetProcess, TaskCompletionSource<string> taskCompletionSource)
141+
private static void CompleteTaskWhenServerIsReady(Process aspNetProcess, bool isFirefox, TaskCompletionSource<string> taskCompletionSource)
140142
{
141143
string? capturedUrl = null;
142144
var errorEncountered = false;
@@ -169,7 +171,7 @@ void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs)
169171
return;
170172
}
171173

172-
if (ApplicationStartedRegex.IsMatch(eventArgs.Data))
174+
if (ApplicationStartedRegex.IsMatch(eventArgs.Data) && !isFirefox)
173175
{
174176
aspNetProcess.OutputDataReceived -= OnOutputDataReceived;
175177
aspNetProcess.ErrorDataReceived -= OnErrorDataReceived;
@@ -185,6 +187,15 @@ void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs)
185187
}
186188
else
187189
{
190+
var matchFirefox = NowListeningFirefoxRegex.Match(eventArgs.Data);
191+
if (matchFirefox.Success && isFirefox)
192+
{
193+
aspNetProcess.OutputDataReceived -= OnOutputDataReceived;
194+
aspNetProcess.ErrorDataReceived -= OnErrorDataReceived;
195+
capturedUrl = matchFirefox.Groups["url"].Value;
196+
taskCompletionSource.TrySetResult(capturedUrl);
197+
return;
198+
}
188199
var match = NowListeningRegex.Match(eventArgs.Data);
189200
if (match.Success)
190201
{
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.WebAssembly.Server.TargetPickerUi.DisplayFirefox(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!

src/Components/WebAssembly/Server/src/TargetPickerUi.cs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
using System.Linq;
66
using System.Net;
77
using System.Net.Http;
8+
using System.Net.Sockets;
9+
using System.Text;
810
using System.Text.Json;
911
using System.Text.Json.Serialization;
1012
using Microsoft.AspNetCore.Http;
13+
using System.Dynamic;
1114

1215
namespace Microsoft.AspNetCore.Components.WebAssembly.Server;
1316

@@ -37,6 +40,221 @@ public TargetPickerUi([StringSyntax(StringSyntaxAttribute.Uri)] string debugProx
3740
_browserHost = devToolsHost;
3841
}
3942

43+
/// <summary>
44+
/// Display the ui.
45+
/// </summary>
46+
/// <param name="context">The <see cref="HttpContext"/>.</param>
47+
/// <returns>The <see cref="Task"/>.</returns>
48+
public async Task DisplayFirefox(HttpContext context)
49+
{
50+
static async Task SendMessageToBrowser(NetworkStream toStream, ExpandoObject args, CancellationToken token)
51+
{
52+
var msg = JsonSerializer.Serialize(args);
53+
var bytes = Encoding.UTF8.GetBytes(msg);
54+
var bytesWithHeader = Encoding.UTF8.GetBytes($"{bytes.Length}:").Concat(bytes).ToArray();
55+
await toStream.WriteAsync(bytesWithHeader, token).AsTask();
56+
}
57+
#pragma warning disable CA1835
58+
static async Task<string> ReceiveMessageLoop(TcpClient browserDebugClientConnect, CancellationToken token)
59+
{
60+
var toStream = browserDebugClientConnect.GetStream();
61+
var bytesRead = 0;
62+
var _lengthBuffer = new byte[10];
63+
while (bytesRead == 0 || Convert.ToChar(_lengthBuffer[bytesRead - 1]) != ':')
64+
{
65+
if (!browserDebugClientConnect.Connected)
66+
{
67+
return "";
68+
}
69+
70+
if (bytesRead + 1 > _lengthBuffer.Length)
71+
{
72+
throw new IOException($"Protocol error: did not get the expected length preceding a message, " +
73+
$"after reading {bytesRead} bytes. Instead got: {Encoding.UTF8.GetString(_lengthBuffer)}");
74+
}
75+
76+
int readLen = await toStream.ReadAsync(_lengthBuffer, bytesRead, 1, token);
77+
bytesRead += readLen;
78+
}
79+
string str = Encoding.UTF8.GetString(_lengthBuffer, 0, bytesRead - 1);
80+
if (!int.TryParse(str, out int messageLen))
81+
{
82+
return "";
83+
}
84+
byte[] buffer = new byte[messageLen];
85+
bytesRead = await toStream.ReadAsync(buffer, 0, messageLen, token);
86+
while (bytesRead != messageLen)
87+
{
88+
if (!browserDebugClientConnect.Connected)
89+
{
90+
return "";
91+
}
92+
bytesRead += await toStream.ReadAsync(buffer, bytesRead, messageLen - bytesRead, token);
93+
}
94+
var messageReceived = Encoding.UTF8.GetString(buffer, 0, messageLen);
95+
return messageReceived;
96+
}
97+
static async Task EvaluateOnBrowser(NetworkStream toStream, string? to, string text, CancellationToken token)
98+
{
99+
dynamic message = new ExpandoObject();
100+
dynamic options = new ExpandoObject();
101+
dynamic awaitObj = new ExpandoObject();
102+
awaitObj.@await = true;
103+
options.eager = true;
104+
options.mapped = awaitObj;
105+
message.to = to;
106+
message.type = "evaluateJSAsync";
107+
message.text = text;
108+
message.options = options;
109+
await SendMessageToBrowser(toStream, message, token);
110+
}
111+
#pragma warning restore CA1835
112+
113+
context.Response.ContentType = "text/html";
114+
var request = context.Request;
115+
var targetApplicationUrl = request.Query["url"];
116+
var browserDebugClientConnect = new TcpClient();
117+
if (IPEndPoint.TryParse(_debugProxyUrl, out IPEndPoint? endpoint))
118+
{
119+
try
120+
{
121+
await browserDebugClientConnect.ConnectAsync(endpoint.Address, 6000);
122+
}
123+
catch (Exception)
124+
{
125+
context.Response.StatusCode = 404;
126+
await context.Response.WriteAsync($@"WARNING:
127+
Open about:config:
128+
- enable devtools.debugger.remote-enabled
129+
- enable devtools.chrome.enabled
130+
- disable devtools.debugger.prompt-connection
131+
Open firefox with remote debugging enabled on port 6000:
132+
firefox --start-debugger-server 6000 -new-tab about:debugging");
133+
return;
134+
}
135+
var source = new CancellationTokenSource();
136+
var token = source.Token;
137+
var toStream = browserDebugClientConnect.GetStream();
138+
dynamic messageListTabs = new ExpandoObject();
139+
messageListTabs.type = "listTabs";
140+
messageListTabs.to = "root";
141+
await SendMessageToBrowser(toStream, messageListTabs, token);
142+
var tabToRedirect = -1;
143+
var foundAboutDebugging = false;
144+
string? consoleActorId = null;
145+
string? toCmd = null;
146+
while (browserDebugClientConnect.Connected)
147+
{
148+
var res = System.Text.Json.JsonDocument.Parse(await ReceiveMessageLoop(browserDebugClientConnect, token)).RootElement;
149+
var hasTabs = res.TryGetProperty("tabs", out var tabs);
150+
var hasType = res.TryGetProperty("type", out var type);
151+
if (hasType && type.GetString()?.Equals("tabListChanged", StringComparison.Ordinal) == true)
152+
{
153+
await SendMessageToBrowser(toStream, messageListTabs, token);
154+
}
155+
else
156+
{
157+
if (hasTabs)
158+
{
159+
var tabsList = tabs.Deserialize<JsonElement[]>();
160+
if (tabsList == null)
161+
{
162+
continue;
163+
}
164+
foreach (var tab in tabsList)
165+
{
166+
var hasUrl = tab.TryGetProperty("url", out var urlInTab);
167+
var hasActor = tab.TryGetProperty("actor", out var actorInTab);
168+
var hasBrowserId = tab.TryGetProperty("browserId", out var browserIdInTab);
169+
if (string.IsNullOrEmpty(consoleActorId))
170+
{
171+
if (hasUrl && urlInTab.GetString()?.StartsWith("about:debugging#", StringComparison.InvariantCultureIgnoreCase) == true)
172+
{
173+
foundAboutDebugging = true;
174+
175+
toCmd = hasActor ? actorInTab.GetString() : "";
176+
if (tabToRedirect != -1)
177+
{
178+
break;
179+
}
180+
}
181+
if (hasUrl && urlInTab.GetString()?.Equals(targetApplicationUrl, StringComparison.Ordinal) == true)
182+
{
183+
tabToRedirect = hasBrowserId ? browserIdInTab.GetInt32() : -1;
184+
if (foundAboutDebugging)
185+
{
186+
break;
187+
}
188+
}
189+
}
190+
else if (hasUrl && urlInTab.GetString()?.StartsWith("about:devtools", StringComparison.InvariantCultureIgnoreCase) == true)
191+
{
192+
return;
193+
}
194+
}
195+
if (!foundAboutDebugging)
196+
{
197+
context.Response.StatusCode = 404;
198+
await context.Response.WriteAsync("WARNING: Open about:debugging tab before pressing Debugging Hotkey");
199+
return;
200+
}
201+
if (string.IsNullOrEmpty(consoleActorId))
202+
{
203+
await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token);
204+
}
205+
}
206+
}
207+
if (!string.IsNullOrEmpty(consoleActorId))
208+
{
209+
var hasInput = res.TryGetProperty("input", out var input);
210+
if (hasInput && input.GetString()?.StartsWith("AboutDebugging.actions.addNetworkLocation(", StringComparison.InvariantCultureIgnoreCase) == true)
211+
{
212+
await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token);
213+
}
214+
if (hasInput && input.GetString()?.StartsWith("if (AboutDebugging.store.getState()", StringComparison.InvariantCultureIgnoreCase) == true)
215+
{
216+
await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token);
217+
}
218+
}
219+
else
220+
{
221+
var hasTarget = res.TryGetProperty("target", out var target);
222+
JsonElement consoleActor = new();
223+
var hasConsoleActor = hasTarget && target.TryGetProperty("consoleActor", out consoleActor);
224+
var hasActor = res.TryGetProperty("actor", out var actor);
225+
if (hasConsoleActor && !string.IsNullOrEmpty(consoleActor.GetString()))
226+
{
227+
consoleActorId = consoleActor.GetString();
228+
await EvaluateOnBrowser(toStream, consoleActorId, $"AboutDebugging.actions.addNetworkLocation(\"{_debugProxyUrl}\"); AboutDebugging.actions.connectRuntime(\"{_debugProxyUrl}\");", token);
229+
}
230+
else if (hasActor && !string.IsNullOrEmpty(actor.GetString()))
231+
{
232+
dynamic messageWatchTargets = new ExpandoObject();
233+
messageWatchTargets.type = "watchTargets";
234+
messageWatchTargets.targetType = "frame";
235+
messageWatchTargets.to = actor.GetString();
236+
await SendMessageToBrowser(toStream, messageWatchTargets, token);
237+
dynamic messageWatchResources = new ExpandoObject();
238+
messageWatchResources.type = "watchResources";
239+
messageWatchResources.resourceTypes = new string[1] { "console-message" };
240+
messageWatchResources.to = actor.GetString();
241+
await SendMessageToBrowser(toStream, messageWatchResources, token);
242+
}
243+
else if (!string.IsNullOrEmpty(toCmd))
244+
{
245+
dynamic messageGetWatcher = new ExpandoObject();
246+
messageGetWatcher.type = "getWatcher";
247+
messageGetWatcher.isServerTargetSwitchingEnabled = true;
248+
messageGetWatcher.to = toCmd;
249+
await SendMessageToBrowser(toStream, messageGetWatcher, token);
250+
}
251+
}
252+
}
253+
254+
}
255+
return;
256+
}
257+
40258
/// <summary>
41259
/// Display the ui.
42260
/// </summary>

0 commit comments

Comments
 (0)