未验证 提交 c26475cd 编写于 作者: A Ankit Jain 提交者: GitHub

[wasm] Misc debugger improvements (#68988)

* [wasm] Move the browser provisioning stuff to a new targets file

.. from `DebuggerTestSuite.csproj`. This will allow other projects to
use this too.

* [wasm][debugger] Handle failure to connect to the browser

* [wasm] Improve the browser path lookup

so it can be used outside the debugger tests project too. For example,
with Wasm.Build.Tests+playwright .

* [wasm][debugger] Throw exceptions from tasks correctly

.. using `ExceptionDispatchInfo.Capture` so we get the original stack
trace.

* [wasm][debugger] General improvements in debug proxy

- like logging
- updated API to make it easier to use by other projects, like the
  upcoming wasm-app-host .

* [wasm][debugger] Add support for setting an automatic breakpoint

.. on the first line of the entrypoint method (`Main`). This will be
useful for debugging with the upcoming wasm-app-host, along with the use
of `wait-for-debugger`.

Implemented by @thaystg .

* [wasm][debugger] Add support for wait_for_debugger

If requested, then it will cause invocation of `main` to be delayed till
a debugger is attached.

Implemented by @thaystg

* [wasm] Cleanup in Wasm.Build.Tests

.. in the lead up to wasm-app-host tests.

* [wasm] Update the default paths used to trigger builds on CI

.. to include templates, and provisioning props.

* disable non-wasm builds

* [wasm][debugger] Fix path to artifacts dir

* [wasm] Emit message with bundle path

* [wasm][templates] Make the project use the target dir's name as the

.. project name, instead of always creating `browser.dll`, and
`console.dll`.

* [wasm][debugger] Use a single static instance of HttpClient

.. as recommended by the documentation.

* Revert "disable non-wasm builds"

This reverts commit 7b8b60d58c886e7e66cf2fea910f1feb4d6374f1.

* Address review feedback, and improve the autogenerated bpid
上级 4751154c
......@@ -109,6 +109,7 @@ jobs:
- src/tests/BuildWasmApps/*
- src/mono/wasm/build/*
- src/mono/wasm/runtime/*
- src/mono/wasm/templates/*
- src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/*
- src/mono/nuget/Microsoft.NET.Runtime.WebAssembly.Sdk/*
- src/mono/nuget/Microsoft.NET.Runtime.wasm.Sample.Mono/*
......@@ -125,7 +126,9 @@ jobs:
include:
- src/mono/wasm/debugger/*
- src/mono/wasm/runtime/*
- src/mono/wasm/BrowsersForTesting.props
- src/tests/BuildWasmApps/*
- eng/testing/ProvisioningVersions.props
- eng/testing/scenarios/WasmDebuggerTestsJobsList.txt
- src/mono/mono/*
- subset: allwasm
include:
......
<Project>
<PropertyGroup>
<ChromeDir>$(ArtifactsBinDir)chrome\</ChromeDir>
<BrowserStampDir>$(ArtifactsBinDir)\</BrowserStampDir>
<ChromeStampFile>$(BrowserStampDir).install-chrome-$(ChromiumRevision).stamp</ChromeStampFile>
<FirefoxDir>$(ArtifactsBinDir)firefox\</FirefoxDir>
<FirefoxStampFile>$(BrowserStampDir).install-firefox-$(FirefoxRevision).stamp</FirefoxStampFile>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)ProvisioningVersions.props" />
<Target Name="DownloadAndInstallChrome"
AfterTargets="Build"
Condition="!Exists($(ChromeStampFile)) and '$(InstallChromeForTests)' == 'true'">
<ItemGroup>
<_StampFile Include="$(BrowserStampDir).install-chrome*.stamp" />
</ItemGroup>
<Delete Files="@(_StampFile)" />
<RemoveDir Directories="$(ChromeDir)" />
<DownloadFile SourceUrl="$(ChromiumUrl)" DestinationFolder="$(ChromeDir)" SkipUnchangedFiles="true">
<Output TaskParameter="DownloadedFile" PropertyName="_DownloadedFile" />
</DownloadFile>
<Unzip SourceFiles="$(_DownloadedFile)" DestinationFolder="$(ChromeDir)" />
<PropertyGroup>
<_ChromeBinaryPath>$([MSBuild]::NormalizePath($(ChromeDir), $(ChromiumDirName), $(ChromiumBinaryName)))</_ChromeBinaryPath>
</PropertyGroup>
<Error Text="Cannot find chrome at $(_ChromeBinaryPath) in the downloaded copy"
Condition="!Exists($(_ChromeBinaryPath))" />
<Exec Command="chmod +x $(_ChromeBinaryPath)" Condition="!$([MSBuild]::IsOSPlatform('windows'))" />
<Touch Files="$(ChromeStampFile)" AlwaysCreate="true" />
</Target>
<Target Name="DownloadAndInstallFirefox"
AfterTargets="Build"
Condition="!Exists($(FirefoxStampFile)) and '$(InstallFirefoxForTests)' == 'true' and !$([MSBuild]::IsOSPlatform('windows'))">
<ItemGroup>
<_StampFile Include="$(BrowserStampDir).install-firefox*.stamp" />
</ItemGroup>
<Delete Files="@(_StampFile)" />
<RemoveDir Directories="$(FirefoxDir)" />
<DownloadFile SourceUrl="$(FirefoxUrl)" DestinationFolder="$(FirefoxDir)" SkipUnchangedFiles="true">
<Output TaskParameter="DownloadedFile" PropertyName="_DownloadedFile" />
</DownloadFile>
<Exec Command="tar -xf $(_DownloadedFile) -C $(FirefoxDir)"/>
<Exec Command="rm -rf $(_DownloadedFile)"/>
<PropertyGroup>
<_FirefoxBinaryPath>$([MSBuild]::NormalizePath($(FirefoxDir), $(FirefoxBinaryName)))</_FirefoxBinaryPath>
</PropertyGroup>
<Error Text="Cannot find firefox at $(_FirefoxBinaryPath) in the downloaded copy"
Condition="!Exists($(_FirefoxBinaryPath))" />
<Exec Command="chmod +x $(_FirefoxBinaryPath)"/>
<Touch Files="$(FirefoxStampFile)" AlwaysCreate="true" />
</Target>
</Project>
......@@ -115,7 +115,7 @@
</When>
</Choose>
<Import Project="$(RepoRoot)src\mono\wasm\BrowsersForTesting.props" />
<Import Project="$(RepositoryEngineeringDir)testing\ProvisioningVersions.props" />
<Target Name="PrepareForBuildHelixWorkItems_Wasm">
<PropertyGroup>
......
......@@ -305,6 +305,7 @@
</WasmAppBuilder>
<CallTarget Targets="_GenerateRunV8Script" Condition="'$(WasmGenerateRunV8Script)' == 'true'" />
<Message Text="Generated app bundle at $(WasmAppDir)" Importance="High" />
<WriteLinesToFile File="$(WasmAppDir)\.stamp" Lines="" Overwrite="true" />
</Target>
......
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
#nullable enable
namespace Microsoft.WebAssembly.Diagnostics;
public static class DebugProxyHost
{
public static Task RunDebugProxyAsync(ProxyOptions options, string[] args, ILoggerFactory loggerFactory, CancellationToken token)
{
return Task.WhenAny(
RunFirefoxServerLoopAsync(options, args, loggerFactory, token),
RunDevToolsProxyAsync(options, args, loggerFactory, token)
)
.ContinueWith(t => Console.WriteLine($"Debug proxy server failed with {t.Exception}"),
token,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.Default);
}
public static Task RunFirefoxServerLoopAsync(ProxyOptions options, string[] args, ILoggerFactory loggerFactory, CancellationToken token)
=> FirefoxDebuggerProxy.RunServerLoopAsync(browserPort: options.FirefoxDebugPort,
proxyPort: options.FirefoxProxyPort,
loggerFactory,
loggerFactory.CreateLogger("FirefoxMonoProxy"),
token,
autoSetBreakpointOnEntryPoint: options.AutoSetBreakpointOnEntryPoint);
public static async Task RunDevToolsProxyAsync(ProxyOptions options, string[] args, ILoggerFactory loggerFactory, CancellationToken token)
{
string proxyUrl = $"http://127.0.0.1:{options.DevToolsProxyPort}";
IWebHost host = new WebHostBuilder()
.UseSetting("UseIISIntegration", false.ToString())
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.ConfigureServices(services =>
{
services.AddSingleton(loggerFactory);
services.AddLogging(configure => configure.AddSimpleConsole().AddFilter(null, LogLevel.Information));
services.AddSingleton(Options.Create(options));
services.AddRouting();
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddCommandLine(args);
})
.UseUrls(proxyUrl)
.Build();
token.Register(async () => { Console.WriteLine($"-- token got cancelled, stopping host"); await host.StopAsync(); });
await host.RunAsync(token);
}
}
......@@ -2,37 +2,24 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
#nullable enable
namespace Microsoft.WebAssembly.Diagnostics
{
public class ProxyOptions
{
public Uri DevToolsUrl { get; set; } = new Uri("http://localhost:9222");
public int? OwnerPid { get; set; }
}
public class Program
{
public static void Main(string[] args)
public static async Task Main(string[] args)
{
IConfigurationRoot config = new ConfigurationBuilder().AddCommandLine(args).Build();
int proxyPort = 0;
if (config["proxy-port"] is not null && int.TryParse(config["proxy-port"], out int port))
proxyPort = port;
int firefoxDebugPort = 6000;
if (config["firefox-debug-port"] is not null && int.TryParse(config["firefox-debug-port"], out int ffport))
firefoxDebugPort = ffport;
string? logPath = config["log-path"];
ProxyOptions options = new();
config.Bind(options);
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
......@@ -40,30 +27,28 @@ public static void Main(string[] args)
{
options.TimestampFormat = "[HH:mm:ss] ";
})
.AddFilter(null, LogLevel.Debug);
.AddFilter("DevToolsProxy", LogLevel.Information)
.AddFilter("FirefoxMonoProxy", LogLevel.Information)
.AddFilter(null, LogLevel.Warning);
if (!string.IsNullOrEmpty(logPath))
builder.AddFile(Path.Combine(logPath, "proxy.log"),
if (!string.IsNullOrEmpty(options.LogPath))
builder.AddFile(Path.Combine(options.LogPath, "proxy.log"),
minimumLevel: LogLevel.Trace,
outputTemplate: "{Timestamp:o} [{Level:u3}] {SourceContext}: {Message}{NewLine}{Exception}");
});
ILogger logger = loggerFactory.CreateLogger("FirefoxMonoProxy");
_ = FirefoxDebuggerProxy.Run(browserPort: firefoxDebugPort, proxyPort: proxyPort, loggerFactory, logger);
CancellationTokenSource cts = new();
_ = Task.Run(() => DebugProxyHost.RunDebugProxyAsync(options, args, loggerFactory, cts.Token))
.ConfigureAwait(false);
IWebHost host = new WebHostBuilder()
.UseSetting("UseIISIntegration", false.ToString())
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddCommandLine(args);
})
.UseUrls($"http://127.0.0.1:{proxyPort}")
.Build();
TaskCompletionSource tcs = new();
Console.CancelKeyPress += (_, _) =>
{
tcs.SetResult();
cts.Cancel();
};
host.Run();
await tcs.Task;
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
#nullable enable
namespace Microsoft.WebAssembly.Diagnostics;
public class ProxyOptions
{
public Uri DevToolsUrl { get; set; } = new Uri($"http://localhost:9222");
public int? OwnerPid { get; set; }
public int FirefoxProxyPort { get; set; } = 6300;
public int FirefoxDebugPort { get; set; } = 6000;
public int DevToolsProxyPort { get; set; } = 9300;
public int DevToolsDebugPort
{
get => DevToolsUrl.Port;
set
{
var builder = new UriBuilder(DevToolsUrl)
{
Port = value
};
DevToolsUrl = builder.Uri;
}
}
public string? LogPath { get; set; }
public bool AutoSetBreakpointOnEntryPoint { get; set; }
}
......@@ -10,7 +10,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
......@@ -24,12 +24,6 @@ namespace Microsoft.WebAssembly.Diagnostics
{
internal sealed class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services) =>
services.AddRouting()
.Configure<ProxyOptions>(Configuration);
public Startup(IConfiguration configuration) =>
Configuration = configuration;
......@@ -37,9 +31,9 @@ internal sealed class Startup
#pragma warning disable CA1822
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IOptionsMonitor<ProxyOptions> optionsAccessor, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime)
public void Configure(IApplicationBuilder app, IOptions<ProxyOptions> optionsContainer, ILogger<Startup> logger, IHostApplicationLifetime applicationLifetime)
{
ProxyOptions options = optionsAccessor.CurrentValue;
ProxyOptions options = optionsContainer.Value;
if (options.OwnerPid.HasValue)
{
......@@ -54,15 +48,30 @@ public void Configure(IApplicationBuilder app, IOptionsMonitor<ProxyOptions> opt
}
}
applicationLifetime.ApplicationStarted.Register(() =>
{
string ipAddress = app.ServerFeatures
.Get<IServerAddressesFeature>()?
.Addresses?
.Where(a => a.StartsWith("http:", StringComparison.InvariantCultureIgnoreCase))
.Select(a => new Uri(a))
.Select(uri => uri.ToString())
.FirstOrDefault();
Console.WriteLine($"{Environment.NewLine}Debug proxy for chrome now listening on {ipAddress}. And expecting chrome at {options.DevToolsUrl}");
});
app.UseDeveloperExceptionPage()
.UseWebSockets()
.UseDebugProxy(options);
.UseDebugProxy(logger, options);
}
#pragma warning restore CA1822
}
internal static class DebugExtensions
{
private static readonly HttpClient s_httpClient = new();
public static Dictionary<string, string> MapValues(Dictionary<string, string> response, HttpContext context, Uri debuggerHost)
{
var filtered = new Dictionary<string, string>();
......@@ -88,11 +97,12 @@ internal static class DebugExtensions
return filtered;
}
public static IApplicationBuilder UseDebugProxy(this IApplicationBuilder app, ProxyOptions options) =>
UseDebugProxy(app, options, MapValues);
public static IApplicationBuilder UseDebugProxy(this IApplicationBuilder app, ILogger logger, ProxyOptions options) =>
UseDebugProxy(app, logger, options, MapValues);
public static IApplicationBuilder UseDebugProxy(
this IApplicationBuilder app,
ILogger logger,
ProxyOptions options,
Func<Dictionary<string, string>, HttpContext, Uri, Dictionary<string, string>> mapFunc)
{
......@@ -117,33 +127,54 @@ string GetEndpoint(HttpContext context)
async Task Copy(HttpContext context)
{
using (var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) })
try
{
HttpResponseMessage response = await httpClient.GetAsync(GetEndpoint(context));
HttpResponseMessage response = await s_httpClient.GetAsync(GetEndpoint(context));
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
if ((response.Content.Headers.ContentLength ?? 0) > 0)
context.Response.ContentLength = response.Content.Headers.ContentLength;
byte[] bytes = await response.Content.ReadAsByteArrayAsync();
await context.Response.Body.WriteAsync(bytes);
}
catch (HostConnectionException hce)
{
logger.LogWarning(hce.Message);
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
}
}
async Task RewriteSingle(HttpContext context)
{
Dictionary<string, string> version = await ProxyGetJsonAsync<Dictionary<string, string>>(GetEndpoint(context));
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(mapFunc(version, context, devToolsHost)));
try
{
Dictionary<string, string> version = await ProxyGetJsonAsync<Dictionary<string, string>>(GetEndpoint(context));
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(mapFunc(version, context, devToolsHost)));
}
catch (HostConnectionException hce)
{
logger.LogWarning(hce.Message);
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
}
}
async Task RewriteArray(HttpContext context)
{
Dictionary<string, string>[] tabs = await ProxyGetJsonAsync<Dictionary<string, string>[]>(GetEndpoint(context));
Dictionary<string, string>[] alteredTabs = tabs.Select(t => mapFunc(t, context, devToolsHost)).ToArray();
context.Response.ContentType = "application/json";
string text = JsonSerializer.Serialize(alteredTabs);
context.Response.ContentLength = text.Length;
await context.Response.WriteAsync(text);
try
{
Dictionary<string, string>[] tabs = await ProxyGetJsonAsync<Dictionary<string, string>[]>(GetEndpoint(context));
Dictionary<string, string>[] alteredTabs = tabs.Select(t => mapFunc(t, context, devToolsHost)).ToArray();
context.Response.ContentType = "application/json";
string text = JsonSerializer.Serialize(alteredTabs);
context.Response.ContentLength = text.Length;
await context.Response.WriteAsync(text);
}
catch (HostConnectionException hce)
{
logger.LogWarning(hce.Message);
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
}
}
async Task ConnectProxy(HttpContext context)
......@@ -165,25 +196,19 @@ async Task ConnectProxy(HttpContext context)
CancellationTokenSource cts = new();
try
{
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "[HH:mm:ss] ";
})
.AddFilter(null, LogLevel.Information)
);
var loggerFactory = context.RequestServices.GetService<ILoggerFactory>();
context.Request.Query.TryGetValue("urlSymbolServer", out StringValues urlSymbolServerList);
var proxy = new DebuggerProxy(loggerFactory, urlSymbolServerList.ToList(), runtimeId);
System.Net.WebSockets.WebSocket ideSocket = await context.WebSockets.AcceptWebSocketAsync();
logger.LogInformation("Connection accepted from IDE. Starting debug proxy...");
await proxy.Run(endpoint, ideSocket, cts);
}
catch (Exception e)
{
Console.WriteLine("got exception {0}", e);
logger.LogError($"Failed to start proxy: {e}");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
cts.Cancel();
}
}
......@@ -193,11 +218,22 @@ async Task ConnectProxy(HttpContext context)
private static async Task<T> ProxyGetJsonAsync<T>(string url)
{
using (var httpClient = new HttpClient())
try
{
HttpResponseMessage response = await httpClient.GetAsync(url);
HttpResponseMessage response = await s_httpClient.GetAsync(url);
return await JsonSerializer.DeserializeAsync<T>(await response.Content.ReadAsStreamAsync());
}
catch (HttpRequestException hre)
{
throw new HostConnectionException($"Failed to read from the host at {url}. Make sure the host is running. error: {hre.Message}", hre);
}
}
}
internal sealed class HostConnectionException : Exception
{
public HostConnectionException(string message, Exception innerException) : base(message, innerException)
{
}
}
}
......@@ -17,10 +17,8 @@
using System.Reflection.PortableExecutable;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Microsoft.CodeAnalysis.Debugging;
using System.IO.Compression;
using System.Reflection;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
......@@ -79,11 +77,6 @@ internal sealed class BreakpointRequest
public BreakpointRequest()
{ }
public BreakpointRequest(string id, MethodInfo method)
{
Id = id;
Method = method;
}
public BreakpointRequest(string id, JObject request)
{
......@@ -797,11 +790,13 @@ internal sealed class AssemblyInfo
internal string PdbName { get; }
internal bool PdbInformationAvailable { get; }
public bool TriedToLoadSymbolsOnDemand { get; set; }
public MethodInfo EntryPoint { get; private set; }
public unsafe AssemblyInfo(MonoProxy monoProxy, SessionId sessionId, string url, byte[] assembly, byte[] pdb, CancellationToken token)
public unsafe AssemblyInfo(MonoProxy monoProxy, SessionId sessionId, string url, byte[] assembly, byte[] pdb, ILogger logger, CancellationToken token)
{
debugId = -1;
this.id = Interlocked.Increment(ref next_id);
this.logger = logger;
using var asmStream = new MemoryStream(assembly);
peReader = new PEReader(asmStream);
var entries = peReader.ReadDebugDirectory();
......@@ -867,11 +862,6 @@ public bool EnC(byte[] meta, byte[] pdb)
return true;
}
public AssemblyInfo(ILogger logger)
{
this.logger = logger;
}
private void PopulateEnC(MetadataReader asmMetadataReaderParm, MetadataReader pdbMetadataReaderParm)
{
int i = 1;
......@@ -909,6 +899,11 @@ SourceFile FindSource(DocumentHandle doc, int rowid, string documentName)
if (pdbMetadataReader != null)
ProcessSourceLink();
MethodDefinitionHandle entryPointHandle = default;
if (asmMetadataReader.DebugMetadataHeader is not null)
entryPointHandle = asmMetadataReader.DebugMetadataHeader.EntryPoint;
if (pdbMetadataReader is not null && pdbMetadataReader.DebugMetadataHeader is not null)
entryPointHandle = pdbMetadataReader.DebugMetadataHeader.EntryPoint;
foreach (TypeDefinitionHandle type in asmMetadataReader.TypeDefinitions)
{
var typeDefinition = asmMetadataReader.GetTypeDefinition(type);
......@@ -936,6 +931,11 @@ SourceFile FindSource(DocumentHandle doc, int rowid, string documentName)
source.AddMethod(methodInfo);
typeInfo.Methods.Add(methodInfo);
if (entryPointHandle.IsNil || EntryPoint is not null)
continue;
if (method.Equals(entryPointHandle))
EntryPoint = methodInfo;
}
}
}
......@@ -1129,8 +1129,7 @@ private async Task<MemoryStream> GetDataAsync(Uri uri, CancellationToken token)
}
else if (uri.Scheme == "http" || uri.Scheme == "https")
{
using (var client = new HttpClient())
using (Stream stream = await client.GetStreamAsync(uri, token))
using (Stream stream = await MonoProxy.HttpClient.GetStreamAsync(uri, token))
{
await stream.CopyToAsync(mem, token).ConfigureAwait(false);
mem.Position = 0;
......@@ -1226,20 +1225,16 @@ public object ToScriptSource(int executionContextId, object executionContextAuxD
internal sealed class DebugStore
{
internal List<AssemblyInfo> assemblies = new List<AssemblyInfo>();
private readonly HttpClient client;
private readonly ILogger logger;
private readonly MonoProxy monoProxy;
private MethodInfo _entryPoint;
public DebugStore(MonoProxy monoProxy, ILogger logger, HttpClient client)
public DebugStore(MonoProxy monoProxy, ILogger logger)
{
this.client = client;
this.logger = logger;
this.monoProxy = monoProxy;
}
public DebugStore(MonoProxy monoProxy, ILogger logger) : this(monoProxy, logger, new HttpClient())
{ }
private sealed class DebugItem
{
public string Url { get; set; }
......@@ -1261,7 +1256,7 @@ public IEnumerable<SourceFile> Add(SessionId id, string name, byte[] assembly_da
AssemblyInfo assembly;
try
{
assembly = new AssemblyInfo(monoProxy, id, name, assembly_data, pdb_data, token);
assembly = new AssemblyInfo(monoProxy, id, name, assembly_data, pdb_data, logger, token);
}
catch (Exception e)
{
......@@ -1309,7 +1304,7 @@ public async IAsyncEnumerable<SourceFile> Load(SessionId id, string[] loaded_fil
new DebugItem
{
Url = url,
Data = Task.WhenAll(client.GetByteArrayAsync(url, token), pdb != null ? client.GetByteArrayAsync(pdb, token) : Task.FromResult<byte[]>(null))
Data = Task.WhenAll(MonoProxy.HttpClient.GetByteArrayAsync(url, token), pdb != null ? MonoProxy.HttpClient.GetByteArrayAsync(pdb, token) : Task.FromResult<byte[]>(null))
});
}
catch (Exception e)
......@@ -1324,7 +1319,7 @@ public async IAsyncEnumerable<SourceFile> Load(SessionId id, string[] loaded_fil
try
{
byte[][] bytes = await step.Data.ConfigureAwait(false);
assembly = new AssemblyInfo(monoProxy, id, step.Url, bytes[0], bytes[1], token);
assembly = new AssemblyInfo(monoProxy, id, step.Url, bytes[0], bytes[1], logger, token);
}
catch (Exception e)
{
......@@ -1350,7 +1345,13 @@ public async IAsyncEnumerable<SourceFile> Load(SessionId id, string[] loaded_fil
public SourceFile GetFileById(SourceId id) => AllSources().SingleOrDefault(f => f.SourceId.Equals(id));
public AssemblyInfo GetAssemblyByName(string name) => assemblies.FirstOrDefault(a => a.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
public MethodInfo FindEntryPoint()
{
if (_entryPoint is null)
_entryPoint = assemblies.Where(asm => asm.EntryPoint is not null).Select(asm => asm.EntryPoint).FirstOrDefault();
return _entryPoint;
}
/*
V8 uses zero based indexing for both line and column.
......
......@@ -18,9 +18,13 @@ public class DebuggerProxy : DebuggerProxyBase
{
internal MonoProxy MonoProxy { get; }
public DebuggerProxy(ILoggerFactory loggerFactory, IList<string> urlSymbolServerList, int runtimeId = 0, string loggerId = "")
public DebuggerProxy(ILoggerFactory loggerFactory, IList<string> urlSymbolServerList, int runtimeId = 0, string loggerId = "", bool autoSetBreakpointOnEntryPoint = false)
{
MonoProxy = new MonoProxy(loggerFactory, urlSymbolServerList, runtimeId, loggerId);
string suffix = loggerId.Length > 0 ? $"-{loggerId}" : string.Empty;
MonoProxy = new MonoProxy(loggerFactory.CreateLogger($"DevToolsProxy{suffix}"), urlSymbolServerList, runtimeId, loggerId)
{
AutoSetBreakpointOnEntryPoint = autoSetBreakpointOnEntryPoint
};
}
public Task Run(Uri browserUri, WebSocket ideSocket, CancellationTokenSource cts)
......
......@@ -301,6 +301,8 @@ internal sealed class MonoCommands
public static MonoCommands GetLoadedFiles(int runtimeId) => new MonoCommands($"getDotnetRuntime({runtimeId}).INTERNAL.mono_wasm_get_loaded_files()");
public static MonoCommands SetDebuggerAttached(int runtimeId) => new MonoCommands($"getDotnetRuntime({runtimeId}).INTERNAL.mono_wasm_debugger_attached()");
public static MonoCommands SendDebuggerAgentCommand(int runtimeId, int id, int command_set, int command, string command_parameters)
{
return new MonoCommands($"getDotnetRuntime({runtimeId}).INTERNAL.mono_wasm_send_dbg_command ({id}, {command_set}, {command},'{command_parameters}')");
......@@ -435,6 +437,7 @@ public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData)
private Dictionary<int, PerScopeCache> perScopeCaches { get; } = new Dictionary<int, PerScopeCache>();
internal int TempBreakpointForSetNextIP { get; set; }
internal bool FirstBreakpoint { get; set; }
public DebugStore Store
{
......
......@@ -34,11 +34,10 @@ internal class DevToolsProxy
public bool IsRunning => Stopped is null;
public RunLoopExitState Stopped { get; private set; }
public DevToolsProxy(ILoggerFactory loggerFactory, string loggerId)
public DevToolsProxy(ILogger logger, string loggerId)
{
_loggerId = loggerId;
string loggerSuffix = string.IsNullOrEmpty(loggerId) ? string.Empty : $"-{loggerId}";
logger = loggerFactory.CreateLogger($"DevToolsProxy{loggerSuffix}");
this.logger = logger;
var channel = Channel.CreateUnbounded<Task>(new UnboundedChannelOptions { SingleReader = true });
_channelWriter = channel.Writer;
......
......@@ -22,23 +22,23 @@ public class FirefoxDebuggerProxy : DebuggerProxyBase
internal FirefoxMonoProxy? FirefoxMonoProxy { get; private set; }
[MemberNotNull(nameof(s_tcpListener))]
public static void StartListener(int proxyPort, ILogger logger)
public static void StartListener(int proxyPort, ILogger logger, int browserPort = -1)
{
if (s_tcpListener is null)
{
s_tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), proxyPort);
s_tcpListener.Start();
logger.LogInformation($"Now listening for Firefox on: {s_tcpListener.LocalEndpoint}");
Console.WriteLine($"{Environment.NewLine}Debug proxy for firefox now listening on tcp://{s_tcpListener.LocalEndpoint}." +
(browserPort >= 0 ? $"And expecting firefox at port {browserPort}" : string.Empty));
}
}
public static async Task Run(int browserPort, int proxyPort, ILoggerFactory loggerFactory, ILogger logger)
public static async Task RunServerLoopAsync(int browserPort, int proxyPort, ILoggerFactory loggerFactory, ILogger logger, CancellationToken token, bool autoSetBreakpointOnEntryPoint = false)
{
StartListener(proxyPort, logger);
logger.LogInformation($"Expecting firefox to be listening on {browserPort}");
while (true)
StartListener(proxyPort, logger, browserPort);
while (!token.IsCancellationRequested)
{
TcpClient ideClient = await s_tcpListener.AcceptTcpClientAsync();
TcpClient ideClient = await s_tcpListener.AcceptTcpClientAsync(token);
_ = Task.Run(async () =>
{
CancellationTokenSource cts = new();
......@@ -46,29 +46,31 @@ public static async Task Run(int browserPort, int proxyPort, ILoggerFactory logg
{
int id = Interlocked.Increment(ref s_nextId);
logger.LogInformation($"IDE connected to the proxy, id: {id}");
var monoProxy = new FirefoxMonoProxy(loggerFactory, id.ToString());
var monoProxy = new FirefoxMonoProxy(loggerFactory.CreateLogger($"{nameof(FirefoxMonoProxy)}-{id}"), id.ToString())
{
AutoSetBreakpointOnEntryPoint = autoSetBreakpointOnEntryPoint
};
await monoProxy.RunForFirefox(ideClient: ideClient, browserPort, cts);
}
catch (Exception ex)
{
logger.LogError($"{nameof(FirefoxMonoProxy)} crashed with {ex}");
throw;
}
finally
{
cts.Cancel();
}
}, CancellationToken.None)
}, token)
.ConfigureAwait(false);
}
}
public async Task RunForTests(int browserPort, int proxyPort, string testId, ILoggerFactory loggerFactory, ILogger logger, CancellationTokenSource cts)
{
StartListener(proxyPort, logger);
StartListener(proxyPort, logger, browserPort);
TcpClient ideClient = await s_tcpListener.AcceptTcpClientAsync(cts.Token);
FirefoxMonoProxy = new FirefoxMonoProxy(loggerFactory, testId);
FirefoxMonoProxy = new FirefoxMonoProxy(loggerFactory.CreateLogger($"FirefoxMonoProxy-{testId}"), testId);
FirefoxMonoProxy.RunLoopStopped += (_, args) => ExitState = args;
await FirefoxMonoProxy.RunForFirefox(ideClient: ideClient, browserPort, cts);
}
......
......@@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using BrowserDebugProxy;
......@@ -16,7 +17,7 @@ namespace Microsoft.WebAssembly.Diagnostics;
internal sealed class FirefoxMonoProxy : MonoProxy
{
public FirefoxMonoProxy(ILoggerFactory loggerFactory, string loggerId = null) : base(loggerFactory, null, loggerId: loggerId)
public FirefoxMonoProxy(ILogger logger, string loggerId = null) : base(logger, null, loggerId: loggerId)
{
}
......@@ -42,7 +43,7 @@ public async Task RunForFirefox(TcpClient ideClient, int portBrowser, Cancellati
await StartRunLoop(ideConn, browserConn, cts);
if (Stopped?.reason == RunLoopStopReason.Exception)
throw Stopped.exception;
ExceptionDispatchInfo.Capture(Stopped.exception).Throw();
}
finally
{
......
......@@ -19,16 +19,19 @@ namespace Microsoft.WebAssembly.Diagnostics
internal class MonoProxy : DevToolsProxy
{
private IList<string> urlSymbolServerList;
private static HttpClient client = new HttpClient();
private HashSet<SessionId> sessions = new HashSet<SessionId>();
protected Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext>();
private const string sPauseOnUncaught = "pause_on_uncaught";
private const string sPauseOnCaught = "pause_on_caught";
public static HttpClient HttpClient => new HttpClient();
// index of the runtime in a same JS page/process
public int RuntimeId { get; private init; }
public bool JustMyCode { get; private set; }
public bool AutoSetBreakpointOnEntryPoint { get; set; }
public MonoProxy(ILoggerFactory loggerFactory, IList<string> urlSymbolServerList, int runtimeId = 0, string loggerId = "") : base(loggerFactory, loggerId)
public MonoProxy(ILogger logger, IList<string> urlSymbolServerList, int runtimeId = 0, string loggerId = "") : base(logger, loggerId)
{
this.urlSymbolServerList = urlSymbolServerList ?? new List<string>();
RuntimeId = runtimeId;
......@@ -1153,7 +1156,7 @@ internal async Task<MethodInfo> LoadSymbolsOnDemand(AssemblyInfo asm, int method
try
{
using HttpResponseMessage response = await client.GetAsync(downloadURL, token);
using HttpResponseMessage response = await HttpClient.GetAsync(downloadURL, token);
if (!response.IsSuccessStatusCode)
{
Log("info", $"Unable to download symbols on demand url:{downloadURL} assembly: {asm.Name}");
......@@ -1436,10 +1439,36 @@ await foreach (SourceFile source in context.store.Load(sessionId, loaded_files,
{
await OnSourceFileAdded(sessionId, source, context, token);
}
if (AutoSetBreakpointOnEntryPoint)
{
var entryPoint = context.store.FindEntryPoint();
if (entryPoint is not null)
{
var sourceFile = entryPoint.Assembly.Sources.Single(sf => sf.SourceId == entryPoint.SourceId);
string bpId = $"auto:{entryPoint.StartLocation.Line}:{entryPoint.StartLocation.Column}:{sourceFile.DotNetUrl}";
BreakpointRequest request = new(bpId, JObject.FromObject(new
{
lineNumber = entryPoint.StartLocation.Line,
columnNumber = entryPoint.StartLocation.Column,
url = sourceFile.Url
}));
logger.LogDebug($"Adding bp req {request}");
context.BreakpointRequests[bpId] = request;
request.TryResolve(sourceFile);
if (request.TryResolve(sourceFile))
await SetBreakpoint(sessionId, context.store, request, true, true, token);
}
else
{
logger.LogDebug($"No entrypoint found, for setting automatic breakpoint");
}
}
}
}
catch (Exception e)
{
logger.LogError($"failed: {e}");
context.Source.SetException(e);
}
......@@ -1485,6 +1514,7 @@ protected async Task<DebugStore> RuntimeReady(SessionId sessionId, CancellationT
DebugStore store = await LoadStore(sessionId, token);
context.ready.SetResult(store);
await SendEvent(sessionId, "Mono.runtimeReady", new JObject(), token);
await SendMonoCommand(sessionId, MonoCommands.SetDebuggerAttached(RuntimeId), token);
context.SdbAgent.ResetStore(store);
return store;
}
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
namespace Wasm.Tests.Internal;
internal static class BrowserLocator
{
public static string FindChrome(string artifactsBinDir, string envVarName)
=> GetBrowserPath(GetChromePathsToProbe(artifactsBinDir), envVarName);
public static string FindFirefox(string artifactsBinDir, string envVarName)
=> GetBrowserPath(GetFirefoxPathsToProbe(artifactsBinDir), envVarName);
private static string GetBrowserPath(IEnumerable<string> pathsToProbe, string envVarName)
{
string? browserPath = FindBrowserPath();
if (!string.IsNullOrEmpty(browserPath))
return Path.GetFullPath(browserPath);
throw new Exception($"Could not find an installed browser to use. Tried paths: {string.Join(", ", pathsToProbe)}");
string? FindBrowserPath()
{
if (!string.IsNullOrEmpty(envVarName) &&
Environment.GetEnvironmentVariable(envVarName) is string _browserPath_env_var &&
!string.IsNullOrEmpty(_browserPath_env_var))
{
if (File.Exists(_browserPath_env_var))
return _browserPath_env_var;
Console.WriteLine ($"warning: Could not find {envVarName}={_browserPath_env_var}");
}
return pathsToProbe.FirstOrDefault(p => File.Exists(p));
}
}
private static IEnumerable<string> GetChromePathsToProbe(string artifactsBinDir)
{
List<string> paths = new();
if (!string.IsNullOrEmpty(artifactsBinDir))
{
// Look for a browser installed in artifacts, for local runs
paths.Add(Path.Combine(artifactsBinDir, "chrome", "chrome-linux", "chrome"));
paths.Add(Path.Combine(artifactsBinDir, "chrome", "chrome-win", "chrome.exe"));
}
paths.AddRange(new[]
{
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
"/usr/bin/chromium",
"C:/Program Files/Google/Chrome/Application/chrome.exe",
"/usr/bin/chromium-browser"
});
return paths;
}
private static IEnumerable<string> GetFirefoxPathsToProbe(string artifactsBinDir)
{
List<string> paths = new();
if (!string.IsNullOrEmpty(artifactsBinDir))
{
// Look for a browser installed in artifacts, for local runs
paths.Add(Path.Combine(artifactsBinDir, "firefox", "firefox", "firefox"));
paths.Add(Path.Combine(artifactsBinDir, "firefox", "firefox", "firefox.exe"));
}
paths.AddRange(new[]
{
"C:/Program Files/Mozilla Firefox/firefox.exe",
"/Applications/Firefox.app/Contents/MacOS/firefox",
});
return paths;
}
}
......@@ -14,6 +14,7 @@
using Microsoft.WebAssembly.Diagnostics;
using System.Threading;
using System.Collections.Generic;
using Wasm.Tests.Internal;
#nullable enable
......@@ -24,7 +25,11 @@ internal class ChromeProvider : WasmHostProvider
static readonly Regex s_parseConnection = new (@"listening on (ws?s://[^\s]*)");
private WebSocket? _ideWebSocket;
private DebuggerProxy? _debuggerProxy;
private static readonly Lazy<string> s_browserPath = new(() => GetBrowserPath(GetPathsToProbe()));
private static readonly Lazy<string> s_browserPath = new(() =>
{
string artifactsBinDir = Path.Combine(Path.GetDirectoryName(typeof(ChromeProvider).Assembly.Location)!, "..", "..", "..");
return BrowserLocator.FindChrome(artifactsBinDir, "BROWSER_PATH_FOR_TESTS");
});
public ChromeProvider(string id, ILogger logger) : base(id, logger)
{
......@@ -153,28 +158,5 @@ private static string GetInitParms(int port)
return str;
}
private static IEnumerable<string> GetPathsToProbe()
{
List<string> paths = new();
string? asmLocation = Path.GetDirectoryName(typeof(ChromeProvider).Assembly.Location);
if (asmLocation is not null)
{
string baseDir = Path.Combine(asmLocation, "..", "..");
paths.Add(Path.Combine(baseDir, "chrome", "chrome-linux", "chrome"));
paths.Add(Path.Combine(baseDir, "chrome", "chrome-win", "chrome.exe"));
}
paths.AddRange(new[]
{
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
"/usr/bin/chromium",
"C:/Program Files/Google/Chrome/Application/chrome.exe",
"/usr/bin/chromium-browser"
});
return paths;
}
}
......@@ -7,21 +7,12 @@
<IsTestProject>true</IsTestProject>
<DebuggerHost Condition="'$(DebuggerHost)' == '' or ('$(DebuggerHost)' != 'chrome' and '$(DebuggerHost)' != 'firefox')">chrome</DebuggerHost>
<DefineConstants Condition="'$(DebuggerHost)' == 'chrome'">$(DefineConstants);RUN_IN_CHROME</DefineConstants>
<VersionsPropsFile>$(MSBuildThisFileDirectory)..\..\BrowsersForTesting.props</VersionsPropsFile>
<BrowserHost Condition="$([MSBuild]::IsOSPlatform('windows'))">windows</BrowserHost>
<InstallChromeForDebuggerTests Condition="'$(InstallChromeForDebuggerTests)' == '' and '$(DebuggerHost)' == 'chrome' and '$(ContinuousIntegrationBuild)' != 'true' and Exists('/.dockerenv')">true</InstallChromeForDebuggerTests>
<InstallFirefoxForDebuggerTests Condition="'$(DebuggerHost)' == 'firefox' and '$(InstallFirefoxForDebuggerTests)' == '' and '$(ContinuousIntegrationBuild)' != 'true' and Exists('/.dockerenv')">true</InstallFirefoxForDebuggerTests>
<InstallChromeForTests Condition="'$(InstallChromeForTests)' == '' and '$(DebuggerHost)' == 'chrome' and '$(ContinuousIntegrationBuild)' != 'true' and Exists('/.dockerenv')">true</InstallChromeForTests>
<InstallFirefoxForTests Condition="'$(DebuggerHost)' == 'firefox' and '$(InstallFirefoxForTests)' == '' and '$(ContinuousIntegrationBuild)' != 'true' and Exists('/.dockerenv')">true</InstallFirefoxForTests>
</PropertyGroup>
<Import Project="$(VersionsPropsFile)" />
<PropertyGroup>
<ChromeDir>$(ArtifactsBinDir)DebuggerTestSuite\chrome\</ChromeDir>
<BrowserStampDir>$(ArtifactsBinDir)DebuggerTestSuite\</BrowserStampDir>
<ChromeStampFile>$(BrowserStampDir).install-chrome-$(ChromiumRevision).stamp</ChromeStampFile>
<FirefoxDir>$(ArtifactsBinDir)DebuggerTestSuite\firefox\</FirefoxDir>
<FirefoxStampFile>$(BrowserStampDir).install-firefox-$(FirefoxRevision).stamp</FirefoxStampFile>
</PropertyGroup>
<Import Project="$(RepositoryEngineeringDir)testing\provisioning.targets" />
<ItemGroup>
<Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
......@@ -51,59 +42,4 @@
<Copy SourceFiles="@(_FilesToCopy)" DestinationFiles="$(ArchiveDirForHelix)\%(TargetPath)\%(RecursiveDir)%(FileName)%(Extension)" />
</Target>
<Target Name="DownloadAndInstallChrome"
AfterTargets="Build"
Condition="!Exists($(ChromeStampFile)) and '$(InstallChromeForDebuggerTests)' == 'true'">
<ItemGroup>
<_StampFile Include="$(BrowserStampDir).install-chrome*.stamp" />
</ItemGroup>
<Delete Files="@(_StampFile)" />
<RemoveDir Directories="$(ChromeDir)" />
<DownloadFile SourceUrl="$(ChromiumUrl)" DestinationFolder="$(ChromeDir)" SkipUnchangedFiles="true">
<Output TaskParameter="DownloadedFile" PropertyName="_DownloadedFile" />
</DownloadFile>
<Unzip SourceFiles="$(_DownloadedFile)" DestinationFolder="$(ChromeDir)" />
<PropertyGroup>
<_ChromeBinaryPath>$([MSBuild]::NormalizePath($(ChromeDir), $(ChromiumDirName), $(ChromiumBinaryName)))</_ChromeBinaryPath>
</PropertyGroup>
<Error Text="Cannot find chrome at $(_ChromeBinaryPath) in the downloaded copy"
Condition="!Exists($(_ChromeBinaryPath))" />
<Exec Command="chmod +x $(_ChromeBinaryPath)" Condition="!$([MSBuild]::IsOSPlatform('windows'))" />
<Touch Files="$(ChromeStampFile)" AlwaysCreate="true" />
</Target>
<Target Name="DownloadAndInstallFirefox"
AfterTargets="Build"
Condition="!Exists($(FirefoxStampFile)) and '$(InstallFirefoxForDebuggerTests)' == 'true' and !$([MSBuild]::IsOSPlatform('windows'))">
<ItemGroup>
<_StampFile Include="$(BrowserStampDir).install-firefox*.stamp" />
</ItemGroup>
<Delete Files="@(_StampFile)" />
<RemoveDir Directories="$(FirefoxDir)" />
<DownloadFile SourceUrl="$(FirefoxUrl)" DestinationFolder="$(FirefoxDir)" SkipUnchangedFiles="true">
<Output TaskParameter="DownloadedFile" PropertyName="_DownloadedFile" />
</DownloadFile>
<Exec Command="tar -xf $(_DownloadedFile) -C $(FirefoxDir)"/>
<Exec Command="rm -rf $(_DownloadedFile)"/>
<PropertyGroup>
<_FirefoxBinaryPath>$([MSBuild]::NormalizePath($(FirefoxDir), $(FirefoxBinaryName)))</_FirefoxBinaryPath>
</PropertyGroup>
<Error Text="Cannot find firefox at $(_FirefoxBinaryPath) in the downloaded copy"
Condition="!Exists($(_FirefoxBinaryPath))" />
<Exec Command="chmod +x $(_FirefoxBinaryPath)"/>
<Touch Files="$(FirefoxStampFile)" AlwaysCreate="true" />
</Target>
</Project>
......@@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.WebAssembly.Diagnostics;
using Wasm.Tests.Internal;
#nullable enable
......@@ -20,7 +21,11 @@ internal class FirefoxProvider : WasmHostProvider
{
private WebSocket? _ideWebSocket;
private FirefoxDebuggerProxy? _firefoxDebuggerProxy;
private static readonly Lazy<string> s_browserPath = new(() => GetBrowserPath(GetPathsToProbe()));
private static readonly Lazy<string> s_browserPath = new(() =>
{
string artifactsBinDir = Path.Combine(Path.GetDirectoryName(typeof(ChromeProvider).Assembly.Location)!, "..", "..");
return BrowserLocator.FindFirefox(artifactsBinDir, "BROWSER_PATH_FOR_TESTS");
});
public FirefoxProvider(string id, ILogger logger) : base(id, logger)
{
......@@ -142,24 +147,4 @@ private static string GetProfilePath(string Id)
return profilePath;
}
private static IEnumerable<string> GetPathsToProbe()
{
List<string> paths = new();
string? asmLocation = Path.GetDirectoryName(typeof(ChromeProvider).Assembly.Location);
if (asmLocation is not null)
{
string baseDir = Path.Combine(asmLocation, "..", "..");
paths.Add(Path.Combine(baseDir, "firefox", "firefox", "firefox"));
paths.Add(Path.Combine(baseDir, "firefox", "firefox", "firefox.exe"));
}
paths.AddRange(new[]
{
"C:/Program Files/Mozilla Firefox/firefox.exe",
"/Applications/Firefox.app/Contents/MacOS/firefox",
});
return paths;
}
}
......@@ -12,6 +12,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.WebAssembly.Diagnostics;
using Newtonsoft.Json.Linq;
using System.Runtime.ExceptionServices;
#nullable enable
......@@ -235,10 +236,10 @@ public async Task LaunchBrowser(DateTime start, TimeSpan span)
string uriStr = $"ws://{TestHarnessProxy.Endpoint.Authority}/launch-host-and-connect/?test_id={Id}";
if (!DebuggerTestBase.RunningOnChrome)
{
uriStr += "&host=firefox&firefox-proxy-port=6002";
uriStr += $"&host=firefox&firefox-proxy-port={DebuggerTestBase.FirefoxProxyPort}";
// Ensure the listener is running early, so trying to
// connect to that does not race with the starting of it
FirefoxDebuggerProxy.StartListener(6002, _logger);
FirefoxDebuggerProxy.StartListener(DebuggerTestBase.FirefoxProxyPort, _logger);
}
await Client.Connect(new Uri(uriStr), OnMessage, _cancellationTokenSource);
......@@ -330,7 +331,7 @@ public async Task OpenSessionAsync(Func<InspectorClient, CancellationToken, List
{
Console.WriteLine ($"OpenSession crashing. proxy state: {state}");
if (state.reason == RunLoopStopReason.Exception && state.exception is not null)
throw state.exception;
ExceptionDispatchInfo.Capture(state.exception).Throw();
}
throw;
......
......@@ -96,7 +96,6 @@ public void Configure(IApplicationBuilder app, IOptionsMonitor<TestHarnessOption
ContentTypeProvider = provider
});
var devToolsUrl = options.DevToolsUrl;
app.UseRouter(router =>
{
router.MapGet("launch-host-and-connect", async context =>
......
......@@ -117,27 +117,4 @@ public virtual void Dispose()
}
}
protected static string GetBrowserPath(IEnumerable<string> pathsToProbe)
{
string? browserPath = FindBrowserPath();
if (!string.IsNullOrEmpty(browserPath))
return Path.GetFullPath(browserPath);
throw new Exception("Could not find an installed chrome to use");
string? FindBrowserPath()
{
string? _browserPath_env_var = Environment.GetEnvironmentVariable("BROWSER_PATH_FOR_DEBUGGER_TESTS");
if (!string.IsNullOrEmpty(_browserPath_env_var))
{
if (File.Exists(_browserPath_env_var))
return _browserPath_env_var;
Console.WriteLine ($"warning: Could not find BROWSER_PATH_FOR_DEBUGGER_TESTS={_browserPath_env_var}");
}
// Look for a browser installed in artifacts, for local runs
return pathsToProbe.FirstOrDefault(p => File.Exists(p));
}
}
}
......@@ -142,10 +142,26 @@ export function mono_wasm_raise_debug_event(event: WasmEvent, args = {}): void {
// Used by the debugger to enumerate loaded dlls and pdbs
export function mono_wasm_get_loaded_files(): string[] {
cwraps.mono_wasm_set_is_debugger_attached(true);
return MONO.loaded_files;
}
export function mono_wasm_wait_for_debugger(): Promise<void> {
return new Promise<void>((resolve) => {
const interval = setInterval(() => {
if (runtimeHelpers.wait_for_debugger != 1) {
return;
}
clearInterval(interval);
resolve();
}, 100);
});
}
export function mono_wasm_debugger_attached(): void {
runtimeHelpers.wait_for_debugger = 1;
cwraps.mono_wasm_set_is_debugger_attached(true);
}
function _create_proxy_from_object_id(objectId: string, details: any) {
if (objectId.startsWith("dotnet:array:")) {
let ret: Array<any>;
......
......@@ -153,6 +153,7 @@ declare type MonoConfig = {
aot_profiler_options?: AOTProfilerOptions;
coverage_profiler_options?: CoverageProfilerOptions;
ignore_pdb_load_errors?: boolean;
wait_for_debugger?: number;
};
declare type MonoConfigError = {
isError: true;
......
......@@ -27,6 +27,7 @@ import {
mono_wasm_change_debugger_log_level,
mono_wasm_symbolicate_string,
mono_wasm_stringify_as_error_with_stack,
mono_wasm_debugger_attached,
} from "./debug";
import { ENVIRONMENT_IS_WEB, ExitStatusError, runtimeHelpers, setImportsAndExports } from "./imports";
import { DotnetModuleConfigImports, DotnetModule } from "./types";
......@@ -387,6 +388,7 @@ const INTERNAL: any = {
mono_wasm_detach_debugger,
mono_wasm_raise_debug_event,
mono_wasm_change_debugger_log_level,
mono_wasm_debugger_attached,
mono_wasm_runtime_is_ready: runtimeHelpers.mono_wasm_runtime_is_ready,
};
......
import { ExitStatus, INTERNAL, Module, quit } from "./imports";
import { ExitStatus, INTERNAL, Module, quit, runtimeHelpers } from "./imports";
import { mono_call_assembly_entry_point } from "./method-calls";
import { mono_wasm_wait_for_debugger } from "./debug";
import { mono_wasm_set_main_args, runtime_is_initialized_reject } from "./startup";
export async function mono_run_main_and_exit(main_assembly_name: string, args: string[]): Promise<void> {
......@@ -14,8 +15,13 @@ export async function mono_run_main_and_exit(main_assembly_name: string, args: s
}
}
export async function mono_run_main(main_assembly_name: string, args: string[]): Promise<number> {
mono_wasm_set_main_args(main_assembly_name, args);
if (runtimeHelpers.wait_for_debugger == -1) {
console.log("waiting for debugger...");
return await mono_wasm_wait_for_debugger().then(() => mono_call_assembly_entry_point(main_assembly_name, [args], "m"));
}
return mono_call_assembly_entry_point(main_assembly_name, [args], "m");
}
......
......@@ -324,6 +324,7 @@ function finalize_startup(config: MonoConfig | MonoConfigError | undefined): voi
mono_wasm_globalization_init(config.globalization_mode!, config.diagnostic_tracing!);
cwraps.mono_wasm_load_runtime("unused", config.debug_level || 0);
runtimeHelpers.wait_for_debugger = config.wait_for_debugger;
} catch (err: any) {
Module.printErr("MONO_WASM: mono_wasm_load_runtime () failed: " + err);
Module.printErr("MONO_WASM: Stacktrace: \n");
......
......@@ -78,7 +78,8 @@ export type MonoConfig = {
runtime_options?: string[], // array of runtime options as strings
aot_profiler_options?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized.
coverage_profiler_options?: CoverageProfilerOptions, // dictionary-style Object. If omitted, coverage profiler will not be initialized.
ignore_pdb_load_errors?: boolean
ignore_pdb_load_errors?: boolean,
wait_for_debugger ?: number
};
export type MonoConfigError = {
......@@ -152,6 +153,7 @@ export type RuntimeHelpers = {
loaded_files: string[];
config: MonoConfig | MonoConfigError;
wait_for_debugger?: number;
fetch: (url: string) => Promise<Response>;
}
......
......@@ -15,11 +15,11 @@
<ContentTargetFolders>content</ContentTargetFolders>
<NoWarn>$(NoWarn);NU5128</NoWarn>
<IsPackable>true</IsPackable>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Content Include="templates\**\*" Exclude="templates\**\bin\**;templates\**\obj\**" />
<Compile Remove="**\*" />
</ItemGroup>
<Target Name="CreateManifestResourceNames" />
......
......@@ -5,6 +5,7 @@
"identity": "WebAssembly.Browser",
"name": "WebAssembly Browser App",
"shortName": "wasmbrowser",
"sourceName": "browser.0",
"tags": {
"language": "C#",
"type": "project"
......
using System;
using System.Runtime.CompilerServices;
Console.WriteLine ("Hello, Console!");
Console.WriteLine ("Hello, Browser!");
public class MyClass {
[MethodImpl(MethodImplOptions.NoInlining)]
......
......@@ -4,7 +4,7 @@
<html>
<head>
<title>Sample ES6</title>
<title>browser.0</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="modulepreload" href="./main.js" />
......
......@@ -2,11 +2,11 @@ import createDotnetRuntime from './dotnet.js'
try {
const { MONO, BINDING, Module, RuntimeBuildInfo } = await createDotnetRuntime();
const managedMethod = BINDING.bind_static_method("[browser] MyClass:CallMeFromJS");
const managedMethod = BINDING.bind_static_method("[browser.0] MyClass:CallMeFromJS");
const text = managedMethod();
document.getElementById("out").innerHTML = `${text}`;
await MONO.mono_run_main("browser.dll", []);
await MONO.mono_run_main("browser.0.dll", []);
} catch (err) {
console.log(`WASM ERROR ${err}`);
document.getElementById("out").innerHTML = `error: ${err}`;
......
......@@ -5,6 +5,7 @@
"identity": "WebAssembly.Console",
"name": "WebAssembly Console App",
"shortName": "wasmconsole",
"sourceName": "console.0",
"tags": {
"language": "C#",
"type": "project"
......
......@@ -3,6 +3,6 @@ const createDotnetRuntime = require("./dotnet.js");
async function main() {
const { MONO } = await createDotnetRuntime();
const app_args = process.argv.slice(2);
await MONO.mono_run_main_and_exit("console.dll", app_args);
await MONO.mono_run_main_and_exit("console.0.dll", app_args);
};
main();
......@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
#nullable enable
......@@ -43,7 +44,18 @@ public BuildEnvironment()
string? sdkForWorkloadPath = EnvironmentVariables.SdkForWorkloadTestingPath;
if (string.IsNullOrEmpty(sdkForWorkloadPath))
throw new Exception($"Environment variable SDK_FOR_WORKLOAD_TESTING_PATH not set");
{
// Is this a "local run?
string probePath = Path.Combine(Path.GetDirectoryName(typeof(BuildEnvironment).Assembly.Location)!,
"..",
"..",
"..",
"dotnet-workload");
if (Directory.Exists(probePath))
sdkForWorkloadPath = Path.GetFullPath(probePath);
else
throw new Exception($"Environment variable SDK_FOR_WORKLOAD_TESTING_PATH not set, and could not find it at {probePath}");
}
if (!Directory.Exists(sdkForWorkloadPath))
throw new Exception($"Could not find SDK_FOR_WORKLOAD_TESTING_PATH={sdkForWorkloadPath}");
......
......@@ -12,6 +12,7 @@
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Threading;
using System.Xml;
using Xunit;
......@@ -553,25 +554,16 @@ protected static void AssertFilesDontExist(string dir, string[] filenames, strin
protected static void AssertFilesExist(string dir, string[] filenames, string? label = null, bool expectToExist=true)
{
string prefix = label != null ? $"{label}: " : string.Empty;
Assert.True(Directory.Exists(dir), $"[{label}] {dir} not found");
foreach (string filename in filenames)
{
string path = Path.Combine(dir, filename);
if (expectToExist && !File.Exists(path))
throw new XunitException($"{prefix}Expected the file to exist: {path}");
if (expectToExist)
{
Assert.True(File.Exists(path),
label != null
? $"{label}: File exists: {path}"
: $"File exists: {path}");
}
else
{
Assert.False(File.Exists(path),
label != null
? $"{label}: {path} should not exist"
: $"{path} should not exist");
}
if (!expectToExist && File.Exists(path))
throw new XunitException($"{prefix}Expected the file to *not* exist: {path}");
}
}
......@@ -586,10 +578,11 @@ protected static void AssertFile(string file0, string file1, string? label=null,
FileInfo finfo0 = new(file0);
FileInfo finfo1 = new(file1);
if (same)
Assert.True(finfo0.Length == finfo1.Length, $"{label}:{Environment.NewLine} File sizes don't match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})");
else
Assert.True(finfo0.Length != finfo1.Length, $"{label}:{Environment.NewLine} File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})");
if (same && finfo0.Length != finfo1.Length)
throw new XunitException($"{label}:{Environment.NewLine} File sizes don't match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})");
if (!same && finfo0.Length == finfo1.Length)
throw new XunitException($"{label}:{Environment.NewLine} File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})");
}
protected (int exitCode, string buildOutput) AssertBuild(string args, string label="build", bool expectSuccess=true, IDictionary<string, string>? envVars=null, int? timeoutMs=null)
......@@ -685,6 +678,20 @@ protected string GetObjDir(string config, string targetFramework=DefaultTargetFr
string? label = null,
bool logToXUnit = true,
int? timeoutMs = null)
{
var t = RunProcessAsync(path, _testOutput, args, envVars, workingDir, label, logToXUnit, timeoutMs);
t.Wait();
return t.Result;
}
public static async Task<(int exitCode, string buildOutput)> RunProcessAsync(string path,
ITestOutputHelper _testOutput,
string args = "",
IDictionary<string, string>? envVars = null,
string? workingDir = null,
string? label = null,
bool logToXUnit = true,
int? timeoutMs = null)
{
_testOutput.WriteLine($"Running {path} {args}");
_testOutput.WriteLine($"WorkingDirectory: {workingDir}");
......@@ -756,7 +763,7 @@ protected string GetObjDir(string config, string targetFramework=DefaultTargetFr
// this will ensure that all the async event handling
// has completed
// https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=net-5.0#System_Diagnostics_Process_WaitForExit_System_Int32_
process.WaitForExit();
await process.WaitForExitAsync();
}
process.ErrorDataReceived -= logStdErr;
......
......@@ -10,11 +10,12 @@ public class DotNetCommand : ToolCommand
private BuildEnvironment _buildEnvironment;
private bool _useDefaultArgs;
public DotNetCommand(BuildEnvironment buildEnv, ITestOutputHelper _testOutput, bool useDefaultArgs=true) : base(buildEnv.DotNet, _testOutput)
public DotNetCommand(BuildEnvironment buildEnv, ITestOutputHelper _testOutput, bool useDefaultArgs=true, string label="") : base(buildEnv.DotNet, _testOutput, label)
{
_buildEnvironment = buildEnv;
_useDefaultArgs = useDefaultArgs;
WithEnvironmentVariables(buildEnv.EnvVars);
if (useDefaultArgs)
WithEnvironmentVariables(buildEnv.EnvVars);
}
protected override string GetFullArgs(params string[] args)
......
......@@ -124,8 +124,6 @@ public static Task StartAndWaitForExitAsync(this Process subject)
subject.Exited += (s, a) =>
{
taskCompletionSource.SetResult(null);
subject.Dispose();
};
subject.Start();
......
......@@ -11,7 +11,7 @@
namespace Wasm.Build.Tests
{
public class ToolCommand
public class ToolCommand : IDisposable
{
private string _label;
protected ITestOutputHelper _testOutput;
......@@ -58,6 +58,18 @@ public ToolCommand WithEnvironmentVariables(IDictionary<string, string>? extraEn
return this;
}
public ToolCommand WithOutputDataReceived(Action<string?> handler)
{
OutputDataReceived += (_, args) => handler(args.Data);
return this;
}
public ToolCommand WithErrorDataReceived(Action<string?> handler)
{
ErrorDataReceived += (_, args) => handler(args.Data);
return this;
}
public virtual CommandResult Execute(params string[] args)
{
return Task.Run(async () => await ExecuteAsync(args)).Result;
......@@ -79,6 +91,16 @@ public virtual CommandResult ExecuteWithCapturedOutput(params string[] args)
return Task.Run(async () => await ExecuteAsyncInternal(resolvedCommand, fullArgs)).Result;
}
public virtual void Dispose()
{
if (CurrentProcess is not null && !CurrentProcess.HasExited)
{
CurrentProcess.KillTree();
CurrentProcess.Dispose();
CurrentProcess = null;
}
}
protected virtual string GetFullArgs(params string[] args) => string.Join(" ", args);
private async Task<CommandResult> ExecuteAsyncInternal(string executable, string args)
......@@ -110,7 +132,6 @@ private async Task<CommandResult> ExecuteAsyncInternal(string executable, string
CurrentProcess.BeginErrorReadLine();
await completionTask;
CurrentProcess.WaitForExit();
RemoveNullTerminator(output);
return new CommandResult(
......
......@@ -6,6 +6,7 @@
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using System.Threading.Tasks;
#nullable enable
......@@ -23,9 +24,9 @@ public WasmTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixtur
[InlineData("Release")]
public void BrowserBuildThenPublish(string config)
{
string id = $"{config}_{Path.GetRandomFileName()}";
string projectName = $"browser";
CreateWasmTemplateProject(id, "wasmbrowser");
string id = $"browser_{config}_{Path.GetRandomFileName()}";
string projectFile = CreateWasmTemplateProject(id, "wasmbrowser");
string projectName = Path.GetFileNameWithoutExtension(projectFile);
var buildArgs = new BuildArgs(projectName, config, false, id, null);
buildArgs = ExpandBuildArgs(buildArgs);
......@@ -67,8 +68,8 @@ public void BrowserBuildThenPublish(string config)
public void ConsoleBuildThenPublish(string config)
{
string id = $"{config}_{Path.GetRandomFileName()}";
string projectName = $"console";
CreateWasmTemplateProject(id, "wasmconsole");
string projectFile = CreateWasmTemplateProject(id, "wasmconsole");
string projectName = Path.GetFileNameWithoutExtension(projectFile);
var buildArgs = new BuildArgs(projectName, config, false, id, null);
buildArgs = ExpandBuildArgs(buildArgs);
......
......@@ -38,12 +38,12 @@
</ItemGroup>
<ItemGroup Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(DebuggerHost)' == 'chrome'">
<RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export BROWSER_PATH_FOR_DEBUGGER_TESTS=$HELIX_CORRELATION_PAYLOAD/chrome-linux/chrome" />
<RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set BROWSER_PATH_FOR_DEBUGGER_TESTS=%HELIX_CORRELATION_PAYLOAD%\chrome-win\chrome.exe" />
<RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export BROWSER_PATH_FOR_TESTS=$HELIX_CORRELATION_PAYLOAD/chrome-linux/chrome" />
<RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set BROWSER_PATH_FOR_TESTS=%HELIX_CORRELATION_PAYLOAD%\chrome-win\chrome.exe" />
</ItemGroup>
<ItemGroup Condition="'$(ContinuousIntegrationBuild)' == 'true' and '$(DebuggerHost)' == 'firefox'">
<RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export BROWSER_PATH_FOR_DEBUGGER_TESTS=$HELIX_CORRELATION_PAYLOAD/firefox/firefox" />
<RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export BROWSER_PATH_FOR_TESTS=$HELIX_CORRELATION_PAYLOAD/firefox/firefox" />
</ItemGroup>
<PropertyGroup>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册