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
|
||||
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
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x86.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -573,6 +589,8 @@ Global
|
|||
{EAF50654-98ED-44BB-A120-0436EC0CD3E0} = {CBD2BB24-3EC3-4950-ABE4-8C521D258DCD}
|
||||
{E4D756A7-A934-4D7F-BC6E-7B95FE4098AB} = {B29FB58D-FAE5-405E-9695-BCF93582BE9A}
|
||||
{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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
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.Threading.Tasks;
|
||||
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>
|
||||
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
|
||||
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
|
||||
|
||||
<!-- We're deliberately bundling assemblies as content files. Suppress the warning. -->
|
||||
<NoWarn>$(NoWarn);NU5100</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
<Reference Include="Newtonsoft.Json" />
|
||||
<!-- Emit debug proxy binaries to output directory. This lets us launch it as a separate process while keeping the build output self-contained. -->
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<!--
|
||||
For when we're building a package, we use Pack and PackagePath to bundle the debug proxy binaries into 'tools'.
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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