diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 811e858ca839de..fbd16363dd9ea3 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -115,6 +115,11 @@ -1 + + + + + <_AppBundleDirForRunCommand Condition="'$(WasmAppDir)' != ''">$(WasmAppDir) diff --git a/src/mono/wasm/host/BrowserHost.cs b/src/mono/wasm/host/BrowserHost.cs index bccca9ff4f179a..a592ca7386fff3 100644 --- a/src/mono/wasm/host/BrowserHost.cs +++ b/src/mono/wasm/host/BrowserHost.cs @@ -71,9 +71,13 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke debugging: _args.CommonConfig.Debugging); runArgsJson.Save(Path.Combine(_args.CommonConfig.AppPath, "runArgs.json")); + string[] urls = envVars.TryGetValue("ASPNETCORE_URLS", out string? aspnetUrls) + ? aspnetUrls.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + : new string[] { $"http://127.0.0.1:{_args.CommonConfig.HostProperties.WebServerPort}", "https://127.0.0.1:0" }; + (ServerURLs serverURLs, IWebHost host) = await StartWebServerAsync(_args.CommonConfig.AppPath, _args.ForwardConsoleOutput ?? false, - _args.CommonConfig.HostProperties.WebServerPort, + urls, token); string[] fullUrls = BuildUrls(serverURLs, _args.AppArgs); @@ -84,7 +88,7 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke await host.WaitForShutdownAsync(token); } - private async Task<(ServerURLs, IWebHost)> StartWebServerAsync(string appPath, bool forwardConsole, int port, CancellationToken token) + private async Task<(ServerURLs, IWebHost)> StartWebServerAsync(string appPath, bool forwardConsole, string[] urls, CancellationToken token) { WasmTestMessagesProcessor? logProcessor = null; if (forwardConsole) @@ -100,7 +104,7 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke ContentRootPath: Path.GetFullPath(appPath), WebServerUseCors: true, WebServerUseCrossOriginPolicy: true, - Port: port + Urls: urls ); (ServerURLs serverURLs, IWebHost host) = await WebServer.StartAsync(options, _logger, token); diff --git a/src/mono/wasm/host/WebServer.cs b/src/mono/wasm/host/WebServer.cs index 51bda60716740d..aed1948470334c 100644 --- a/src/mono/wasm/host/WebServer.cs +++ b/src/mono/wasm/host/WebServer.cs @@ -20,7 +20,7 @@ public class WebServer { internal static async Task<(ServerURLs, IWebHost)> StartAsync(WebServerOptions options, ILogger logger, CancellationToken token) { - string[]? urls = new string[] { $"http://127.0.0.1:{options.Port}", "https://127.0.0.1:0" }; + string[] urls = options.Urls; IWebHostBuilder builder = new WebHostBuilder() .UseKestrel() diff --git a/src/mono/wasm/host/WebServerOptions.cs b/src/mono/wasm/host/WebServerOptions.cs index bc8b5f2acee63a..43e05c10b7c82e 100644 --- a/src/mono/wasm/host/WebServerOptions.cs +++ b/src/mono/wasm/host/WebServerOptions.cs @@ -15,6 +15,6 @@ internal sealed record WebServerOptions string? ContentRootPath, bool WebServerUseCors, bool WebServerUseCrossOriginPolicy, - int Port, + string [] Urls, string DefaultFileName = "index.html" ); diff --git a/src/mono/wasm/host/WebServerStartup.cs b/src/mono/wasm/host/WebServerStartup.cs index ae5e906c518a65..e09cdd61963fb2 100644 --- a/src/mono/wasm/host/WebServerStartup.cs +++ b/src/mono/wasm/host/WebServerStartup.cs @@ -1,13 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; using System.Net.WebSockets; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Web; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; +using Microsoft.WebAssembly.Diagnostics; #nullable enable @@ -16,9 +26,32 @@ namespace Microsoft.WebAssembly.AppHost; internal sealed class WebServerStartup { private readonly IWebHostEnvironment _hostingEnvironment; - + private static readonly object LaunchLock = new object(); + private static string LaunchedDebugProxyUrl = ""; public WebServerStartup(IWebHostEnvironment hostingEnvironment) => _hostingEnvironment = hostingEnvironment; + public static int StartDebugProxy(string devToolsHost) + { + //we need to start another process, otherwise it will be running the BrowserDebugProxy in the same process that will be debugged, so pausing in a breakpoint + //on managed code will freeze because it will not be able to continue executing the BrowserDebugProxy to get the locals value + var executablePath = Path.Combine(System.AppContext.BaseDirectory, "BrowserDebugHost.dll"); + var ownerPid = Environment.ProcessId; + var generateRandomPort = new Random().Next(5000, 5300); + var processStartInfo = new ProcessStartInfo + { + FileName = "dotnet" + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""), + Arguments = $"exec \"{executablePath}\" --OwnerPid {ownerPid} --DevToolsUrl {devToolsHost} --DevToolsProxyPort {generateRandomPort}", + UseShellExecute = false, + RedirectStandardOutput = true, + }; + var debugProxyProcess = Process.Start(processStartInfo); + if (debugProxyProcess is null) + { + throw new InvalidOperationException("Unable to start debug proxy process."); + } + return generateRandomPort; + } + public void Configure(IApplicationBuilder app, IOptions optionsContainer) { var provider = new FileExtensionContentTypeProvider(); @@ -73,9 +106,43 @@ public void Configure(IApplicationBuilder app, IOptions option }); } - // app.UseEndpoints(endpoints => - // { - // endpoints.MapFallbackToFile(options.DefaultFileName); - // }); + app.Map("/debug", app => + { + app.Run(async (context) => + { + //debug from VS + var queryParams = HttpUtility.ParseQueryString(context.Request.QueryString.Value!); + var browserParam = queryParams.Get("browser"); + Uri? browserUrl = null; + var devToolsHost = "http://localhost:9222"; + if (browserParam != null) + { + browserUrl = new Uri(browserParam); + devToolsHost = $"http://{browserUrl.Host}:{browserUrl.Port}"; + } + lock (LaunchLock) + { + if (LaunchedDebugProxyUrl == "") + { + LaunchedDebugProxyUrl = $"http://localhost:{StartDebugProxy(devToolsHost)}"; + } + } + var requestPath = context.Request.Path.ToString(); + if (requestPath == string.Empty) + { + requestPath = "/"; + } + context.Response.Redirect($"{LaunchedDebugProxyUrl}{browserUrl!.PathAndQuery}"); + await Task.FromResult(0); + }); + }); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", context => + { + context.Response.Redirect("index.html", permanent: false); + return Task.CompletedTask; + }); + }); } } diff --git a/src/mono/wasm/templates/templates/browser/.template.config/template.json b/src/mono/wasm/templates/templates/browser/.template.config/template.json index d7d22668accdde..b2ae216e23db65 100644 --- a/src/mono/wasm/templates/templates/browser/.template.config/template.json +++ b/src/mono/wasm/templates/templates/browser/.template.config/template.json @@ -16,6 +16,24 @@ "type": "project" }, "symbols": { + "kestrelHttpPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 5000, + "high": 5300 + }, + "replaces": "5000" + }, + "kestrelHttpsPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 7000, + "high": 7300 + }, + "replaces": "5001" + }, "framework": { "type": "parameter", "description": "The target framework for the project.", diff --git a/src/mono/wasm/templates/templates/browser/Properties/launchSettings.json b/src/mono/wasm/templates/templates/browser/Properties/launchSettings.json new file mode 100644 index 00000000000000..0e5b784b708843 --- /dev/null +++ b/src/mono/wasm/templates/templates/browser/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "browser.0": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/debug?browser={browserInspectUri}" + } + } +}