Debug proxy as external tool (#19767)
This commit is contained in:
parent
147c39289a
commit
2e9bb2ff5f
|
|
@ -103,6 +103,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Msal", "Auth
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Authentication.WebAssembly.Msal", "WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj", "{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Authentication.WebAssembly.Msal", "WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj", "{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugProxy", "DebugProxy", "{96DE9B14-D81F-422E-A33A-728BFB9C153A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebAssembly.DebugProxy", "WebAssembly\DebugProxy\src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj", "{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -521,6 +525,18 @@ Global
|
||||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x64.Build.0 = Release|Any CPU
|
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x86.ActiveCfg = Release|Any CPU
|
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x86.Build.0 = Release|Any CPU
|
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -573,6 +589,8 @@ Global
|
||||||
{EAF50654-98ED-44BB-A120-0436EC0CD3E0} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
{EAF50654-98ED-44BB-A120-0436EC0CD3E0} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||||
{E4D756A7-A934-4D7F-BC6E-7B95FE4098AB} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
{E4D756A7-A934-4D7F-BC6E-7B95FE4098AB} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||||
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43} = {E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}
|
{2F105FA7-74DA-4855-9D8E-818DEE1F8D43} = {E4D756A7-A934-4D7F-BC6E-7B95FE4098AB}
|
||||||
|
{96DE9B14-D81F-422E-A33A-728BFB9C153A} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||||
|
{8BB1A8BE-F002-40A2-9B8E-439284B21C1C} = {96DE9B14-D81F-422E-A33A-728BFB9C153A}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
SolutionGuid = {27A36094-AA50-4FFD-ADE6-C055E391F741}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
|
{
|
||||||
|
public class DebugProxyOptions
|
||||||
|
{
|
||||||
|
public string BrowserHost { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<PackageId>Microsoft.AspNetCore.Components.WebAssembly.DebugProxy</PackageId>
|
||||||
|
<IsShippingPackage>true</IsShippingPackage>
|
||||||
|
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||||
|
<Description>Debug proxy for use when building Blazor applications.</Description>
|
||||||
|
<!-- Set this to false because assemblies should not reference this assembly directly, (except for tests, of course). -->
|
||||||
|
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
|
||||||
|
|
||||||
|
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||||
|
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||||
|
<MicrosoftAspNetCoreAppVersion>3.1.0</MicrosoftAspNetCoreAppVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
|
||||||
|
|
||||||
|
<!-- Dependencies of ws-proxy sources -->
|
||||||
|
<Reference Include="Newtonsoft.Json" />
|
||||||
|
<Reference Include="Mono.Cecil" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
static int Main(string[] args)
|
||||||
|
{
|
||||||
|
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
|
||||||
|
{
|
||||||
|
Name = "webassembly-debugproxy"
|
||||||
|
};
|
||||||
|
app.HelpOption("-?|-h|--help");
|
||||||
|
|
||||||
|
var browserHostOption = new CommandOption("-b|--browser-host", CommandOptionType.SingleValue)
|
||||||
|
{
|
||||||
|
Description = "Host on which the browser is listening for debug connections. Example: http://localhost:9300"
|
||||||
|
};
|
||||||
|
|
||||||
|
var ownerPidOption = new CommandOption("-op|--owner-pid", CommandOptionType.SingleValue)
|
||||||
|
{
|
||||||
|
Description = "ID of the owner process. The debug proxy will shut down if this process exits."
|
||||||
|
};
|
||||||
|
|
||||||
|
app.Options.Add(browserHostOption);
|
||||||
|
app.Options.Add(ownerPidOption);
|
||||||
|
|
||||||
|
app.OnExecute(() =>
|
||||||
|
{
|
||||||
|
var host = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||||
|
{
|
||||||
|
config.AddCommandLine(args);
|
||||||
|
})
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
|
||||||
|
// By default we bind to a dyamic port
|
||||||
|
// This can be overridden using an option like "--urls http://localhost:9500"
|
||||||
|
webBuilder.UseUrls("http://127.0.0.1:0");
|
||||||
|
})
|
||||||
|
.ConfigureServices(serviceCollection =>
|
||||||
|
{
|
||||||
|
serviceCollection.AddSingleton(new DebugProxyOptions
|
||||||
|
{
|
||||||
|
BrowserHost = browserHostOption.HasValue()
|
||||||
|
? browserHostOption.Value()
|
||||||
|
: "http://127.0.0.1:9222",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
if (ownerPidOption.HasValue())
|
||||||
|
{
|
||||||
|
var ownerProcess = Process.GetProcessById(int.Parse(ownerPidOption.Value()));
|
||||||
|
ownerProcess.EnableRaisingEvents = true;
|
||||||
|
ownerProcess.Exited += async (sender, eventArgs) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine("Exiting because parent process has exited");
|
||||||
|
await host.StopAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
host.Run();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return app.Execute(args);
|
||||||
|
}
|
||||||
|
catch (CommandParsingException cex)
|
||||||
|
{
|
||||||
|
app.Error.WriteLine(cex.Message);
|
||||||
|
app.ShowHelp();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using WebAssembly.Net.Debugging;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public void Configure(IApplicationBuilder app, DebugProxyOptions debugProxyOptions)
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
app.UseWebSockets();
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
// At the homepage, we check whether we can uniquely identify the target tab
|
||||||
|
// - If yes, we redirect directly to the debug tools, proxying to that tab
|
||||||
|
// - If no, we present a list of available tabs for the user to pick from
|
||||||
|
endpoints.MapGet("/", new TargetPickerUi(debugProxyOptions).Display);
|
||||||
|
|
||||||
|
// At this URL, we wire up the actual WebAssembly proxy
|
||||||
|
endpoints.MapGet("/ws-proxy", async (context) =>
|
||||||
|
{
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loggerFactory = context.RequestServices.GetRequiredService<ILoggerFactory>();
|
||||||
|
var browserUri = new Uri(context.Request.Query["browser"]);
|
||||||
|
var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
await new MonoProxy(loggerFactory).Run(browserUri, ideSocket);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
|
||||||
|
{
|
||||||
|
public class TargetPickerUi
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
IgnoreNullValues = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly DebugProxyOptions _options;
|
||||||
|
|
||||||
|
public TargetPickerUi(DebugProxyOptions options)
|
||||||
|
{
|
||||||
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Display(HttpContext context)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "text/html";
|
||||||
|
|
||||||
|
var request = context.Request;
|
||||||
|
var targetApplicationUrl = request.Query["url"];
|
||||||
|
|
||||||
|
var debuggerTabsListUrl = $"{_options.BrowserHost}/json";
|
||||||
|
IEnumerable<BrowserTab> availableTabs;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
availableTabs = await GetOpenedBrowserTabs();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync($@"
|
||||||
|
<h1>Unable to find debuggable browser tab</h1>
|
||||||
|
<p>
|
||||||
|
Could not get a list of browser tabs from <code>{debuggerTabsListUrl}</code>.
|
||||||
|
Ensure your browser is running with debugging enabled.
|
||||||
|
</p>
|
||||||
|
<h2>Resolution</h2>
|
||||||
|
<p>
|
||||||
|
<h4>If you are using Google Chrome for your development, follow these instructions:</h4>
|
||||||
|
{GetLaunchChromeInstructions(targetApplicationUrl)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<h4>If you are using Microsoft Edge (Chromium) for your development, follow these instructions:</h4>
|
||||||
|
{GetLaunchEdgeInstructions(targetApplicationUrl)}
|
||||||
|
</p>
|
||||||
|
<strong>This should launch a new browser window with debugging enabled..</p>
|
||||||
|
<h2>Underlying exception:</h2>
|
||||||
|
<pre>{ex}</pre>
|
||||||
|
");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchingTabs = string.IsNullOrEmpty(targetApplicationUrl)
|
||||||
|
? availableTabs.ToList()
|
||||||
|
: availableTabs.Where(t => t.Url.Equals(targetApplicationUrl, StringComparison.Ordinal)).ToList();
|
||||||
|
|
||||||
|
if (matchingTabs.Count == 1)
|
||||||
|
{
|
||||||
|
// We know uniquely which tab to debug, so just redirect
|
||||||
|
var devToolsUrlWithProxy = GetDevToolsUrlWithProxy(request, matchingTabs.Single());
|
||||||
|
context.Response.Redirect(devToolsUrlWithProxy);
|
||||||
|
}
|
||||||
|
else if (matchingTabs.Count == 0)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("<h1>No inspectable pages found</h1>");
|
||||||
|
|
||||||
|
var suffix = string.IsNullOrEmpty(targetApplicationUrl)
|
||||||
|
? string.Empty
|
||||||
|
: $" matching the URL {WebUtility.HtmlEncode(targetApplicationUrl)}";
|
||||||
|
await context.Response.WriteAsync($"<p>The list of targets returned by {WebUtility.HtmlEncode(debuggerTabsListUrl)} contains no entries{suffix}.</p>");
|
||||||
|
await context.Response.WriteAsync("<p>Make sure your browser is displaying the target application.</p>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("<h1>Inspectable pages</h1>");
|
||||||
|
await context.Response.WriteAsync(@"
|
||||||
|
<style type='text/css'>
|
||||||
|
body {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
margin: 2rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspectable-page {
|
||||||
|
display: block;
|
||||||
|
background-color: #eee;
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspectable-page:hover {
|
||||||
|
background-color: #fed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspectable-page h3 {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach (var tab in matchingTabs)
|
||||||
|
{
|
||||||
|
var devToolsUrlWithProxy = GetDevToolsUrlWithProxy(request, tab);
|
||||||
|
await context.Response.WriteAsync(
|
||||||
|
$"<a class='inspectable-page' href='{WebUtility.HtmlEncode(devToolsUrlWithProxy)}'>"
|
||||||
|
+ $"<h3>{WebUtility.HtmlEncode(tab.Title)}</h3>{WebUtility.HtmlEncode(tab.Url)}"
|
||||||
|
+ $"</a>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDevToolsUrlWithProxy(HttpRequest request, BrowserTab tabToDebug)
|
||||||
|
{
|
||||||
|
var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl;
|
||||||
|
var proxyEndpoint = GetProxyEndpoint(request, underlyingV8Endpoint);
|
||||||
|
var devToolsUrlAbsolute = new Uri(_options.BrowserHost + tabToDebug.DevtoolsFrontendUrl);
|
||||||
|
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{proxyEndpoint.Scheme}={proxyEndpoint.Authority}{proxyEndpoint.PathAndQuery}";
|
||||||
|
return devToolsUrlWithProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLaunchChromeInstructions(string targetApplicationUrl)
|
||||||
|
{
|
||||||
|
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
|
||||||
|
var debuggerPort = new Uri(_options.BrowserHost).Port;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return $@"<p>Press Win+R and enter the following:</p>
|
||||||
|
<p><strong><code>chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {targetApplicationUrl}</code></strong></p>";
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
return $@"<p>In a terminal window execute the following:</p>
|
||||||
|
<p><strong><code>google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}</code></strong></p>";
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
return $@"<p>Execute the following:</p>
|
||||||
|
<p><strong><code>open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}</code></strong></p>";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unknown OS platform");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLaunchEdgeInstructions(string targetApplicationUrl)
|
||||||
|
{
|
||||||
|
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
|
||||||
|
var debuggerPort = new Uri(_options.BrowserHost).Port;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return $@"<p>Press Win+R and enter the following:</p>
|
||||||
|
<p><strong><code>msedge --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" --no-first-run {targetApplicationUrl}</code></strong></p>";
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
return $@"<p>In a terminal window execute the following:</p>
|
||||||
|
<p><strong><code>open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}</code></strong></p>";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return $@"<p>Edge is not current supported on your platform</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri GetProxyEndpoint(HttpRequest incomingRequest, string browserEndpoint)
|
||||||
|
{
|
||||||
|
var builder = new UriBuilder(
|
||||||
|
schemeName: incomingRequest.IsHttps ? "wss" : "ws",
|
||||||
|
hostName: incomingRequest.Host.Host)
|
||||||
|
{
|
||||||
|
Path = $"{incomingRequest.PathBase}/ws-proxy",
|
||||||
|
Query = $"browser={WebUtility.UrlEncode(browserEndpoint)}"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (incomingRequest.Host.Port.HasValue)
|
||||||
|
{
|
||||||
|
builder.Port = incomingRequest.Host.Port.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
||||||
|
var jsonResponse = await httpClient.GetStringAsync($"{_options.BrowserHost}/json");
|
||||||
|
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BrowserTab
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string DevtoolsFrontendUrl { get; set; }
|
||||||
|
public string WebSocketDebuggerUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
{
|
||||||
|
internal static class DebugProxyLauncher
|
||||||
|
{
|
||||||
|
private static readonly object LaunchLock = new object();
|
||||||
|
private static readonly TimeSpan DebugProxyLaunchTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
private static Task<string> LaunchedDebugProxyUrl;
|
||||||
|
private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$", RegexOptions.None, TimeSpan.FromSeconds(10));
|
||||||
|
private static readonly Regex ApplicationStartedRegex = new Regex(@"^\s*Application started\. Press Ctrl\+C to shut down\.$", RegexOptions.None, TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
public static Task<string> EnsureLaunchedAndGetUrl(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
lock (LaunchLock)
|
||||||
|
{
|
||||||
|
if (LaunchedDebugProxyUrl == null)
|
||||||
|
{
|
||||||
|
LaunchedDebugProxyUrl = LaunchAndGetUrl(serviceProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LaunchedDebugProxyUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> LaunchAndGetUrl(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<string>();
|
||||||
|
|
||||||
|
var environment = serviceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||||
|
var executablePath = LocateDebugProxyExecutable(environment);
|
||||||
|
var muxerPath = DotNetMuxer.MuxerPathOrDefault();
|
||||||
|
var ownerPid = Process.GetCurrentProcess().Id;
|
||||||
|
var processStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = muxerPath,
|
||||||
|
Arguments = $"exec \"{executablePath}\" --owner-pid {ownerPid}",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
};
|
||||||
|
RemoveUnwantedEnvironmentVariables(processStartInfo.Environment);
|
||||||
|
|
||||||
|
var debugProxyProcess = Process.Start(processStartInfo);
|
||||||
|
CompleteTaskWhenServerIsReady(debugProxyProcess, tcs);
|
||||||
|
|
||||||
|
new CancellationTokenSource(DebugProxyLaunchTimeout).Token.Register(() =>
|
||||||
|
{
|
||||||
|
tcs.TrySetException(new TimeoutException($"Failed to start the debug proxy within the timeout period of {DebugProxyLaunchTimeout.TotalSeconds} seconds."));
|
||||||
|
});
|
||||||
|
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveUnwantedEnvironmentVariables(IDictionary<string, string> environment)
|
||||||
|
{
|
||||||
|
// Generally we expect to pass through most environment variables, since dotnet might
|
||||||
|
// need them for arbitrary reasons to function correctly. However, we specifically don't
|
||||||
|
// want to pass through any ASP.NET Core hosting related ones, since the child process
|
||||||
|
// shouldn't be trying to use the same port numbers, etc. In particular we need to break
|
||||||
|
// the association with IISExpress and the MS-ASPNETCORE-TOKEN check.
|
||||||
|
var keysToRemove = environment.Keys.Where(key => key.StartsWith("ASPNETCORE_")).ToList();
|
||||||
|
foreach (var key in keysToRemove)
|
||||||
|
{
|
||||||
|
environment.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string LocateDebugProxyExecutable(IWebHostEnvironment environment)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.Load(environment.ApplicationName);
|
||||||
|
var debugProxyPath = Path.Combine(
|
||||||
|
Path.GetDirectoryName(assembly.Location),
|
||||||
|
"BlazorDebugProxy",
|
||||||
|
"Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.dll");
|
||||||
|
|
||||||
|
if (!File.Exists(debugProxyPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
$"Cannot start debug proxy because it cannot be found at '{debugProxyPath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return debugProxyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CompleteTaskWhenServerIsReady(Process aspNetProcess, TaskCompletionSource<string> taskCompletionSource)
|
||||||
|
{
|
||||||
|
string capturedUrl = null;
|
||||||
|
aspNetProcess.OutputDataReceived += OnOutputDataReceived;
|
||||||
|
aspNetProcess.BeginOutputReadLine();
|
||||||
|
|
||||||
|
void OnOutputDataReceived(object sender, DataReceivedEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (ApplicationStartedRegex.IsMatch(eventArgs.Data))
|
||||||
|
{
|
||||||
|
aspNetProcess.OutputDataReceived -= OnOutputDataReceived;
|
||||||
|
if (!string.IsNullOrEmpty(capturedUrl))
|
||||||
|
{
|
||||||
|
taskCompletionSource.TrySetResult(capturedUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
taskCompletionSource.TrySetException(new InvalidOperationException(
|
||||||
|
"The application started listening without first advertising a URL"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var match = NowListeningRegex.Match(eventArgs.Data);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
capturedUrl = match.Groups["url"].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,17 +7,43 @@
|
||||||
<HasReferenceAssembly>false</HasReferenceAssembly>
|
<HasReferenceAssembly>false</HasReferenceAssembly>
|
||||||
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||||
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||||
|
|
||||||
|
<!-- We're deliberately bundling assemblies as content files. Suppress the warning. -->
|
||||||
|
<NoWarn>$(NoWarn);NU5100</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
<Compile Include="$(ComponentsSharedSourceRoot)\src\CacheHeaderSettings.cs" Link="Shared\CacheHeaderSettings.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)\CommandLineUtils\Utilities\DotNetMuxer.cs" Link="Shared\DotNetMuxer.cs" />
|
||||||
|
|
||||||
|
<!-- Ensure debug proxy is built first, but don't create an actual reference, since we don't want its transitive dependencies. -->
|
||||||
|
<ProjectReference
|
||||||
|
Include="..\..\DebugProxy\src\Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj"
|
||||||
|
ReferenceOutputAssembly="false" />
|
||||||
|
|
||||||
|
<Content Include="build\**" Pack="true" PackagePath="build\%(RecursiveDir)%(FileName)%(Extension)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<!-- Emit debug proxy binaries to output directory. This lets us launch it as a separate process while keeping the build output self-contained. -->
|
||||||
<Reference Include="Newtonsoft.Json" />
|
<Target Name="IncludeDebugProxyBinariesAsContent" BeforeTargets="AssignTargetPaths">
|
||||||
|
<ItemGroup>
|
||||||
|
<DebugProxyBinaries Include="..\..\DebugProxy\src\bin\$(Configuration)\$(DefaultNetCoreTargetFramework)\**" />
|
||||||
|
|
||||||
<!-- Used by ws-proxy sources only. Remove this once we're able to consume ws-proxy as a NuGet package. -->
|
<!--
|
||||||
<Reference Include="Mono.Cecil" />
|
For when we're building a package, we use Pack and PackagePath to bundle the debug proxy binaries into 'tools'.
|
||||||
</ItemGroup>
|
Then we have a custom build target that converts these items into content files in consuming projects. We *don't*
|
||||||
|
use PackageCopyToOutput because that creates entries that show up in Solution Explorer in consuming projects.
|
||||||
|
|
||||||
|
For when we're consuming this from source in this repo, we use Link and CopyToOutputDirectory to produce the
|
||||||
|
same effect without having the custom build target.
|
||||||
|
-->
|
||||||
|
<Content
|
||||||
|
Include="@(DebugProxyBinaries)"
|
||||||
|
Pack="true"
|
||||||
|
PackagePath="tools\BlazorDebugProxy\%(RecursiveDir)%(FileName)%(Extension)"
|
||||||
|
Link="BlazorDebugProxy\%(RecursiveDir)%(FileName)%(Extension)"
|
||||||
|
CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,370 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using WebAssembly.Net.Debugging;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides infrastructure for debugging Blazor WebAssembly applications.
|
|
||||||
/// </summary>
|
|
||||||
public static class WebAssemblyNetDebugProxyAppBuilderExtensions
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
IgnoreNullValues = true
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly string DefaultDebuggerHost = "http://localhost:9222";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds middleware for needed for debugging Blazor WebAssembly applications
|
|
||||||
/// inside Chromium dev tools.
|
|
||||||
/// </summary>
|
|
||||||
public static void UseWebAssemblyDebugging(this IApplicationBuilder app)
|
|
||||||
{
|
|
||||||
app.UseWebSockets();
|
|
||||||
|
|
||||||
app.UseVisualStudioDebuggerConnectionRequestHandlers();
|
|
||||||
|
|
||||||
app.Use((context, next) =>
|
|
||||||
{
|
|
||||||
var requestPath = context.Request.Path;
|
|
||||||
if (!requestPath.StartsWithSegments("/_framework/debug"))
|
|
||||||
{
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestPath.Equals("/_framework/debug/ws-proxy", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var loggerFactory = app.ApplicationServices.GetRequiredService<ILoggerFactory>();
|
|
||||||
return DebugWebSocketProxyRequest(loggerFactory, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestPath.Equals("/_framework/debug", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return DebugHome(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetDebuggerHost()
|
|
||||||
{
|
|
||||||
var envVar = Environment.GetEnvironmentVariable("ASPNETCORE_WEBASSEMBLYDEBUGHOST");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(envVar))
|
|
||||||
{
|
|
||||||
return DefaultDebuggerHost;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return envVar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetDebuggerPort()
|
|
||||||
{
|
|
||||||
var host = GetDebuggerHost();
|
|
||||||
return new Uri(host).Port;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UseVisualStudioDebuggerConnectionRequestHandlers(this IApplicationBuilder app)
|
|
||||||
{
|
|
||||||
// Unfortunately VS doesn't send any deliberately distinguishing information so we know it's
|
|
||||||
// not a regular browser or API client. The closest we can do is look for the *absence* of a
|
|
||||||
// User-Agent header. In the future, we should try to get VS to send a special header to indicate
|
|
||||||
// this is a debugger metadata request.
|
|
||||||
app.Use(async (context, next) =>
|
|
||||||
{
|
|
||||||
var request = context.Request;
|
|
||||||
var requestPath = request.Path;
|
|
||||||
if (requestPath.StartsWithSegments("/json")
|
|
||||||
&& !request.Headers.ContainsKey("User-Agent"))
|
|
||||||
{
|
|
||||||
if (requestPath.Equals("/json", StringComparison.OrdinalIgnoreCase) || requestPath.Equals("/json/list", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var availableTabs = await GetOpenedBrowserTabs();
|
|
||||||
|
|
||||||
// Filter the list to only include tabs displaying the requested app,
|
|
||||||
// but only during the "choose application to debug" phase. We can't apply
|
|
||||||
// the same filter during the "connecting" phase (/json/list), nor do we need to.
|
|
||||||
if (requestPath.Equals("/json"))
|
|
||||||
{
|
|
||||||
availableTabs = availableTabs.Where(tab => tab.Url.StartsWith($"{request.Scheme}://{request.Host}{request.PathBase}/"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxiedTabInfos = availableTabs.Select(tab =>
|
|
||||||
{
|
|
||||||
var underlyingV8Endpoint = tab.WebSocketDebuggerUrl;
|
|
||||||
var proxiedScheme = request.IsHttps ? "wss" : "ws";
|
|
||||||
var proxiedV8Endpoint = $"{proxiedScheme}://{request.Host}{request.PathBase}/_framework/debug/ws-proxy?browser={WebUtility.UrlEncode(underlyingV8Endpoint)}";
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
description = "",
|
|
||||||
devtoolsFrontendUrl = "",
|
|
||||||
id = tab.Id,
|
|
||||||
title = tab.Title,
|
|
||||||
type = tab.Type,
|
|
||||||
url = tab.Url,
|
|
||||||
webSocketDebuggerUrl = proxiedV8Endpoint
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(proxiedTabInfos));
|
|
||||||
}
|
|
||||||
else if (requestPath.Equals("/json/version", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
// VS Code's "js-debug" nightly extension, when configured to use the "pwa-chrome"
|
|
||||||
// debug type, uses the /json/version endpoint to find the websocket endpoint for
|
|
||||||
// debugging the browser that listens on a user-specified port.
|
|
||||||
//
|
|
||||||
// To make this flow work with the Mono debug proxy, we pass the request through
|
|
||||||
// to the underlying browser (to get its actual version info) but then overwrite
|
|
||||||
// the "webSocketDebuggerUrl" with the URL to the proxy.
|
|
||||||
//
|
|
||||||
// This whole connection flow isn't very good because it doesn't have any way
|
|
||||||
// to specify the debug port for the underlying browser. So, we end up assuming
|
|
||||||
// the default port 9222 in all cases. This is good enough for a manual "attach"
|
|
||||||
// but isn't good enough if the IDE is responsible for launching the browser,
|
|
||||||
// as it will be on a random port. So,
|
|
||||||
//
|
|
||||||
// - VS isn't going to use this. Instead it will use a configured "debugEndpoint"
|
|
||||||
// property from which it can construct the proxy URL directly (including adding
|
|
||||||
// a "browser" querystring value to specify the underlying endpoint), bypassing
|
|
||||||
// /json/version altogether
|
|
||||||
// - We will need to update the VS Code debug adapter to make it do the same as VS
|
|
||||||
// if there is a "debugEndpoint" property configured
|
|
||||||
//
|
|
||||||
// Once both VS and VS Code support the "debugEndpoint" flow, we should be able to
|
|
||||||
// remove this /json/version code altogether. We should check that in-browser
|
|
||||||
// debugging still works at that point.
|
|
||||||
|
|
||||||
var browserVersionJsonStream = await GetBrowserVersionInfoAsync();
|
|
||||||
var browserVersion = await JsonSerializer.DeserializeAsync<Dictionary<string, object>>(browserVersionJsonStream);
|
|
||||||
|
|
||||||
if (browserVersion.TryGetValue("webSocketDebuggerUrl", out var browserEndpoint))
|
|
||||||
{
|
|
||||||
var proxyEndpoint = GetProxyEndpoint(request, ((JsonElement)browserEndpoint).GetString());
|
|
||||||
browserVersion["webSocketDebuggerUrl"] = proxyEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
await JsonSerializer.SerializeAsync(context.Response.Body, browserVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task DebugWebSocketProxyRequest(ILoggerFactory loggerFactory, HttpContext context)
|
|
||||||
{
|
|
||||||
if (!context.WebSockets.IsWebSocketRequest)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = 400;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var browserUri = new Uri(context.Request.Query["browser"]);
|
|
||||||
var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
|
|
||||||
await new MonoProxy(loggerFactory).Run(browserUri, ideSocket);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task DebugHome(HttpContext context)
|
|
||||||
{
|
|
||||||
context.Response.ContentType = "text/html";
|
|
||||||
|
|
||||||
var request = context.Request;
|
|
||||||
var appRootUrl = $"{request.Scheme}://{request.Host}{request.PathBase}/";
|
|
||||||
var targetTabUrl = request.Query["url"];
|
|
||||||
if (string.IsNullOrEmpty(targetTabUrl))
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
|
||||||
await context.Response.WriteAsync("No value specified for 'url'");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Allow overriding port (but not hostname, as we're connecting to the
|
|
||||||
// local browser, not to the webserver serving the app)
|
|
||||||
var debuggerHost = GetDebuggerHost();
|
|
||||||
var debuggerTabsListUrl = $"{debuggerHost}/json";
|
|
||||||
IEnumerable<BrowserTab> availableTabs;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
availableTabs = await GetOpenedBrowserTabs();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await context.Response.WriteAsync($@"
|
|
||||||
<h1>Unable to find debuggable browser tab</h1>
|
|
||||||
<p>
|
|
||||||
Could not get a list of browser tabs from <code>{debuggerTabsListUrl}</code>.
|
|
||||||
Ensure your browser is running with debugging enabled.
|
|
||||||
</p>
|
|
||||||
<h2>Resolution</h2>
|
|
||||||
<p>
|
|
||||||
<h4>If you are using Google Chrome for your development, follow these instructions:</h4>
|
|
||||||
{GetLaunchChromeInstructions(appRootUrl)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<h4>If you are using Microsoft Edge (Chromium) for your development, follow these instructions:</h4>
|
|
||||||
{GetLaunchEdgeInstructions(appRootUrl)}
|
|
||||||
</p>
|
|
||||||
<strong>This should launch a new browser window with debugging enabled..</p>
|
|
||||||
<h2>Underlying exception:</h2>
|
|
||||||
<pre>{ex}</pre>
|
|
||||||
");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchingTabs = availableTabs
|
|
||||||
.Where(t => t.Url.Equals(targetTabUrl, StringComparison.Ordinal))
|
|
||||||
.ToList();
|
|
||||||
if (matchingTabs.Count == 0)
|
|
||||||
{
|
|
||||||
await context.Response.WriteAsync($@"
|
|
||||||
<h1>Unable to find debuggable browser tab</h1>
|
|
||||||
<p>
|
|
||||||
The response from <code>{debuggerTabsListUrl}</code> does not include
|
|
||||||
any entry for <code>{targetTabUrl}</code>.
|
|
||||||
</p>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (matchingTabs.Count > 1)
|
|
||||||
{
|
|
||||||
// TODO: Automatically disambiguate by adding a GUID to the page title
|
|
||||||
// when you press the debugger hotkey, include it in the querystring passed
|
|
||||||
// here, then remove it once the debugger connects.
|
|
||||||
await context.Response.WriteAsync($@"
|
|
||||||
<h1>Multiple matching tabs are open</h1>
|
|
||||||
<p>
|
|
||||||
There is more than one browser tab at <code>{targetTabUrl}</code>.
|
|
||||||
Close the ones you do not wish to debug, then refresh this page.
|
|
||||||
</p>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we know uniquely which tab to debug, construct the URL to the debug
|
|
||||||
// page and redirect there
|
|
||||||
var tabToDebug = matchingTabs.Single();
|
|
||||||
var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl;
|
|
||||||
var proxyEndpoint = GetProxyEndpoint(request, underlyingV8Endpoint);
|
|
||||||
var devToolsUrlAbsolute = new Uri(debuggerHost + tabToDebug.DevtoolsFrontendUrl);
|
|
||||||
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{proxyEndpoint.Scheme}={proxyEndpoint.Authority}{proxyEndpoint.PathAndQuery}";
|
|
||||||
context.Response.Redirect(devToolsUrlWithProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Uri GetProxyEndpoint(HttpRequest incomingRequest, string browserEndpoint)
|
|
||||||
{
|
|
||||||
var builder = new UriBuilder(
|
|
||||||
schemeName: incomingRequest.IsHttps ? "wss" : "ws",
|
|
||||||
hostName: incomingRequest.Host.Host)
|
|
||||||
{
|
|
||||||
Path = $"{incomingRequest.PathBase}/_framework/debug/ws-proxy",
|
|
||||||
Query = $"browser={WebUtility.UrlEncode(browserEndpoint)}"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (incomingRequest.Host.Port.HasValue)
|
|
||||||
{
|
|
||||||
builder.Port = incomingRequest.Host.Port.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.Uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLaunchChromeInstructions(string appRootUrl)
|
|
||||||
{
|
|
||||||
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
|
|
||||||
var debuggerPort = GetDebuggerPort();
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return $@"<p>Press Win+R and enter the following:</p>
|
|
||||||
<p><strong><code>chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {appRootUrl}</code></strong></p>";
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
{
|
|
||||||
return $@"<p>In a terminal window execute the following:</p>
|
|
||||||
<p><strong><code>google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
||||||
{
|
|
||||||
return $@"<p>Execute the following:</p>
|
|
||||||
<p><strong><code>open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unknown OS platform");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLaunchEdgeInstructions(string appRootUrl)
|
|
||||||
{
|
|
||||||
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
|
|
||||||
var debugggerPort = GetDebuggerPort();
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return $@"<p>Press Win+R and enter the following:</p>
|
|
||||||
<p><strong><code>msedge --remote-debugging-port={debugggerPort} --user-data-dir=""{profilePath}"" --no-first-run {appRootUrl}</code></strong></p>";
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
||||||
{
|
|
||||||
return $@"<p>In a terminal window execute the following:</p>
|
|
||||||
<p><strong><code>open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debugggerPort} --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return $@"<p>Edge is not current supported on your platform</p>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<Stream> GetBrowserVersionInfoAsync()
|
|
||||||
{
|
|
||||||
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
|
||||||
var debuggerHost = GetDebuggerHost();
|
|
||||||
var response = await httpClient.GetAsync($"{debuggerHost}/json/version");
|
|
||||||
return await response.Content.ReadAsStreamAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
|
|
||||||
{
|
|
||||||
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
|
|
||||||
var debuggerHost = GetDebuggerHost();
|
|
||||||
var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json");
|
|
||||||
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
class BrowserTab
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Type { get; set; }
|
|
||||||
public string Url { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string DevtoolsFrontendUrl { get; set; }
|
|
||||||
public string WebSocketDebuggerUrl { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides infrastructure for debugging Blazor WebAssembly applications.
|
||||||
|
/// </summary>
|
||||||
|
public static class WebAssemblyNetDebugProxyAppBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds middleware for needed for debugging Blazor WebAssembly applications
|
||||||
|
/// inside Chromium dev tools.
|
||||||
|
/// </summary>
|
||||||
|
public static void UseWebAssemblyDebugging(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.Map("/_framework/debug", app =>
|
||||||
|
{
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
var debugProxyBaseUrl = await DebugProxyLauncher.EnsureLaunchedAndGetUrl(context.RequestServices);
|
||||||
|
var requestPath = context.Request.Path.ToString();
|
||||||
|
if (requestPath == string.Empty)
|
||||||
|
{
|
||||||
|
requestPath = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although we could redirect for every URL we see here, we filter the allowed set
|
||||||
|
// to ensure this doesn't get misused as some kind of more general redirector
|
||||||
|
switch (requestPath)
|
||||||
|
{
|
||||||
|
case "/":
|
||||||
|
case "/ws-proxy":
|
||||||
|
context.Response.Redirect($"{debugProxyBaseUrl}{requestPath}{context.Request.QueryString}");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<Project>
|
||||||
|
|
||||||
|
<Target Name="_IncludeDebugProxyBinariesAsContent" BeforeTargets="AssignTargetPaths"
|
||||||
|
Condition="'$(BlazorWebAssemblyOmitDebugProxyOutput)' != 'true'">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<_DebugProxyBinaries Include="$(MSBuildThisFileDirectory)..\tools\BlazorDebugProxy\**" />
|
||||||
|
<Content
|
||||||
|
Include="@(_DebugProxyBinaries)"
|
||||||
|
Link="BlazorDebugProxy\%(RecursiveDir)%(FileName)%(Extension)"
|
||||||
|
CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:56500/",
|
||||||
|
"sslPort": 44347
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HostedInAspNet.Server": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:56502/",
|
||||||
|
"sslPort": 44332
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"StandaloneApp": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
// System.AppContext.GetData is not available in these frameworks
|
||||||
|
#if !NET451 && !NET452 && !NET46 && !NET461
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.CommandLineUtils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utilities for finding the "dotnet.exe" file from the currently running .NET Core application
|
||||||
|
/// </summary>
|
||||||
|
internal static class DotNetMuxer
|
||||||
|
{
|
||||||
|
private const string MuxerName = "dotnet";
|
||||||
|
|
||||||
|
static DotNetMuxer()
|
||||||
|
{
|
||||||
|
MuxerPath = TryFindMuxerPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The full filepath to the .NET Core muxer.
|
||||||
|
/// </summary>
|
||||||
|
public static string MuxerPath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the full filepath to the .NET Core muxer,
|
||||||
|
/// or returns a string containing the default name of the .NET Core muxer ('dotnet').
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The path or a string named 'dotnet'.</returns>
|
||||||
|
public static string MuxerPathOrDefault()
|
||||||
|
=> MuxerPath ?? MuxerName;
|
||||||
|
|
||||||
|
private static string TryFindMuxerPath()
|
||||||
|
{
|
||||||
|
var fileName = MuxerName;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
fileName += ".exe";
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainModule = Process.GetCurrentProcess().MainModule;
|
||||||
|
if (!string.IsNullOrEmpty(mainModule?.FileName)
|
||||||
|
&& Path.GetFileName(mainModule.FileName).Equals(fileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return mainModule.FileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Loading…
Reference in New Issue