diff --git a/src/Components/Blazor.sln b/src/Components/Blazor.sln
index f203a56ddfce..e31a67540866 100644
--- a/src/Components/Blazor.sln
+++ b/src/Components/Blazor.sln
@@ -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}
diff --git a/src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs b/src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs
new file mode 100644
index 000000000000..70a76258c6cf
--- /dev/null
+++ b/src/Components/WebAssembly/DebugProxy/src/DebugProxyOptions.cs
@@ -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; }
+ }
+}
diff --git a/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj b/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj
new file mode 100644
index 000000000000..933fcb938685
--- /dev/null
+++ b/src/Components/WebAssembly/DebugProxy/src/Microsoft.AspNetCore.Components.WebAssembly.DebugProxy.csproj
@@ -0,0 +1,26 @@
+
+
+
+ $(DefaultNetCoreTargetFramework)
+ Exe
+ Microsoft.AspNetCore.Components.WebAssembly.DebugProxy
+ true
+ false
+ Debug proxy for use when building Blazor applications.
+
+ false
+
+
+ true
+ 3.1.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs
similarity index 100%
rename from src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs
rename to src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DebugStore.cs
diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs
similarity index 99%
rename from src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs
rename to src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs
index e6aee451de1f..a46b0a312829 100644
--- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs
+++ b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/DevToolsProxy.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs
similarity index 100%
rename from src/Components/WebAssembly/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs
rename to src/Components/WebAssembly/DebugProxy/src/MonoDebugProxy/ws-proxy/MonoProxy.cs
diff --git a/src/Components/WebAssembly/DebugProxy/src/Program.cs b/src/Components/WebAssembly/DebugProxy/src/Program.cs
new file mode 100644
index 000000000000..0507d82130b0
--- /dev/null
+++ b/src/Components/WebAssembly/DebugProxy/src/Program.cs
@@ -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();
+
+ // 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;
+ }
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/DebugProxy/src/Startup.cs b/src/Components/WebAssembly/DebugProxy/src/Startup.cs
new file mode 100644
index 000000000000..9a80b2a2527a
--- /dev/null
+++ b/src/Components/WebAssembly/DebugProxy/src/Startup.cs
@@ -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();
+ var browserUri = new Uri(context.Request.Query["browser"]);
+ var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
+ await new MonoProxy(loggerFactory).Run(browserUri, ideSocket);
+ });
+ });
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/DebugProxy/src/TargetPickerUi.cs b/src/Components/WebAssembly/DebugProxy/src/TargetPickerUi.cs
new file mode 100644
index 000000000000..54d1a47e5532
--- /dev/null
+++ b/src/Components/WebAssembly/DebugProxy/src/TargetPickerUi.cs
@@ -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 availableTabs;
+
+ try
+ {
+ availableTabs = await GetOpenedBrowserTabs();
+ }
+ catch (Exception ex)
+ {
+ await context.Response.WriteAsync($@"
+Unable to find debuggable browser tab
+
+ Could not get a list of browser tabs from {debuggerTabsListUrl}.
+ Ensure your browser is running with debugging enabled.
+
+Resolution
+
+
If you are using Google Chrome for your development, follow these instructions:
+ {GetLaunchChromeInstructions(targetApplicationUrl)}
+
+
+
If you are using Microsoft Edge (Chromium) for your development, follow these instructions:
+ {GetLaunchEdgeInstructions(targetApplicationUrl)}
+
+This should launch a new browser window with debugging enabled..
+Underlying exception:
+{ex}
+ ");
+
+ 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("No inspectable pages found
");
+
+ var suffix = string.IsNullOrEmpty(targetApplicationUrl)
+ ? string.Empty
+ : $" matching the URL {WebUtility.HtmlEncode(targetApplicationUrl)}";
+ await context.Response.WriteAsync($"The list of targets returned by {WebUtility.HtmlEncode(debuggerTabsListUrl)} contains no entries{suffix}.
");
+ await context.Response.WriteAsync("Make sure your browser is displaying the target application.
");
+ }
+ else
+ {
+ await context.Response.WriteAsync("Inspectable pages
");
+ await context.Response.WriteAsync(@"
+
+ ");
+
+ foreach (var tab in matchingTabs)
+ {
+ var devToolsUrlWithProxy = GetDevToolsUrlWithProxy(request, tab);
+ await context.Response.WriteAsync(
+ $""
+ + $"{WebUtility.HtmlEncode(tab.Title)}
{WebUtility.HtmlEncode(tab.Url)}"
+ + $"");
+ }
+ }
+ }
+
+ 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 $@"Press Win+R and enter the following:
+ chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {targetApplicationUrl}
";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return $@"In a terminal window execute the following:
+ google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}
";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return $@"Execute the following:
+ open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}
";
+ }
+ 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 $@"Press Win+R and enter the following:
+ msedge --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" --no-first-run {targetApplicationUrl}
";
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return $@"In a terminal window execute the following:
+ open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {targetApplicationUrl}
";
+ }
+ else
+ {
+ return $@"Edge is not current supported on your platform
";
+ }
+ }
+
+ 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> GetOpenedBrowserTabs()
+ {
+ using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
+ var jsonResponse = await httpClient.GetStringAsync($"{_options.BrowserHost}/json");
+ return JsonSerializer.Deserialize(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; }
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs b/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs
new file mode 100644
index 000000000000..fc55e94aec0c
--- /dev/null
+++ b/src/Components/WebAssembly/Server/src/DebugProxyLauncher.cs
@@ -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 LaunchedDebugProxyUrl;
+ private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?.*)$", 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 EnsureLaunchedAndGetUrl(IServiceProvider serviceProvider)
+ {
+ lock (LaunchLock)
+ {
+ if (LaunchedDebugProxyUrl == null)
+ {
+ LaunchedDebugProxyUrl = LaunchAndGetUrl(serviceProvider);
+ }
+
+ return LaunchedDebugProxyUrl;
+ }
+ }
+
+ private static async Task LaunchAndGetUrl(IServiceProvider serviceProvider)
+ {
+ var tcs = new TaskCompletionSource();
+
+ var environment = serviceProvider.GetRequiredService();
+ 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 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 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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj b/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj
index 0ad19d5362ce..b0a60bc9c7ba 100644
--- a/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj
+++ b/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj
@@ -7,17 +7,43 @@
false
true
+
+
+ $(NoWarn);NU5100
-
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/WebAssembly/Server/src/MonoDebugProxy/WebAssemblyNetDebugProxyAppBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/MonoDebugProxy/WebAssemblyNetDebugProxyAppBuilderExtensions.cs
deleted file mode 100644
index 1f3ec84149fe..000000000000
--- a/src/Components/WebAssembly/Server/src/MonoDebugProxy/WebAssemblyNetDebugProxyAppBuilderExtensions.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Provides infrastructure for debugging Blazor WebAssembly applications.
- ///
- 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";
-
- ///
- /// Adds middleware for needed for debugging Blazor WebAssembly applications
- /// inside Chromium dev tools.
- ///
- 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();
- 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>(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 availableTabs;
-
- try
- {
- availableTabs = await GetOpenedBrowserTabs();
- }
- catch (Exception ex)
- {
- await context.Response.WriteAsync($@"
-Unable to find debuggable browser tab
-
- Could not get a list of browser tabs from {debuggerTabsListUrl}.
- Ensure your browser is running with debugging enabled.
-
-Resolution
-
-
If you are using Google Chrome for your development, follow these instructions:
- {GetLaunchChromeInstructions(appRootUrl)}
-
-
-
If you are using Microsoft Edge (Chromium) for your development, follow these instructions:
- {GetLaunchEdgeInstructions(appRootUrl)}
-
-This should launch a new browser window with debugging enabled..
-Underlying exception:
-{ex}
- ");
-
- return;
- }
-
- var matchingTabs = availableTabs
- .Where(t => t.Url.Equals(targetTabUrl, StringComparison.Ordinal))
- .ToList();
- if (matchingTabs.Count == 0)
- {
- await context.Response.WriteAsync($@"
- Unable to find debuggable browser tab
-
- The response from {debuggerTabsListUrl} does not include
- any entry for {targetTabUrl}.
-
");
- 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($@"
- Multiple matching tabs are open
-
- There is more than one browser tab at {targetTabUrl}.
- Close the ones you do not wish to debug, then refresh this page.
-
");
- 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 $@"Press Win+R and enter the following:
- chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {appRootUrl}
";
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- return $@"In a terminal window execute the following:
- google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}
";
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- return $@"Execute the following:
- open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}
";
- }
- 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 $@"Press Win+R and enter the following:
- msedge --remote-debugging-port={debugggerPort} --user-data-dir=""{profilePath}"" --no-first-run {appRootUrl}
";
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- return $@"In a terminal window execute the following:
- open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debugggerPort} --user-data-dir={profilePath} {appRootUrl}
";
- }
- else
- {
- return $@"Edge is not current supported on your platform
";
- }
- }
-
- private static async Task 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> GetOpenedBrowserTabs()
- {
- using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
- var debuggerHost = GetDebuggerHost();
- var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json");
- return JsonSerializer.Deserialize(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; }
- }
- }
-}
diff --git a/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs
new file mode 100644
index 000000000000..2ad827c336be
--- /dev/null
+++ b/src/Components/WebAssembly/Server/src/WebAssemblyNetDebugProxyAppBuilderExtensions.cs
@@ -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
+{
+ ///
+ /// Provides infrastructure for debugging Blazor WebAssembly applications.
+ ///
+ public static class WebAssemblyNetDebugProxyAppBuilderExtensions
+ {
+ ///
+ /// Adds middleware for needed for debugging Blazor WebAssembly applications
+ /// inside Chromium dev tools.
+ ///
+ 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;
+ }
+ });
+ });
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/Server/src/build/Microsoft.AspNetCore.Components.WebAssembly.Server.targets b/src/Components/WebAssembly/Server/src/build/Microsoft.AspNetCore.Components.WebAssembly.Server.targets
new file mode 100644
index 000000000000..ffa102691e2b
--- /dev/null
+++ b/src/Components/WebAssembly/Server/src/build/Microsoft.AspNetCore.Components.WebAssembly.Server.targets
@@ -0,0 +1,16 @@
+
+
+
+
+
+ <_DebugProxyBinaries Include="$(MSBuildThisFileDirectory)..\tools\BlazorDebugProxy\**" />
+
+
+
+
+
+
diff --git a/src/Components/WebAssembly/testassets/HostedInAspNet.Server/Properties/launchSettings.json b/src/Components/WebAssembly/testassets/HostedInAspNet.Server/Properties/launchSettings.json
new file mode 100644
index 000000000000..959797907279
--- /dev/null
+++ b/src/Components/WebAssembly/testassets/HostedInAspNet.Server/Properties/launchSettings.json
@@ -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"
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/testassets/StandaloneApp/Properties/launchSettings.json b/src/Components/WebAssembly/testassets/StandaloneApp/Properties/launchSettings.json
new file mode 100644
index 000000000000..541c4c44680a
--- /dev/null
+++ b/src/Components/WebAssembly/testassets/StandaloneApp/Properties/launchSettings.json
@@ -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"
+ }
+ }
+}
diff --git a/src/Shared/CommandLineUtils/Utilities/DotNetMuxer.cs b/src/Shared/CommandLineUtils/Utilities/DotNetMuxer.cs
new file mode 100644
index 000000000000..52c98b5eb251
--- /dev/null
+++ b/src/Shared/CommandLineUtils/Utilities/DotNetMuxer.cs
@@ -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
+{
+ ///
+ /// Utilities for finding the "dotnet.exe" file from the currently running .NET Core application
+ ///
+ internal static class DotNetMuxer
+ {
+ private const string MuxerName = "dotnet";
+
+ static DotNetMuxer()
+ {
+ MuxerPath = TryFindMuxerPath();
+ }
+
+ ///
+ /// The full filepath to the .NET Core muxer.
+ ///
+ public static string MuxerPath { get; }
+
+ ///
+ /// Finds the full filepath to the .NET Core muxer,
+ /// or returns a string containing the default name of the .NET Core muxer ('dotnet').
+ ///
+ /// The path or a string named 'dotnet'.
+ 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