diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6d1a5acb6c39ad..0af6039115b844 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -33,4 +33,9 @@ RUN sudo apt-get install libnss3 -y \ && apt-get install libgbm-dev -y \ && apt-get install libpango-1.0-0 -y \ && apt-get install libcairo2 -y \ - && apt-get install libasound2 -y \ No newline at end of file + && apt-get install libasound2 -y + +#install firefox dependecies to run debugger tests: +RUN sudo apt-get install libdbus-glib-1-2 -y \ + && apt-get install libgtk-3-0 -y \ + && apt-get install libx11-xcb-dev -y \ No newline at end of file diff --git a/eng/pipelines/common/platform-matrix.yml b/eng/pipelines/common/platform-matrix.yml index 8a893c81c4d5fd..60965d87080c2e 100644 --- a/eng/pipelines/common/platform-matrix.yml +++ b/eng/pipelines/common/platform-matrix.yml @@ -314,6 +314,30 @@ jobs: platforms: ${{ parameters.platforms }} ${{ insert }}: ${{ parameters.jobParameters }} +# WebAssembly Linux Firefox + +- ${{ if containsValue(parameters.platforms, 'Browser_wasm_firefox') }}: + - template: xplat-setup.yml + parameters: + jobTemplate: ${{ parameters.jobTemplate }} + helixQueuesTemplate: ${{ parameters.helixQueuesTemplate }} + variables: ${{ parameters.variables }} + osGroup: Browser + archType: wasm + targetRid: browser-wasm + platform: Browser_wasm_firefox + container: + image: ubuntu-18.04-webassembly-20220317214646-1ad56e8 + registry: mcr + jobParameters: + hostedOs: Linux + runtimeFlavor: ${{ parameters.runtimeFlavor }} + stagedBuild: ${{ parameters.stagedBuild }} + buildConfig: ${{ parameters.buildConfig }} + ${{ if eq(parameters.passPlatforms, true) }}: + platforms: ${{ parameters.platforms }} + ${{ insert }}: ${{ parameters.jobParameters }} + # WebAssembly on Windows - ${{ if containsValue(parameters.platforms, 'Browser_wasm_win') }}: diff --git a/eng/pipelines/common/templates/wasm-debugger-tests.yml b/eng/pipelines/common/templates/wasm-debugger-tests.yml index 49ad67739f0cb5..2c52fcfc612fa0 100644 --- a/eng/pipelines/common/templates/wasm-debugger-tests.yml +++ b/eng/pipelines/common/templates/wasm-debugger-tests.yml @@ -1,6 +1,7 @@ parameters: alwaysRun: false isExtraPlatformsBuild: false + browser: 'chrome' platforms: [] jobs: @@ -24,8 +25,8 @@ jobs: jobParameters: testGroup: innerloop isExtraPlatforms: ${{ parameters.isExtraPlatformsBuild }} - nameSuffix: Mono_DebuggerTests - buildArgs: -s mono+libs+libs.tests -c $(_BuildConfig) /p:ArchiveTests=true /p:TestWasmDebuggerTests=true /p:TestAssemblies=false /p:BrowserHost=$(_hostedOs) + nameSuffix: Mono_DebuggerTests_${{ parameters.browser }} + buildArgs: -s mono+libs+libs.tests -c $(_BuildConfig) /p:ArchiveTests=true /p:TestWasmDebuggerTests=true /p:TestAssemblies=false /p:BrowserHost=$(_hostedOs) /p:DebuggerHost=${{ parameters.browser }} timeoutInMinutes: 180 condition: >- or( @@ -36,7 +37,7 @@ jobs: extraStepsTemplate: /eng/pipelines/libraries/helix.yml extraStepsParameters: creator: dotnet-bot - testRunNamePrefixSuffix: Mono_$(_BuildConfig) - extraHelixArguments: /p:BrowserHost=$(_hostedOs) + testRunNamePrefixSuffix: Mono_${{ parameters.browser }}_$(_BuildConfig) + extraHelixArguments: /p:BrowserHost=$(_hostedOs) /p:_DebuggerHosts=${{ parameters.browser }} scenarios: - wasmdebuggertests diff --git a/eng/pipelines/libraries/helix-queues-setup.yml b/eng/pipelines/libraries/helix-queues-setup.yml index ecca458ba59323..3ff7f67ee99499 100644 --- a/eng/pipelines/libraries/helix-queues-setup.yml +++ b/eng/pipelines/libraries/helix-queues-setup.yml @@ -177,6 +177,10 @@ jobs: - ${{ if eq(parameters.platform, 'Browser_wasm') }}: - Ubuntu.1804.Amd64.Open + # WebAssembly Firefox + - ${{ if eq(parameters.platform, 'Browser_wasm_firefox') }}: + - (Ubuntu.1804.Amd64)Ubuntu.1804.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-webassembly-20220408155625-5bc1463 + # WebAssembly windows - ${{ if eq(parameters.platform, 'Browser_wasm_win') }}: - (Windows.Amd64.Server2022.Open)windows.amd64.server2022.open@mcr.microsoft.com/dotnet-buildtools/prereqs:windowsservercore-ltsc2022-helix-webassembly-20220317203903-1ad56e8 diff --git a/eng/pipelines/runtime-extra-platforms-wasm.yml b/eng/pipelines/runtime-extra-platforms-wasm.yml index c36241d2df7ce2..a8f345ee363ff2 100644 --- a/eng/pipelines/runtime-extra-platforms-wasm.yml +++ b/eng/pipelines/runtime-extra-platforms-wasm.yml @@ -117,3 +117,10 @@ jobs: platforms: - Browser_wasm alwaysRun: ${{ parameters.isWasmOnlyBuild }} + + - template: /eng/pipelines/common/templates/wasm-debugger-tests.yml + parameters: + platforms: + - Browser_wasm_firefox + browser: firefox + alwaysRun: ${{ parameters.isWasmOnlyBuild }} diff --git a/eng/pipelines/runtime-staging.yml b/eng/pipelines/runtime-staging.yml index 9fce5f45b6630d..6f66971d3694ad 100644 --- a/eng/pipelines/runtime-staging.yml +++ b/eng/pipelines/runtime-staging.yml @@ -92,6 +92,13 @@ jobs: - Browser_wasm_win alwaysRun: ${{ variables.isRollingBuild }} +- template: /eng/pipelines/common/templates/wasm-debugger-tests.yml + parameters: + platforms: + - Browser_wasm_firefox + browser: firefox + alwaysRun: ${{ variables.isRollingBuild }} + # # Build the whole product using Mono and run libraries tests # diff --git a/src/libraries/sendtohelix-wasm.targets b/src/libraries/sendtohelix-wasm.targets index 8266568b837b1f..d1872e3ae55b24 100644 --- a/src/libraries/sendtohelix-wasm.targets +++ b/src/libraries/sendtohelix-wasm.targets @@ -2,13 +2,14 @@ <_workItemTimeout Condition="'$(Scenario)' == 'BuildWasmApps' and '$(_workItemTimeout)' == ''">01:30:00 <_workItemTimeout Condition="'$(NeedsToBuildWasmAppsOnHelix)' == 'true'">01:00:00 - <_workItemTimeout Condition="'$(Scenario)' == 'WasmDebuggerTests'">00:30:00 + <_workItemTimeout Condition="'$(Scenario)' == 'WasmDebuggerTests'">01:00:00 <_workItemTimeout Condition="'$(Scenario)' == 'WasmTestOnBrowser' and '$(BrowserHost)' == 'windows'">00:45:00 true true $(BuildHelixWorkItemsDependsOn);StageEmSdkForHelix;PrepareForBuildHelixWorkItems_Wasm + $(BuildHelixWorkItemsDependsOn);DownloadFirefoxToSendToHelix false false @@ -18,6 +19,8 @@ $(RepoRoot)src\mono\wasm\emsdk\ $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'src', 'mono', 'wasm', 'emsdk')) + $(TestArchiveRoot)firefox.zip + chrome true true @@ -47,8 +50,8 @@ - - + + @@ -60,8 +63,8 @@ - - + + @@ -127,8 +130,11 @@ - - + + + + + @@ -198,7 +204,7 @@ - + $(_WasmDebuggerTestsPayloadArchive) @@ -239,4 +245,11 @@ + + + + + + + diff --git a/src/libraries/sendtohelix.proj b/src/libraries/sendtohelix.proj index 3b938a01682001..60de339e34023a 100644 --- a/src/libraries/sendtohelix.proj +++ b/src/libraries/sendtohelix.proj @@ -48,9 +48,11 @@ HelixTargetQueues=$(HelixTargetQueues); BuildTargetFramework=$(BuildTargetFramework) + <_DebuggerHosts Condition="'$(_DebuggerHosts)' == ''">chrome + @@ -69,7 +71,7 @@ <_Scenarios Include="$(_Scenarios.Split(','))" /> - <_BaseProjectsToBuild Include="$(PerScenarioProjectFile)" Condition="'%(_Scenarios.Identity)' != 'buildwasmapps' and '%(_Scenarios.Identity)' != 'buildiosapps'"> + <_BaseProjectsToBuild Include="$(PerScenarioProjectFile)" Condition="'%(_Scenarios.Identity)' != 'buildwasmapps' and '%(_Scenarios.Identity)' != 'buildiosapps' and '%(_Scenarios.Identity)' != 'wasmdebuggertests'"> $(_PropertiesToPass);Scenario=%(_Scenarios.Identity);TestArchiveRuntimeFile=$(TestArchiveRuntimeFile) %(_BaseProjectsToBuild.AdditionalProperties);NeedsToBuildWasmAppsOnHelix=$(NeedsToBuildWasmAppsOnHelix) @@ -85,6 +87,14 @@ + + <_DebuggerHostsItem Include="$(_DebuggerHosts.Split('/'))" /> + + <_WasmDebuggerTestsProjectsToBuild Include="$(PerScenarioProjectFile)"> + $(_PropertiesToPass);Scenario=WasmDebuggerTests;TestArchiveRuntimeFile=$(TestArchiveRuntimeFile);DebuggerHost=%(_DebuggerHostsItem.Identity) + + + <_TestUsingWorkloadsValues Include="false" /> @@ -96,7 +106,7 @@ - <_ProjectsToBuild Include="@(_BuildWasmAppsProjectsToBuild);@(_BuildiOSAppsProjectsToBuild);@(_BaseProjectsToBuild)" /> + <_ProjectsToBuild Include="@(_BuildWasmAppsProjectsToBuild);@(_WasmDebuggerTestsProjectsToBuild);@(_BuildiOSAppsProjectsToBuild);@(_BaseProjectsToBuild)" /> diff --git a/src/mono/mono/component/debugger-agent.c b/src/mono/mono/component/debugger-agent.c index 2bd5aeef4c06f0..75b2e0d9d94400 100644 --- a/src/mono/mono/component/debugger-agent.c +++ b/src/mono/mono/component/debugger-agent.c @@ -8997,7 +8997,7 @@ thread_commands (int command, guint8 *p, guint8 *end, Buffer *buf) start_frame = decode_int (p, &p, end); length = decode_int (p, &p, end); - if (start_frame != 0 || length != -1) + if (start_frame != 0) return ERR_NOT_IMPLEMENTED; GET_TLS_DATA_FROM_THREAD (thread); if (tls == NULL) @@ -9005,8 +9005,8 @@ thread_commands (int command, guint8 *p, guint8 *end, Buffer *buf) compute_frame_info (thread, tls, TRUE); //the last parameter is TRUE to force that the frame info that will be send is synchronised with the debugged thread - buffer_add_int (buf, tls->frame_count); - for (i = 0; i < tls->frame_count; ++i) { + buffer_add_int (buf, length != -1 ? (length > tls->frame_count ? tls->frame_count : length) : tls->frame_count); + for (i = 0; i < tls->frame_count && (i < length || length == -1); ++i) { buffer_add_int (buf, tls->frames [i]->id); buffer_add_methodid (buf, tls->frames [i]->de.domain, tls->frames [i]->actual_method); buffer_add_int (buf, tls->frames [i]->il_offset); diff --git a/src/mono/wasm/BrowsersForTesting.props b/src/mono/wasm/BrowsersForTesting.props index d0b3383b0cead3..63fc5a13c7e77c 100644 --- a/src/mono/wasm/BrowsersForTesting.props +++ b/src/mono/wasm/BrowsersForTesting.props @@ -15,6 +15,9 @@ chrome-linux chromedriver_linux64 chrome + 97.0.1 + https://ftp.mozilla.org/pub/firefox/releases/$(FirefoxRevision)/linux-x86_64/en-US/firefox-$(FirefoxRevision).tar.bz2 + firefox diff --git a/src/mono/wasm/Makefile b/src/mono/wasm/Makefile index d5060640112c4b..a6b764ef77b4bd 100644 --- a/src/mono/wasm/Makefile +++ b/src/mono/wasm/Makefile @@ -129,6 +129,7 @@ submit-tests-helix: $(MSBUILD_ARGS) run-debugger-tests: + rm -f $(TOP)/artifacts/bin/DebuggerTestSuite/x64/Debug/*log; \ if [ ! -z "$(TEST_FILTER)" ]; then \ $(DOTNET) test $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite $(MSBUILD_ARGS) --filter "Category!=failing&FullyQualifiedName~$(TEST_FILTER)" $(TEST_ARGS); \ else \ diff --git a/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj b/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj index 66d0b287f76559..a397c2074532b2 100644 --- a/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj +++ b/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj @@ -7,6 +7,8 @@ + + diff --git a/src/mono/wasm/debugger/BrowserDebugHost/Program.cs b/src/mono/wasm/debugger/BrowserDebugHost/Program.cs index 52380a079997a2..61100fb4dd7169 100644 --- a/src/mono/wasm/debugger/BrowserDebugHost/Program.cs +++ b/src/mono/wasm/debugger/BrowserDebugHost/Program.cs @@ -2,10 +2,12 @@ // 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; #nullable enable @@ -26,6 +28,28 @@ public static void Main(string[] args) 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"]; + + + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(options => + { + options.TimestampFormat = "[HH:mm:ss] "; + }) + .AddFilter(null, LogLevel.Debug); + + if (!string.IsNullOrEmpty(logPath)) + builder.AddFile(Path.Combine(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); IWebHost host = new WebHostBuilder() .UseSetting("UseIISIntegration", false.ToString()) diff --git a/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs b/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs index c2837165a4c8bc..6b58163104fd31 100644 --- a/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs +++ b/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -124,7 +125,6 @@ async Task Copy(HttpContext context) context.Response.ContentLength = response.Content.Headers.ContentLength; byte[] bytes = await response.Content.ReadAsByteArrayAsync(); await context.Response.Body.WriteAsync(bytes); - } } @@ -161,6 +161,8 @@ async Task ConnectProxy(HttpContext context) { runtimeId = parsedId; } + + CancellationTokenSource cts = new(); try { using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => @@ -177,11 +179,12 @@ async Task ConnectProxy(HttpContext context) System.Net.WebSockets.WebSocket ideSocket = await context.WebSockets.AcceptWebSocketAsync(); - await proxy.Run(endpoint, ideSocket); + await proxy.Run(endpoint, ideSocket, cts); } catch (Exception e) { Console.WriteLine("got exception {0}", e); + cts.Cancel(); } } }); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Common/DevToolsDebuggerConnection.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Common/DevToolsDebuggerConnection.cs new file mode 100644 index 00000000000000..d6a658af441d68 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Common/DevToolsDebuggerConnection.cs @@ -0,0 +1,80 @@ +// 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.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +#nullable enable + +namespace Microsoft.WebAssembly.Diagnostics; + +internal sealed class DevToolsDebuggerConnection : WasmDebuggerConnection +{ + public WebSocket WebSocket { get; init; } + private readonly ILogger _logger; + + public DevToolsDebuggerConnection(WebSocket webSocket, string id, ILogger logger) + : base(id) + { + ArgumentNullException.ThrowIfNull(webSocket); + ArgumentNullException.ThrowIfNull(logger); + WebSocket = webSocket; + _logger = logger; + } + + public override bool IsConnected => WebSocket.State == WebSocketState.Open; + + public override async Task ReadOneAsync(CancellationToken token) + { + byte[] buff = new byte[4000]; + var mem = new MemoryStream(); + + while (true) + { + if (WebSocket.State != WebSocketState.Open) + throw new Exception($"WebSocket is no longer open, state: {WebSocket.State}"); + + ArraySegment buffAsSeg = new(buff); + WebSocketReceiveResult result = await WebSocket.ReceiveAsync(buffAsSeg, token); + if (result.MessageType == WebSocketMessageType.Close) + throw new Exception($"WebSocket close message received, state: {WebSocket.State}"); + + await mem.WriteAsync(new ReadOnlyMemory(buff, 0, result.Count), token); + + if (result.EndOfMessage) + return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int)mem.Length); + } + } + + public override Task SendAsync(byte[] bytes, CancellationToken token) + => WebSocket.SendAsync(new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + token); + + public override async Task ShutdownAsync(CancellationToken cancellationToken) + { + try + { + if (!cancellationToken.IsCancellationRequested && WebSocket.State == WebSocketState.Open) + await WebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken); + } + catch (Exception ex) when (ex is IOException || ex is WebSocketException || ex is OperationCanceledException) + { + _logger.LogDebug($"Shutdown: Close failed, but ignoring: {ex}"); + } + } + + public override void Dispose() + { + WebSocket.Dispose(); + base.Dispose(); + } + + public override string ToString() => $"[ {Id} connection: state: {WebSocket?.State} ]"; +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsQueue.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Common/DevToolsQueue.cs similarity index 82% rename from src/mono/wasm/debugger/BrowserDebugProxy/DevToolsQueue.cs rename to src/mono/wasm/debugger/BrowserDebugProxy/Common/DevToolsQueue.cs index 280d2fd751f2ec..4bafe927b54a90 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsQueue.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Common/DevToolsQueue.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; @@ -18,11 +16,13 @@ internal sealed class DevToolsQueue private Task? current_send; private ConcurrentQueue pending; - public WebSocket Ws { get; private set; } public Task? CurrentSend { get { return current_send; } } - public DevToolsQueue(WebSocket sock) + + public WasmDebuggerConnection Connection { get; init; } + + public DevToolsQueue(WasmDebuggerConnection conn) { - this.Ws = sock; + Connection = conn; pending = new ConcurrentQueue(); } @@ -46,7 +46,7 @@ public bool TryPumpIfCurrentCompleted(CancellationToken token, [NotNullWhen(true current_send = null; if (pending.TryDequeue(out byte[]? bytes)) { - current_send = Ws.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, token); + current_send = Connection.SendAsync(bytes, token); sendTask = current_send; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Common/FirefoxDebuggerConnection.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Common/FirefoxDebuggerConnection.cs new file mode 100644 index 00000000000000..2664cadb0cb3e2 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Common/FirefoxDebuggerConnection.cs @@ -0,0 +1,116 @@ +// 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.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +#nullable enable +namespace Microsoft.WebAssembly.Diagnostics; + +internal sealed class FirefoxDebuggerConnection : WasmDebuggerConnection +{ + public TcpClient TcpClient { get; init; } + private readonly ILogger _logger; + private bool _isDisposed; + private readonly byte[] _lengthBuffer; + + public FirefoxDebuggerConnection(TcpClient tcpClient, string id, ILogger logger) + : base(id) + { + ArgumentNullException.ThrowIfNull(tcpClient); + ArgumentNullException.ThrowIfNull(logger); + TcpClient = tcpClient; + _logger = logger; + _lengthBuffer = new byte[10]; + } + + public override bool IsConnected => TcpClient.Connected; + + public override async Task ReadOneAsync(CancellationToken token) + { +#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + NetworkStream? stream = TcpClient.GetStream(); + int bytesRead = 0; + while (bytesRead == 0 || Convert.ToChar(_lengthBuffer[bytesRead - 1]) != ':') + { + if (CheckFail()) + return null; + + if (bytesRead + 1 > _lengthBuffer.Length) + throw new IOException($"Protocol error: did not get the expected length preceding a message, " + + $"after reading {bytesRead} bytes. Instead got: {Encoding.UTF8.GetString(_lengthBuffer)}"); + + int readLen = await stream.ReadAsync(_lengthBuffer, bytesRead, 1, token); + bytesRead += readLen; + } + + string str = Encoding.UTF8.GetString(_lengthBuffer, 0, bytesRead - 1); + if (!int.TryParse(str, out int messageLen)) + throw new Exception($"Protocol error: Could not parse length prefix: '{str}'"); + + if (CheckFail()) + return null; + + byte[] buffer = new byte[messageLen]; + bytesRead = await stream.ReadAsync(buffer, 0, messageLen, token); + while (bytesRead != messageLen) + { + if (CheckFail()) + return null; + bytesRead += await stream.ReadAsync(buffer, bytesRead, messageLen - bytesRead, token); + } + + return Encoding.UTF8.GetString(buffer, 0, messageLen); + + bool CheckFail() + { + if (token.IsCancellationRequested) + return true; + + if (!TcpClient.Connected) + throw new Exception($"{this} Connection closed"); + + return false; + } + } + + public override Task SendAsync(byte[] bytes, CancellationToken token) + { + byte[]? bytesWithHeader = Encoding.UTF8.GetBytes($"{bytes.Length}:").Concat(bytes).ToArray(); + NetworkStream toStream = TcpClient.GetStream(); + return toStream.WriteAsync(bytesWithHeader, token).AsTask(); + } + + public override Task ShutdownAsync(CancellationToken cancellationToken) + { + TcpClient.Close(); + return Task.CompletedTask; + } + + public override void Dispose() + { + if (_isDisposed) + return; + + try + { + TcpClient.Close(); + base.Dispose(); + + _isDisposed = true; + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to dispose {this}: {ex}"); + throw; + } + } + + public override string ToString() => $"[ {Id} connection ]"; +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Common/WasmDebuggerConnection.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Common/WasmDebuggerConnection.cs new file mode 100644 index 00000000000000..28490716673647 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Common/WasmDebuggerConnection.cs @@ -0,0 +1,25 @@ +// 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.Threading; +using System.Threading.Tasks; + +#nullable enable + +namespace Microsoft.WebAssembly.Diagnostics; + +internal abstract class WasmDebuggerConnection : IDisposable +{ + public string Id { get; init; } + + protected WasmDebuggerConnection(string id) => Id = id; + + public abstract bool IsConnected { get; } + + public abstract Task ReadOneAsync(CancellationToken token); + public abstract Task SendAsync(byte[] bytes, CancellationToken token); + public abstract Task ShutdownAsync(CancellationToken cancellationToken); + public virtual void Dispose() + {} +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs index d18d560518fa9d..ff6f3aff3e14bb 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -62,9 +62,9 @@ internal sealed class BreakpointRequest public string Id { get; private set; } public string Assembly { get; private set; } public string File { get; private set; } - public int Line { get; private set; } - public int Column { get; private set; } - public string Condition { get; private set; } + public int Line { get; set; } + public int Column { get; set; } + public string Condition { get; set; } public MethodInfo Method { get; set; } private JObject request; @@ -138,6 +138,20 @@ public bool TryResolve(DebugStore store) return store.AllSources().FirstOrDefault(source => TryResolve(source)) != null; } + public bool CompareRequest(JObject req) + => this.request["url"].Value() == req["url"].Value() && + this.request["lineNumber"].Value() == req["lineNumber"].Value() && + this.request["columnNumber"].Value() == req["columnNumber"].Value(); + + public void UpdateCondition(string condition) + { + Condition = condition; + foreach (var loc in Locations) + { + loc.Condition = condition; + } + } + } internal sealed class VarInfo @@ -356,11 +370,15 @@ public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle, var sps = DebugInformation.GetSequencePoints(); SequencePoint start = sps.First(); SequencePoint end = sps.First(); - + source.BreakableLines.Add(start.StartLine); foreach (SequencePoint sp in sps) { + if (source.BreakableLines.Last() != sp.StartLine) + source.BreakableLines.Add(sp.StartLine); + if (sp.IsHidden) continue; + if (sp.StartLine < start.StartLine) start = sp; else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn) @@ -998,6 +1016,7 @@ internal sealed class SourceFile private Document doc; private DocumentHandle docHandle; private string url; + internal List BreakableLines { get; } internal SourceFile(AssemblyInfo assembly, int id, DocumentHandle docHandle, Uri sourceLinkUri, string url) { @@ -1009,6 +1028,7 @@ internal SourceFile(AssemblyInfo assembly, int id, DocumentHandle docHandle, Uri this.docHandle = docHandle; this.url = url; this.DebuggerFileName = url.Replace("\\", "/").Replace(":", ""); + this.BreakableLines = new List(); var urlWithSpecialCharCodedHex = EscapeAscii(url); this.SourceUri = new Uri((Path.IsPathRooted(url) ? "file://" : "") + urlWithSpecialCharCodedHex, UriKind.RelativeOrAbsolute); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs index 340cb7577be89a..a33f3a2b58fddc 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net.WebSockets; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -13,18 +14,20 @@ namespace Microsoft.WebAssembly.Diagnostics // This type is the public entrypoint that allows external code to attach the debugger proxy // to a given websocket listener. Everything else in this package can be internal. - public class DebuggerProxy + public class DebuggerProxy : DebuggerProxyBase { - private readonly MonoProxy proxy; + internal MonoProxy MonoProxy { get; } public DebuggerProxy(ILoggerFactory loggerFactory, IList urlSymbolServerList, int runtimeId = 0, string loggerId = "") { - proxy = new MonoProxy(loggerFactory, urlSymbolServerList, runtimeId, loggerId); + MonoProxy = new MonoProxy(loggerFactory, urlSymbolServerList, runtimeId, loggerId); } - public Task Run(Uri browserUri, WebSocket ideSocket) + public Task Run(Uri browserUri, WebSocket ideSocket, CancellationTokenSource cts) { - return proxy.Run(browserUri, ideSocket); + return MonoProxy.RunForDevTools(browserUri, ideSocket, cts); } + + public override void Shutdown() => MonoProxy.Shutdown(); } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxyBase.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxyBase.cs new file mode 100644 index 00000000000000..1f4f330ed6273d --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxyBase.cs @@ -0,0 +1,21 @@ +// 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; + +namespace Microsoft.WebAssembly.Diagnostics; + +public abstract class DebuggerProxyBase +{ + public RunLoopExitState? ExitState { get; set; } + + public virtual void Shutdown() + { + } + + public virtual void Fail(Exception ex) + { + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs index ec8f942846bdb4..e9a4950dd34774 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -39,7 +39,7 @@ public SessionId(string sessionId) public override string ToString() => $"session-{sessionId}"; } - public struct MessageId : IEquatable + public class MessageId : IEquatable { public readonly string sessionId; public readonly int id; @@ -127,10 +127,11 @@ public struct Result { public JObject Value { get; private set; } public JObject Error { get; private set; } + public JObject FullContent { get; private set; } public bool IsOk => Error == null; - private Result(JObject resultOrError, bool isError) + private Result(JObject resultOrError, bool isError, JObject fullContent = null) { if (resultOrError == null) throw new ArgumentNullException(nameof(resultOrError)); @@ -147,6 +148,7 @@ private Result(JObject resultOrError, bool isError) Value = resultOrError; Error = null; } + FullContent = fullContent; } public static Result FromJson(JObject obj) { @@ -156,6 +158,89 @@ public static Result FromJson(JObject obj) var result = (obj["result"] as JObject) ?? new JObject(); return new Result(result, false); } + public static Result FromJsonFirefox(JObject obj) + { + //Log ("protocol", $"from result: {obj}"); + JObject o; + if (obj["ownProperties"] != null && obj["prototype"]?["class"]?.Value() == "Array") + { + var ret = new JArray(); + var arrayItems = obj["ownProperties"]; + foreach (JProperty arrayItem in arrayItems) + { + if (arrayItem.Name != "length") + ret.Add(arrayItem.Value["value"]); + } + o = JObject.FromObject(new + { + result = new + { + value = ret + } + }); + } + else if (obj["result"] is JObject && obj["result"]?["type"]?.Value() == "object") + { + if (obj["result"]["class"].Value() == "Array") + { + o = JObject.FromObject(new + { + result = new + { + value = obj["result"]["preview"]["items"] + } + }); + } + else if (obj["result"]?["preview"] != null) + { + o = JObject.FromObject(new + { + result = new + { + value = obj["result"]?["preview"]?["ownProperties"]?["value"] + } + }); + } + else + { + o = JObject.FromObject(new + { + result = new + { + value = obj["result"] + } + }); + } + } + else if (obj["result"] != null) + { + o = JObject.FromObject(new + { + result = new + { + value = obj["result"], + type = obj["resultType"], + description = obj["resultDescription"] + } + }); + } + else + { + o = JObject.FromObject(new + { + result = new + { + value = obj + } + }); + } + bool resultHasError = obj["hasException"] != null && obj["hasException"].Value(); + if (resultHasError) + { + return new Result(obj["exception"] as JObject, resultHasError, obj); + } + return new Result(o, false, obj); + } public static Result Ok(JObject ok) => new Result(ok, false); @@ -242,6 +327,7 @@ internal enum MonoErrorCodes internal static class MonoConstants { public const string RUNTIME_IS_READY = "mono_wasm_runtime_ready"; + public const string RUNTIME_IS_READY_ID = "fe00e07a-5519-4dfe-b35a-f867dbaf2e28"; public const string EVENT_RAISED = "mono_wasm_debug_event_raised:aef14bca-5519-4dfe-b35a-f867abc123ae"; } @@ -265,7 +351,7 @@ internal sealed class Breakpoint public int RemoteId { get; set; } public BreakpointState State { get; set; } public string StackId { get; private set; } - public string Condition { get; private set; } + public string Condition { get; set; } public bool ConditionAlreadyEvaluatedWithError { get; set; } public static bool TryParseId(string stackId, out int id) { @@ -308,7 +394,7 @@ internal enum PauseOnExceptionsKind All } - internal sealed class ExecutionContext + internal class ExecutionContext { public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData) { @@ -319,7 +405,7 @@ public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData) public string DebugId { get; set; } public Dictionary BreakpointRequests { get; } = new Dictionary(); - + public int breakpointId; public TaskCompletionSource ready; public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted; public bool IsSkippingHiddenMethod { get; set; } @@ -327,6 +413,11 @@ public ExecutionContext(MonoSDBHelper sdbAgent, int id, object auxData) public bool IsResumedAfterBp { get; set; } public int ThreadId { get; set; } public int Id { get; set; } + + public bool PausedOnWasm { get; set; } + + public string PauseKind { get; set; } + public object AuxData { get; set; } public PauseOnExceptionsKind PauseOnExceptions { get; set; } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs index 567b1d7837e66f..806c05cbf40e20 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net.WebSockets; using System.Text; @@ -16,138 +15,106 @@ namespace Microsoft.WebAssembly.Diagnostics { - internal class DevToolsProxy { - private TaskCompletionSource side_exception = new TaskCompletionSource(); - private TaskCompletionSource client_initiated_close = new TaskCompletionSource(); - private Dictionary> pending_cmds = new Dictionary>(); - private ClientWebSocket browser; - private WebSocket ide; + protected TaskCompletionSource side_exception = new(); + protected TaskCompletionSource shutdown_requested = new(); + protected Dictionary> pending_cmds = new Dictionary>(); + protected WasmDebuggerConnection browser; + protected WasmDebuggerConnection ide; private int next_cmd_id; private readonly ChannelWriter _channelWriter; private readonly ChannelReader _channelReader; - private List queues = new List(); + protected List queues = new List(); protected readonly ILogger logger; + private readonly string _loggerId; + + public event EventHandler RunLoopStopped; + public bool IsRunning => Stopped is null; + public RunLoopExitState Stopped { get; private set; } public DevToolsProxy(ILoggerFactory loggerFactory, string loggerId) { + _loggerId = loggerId; string loggerSuffix = string.IsNullOrEmpty(loggerId) ? string.Empty : $"-{loggerId}"; - logger = loggerFactory.CreateLogger($"{nameof(DevToolsProxy)}{loggerSuffix}"); + logger = loggerFactory.CreateLogger($"DevToolsProxy{loggerSuffix}"); var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); _channelWriter = channel.Writer; _channelReader = channel.Reader; } - protected virtual Task AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token) + protected int GetNewCmdId() => Interlocked.Increment(ref next_cmd_id); + protected int ResetCmdId() => next_cmd_id = 0; + protected virtual Task AcceptEvent(SessionId sessionId, JObject args, CancellationToken token) { return Task.FromResult(false); } - protected virtual Task AcceptCommand(MessageId id, string method, JObject args, CancellationToken token) + protected virtual Task AcceptCommand(MessageId id, JObject args, CancellationToken token) { return Task.FromResult(false); } - private async Task ReadOne(WebSocket socket, CancellationToken token) - { - byte[] buff = new byte[4000]; - var mem = new MemoryStream(); - try - { - while (true) - { - if (socket.State != WebSocketState.Open) - { - Log("error", $"DevToolsProxy: Socket is no longer open."); - client_initiated_close.TrySetResult(); - return null; - } - - WebSocketReceiveResult result = await socket.ReceiveAsync(new ArraySegment(buff), token); - if (result.MessageType == WebSocketMessageType.Close) - { - client_initiated_close.TrySetResult(); - return null; - } - - mem.Write(buff, 0, result.Count); + private DevToolsQueue GetQueueForConnection(WasmDebuggerConnection conn) + => queues.FirstOrDefault(q => q.Connection == conn); - if (result.EndOfMessage) - return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int)mem.Length); - } - } - catch (WebSocketException e) - { - if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) - { - client_initiated_close.TrySetResult(); - return null; - } - } - return null; - } - - private DevToolsQueue GetQueueForSocket(WebSocket ws) - { - return queues.FirstOrDefault(q => q.Ws == ws); - } - - private DevToolsQueue GetQueueForTask(Task task) + protected DevToolsQueue GetQueueForTask(Task task) { return queues.FirstOrDefault(q => q.CurrentSend == task); } - private async Task Send(WebSocket to, JObject o, CancellationToken token) + protected async Task Send(WasmDebuggerConnection conn, JObject o, CancellationToken token) { - string sender = browser == to ? "Send-browser" : "Send-ide"; - - //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled") - Log("protocol", $"{sender}: " + JsonConvert.SerializeObject(o)); - byte[] bytes = Encoding.UTF8.GetBytes(o.ToString()); + logger.LogTrace($"to-{conn.Id}: {GetFromOrTo(o)} {o}"); + var msg = o.ToString(Formatting.None); + var bytes = Encoding.UTF8.GetBytes(msg); - DevToolsQueue queue = GetQueueForSocket(to); + DevToolsQueue queue = GetQueueForConnection(conn); Task task = queue.Send(bytes, token); if (task != null) await _channelWriter.WriteAsync(task, token); } - private async Task OnEvent(SessionId sessionId, string method, JObject args, CancellationToken token) + protected virtual async Task OnEvent(SessionId sessionId, JObject parms, CancellationToken token) { try { - if (!await AcceptEvent(sessionId, method, args, token)) + if (!await AcceptEvent(sessionId, parms, token)) { + var method = parms["method"].Value(); + var args = parms["params"] as JObject; //logger.LogDebug ("proxy browser: {0}::{1}",method, args); await SendEventInternal(sessionId, method, args, token); } } catch (Exception e) { - side_exception.TrySetException(e); + side_exception.TrySetResult(e); } } - private async Task OnCommand(MessageId id, string method, JObject args, CancellationToken token) + protected virtual async Task OnCommand(MessageId id, JObject parms, CancellationToken token) { try { - if (!await AcceptCommand(id, method, args, token)) + if (!await AcceptCommand(id, parms, token)) { + var method = parms["method"].Value(); + var args = parms["params"] as JObject; Result res = await SendCommandInternal(id, method, args, token); await SendResponseInternal(id, res, token); } } catch (Exception e) { - side_exception.TrySetException(e); + side_exception.TrySetResult(e); } } - private void OnResponse(MessageId id, Result result) + protected virtual void OnResponse(MessageId id, Result result) { //logger.LogTrace ("got id {0} res {1}", id, result); // Fixme @@ -156,52 +123,68 @@ private void OnResponse(MessageId id, Result result) task.SetResult(result); return; } - logger.LogError("Cannot respond to command: {id} with result: {result} - command is not pending", id, result); + logger.LogError($"Cannot respond to command: {id} with result: {result} - command is not pending"); } - private Task ProcessBrowserMessage(string msg, CancellationToken token) + protected virtual Task ProcessBrowserMessage(string msg, CancellationToken token) { - var res = JObject.Parse(msg); + try + { + var res = JObject.Parse(msg); - //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled") - Log("protocol", $"browser: {msg}"); + //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled") + Log("protocol", $"browser: {msg}"); - if (res["id"] == null) - { - return OnEvent(res.ToObject(), res["method"].Value(), res["params"] as JObject, token); + if (res["id"] == null) + { + return OnEvent(res.ToObject(), res, token); + } + else + { + OnResponse(res.ToObject(), Result.FromJson(res)); + return null; + } } - else + catch (Exception ex) { - OnResponse(res.ToObject(), Result.FromJson(res)); - return null; + side_exception.TrySetResult(ex); + throw; } } - private Task ProcessIdeMessage(string msg, CancellationToken token) + protected virtual Task ProcessIdeMessage(string msg, CancellationToken token) { - Log("protocol", $"ide: {msg}"); - if (!string.IsNullOrEmpty(msg)) + try { - var res = JObject.Parse(msg); - var id = res.ToObject(); - return OnCommand( - id, - res["method"].Value(), - res["params"] as JObject, token); - } + Log("protocol", $"ide: {msg}"); + if (!string.IsNullOrEmpty(msg)) + { + var res = JObject.Parse(msg); + var id = res.ToObject(); + return OnCommand( + id, + res, + token); + } - return null; + return null; + } + catch (Exception ex) + { + side_exception.TrySetResult(ex); + throw; + } } - internal async Task SendCommand(SessionId id, string method, JObject args, CancellationToken token) + public virtual async Task SendCommand(SessionId id, string method, JObject args, CancellationToken token) { - //Log ("verbose", $"sending command {method}: {args}"); + // Log ("protocol", $"sending command {method}: {args}"); return await SendCommandInternal(id, method, args, token); } - private async Task SendCommandInternal(SessionId sessionId, string method, JObject args, CancellationToken token) + protected virtual async Task SendCommandInternal(SessionId sessionId, string method, JObject args, CancellationToken token) { - int id = Interlocked.Increment(ref next_cmd_id); + int id = GetNewCmdId(); var o = JObject.FromObject(new { @@ -214,20 +197,19 @@ private async Task SendCommandInternal(SessionId sessionId, string metho var tcs = new TaskCompletionSource(); var msgId = new MessageId(sessionId.sessionId, id); - //Log ("verbose", $"add cmd id {sessionId}-{id}"); pending_cmds[msgId] = tcs; await Send(browser, o, token); return await tcs.Task; } - public Task SendEvent(SessionId sessionId, string method, JObject args, CancellationToken token) + public virtual Task SendEvent(SessionId sessionId, string method, JObject args, CancellationToken token) { - //Log ("verbose", $"sending event {method}: {args}"); + // logger.LogTrace($"sending event {method}: {args}"); return SendEventInternal(sessionId, method, args, token); } - private Task SendEventInternal(SessionId sessionId, string method, JObject args, CancellationToken token) + protected virtual Task SendEventInternal(SessionId sessionId, string method, JObject args, CancellationToken token) { var o = JObject.FromObject(new { @@ -240,12 +222,12 @@ private Task SendEventInternal(SessionId sessionId, string method, JObject args, return Send(ide, o, token); } - internal void SendResponse(MessageId id, Result result, CancellationToken token) + public virtual void SendResponse(MessageId id, Result result, CancellationToken token) { SendResponseInternal(id, result, token); } - private Task SendResponseInternal(MessageId id, Result result, CancellationToken token) + protected virtual Task SendResponseInternal(MessageId id, Result result, CancellationToken token) { JObject o = result.ToJObject(id); if (!result.IsOk) @@ -254,29 +236,97 @@ private Task SendResponseInternal(MessageId id, Result result, CancellationToken return Send(this.ide, o, token); } - // , HttpContext context) - public async Task Run(Uri browserUri, WebSocket ideSocket) + public virtual Task ForwardMessageToIde(JObject msg, CancellationToken token) + { + // logger.LogTrace($"to-ide: forwarding {GetFromOrTo(msg)} {msg}"); + return Send(ide, msg, token); + } + + public virtual Task ForwardMessageToBrowser(JObject msg, CancellationToken token) { - Log("debug", $"DevToolsProxy: Starting on {browserUri}"); - using (this.ide = ideSocket) + // logger.LogTrace($"to-browser: forwarding {GetFromOrTo(msg)} {msg}"); + return Send(this.browser, msg, token); + } + + public async Task RunForDevTools(Uri browserUri, WebSocket ideSocket, CancellationTokenSource cts) + { + try + { + logger.LogDebug($"DevToolsProxy: Starting for browser at {browserUri}"); + logger.LogDebug($"DevToolsProxy: Proxy waiting for connection to the browser at {browserUri}"); + + ClientWebSocket browserSocket = new(); + browserSocket.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan; + await browserSocket.ConnectAsync(browserUri, cts.Token); + + using var ideConn = new DevToolsDebuggerConnection(ideSocket, "ide", logger); + using var browserConn = new DevToolsDebuggerConnection(browserSocket, "browser", logger); + + await StartRunLoop(ideConn: ideConn, browserConn: browserConn, cts); + } + catch (Exception ex) + { + logger.LogError($"DevToolsProxy.Run: {ex}"); + throw; + } + } + + protected Task StartRunLoop(WasmDebuggerConnection ideConn, WasmDebuggerConnection browserConn, CancellationTokenSource cts) + => Task.Run(async () => { - Log("verbose", $"DevToolsProxy: IDE waiting for connection on {browserUri}"); - queues.Add(new DevToolsQueue(this.ide)); - using (this.browser = new ClientWebSocket()) + try + { + RunLoopExitState exitState; + + try + { + Stopped = await RunLoopActual(ideConn, browserConn, cts); + exitState = Stopped; + } + catch (Exception ex) + { + logger.LogDebug($"RunLoop threw an exception: {ex}"); + Stopped = new(RunLoopStopReason.Exception, ex); + RunLoopStopped?.Invoke(this, Stopped); + return; + } + + try + { + logger.LogDebug($"RunLoop stopped, reason: {exitState}"); + RunLoopStopped?.Invoke(this, Stopped); + } + catch (Exception ex) + { + logger.LogError(ex, $"Invoking RunLoopStopped event ({exitState}) failed with {ex}"); + } + } + finally { - this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan; - await this.browser.ConnectAsync(browserUri, CancellationToken.None); - queues.Add(new DevToolsQueue(this.browser)); + ideConn?.Dispose(); + browserConn?.Dispose(); + } + }); + - Log("verbose", $"DevToolsProxy: Client connected on {browserUri}"); - var x = new CancellationTokenSource(); + private async Task RunLoopActual(WasmDebuggerConnection ideConn, + WasmDebuggerConnection browserConn, + CancellationTokenSource cts) + { + using (ide = ideConn) + { + queues.Add(new DevToolsQueue(ide)); + using (browser = browserConn) + { + queues.Add(new DevToolsQueue(browser)); + var x = cts; List pending_ops = new(); - pending_ops.Add(ReadOne(browser, x.Token)); - pending_ops.Add(ReadOne(ide, x.Token)); + pending_ops.Add(browser.ReadOneAsync(x.Token)); + pending_ops.Add(ide.ReadOneAsync(x.Token)); pending_ops.Add(side_exception.Task); - pending_ops.Add(client_initiated_close.Task); + pending_ops.Add(shutdown_requested.Task); Task readerTask = _channelReader.WaitToReadAsync(x.Token).AsTask(); pending_ops.Add(readerTask); @@ -284,17 +334,35 @@ public async Task Run(Uri browserUri, WebSocket ideSocket) { while (!x.IsCancellationRequested) { - Task completedTask = await Task.WhenAny(pending_ops.ToArray()); + Task completedTask = await Task.WhenAny(pending_ops.ToArray()).ConfigureAwait(false); - if (client_initiated_close.Task.IsCompleted) + if (shutdown_requested.Task.IsCompleted) { - await client_initiated_close.Task.ConfigureAwait(false); - Log("verbose", $"DevToolsProxy: Client initiated close from {browserUri}"); x.Cancel(); + return new(RunLoopStopReason.Shutdown, null); + } + + if (side_exception.Task.IsCompleted) + return new(RunLoopStopReason.Exception, await side_exception.Task); + + if (completedTask.IsFaulted) + { + if (completedTask == pending_ops[0] && !browser.IsConnected) + return new(RunLoopStopReason.HostConnectionClosed, completedTask.Exception); + else if (completedTask == pending_ops[1] && !ide.IsConnected) + return new(RunLoopStopReason.IDEConnectionClosed, completedTask.Exception); - break; + return new(RunLoopStopReason.Exception, completedTask.Exception); } + if (x.IsCancellationRequested) + return new(RunLoopStopReason.Cancelled, null); + + // FIXME: instead of this, iterate through pending_ops, and clear it + // out every time we wake up + if (pending_ops.Where(t => t.IsFaulted).FirstOrDefault() is Task faultedTask) + return new(RunLoopStopReason.Exception, faultedTask.Exception); + if (readerTask.IsCompleted) { while (_channelReader.TryRead(out Task newTask)) @@ -305,13 +373,13 @@ public async Task Run(Uri browserUri, WebSocket ideSocket) pending_ops[4] = _channelReader.WaitToReadAsync(x.Token).AsTask(); } - //logger.LogTrace ("pump {0} {1}", task, pending_ops.IndexOf (task)); + // logger.LogDebug("pump {0} {1}", completedTask, pending_ops.IndexOf (completedTask)); if (completedTask == pending_ops[0]) { - string msg = ((Task)completedTask).Result; + string msg = await (Task)completedTask; if (msg != null) { - pending_ops[0] = ReadOne(browser, x.Token); //queue next read + pending_ops[0] = browser.ReadOneAsync(x.Token); Task newTask = ProcessBrowserMessage(msg, x.Token); if (newTask != null) pending_ops.Add(newTask); @@ -319,10 +387,10 @@ public async Task Run(Uri browserUri, WebSocket ideSocket) } else if (completedTask == pending_ops[1]) { - string msg = ((Task)completedTask).Result; + string msg = await (Task)completedTask; if (msg != null) { - pending_ops[1] = ReadOne(ide, x.Token); //queue next read + pending_ops[1] = ide.ReadOneAsync(x.Token); Task newTask = ProcessIdeMessage(msg, x.Token); if (newTask != null) pending_ops.Add(newTask); @@ -330,8 +398,7 @@ public async Task Run(Uri browserUri, WebSocket ideSocket) } else if (completedTask == pending_ops[2]) { - bool res = ((Task)completedTask).Result; - throw new Exception("side task must always complete with an exception, what's going on???"); + throw await (Task)completedTask; } else { @@ -347,22 +414,48 @@ public async Task Run(Uri browserUri, WebSocket ideSocket) } _channelWriter.Complete(); + if (shutdown_requested.Task.IsCompleted) + return new(RunLoopStopReason.Shutdown, null); + if (x.IsCancellationRequested) + return new(RunLoopStopReason.Cancelled, null); + + return new(RunLoopStopReason.Exception, new InvalidOperationException($"This shouldn't ever get thrown. Unsure why the loop stopped")); } catch (Exception e) { - Log("error", $"DevToolsProxy::Run: Exception {e}"); _channelWriter.Complete(e); - //throw; + throw; } finally { if (!x.IsCancellationRequested) x.Cancel(); + foreach (Task t in pending_ops) + logger.LogDebug($"\t{t}: {t.Status}"); + logger.LogDebug($"browser: {browser.IsConnected}, ide: {ide.IsConnected}"); + + queues?.Clear(); } } } } + public virtual void Shutdown() + { + logger.LogDebug($"Proxy.Shutdown, browser: {browser.IsConnected}, ide: {ide.IsConnected}"); + shutdown_requested.TrySetResult(); + } + + public void Fail(Exception exception) + { + if (side_exception.Task.IsCompleted) + logger.LogError($"Fail requested again with {exception}"); + else + side_exception.TrySetResult(exception); + } + + protected virtual string GetFromOrTo(JObject o) => string.Empty; + protected void Log(string priority, string msg) { switch (priority) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FireforDebuggerProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FireforDebuggerProxy.cs new file mode 100644 index 00000000000000..7f0f6c7f70bef7 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FireforDebuggerProxy.cs @@ -0,0 +1,78 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +#nullable enable + +namespace Microsoft.WebAssembly.Diagnostics; + +public class FirefoxDebuggerProxy : DebuggerProxyBase +{ + private static TcpListener? s_tcpListener; + private static int s_nextId; + internal FirefoxMonoProxy? FirefoxMonoProxy { get; private set; } + + [MemberNotNull(nameof(s_tcpListener))] + public static void StartListener(int proxyPort, ILogger logger) + { + 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}"); + } + } + + public static async Task Run(int browserPort, int proxyPort, ILoggerFactory loggerFactory, ILogger logger) + { + StartListener(proxyPort, logger); + logger.LogInformation($"Expecting firefox to be listening on {browserPort}"); + while (true) + { + TcpClient ideClient = await s_tcpListener.AcceptTcpClientAsync(); + _ = Task.Run(async () => + { + CancellationTokenSource cts = new(); + try + { + int id = Interlocked.Increment(ref s_nextId); + logger.LogInformation($"IDE connected to the proxy, id: {id}"); + var monoProxy = new FirefoxMonoProxy(loggerFactory, id.ToString()); + await monoProxy.RunForFirefox(ideClient: ideClient, browserPort, cts); + } + catch (Exception ex) + { + logger.LogError($"{nameof(FirefoxMonoProxy)} crashed with {ex}"); + throw; + } + finally + { + cts.Cancel(); + } + }, CancellationToken.None) + .ConfigureAwait(false); + } + } + + public async Task RunForTests(int browserPort, int proxyPort, string testId, ILoggerFactory loggerFactory, ILogger logger, CancellationTokenSource cts) + { + StartListener(proxyPort, logger); + + TcpClient ideClient = await s_tcpListener.AcceptTcpClientAsync(cts.Token); + FirefoxMonoProxy = new FirefoxMonoProxy(loggerFactory, testId); + FirefoxMonoProxy.RunLoopStopped += (_, args) => ExitState = args; + await FirefoxMonoProxy.RunForFirefox(ideClient: ideClient, browserPort, cts); + } + + public override void Shutdown() => FirefoxMonoProxy?.Shutdown(); + public override void Fail(Exception ex) => FirefoxMonoProxy?.Fail(ex); +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxExecutionContext.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxExecutionContext.cs new file mode 100644 index 00000000000000..bf975027f88ea0 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxExecutionContext.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; + +#nullable enable + +namespace Microsoft.WebAssembly.Diagnostics; + +internal sealed class FirefoxExecutionContext : ExecutionContext +{ + public string? ActorName { get; set; } + public string? ThreadName { get; set; } + public string? GlobalName { get; set; } + + public FirefoxExecutionContext(MonoSDBHelper sdbAgent, int id, string actorName) : base(sdbAgent, id, actorName) + { + ActorName = actorName; + } + + private int evaluateExpressionResultId; + + public int GetResultID() + { + return Interlocked.Increment(ref evaluateExpressionResultId); + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMessageId.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMessageId.cs new file mode 100644 index 00000000000000..cc7108dfbdea8c --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMessageId.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.WebAssembly.Diagnostics; + +public class FirefoxMessageId : MessageId +{ + public readonly string toId; + + public FirefoxMessageId(string? sessionId, int id, string toId) : base(sessionId, id) + { + this.toId = toId; + } + + public static implicit operator SessionId(FirefoxMessageId id) => new SessionId(id.sessionId); + + public override string ToString() => $"msg-{sessionId}:::{id}:::{toId}"; + + public override int GetHashCode() => (sessionId?.GetHashCode() ?? 0) ^ (toId?.GetHashCode() ?? 0) ^ id.GetHashCode(); + + public override bool Equals(object obj) => (obj is FirefoxMessageId) ? ((FirefoxMessageId)obj).sessionId == sessionId && ((FirefoxMessageId)obj).id == id && ((FirefoxMessageId)obj).toId == toId : false; +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs new file mode 100644 index 00000000000000..08e0f41b0128ea --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/Firefox/FirefoxMonoProxy.cs @@ -0,0 +1,1001 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace Microsoft.WebAssembly.Diagnostics; + +internal sealed class FirefoxMonoProxy : MonoProxy +{ + public FirefoxMonoProxy(ILoggerFactory loggerFactory, string loggerId = null) : base(loggerFactory, null, loggerId: loggerId) + { + } + + public FirefoxExecutionContext GetContextFixefox(SessionId sessionId) + { + if (contexts.TryGetValue(sessionId, out ExecutionContext context)) + return context as FirefoxExecutionContext; + throw new ArgumentException($"Invalid Session: \"{sessionId}\"", nameof(sessionId)); + } + + public async Task RunForFirefox(TcpClient ideClient, int portBrowser, CancellationTokenSource cts) + { + TcpClient browserClient = null; + try + { + using var ideConn = new FirefoxDebuggerConnection(ideClient, "ide", logger); + browserClient = new TcpClient(); + using var browserConn = new FirefoxDebuggerConnection(browserClient, "browser", logger); + + logger.LogDebug($"Connecting to the browser at tcp://127.0.0.1:{portBrowser} .."); + await browserClient.ConnectAsync("127.0.0.1", portBrowser); + logger.LogTrace($".. connected to the browser!"); + + await StartRunLoop(ideConn, browserConn, cts); + if (Stopped?.reason == RunLoopStopReason.Exception) + throw Stopped.exception; + } + finally + { + browserClient?.Close(); + ideClient?.Close(); + } + } + + protected override async Task OnEvent(SessionId sessionId, JObject parms, CancellationToken token) + { + try + { + // logger.LogTrace($"OnEvent: {parms}"); + if (!await AcceptEvent(sessionId, parms, token)) + { + await ForwardMessageToIde(parms, token); + } + } + catch (Exception e) + { + side_exception.TrySetException(e); + } + } + + protected override async Task OnCommand(MessageId id, JObject parms, CancellationToken token) + { + try + { + // logger.LogDebug($"OnCommand: id: {id}, {parms}"); + if (!await AcceptCommand(id, parms, token)) + { + await ForwardMessageToBrowser(parms, token); + } + } + catch (Exception e) + { + logger.LogError($"OnCommand for id: {id}, {parms} failed: {e}"); + side_exception.TrySetException(e); + } + } + + protected override void OnResponse(MessageId id, Result result) + { + if (pending_cmds.Remove(id, out TaskCompletionSource task)) + { + task.SetResult(result); + return; + } + logger.LogError($"Cannot respond to command: {id} with result: {result} - command is not pending"); + } + + protected override Task ProcessBrowserMessage(string msg, CancellationToken token) + { + try + { + logger.LogTrace($"from-browser: {msg}"); + var res = JObject.Parse(msg); + if (res["error"] is not null) + logger.LogDebug($"from-browser: {res}"); + + //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled") + + if (res["prototype"] != null || res["frames"] != null) + { + var msgId = new FirefoxMessageId(null, 0, res["from"].Value()); + // if (pending_cmds.ContainsKey(msgId)) + { + // HACK for now, as we don't correctly handle responses yet + OnResponse(msgId, Result.FromJsonFirefox(res)); + } + } + else if (res["resultID"] == null) + { + return OnEvent(res.ToObject(), res, token); + } + else if (res["type"] == null || res["type"].Value() != "evaluationResult") + { + var o = JObject.FromObject(new + { + type = "evaluationResult", + resultID = res["resultID"].Value() + }); + var id = int.Parse(res["resultID"].Value().Split('-')[1]); + var msgId = new MessageId(null, id + 1); + + return SendCommandInternal(msgId, "", o, token); + } + else if (res["result"] is JObject && res["result"]["type"].Value() == "object" && res["result"]["class"].Value() == "Array") + { + var msgIdNew = new FirefoxMessageId(null, 0, res["result"]["actor"].Value()); + var id = int.Parse(res["resultID"].Value().Split('-')[1]); + + var msgId = new FirefoxMessageId(null, id + 1, ""); + var pendingTask = pending_cmds[msgId]; + pending_cmds.Remove(msgId); + pending_cmds.Add(msgIdNew, pendingTask); + return SendCommandInternal(msgIdNew, "", JObject.FromObject(new + { + type = "prototypeAndProperties", + to = res["result"]["actor"].Value() + }), token); + } + else + { + var id = int.Parse(res["resultID"].Value().Split('-')[1]); + var msgId = new FirefoxMessageId(null, id + 1, ""); + if (pending_cmds.ContainsKey(msgId)) + OnResponse(msgId, Result.FromJsonFirefox(res)); + else + return SendCommandInternal(msgId, "", res, token); + return null; + } + //{"type":"evaluationResult","resultID":"1634575904746-0","hasException":false,"input":"ret = 10","result":10,"startTime":1634575904746,"timestamp":1634575904748,"from":"server1.conn21.child10/consoleActor2"} + + return null; + } + catch (Exception ex) + { + // FIXME: using `side_exception` right now because the runloop doesn't + // immediately look at all faulted tasks + logger.LogError(ex.ToString()); + side_exception.TrySetResult(ex); + throw; + } + } + + protected override Task ProcessIdeMessage(string msg, CancellationToken token) + { + try + { + if (!string.IsNullOrEmpty(msg)) + { + var res = JObject.Parse(msg); + Log("protocol", $"from-ide: {GetFromOrTo(res)} {msg}"); + var id = res.ToObject(); + return OnCommand( + id, + res, + token); + } + return null; + } + catch (Exception ex) + { + logger.LogError(ex.ToString()); + side_exception.TrySetResult(ex); + throw; + } + } + + protected override string GetFromOrTo(JObject o) + { + if (o?["to"]?.Value() is string to) + return $"[ to: {to} ]"; + if (o?["from"]?.Value() is string from) + return $"[ from: {from} ]"; + return string.Empty; + } + + protected override async Task SendCommandInternal(SessionId sessionId, string method, JObject args, CancellationToken token) + { + // logger.LogTrace($"SendCommandInternal: to-browser: {method}, {args}"); + if (method != null && method != "") + { + var tcs = new TaskCompletionSource(); + MessageId msgId; + if (method == "evaluateJSAsync") + { + int id = GetNewCmdId(); + msgId = new FirefoxMessageId(sessionId.sessionId, id, ""); + } + else + { + msgId = new FirefoxMessageId(sessionId.sessionId, 0, args["to"].Value()); + } + pending_cmds.Add(msgId, tcs); + await Send(browser, args, token); + + return await tcs.Task; + } + await Send(browser, args, token); + return await Task.FromResult(Result.OkFromObject(new { })); + } + + protected override Task SendEventInternal(SessionId sessionId, string method, JObject args, CancellationToken token) + { + logger.LogTrace($"to-ide {method}: {args}"); + return method != "" + ? Send(ide, new JObject(JObject.FromObject(new { type = method })), token) + : Send(ide, args, token); + } + + protected override async Task AcceptEvent(SessionId sessionId, JObject args, CancellationToken token) + { + if (args["messages"] != null) + { + // FIXME: duplicate, and will miss any non-runtime-ready messages being forwarded + var messages = args["messages"].Value(); + foreach (var message in messages) + { + var messageArgs = message["message"]?["arguments"]?.Value(); + if (messageArgs != null && messageArgs.Count == 2) + { + if (messageArgs[0].Value() == MonoConstants.RUNTIME_IS_READY && messageArgs[1].Value() == MonoConstants.RUNTIME_IS_READY_ID) + { + ResetCmdId(); + await RuntimeReady(sessionId, token); + } + } + } + return true; + } + if (args["frame"] != null && args["type"] == null) + { + OnDefaultContextUpdate(sessionId, new FirefoxExecutionContext(new MonoSDBHelper (this, logger, sessionId), 0, args["frame"]["consoleActor"].Value())); + return false; + } + + if (args["resultID"] != null) + return true; + + if (args["type"] == null) + return await Task.FromResult(false); + switch (args["type"].Value()) + { + case "paused": + { + var ctx = GetContextFixefox(sessionId); + var topFunc = args["frame"]["displayName"].Value(); + switch (topFunc) + { + case "mono_wasm_fire_debugger_agent_message": + case "_mono_wasm_fire_debugger_agent_message": + { + ctx.PausedOnWasm = true; + return await OnReceiveDebuggerAgentEvent(sessionId, args, token); + } + default: + ctx.PausedOnWasm = false; + return false; + } + } + //when debugging from firefox + case "resource-available-form": + { + var messages = args["resources"].Value(); + foreach (var message in messages) + { + if (message["resourceType"].Value() == "thread-state" && message["state"].Value() == "paused") + { + var context = GetContextFixefox(sessionId); + if (context.PausedOnWasm) + { + await SendPauseToBrowser(sessionId, args, token); + return true; + } + } + if (message["resourceType"].Value() != "console-message") + continue; + var messageArgs = message["message"]?["arguments"]?.Value(); + var ctx = GetContextFixefox(sessionId); + ctx.GlobalName = args["from"].Value(); + if (messageArgs != null && messageArgs.Count == 2) + { + if (messageArgs[0].Value() == MonoConstants.RUNTIME_IS_READY && messageArgs[1].Value() == MonoConstants.RUNTIME_IS_READY_ID) + { + ResetCmdId(); + await Task.WhenAll( + ForwardMessageToIde(args, token), + RuntimeReady(sessionId, token)); + } + } + } + break; + } + case "target-available-form": + { + OnDefaultContextUpdate(sessionId, new FirefoxExecutionContext(new MonoSDBHelper (this, logger, sessionId), 0, args["target"]["consoleActor"].Value())); + break; + } + } + return false; + } + + //from ide + protected override async Task AcceptCommand(MessageId sessionId, JObject args, CancellationToken token) + { + if (args["type"] == null) + return false; + + switch (args["type"].Value()) + { + case "resume": + { + if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + return false; + context.PausedOnWasm = false; + if (context.CallStack == null) + return false; + if (args["resumeLimit"] == null || args["resumeLimit"].Type == JTokenType.Null) + { + await OnResume(sessionId, token); + return false; + } + switch (args["resumeLimit"]["type"].Value()) + { + case "next": + await context.SdbAgent.Step(context.ThreadId, StepKind.Over, token); + break; + case "finish": + await context.SdbAgent.Step(context.ThreadId, StepKind.Out, token); + break; + case "step": + await context.SdbAgent.Step(context.ThreadId, StepKind.Into, token); + break; + } + await SendResume(sessionId, token); + return true; + } + case "isAttached": + case "attach": + { + var ctx = GetContextFixefox(sessionId); + ctx.ThreadName = args["to"].Value(); + break; + } + case "source": + { + return await OnGetScriptSource(sessionId, args["to"].Value(), token); + } + case "getBreakableLines": + { + return await OnGetBreakableLines(sessionId, args["to"].Value(), token); + } + case "getBreakpointPositionsCompressed": + { + //{"positions":{"39":[20,28]},"from":"server1.conn2.child10/source27"} + if (args["to"].Value().StartsWith("dotnet://")) + { + var line = new JObject(); + var offsets = new JArray(); + offsets.Add(0); + line.Add(args["query"]["start"]["line"].Value(), offsets); + var o = JObject.FromObject(new + { + positions = line, + from = args["to"].Value() + }); + + await SendEventInternal(sessionId, "", o, token); + return true; + } + break; + } + case "setBreakpoint": + { + if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + return false; + var req = JObject.FromObject(new + { + url = args["location"]["sourceUrl"].Value(), + lineNumber = args["location"]["line"].Value() - 1, + columnNumber = args["location"]["column"].Value() + }); + + var bp = context.BreakpointRequests.Where(request => request.Value.CompareRequest(req)).FirstOrDefault(); + + if (bp.Value != null) + { + bp.Value.UpdateCondition(args["options"]?["condition"]?.Value()); + await SendCommand(sessionId, "", args, token); + return true; + } + + string bpid = Interlocked.Increment(ref context.breakpointId).ToString(); + + if (args["options"]?["condition"]?.Value() != null) + req["condition"] = args["options"]?["condition"]?.Value(); + + var request = BreakpointRequest.Parse(bpid, req); + bool loaded = context.Source.Task.IsCompleted; + + context.BreakpointRequests[bpid] = request; + + if (await IsRuntimeAlreadyReadyAlready(sessionId, token)) + { + DebugStore store = await RuntimeReady(sessionId, token); + + Log("verbose", $"BP req {args}"); + await SetBreakpoint(sessionId, store, request, !loaded, token); + } + await SendCommand(sessionId, "", args, token); + return true; + } + case "removeBreakpoint": + { + if (!contexts.TryGetValue(sessionId, out ExecutionContext context)) + return false; + Result resp = await SendCommand(sessionId, "", args, token); + + var reqToRemove = JObject.FromObject(new + { + url = args["location"]["sourceUrl"].Value(), + lineNumber = args["location"]["line"].Value() - 1, + columnNumber = args["location"]["column"].Value() + }); + + foreach (var req in context.BreakpointRequests.Values) + { + if (req.CompareRequest(reqToRemove)) + { + foreach (var bp in req.Locations) + { + var breakpoint_removed = await context.SdbAgent.RemoveBreakpoint(bp.RemoteId, token); + if (breakpoint_removed) + { + bp.RemoteId = -1; + bp.State = BreakpointState.Disabled; + } + } + } + } + return true; + } + case "prototypeAndProperties": + case "slice": + { + var to = args?["to"].Value().Replace("propertyIterator", ""); + if (!DotnetObjectId.TryParse(to, out DotnetObjectId objectId)) + return false; + var res = await RuntimeGetPropertiesInternal(sessionId, objectId, args, token); + var variables = ConvertToFirefoxContent(res); + var o = JObject.FromObject(new + { + ownProperties = variables, + from = args["to"].Value() + }); + if (args["type"].Value() == "prototypeAndProperties") + o.Add("prototype", GetPrototype(objectId, args)); + await SendEvent(sessionId, "", o, token); + return true; + } + case "prototype": + { + if (!DotnetObjectId.TryParse(args?["to"], out DotnetObjectId objectId)) + return false; + var o = JObject.FromObject(new + { + prototype = GetPrototype(objectId, args), + from = args["to"].Value() + }); + await SendEvent(sessionId, "", o, token); + return true; + } + case "enumSymbols": + { + if (!DotnetObjectId.TryParse(args?["to"], out DotnetObjectId objectId)) + return false; + var o = JObject.FromObject(new + { + type = "symbolIterator", + count = 0, + actor = args["to"].Value() + "symbolIterator" + }); + + var iterator = JObject.FromObject(new + { + iterator = o, + from = args["to"].Value() + }); + + await SendEvent(sessionId, "", iterator, token); + return true; + } + case "enumProperties": + { + //{"iterator":{"type":"propertyIterator","actor":"server1.conn19.child63/propertyIterator73","count":3},"from":"server1.conn19.child63/obj71"} + if (!DotnetObjectId.TryParse(args?["to"], out DotnetObjectId objectId)) + return false; + var res = await RuntimeGetPropertiesInternal(sessionId, objectId, args, token); + var variables = ConvertToFirefoxContent(res); + var o = JObject.FromObject(new + { + type = "propertyIterator", + count = variables.Count, + actor = args["to"].Value() + "propertyIterator" + }); + + var iterator = JObject.FromObject(new + { + iterator = o, + from = args["to"].Value() + }); + + await SendEvent(sessionId, "", iterator, token); + return true; + } + case "getEnvironment": + { + if (!DotnetObjectId.TryParse(args?["to"], out DotnetObjectId objectId)) + return false; + var ctx = GetContextFixefox(sessionId); + if (ctx.CallStack == null) + return false; + Frame scope = ctx.CallStack.FirstOrDefault(s => s.Id == objectId.Value); + var res = await RuntimeGetPropertiesInternal(sessionId, objectId, args, token); + var variables = ConvertToFirefoxContent(res); + var o = JObject.FromObject(new + { + actor = args["to"].Value() + "_0", + type = "function", + scopeKind = "function", + function = new + { + displayName = scope.Method.Name + }, + bindings = new + { + arguments = new JArray(), + variables + }, + from = args["to"].Value() + }); + + await SendEvent(sessionId, "", o, token); + return true; + } + case "frames": + { + ExecutionContext ctx = GetContextFixefox(sessionId); + if (ctx.PausedOnWasm) + { + try + { + await GetFrames(sessionId, ctx, args, token); + return true; + } + catch (Exception) //if the page is refreshed maybe it stops here. + { + await SendResume(sessionId, token); + return true; + } + } + //var ret = await SendCommand(sessionId, "frames", args, token); + //await SendEvent(sessionId, "", ret.Value["result"]["fullContent"] as JObject, token); + return false; + } + case "evaluateJSAsync": + { + var context = GetContextFixefox(sessionId); + if (context.CallStack != null) + { + var resultID = $"runtimeResult-{context.GetResultID()}"; + var o = JObject.FromObject(new + { + resultID, + from = args["to"].Value() + }); + await SendEvent(sessionId, "", o, token); + + Frame scope = context.CallStack.First(); + + var resolver = new MemberReferenceResolver(this, context, sessionId, scope.Id, logger); + JObject retValue = await resolver.Resolve(args?["text"]?.Value(), token); + if (retValue == null) + retValue = await EvaluateExpression.CompileAndRunTheExpression(args?["text"]?.Value(), resolver, token); + var osend = JObject.FromObject(new + { + type = "evaluationResult", + resultID, + hasException = false, + input = args?["text"], + from = args["to"].Value() + }); + if (retValue["type"].Value() == "object") + { + osend["result"] = JObject.FromObject(new + { + type = retValue["type"], + @class = retValue["className"], + description = retValue["description"], + actor = retValue["objectId"], + }); + } + else + { + osend["result"] = retValue["value"]; + osend["resultType"] = retValue["type"]; + osend["resultDescription"] = retValue["description"]; + } + await SendEvent(sessionId, "", osend, token); + } + else + { + var ret = await SendCommand(sessionId, "evaluateJSAsync", args, token); + var o = JObject.FromObject(new + { + resultID = ret.FullContent["resultID"], + from = args["to"].Value() + }); + await SendEvent(sessionId, "", o, token); + await SendEvent(sessionId, "", ret.FullContent, token); + } + return true; + } + case "DotnetDebugger.getMethodLocation": + { + var ret = await GetMethodLocation(sessionId, args, token); + ret.Value["from"] = "internal"; + await SendEvent(sessionId, "", ret.Value, token); + return true; + } + default: + return false; + } + return false; + } + + private async Task SendPauseToBrowser(SessionId sessionId, JObject args, CancellationToken token) + { + Result res = await SendMonoCommand(sessionId, MonoCommands.GetDebuggerAgentBufferReceived(RuntimeId), token); + if (!res.IsOk) + return false; + + var context = GetContextFixefox(sessionId); + byte[] newBytes = Convert.FromBase64String(res.Value?["result"]?["value"]?["value"]?.Value()); + using var retDebuggerCmdReader = new MonoBinaryReader(newBytes); + retDebuggerCmdReader.ReadBytes(11); + retDebuggerCmdReader.ReadByte(); + var number_of_events = retDebuggerCmdReader.ReadInt32(); + var event_kind = (EventKind)retDebuggerCmdReader.ReadByte(); + if (event_kind == EventKind.Step) + context.PauseKind = "resumeLimit"; + else if (event_kind == EventKind.Breakpoint) + context.PauseKind = "breakpoint"; + + args["resources"][0]["why"]["type"] = context.PauseKind; + await SendEvent(sessionId, "", args, token); + return true; + } + + private static JObject GetPrototype(DotnetObjectId objectId, JObject args) + { + var o = JObject.FromObject(new + { + type = "object", + @class = "Object", + actor = args?["to"], + from = args?["to"] + }); + return o; + } + + private static JObject ConvertToFirefoxContent(ValueOrError res) + { + JObject variables = new JObject(); + //TODO check if res.Error and do something + foreach (var variable in res.Value) + { + JObject variableDesc; + if (variable["get"] != null) + { + variableDesc = JObject.FromObject(new + { + value = JObject.FromObject(new + { + @class = variable["value"]?["className"]?.Value(), + value = variable["value"]?["description"]?.Value(), + actor = variable["get"]["objectId"].Value(), + type = "function" + }), + enumerable = true, + configurable = false, + actor = variable["get"]["objectId"].Value() + }); + } + else if (variable["value"]["objectId"] != null) + { + variableDesc = JObject.FromObject(new + { + value = JObject.FromObject(new + { + @class = variable["value"]?["className"]?.Value(), + value = variable["value"]?["description"]?.Value(), + actor = variable["value"]["objectId"].Value(), + type = "object" + }), + enumerable = true, + configurable = false, + actor = variable["value"]["objectId"].Value() + }); + } + else + { + variableDesc = JObject.FromObject(new + { + writable = variable["writable"], + enumerable = true, + configurable = false, + type = variable["value"]?["type"]?.Value() + }); + if (variable["value"]["value"].Type != JTokenType.Null) + variableDesc.Add("value", variable["value"]["value"]); + else //{"type":"null"} + { + variableDesc.Add("value", JObject.FromObject(new { + type = "null", + @class = variable["value"]["className"] + })); + } + } + variables.Add(variable["name"].Value(), variableDesc); + } + return variables; + } + + protected override async Task SendResume(SessionId id, CancellationToken token) + { + var ctx = GetContextFixefox(id); + await SendCommand(id, "", JObject.FromObject(new + { + to = ctx.ThreadName, + type = "resume" + }), token); + } + + internal override Task SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) + { + var ctx = GetContextFixefox(id); + var o = JObject.FromObject(new + { + to = ctx.ActorName, + type = "evaluateJSAsync", + text = cmd.expression, + options = new { eager = true, mapped = new { await = true } } + }); + return SendCommand(id, "evaluateJSAsync", o, token); + } + + internal override async Task OnSourceFileAdded(SessionId sessionId, SourceFile source, ExecutionContext context, CancellationToken token) + { + //different behavior when debugging from VSCode and from Firefox + var ctx = context as FirefoxExecutionContext; + logger.LogTrace($"sending {source.Url} {context.Id} {sessionId.sessionId}"); + var obj = JObject.FromObject(new + { + actor = source.SourceId.ToString(), + extensionName = (string)null, + url = source.Url, + isBlackBoxed = false, + introductionType = "scriptElement", + resourceType = "source", + dotNetUrl = source.DotNetUrl + }); + JObject sourcesJObj; + if (!string.IsNullOrEmpty(ctx.GlobalName)) + { + sourcesJObj = JObject.FromObject(new + { + type = "resource-available-form", + resources = new JArray(obj), + from = ctx.GlobalName + }); + } + else + { + sourcesJObj = JObject.FromObject(new + { + type = "newSource", + source = obj, + from = ctx.ThreadName + }); + } + await SendEvent(sessionId, "", sourcesJObj, token); + + foreach (var req in context.BreakpointRequests.Values) + { + if (req.TryResolve(source)) + { + await SetBreakpoint(sessionId, context.store, req, true, token); + } + } + } + + protected override async Task SendCallStack(SessionId sessionId, ExecutionContext context, string reason, int thread_id, Breakpoint bp, JObject data, JObject args, EventKind event_kind, CancellationToken token) + { + Frame frame = null; + var commandParamsWriter = new MonoBinaryWriter(); + commandParamsWriter.Write(thread_id); + commandParamsWriter.Write(0); + commandParamsWriter.Write(1); + var retDebuggerCmdReader = await context.SdbAgent.SendDebuggerAgentCommand(CmdThread.GetFrameInfo, commandParamsWriter, token); + var frame_count = retDebuggerCmdReader.ReadInt32(); + if (frame_count > 0) + { + var frame_id = retDebuggerCmdReader.ReadInt32(); + var methodId = retDebuggerCmdReader.ReadInt32(); + var il_pos = retDebuggerCmdReader.ReadInt32(); + retDebuggerCmdReader.ReadByte(); + var method = await context.SdbAgent.GetMethodInfo(methodId, token); + if (method is null) + return false; + + if (await ShouldSkipMethod(sessionId, context, event_kind, 0, method, token)) + { + await SendResume(sessionId, token); + return true; + } + + SourceLocation location = method?.Info.GetLocationByIl(il_pos); + if (location == null) + { + return false; + } + + Log("debug", $"frame il offset: {il_pos} method token: {method.Info.Token} assembly name: {method.Info.Assembly.Name}"); + Log("debug", $"\tmethod {method.Name} location: {location}"); + frame = new Frame(method, location, frame_id); + context.CallStack = new List(); + context.CallStack.Add(frame); + } + if (!await EvaluateCondition(sessionId, context, frame, bp, token)) + { + context.ClearState(); + await SendResume(sessionId, token); + return true; + } + + args["why"]["type"] = context.PauseKind; + + await SendEvent(sessionId, "", args, token); + return true; + } + + private async Task GetFrames(SessionId sessionId, ExecutionContext context, JObject args, CancellationToken token) + { + var ctx = context as FirefoxExecutionContext; + var orig_callframes = await SendCommand(sessionId, "frames", args, token); + + var callFrames = new List(); + var frames = new List(); + var commandParamsWriter = new MonoBinaryWriter(); + commandParamsWriter.Write(context.ThreadId); + commandParamsWriter.Write(0); + commandParamsWriter.Write(-1); + var retDebuggerCmdReader = await context.SdbAgent.SendDebuggerAgentCommand(CmdThread.GetFrameInfo, commandParamsWriter, token); + var frame_count = retDebuggerCmdReader.ReadInt32(); + for (int j = 0; j < frame_count; j++) + { + var frame_id = retDebuggerCmdReader.ReadInt32(); + var methodId = retDebuggerCmdReader.ReadInt32(); + var il_pos = retDebuggerCmdReader.ReadInt32(); + retDebuggerCmdReader.ReadByte(); + MethodInfoWithDebugInformation method = await context.SdbAgent.GetMethodInfo(methodId, token); + if (method is null) + continue; + + SourceLocation location = method.Info?.GetLocationByIl(il_pos); + if (location == null) + { + continue; + } + + Log("debug", $"frame il offset: {il_pos} method token: {method.Info.Token} assembly name: {method.Info.Assembly.Name}"); + Log("debug", $"\tmethod {method.Name} location: {location}"); + frames.Add(new Frame(method, location, frame_id)); + + var frameItem = JObject.FromObject(new + { + actor = $"dotnet:scope:{frame_id}", + displayName = method.Name, + type = "call", + state = "on-stack", + asyncCause = (string)null, + where = new + { + actor = location.Id.ToString(), + line = location.Line + 1, + column = location.Column + } + }); + if (j > 0) + frameItem.Add("depth", j); + callFrames.Add(frameItem); + + context.CallStack = frames; + } + foreach (JObject frame in orig_callframes.Value["result"]?["value"]?["frames"]) + { + string function_name = frame["displayName"]?.Value(); + if (function_name != null && !(function_name.StartsWith("Module._mono_wasm", StringComparison.Ordinal) || + function_name.StartsWith("Module.mono_wasm", StringComparison.Ordinal) || + function_name == "mono_wasm_fire_debugger_agent_message" || + function_name == "_mono_wasm_fire_debugger_agent_message" || + function_name == "(wasmcall)")) + { + callFrames.Add(frame); + } + } + var o = JObject.FromObject(new + { + frames = callFrames, + from = ctx.ThreadName + }); + + await SendEvent(sessionId, "", o, token); + return false; + } + internal async Task OnGetBreakableLines(MessageId msg_id, string script_id, CancellationToken token) + { + if (!SourceId.TryParse(script_id, out SourceId id)) + return false; + + SourceFile src_file = (await LoadStore(msg_id, token)).GetFileById(id); + + await SendEvent(msg_id, "", JObject.FromObject(new { lines = src_file.BreakableLines.ToArray(), from = script_id }), token); + return true; + } + + internal override async Task OnGetScriptSource(MessageId msg_id, string script_id, CancellationToken token) + { + if (!SourceId.TryParse(script_id, out SourceId id)) + return false; + + SourceFile src_file = (await LoadStore(msg_id, token)).GetFileById(id); + + try + { + var uri = new Uri(src_file.Url); + string source = $"// Unable to find document {src_file.SourceUri}"; + + using (Stream data = await src_file.GetSourceAsync(checkHash: false, token: token)) + { + if (data.Length == 0) + return false; + + using (var reader = new StreamReader(data)) + source = await reader.ReadToEndAsync(); + } + await SendEvent(msg_id, "", JObject.FromObject(new { source, from = script_id }), token); + } + catch (Exception e) + { + var o = JObject.FromObject(new + { + source = $"// Unable to read document ({e.Message})\n" + + $"Local path: {src_file?.SourceUri}\n" + + $"SourceLink path: {src_file?.SourceLinkUri}\n", + from = script_id + }); + + await SendEvent(msg_id, "", o, token); + } + return true; + } + +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 55c65e87f0a1c8..bd08211f3af27c 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -16,12 +15,12 @@ namespace Microsoft.WebAssembly.Diagnostics { - internal sealed class MonoProxy : DevToolsProxy + internal class MonoProxy : DevToolsProxy { private IList urlSymbolServerList; private static HttpClient client = new HttpClient(); private HashSet sessions = new HashSet(); - private Dictionary contexts = new Dictionary(); + protected Dictionary contexts = new Dictionary(); private const string sPauseOnUncaught = "pause_on_uncaught"; private const string sPauseOnCaught = "pause_on_caught"; // index of the runtime in a same JS page/process @@ -49,7 +48,7 @@ private bool UpdateContext(SessionId sessionId, ExecutionContext executionContex return previous; } - internal Task SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token); + internal virtual Task SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token); internal void SendLog(SessionId sessionId, string message, CancellationToken token, string type = "warning") { @@ -79,8 +78,10 @@ internal void SendLog(SessionId sessionId, string message, CancellationToken tok SendEvent(sessionId, "Runtime.consoleAPICalled", o, token); } - protected override async Task AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token) + protected override async Task AcceptEvent(SessionId sessionId, JObject parms, CancellationToken token) { + var method = parms["method"].Value(); + var args = parms["params"] as JObject; switch (method) { case "Runtime.consoleAPICalled": @@ -95,7 +96,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth int aCount = a.Count(); if (aCount >= 2 && a[0]?["value"]?.ToString() == MonoConstants.RUNTIME_IS_READY && - a[1]?["value"]?.ToString() == "fe00e07a-5519-4dfe-b35a-f867dbaf2e28") + a[1]?["value"]?.ToString() == MonoConstants.RUNTIME_IS_READY_ID) { if (aCount > 2) { @@ -189,14 +190,14 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth string exceptionError = args?["data"]?["value"]?.Value(); if (exceptionError == sPauseOnUncaught) { - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); if (context.PauseOnExceptions == PauseOnExceptionsKind.Unset) context.PauseOnExceptions = PauseOnExceptionsKind.Uncaught; return true; } if (exceptionError == sPauseOnCaught) { - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); context.PauseOnExceptions = PauseOnExceptionsKind.All; return true; } @@ -211,7 +212,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth case "_mono_wasm_runtime_ready": { await RuntimeReady(sessionId, token); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); return true; } case "mono_wasm_fire_debugger_agent_message": @@ -222,7 +223,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth } catch (Exception) //if the page is refreshed maybe it stops here. { - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); return true; } } @@ -249,7 +250,7 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth return true; } } - Log("verbose", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}"); + logger.LogTrace($"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}"); break; } @@ -269,8 +270,11 @@ protected override async Task AcceptEvent(SessionId sessionId, string meth return false; } - - private async Task IsRuntimeAlreadyReadyAlready(SessionId sessionId, CancellationToken token) + protected virtual async Task SendResume(SessionId id, CancellationToken token) + { + await SendCommand(id, "Debugger.resume", new JObject(), token); + } + protected async Task IsRuntimeAlreadyReadyAlready(SessionId sessionId, CancellationToken token) { if (contexts.TryGetValue(sessionId, out ExecutionContext context) && context.IsRuntimeReady) return true; @@ -279,8 +283,10 @@ private async Task IsRuntimeAlreadyReadyAlready(SessionId sessionId, Cance return res.Value?["result"]?["value"]?.Value() ?? false; } - protected override async Task AcceptCommand(MessageId id, string method, JObject args, CancellationToken token) + protected override async Task AcceptCommand(MessageId id, JObject parms, CancellationToken token) { + var method = parms["method"].Value(); + var args = parms["params"] as JObject; // Inspector doesn't use the Target domain or sessions // so we try to init immediately if (id == SessionId.Null) @@ -539,57 +545,7 @@ protected override async Task AcceptCommand(MessageId id, string method, J } case "DotnetDebugger.getMethodLocation": { - DebugStore store = await RuntimeReady(id, token); - string aname = args["assemblyName"]?.Value(); - string typeName = args["typeName"]?.Value(); - string methodName = args["methodName"]?.Value(); - if (aname == null || typeName == null || methodName == null) - { - SendResponse(id, Result.Err("Invalid protocol message '" + args + "'."), token); - return true; - } - - // GetAssemblyByName seems to work on file names - AssemblyInfo assembly = store.GetAssemblyByName(aname); - if (assembly == null) - assembly = store.GetAssemblyByName(aname + ".exe"); - if (assembly == null) - assembly = store.GetAssemblyByName(aname + ".dll"); - if (assembly == null) - { - SendResponse(id, Result.Err("Assembly '" + aname + "' not found."), token); - return true; - } - - TypeInfo type = assembly.GetTypeByName(typeName); - if (type == null) - { - SendResponse(id, Result.Err($"Type '{typeName}' not found."), token); - return true; - } - - MethodInfo methodInfo = type.Methods.FirstOrDefault(m => m.Name == methodName); - if (methodInfo == null) - { - // Maybe this is an async method, in which case the debug info is attached - // to the async method implementation, in class named: - // `{type_name}.::MoveNext` - methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}.<{methodName}>"))? - .Methods.FirstOrDefault(mi => mi.Name == "MoveNext"); - } - - if (methodInfo == null) - { - SendResponse(id, Result.Err($"Method '{typeName}:{methodName}' not found."), token); - return true; - } - - string src_url = methodInfo.Assembly.Sources.Single(sf => sf.SourceId == methodInfo.SourceId).Url; - SendResponse(id, Result.OkFromObject(new - { - result = new { line = methodInfo.StartLocation.Line, column = methodInfo.StartLocation.Column, url = src_url } - }), token); - + SendResponse(id, await GetMethodLocation(id, args, token), token); return true; } case "Runtime.callFunctionOn": @@ -647,6 +603,57 @@ private void SetJustMyCode(MessageId id, JObject args, CancellationToken token) JustMyCode = isEnabled.Value; SendResponse(id, Result.OkFromObject(new { justMyCodeEnabled = JustMyCode }), token); } + internal async Task GetMethodLocation(MessageId id, JObject args, CancellationToken token) + { + DebugStore store = await RuntimeReady(id, token); + string aname = args["assemblyName"]?.Value(); + string typeName = args["typeName"]?.Value(); + string methodName = args["methodName"]?.Value(); + if (aname == null || typeName == null || methodName == null) + { + return Result.Err("Invalid protocol message '" + args + "'."); + } + + // GetAssemblyByName seems to work on file names + AssemblyInfo assembly = store.GetAssemblyByName(aname); + if (assembly == null) + assembly = store.GetAssemblyByName(aname + ".exe"); + if (assembly == null) + assembly = store.GetAssemblyByName(aname + ".dll"); + if (assembly == null) + { + return Result.Err("Assembly '" + aname + "' not found."); + } + + TypeInfo type = assembly.GetTypeByName(typeName); + if (type == null) + { + return Result.Err($"Type '{typeName}' not found."); + } + + MethodInfo methodInfo = type.Methods.FirstOrDefault(m => m.Name == methodName); + if (methodInfo == null) + { + // Maybe this is an async method, in which case the debug info is attached + // to the async method implementation, in class named: + // `{type_name}.::MoveNext` + methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}.<{methodName}>"))? + .Methods.FirstOrDefault(mi => mi.Name == "MoveNext"); + } + + if (methodInfo == null) + { + return Result.Err($"Method '{typeName}:{methodName}' not found."); + } + + string src_url = methodInfo.Assembly.Sources.Single(sf => sf.SourceId == methodInfo.SourceId).Url; + + return Result.OkFromObject(new + { + result = new { line = methodInfo.StartLocation.Line, column = methodInfo.StartLocation.Column, url = src_url } + }); + } + private async Task CallOnFunction(MessageId id, JObject args, CancellationToken token) { var context = GetContext(id); @@ -811,7 +818,7 @@ internal async Task> RuntimeGetPropertiesInternal(SessionId } } - private async Task EvaluateCondition(SessionId sessionId, ExecutionContext context, Frame mono_frame, Breakpoint bp, CancellationToken token) + protected async Task EvaluateCondition(SessionId sessionId, ExecutionContext context, Frame mono_frame, Breakpoint bp, CancellationToken token) { if (string.IsNullOrEmpty(bp?.Condition) || mono_frame == null) return true; @@ -885,8 +892,66 @@ private async Task SendBreakpointsOfMethodUpdated(SessionId sessionId, Exe return true; } - private async Task SendCallStack(SessionId sessionId, ExecutionContext context, string reason, int thread_id, Breakpoint bp, JObject data, IEnumerable orig_callframes, EventKind event_kind, CancellationToken token) + protected virtual async Task ShouldSkipMethod(SessionId sessionId, ExecutionContext context, EventKind event_kind, int j, MethodInfoWithDebugInformation method, CancellationToken token) + { + var shouldReturn = await SkipMethod( + isSkippable: context.IsSkippingHiddenMethod, + shouldBeSkipped: event_kind != EventKind.UserBreak, + StepKind.Over); + context.IsSkippingHiddenMethod = false; + if (shouldReturn) + return true; + + shouldReturn = await SkipMethod( + isSkippable: context.IsSteppingThroughMethod, + shouldBeSkipped: event_kind != EventKind.UserBreak && event_kind != EventKind.Breakpoint, + StepKind.Over); + context.IsSteppingThroughMethod = false; + if (shouldReturn) + return true; + + if (j == 0 && method?.Info.DebuggerAttrInfo.DoAttributesAffectCallStack(JustMyCode) == true) + { + if (method.Info.DebuggerAttrInfo.ShouldStepOut(event_kind)) + { + if (event_kind == EventKind.Step) + context.IsSkippingHiddenMethod = true; + if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) + return true; + } + if (!method.Info.DebuggerAttrInfo.HasStepperBoundary) + { + if (event_kind == EventKind.Step || + (JustMyCode && (event_kind == EventKind.Breakpoint || event_kind == EventKind.UserBreak))) + { + if (context.IsResumedAfterBp) + context.IsResumedAfterBp = false; + else if (event_kind != EventKind.UserBreak) + context.IsSteppingThroughMethod = true; + if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) + return true; + } + if (event_kind == EventKind.Breakpoint) + context.IsResumedAfterBp = true; + } + } + return false; + async Task SkipMethod(bool isSkippable, bool shouldBeSkipped, StepKind stepKind) + { + if (isSkippable && shouldBeSkipped) + { + await context.SdbAgent.Step(context.ThreadId, stepKind, token); + await SendResume(sessionId, token); + return true; + } + return false; + } + } + + + protected virtual async Task SendCallStack(SessionId sessionId, ExecutionContext context, string reason, int thread_id, Breakpoint bp, JObject data, JObject args, EventKind event_kind, CancellationToken token) { + var orig_callframes = args?["callFrames"]?.Values(); var callFrames = new List(); var frames = new List(); using var commandParamsWriter = new MonoBinaryWriter(); @@ -904,48 +969,9 @@ private async Task SendCallStack(SessionId sessionId, ExecutionContext con DebugStore store = await LoadStore(sessionId, token); var method = await context.SdbAgent.GetMethodInfo(methodId, token); - var shouldReturn = await SkipMethod( - isSkippable: context.IsSkippingHiddenMethod, - shouldBeSkipped: event_kind != EventKind.UserBreak, - StepKind.Over); - context.IsSkippingHiddenMethod = false; - if (shouldReturn) - return true; - - shouldReturn = await SkipMethod( - isSkippable: context.IsSteppingThroughMethod, - shouldBeSkipped: event_kind != EventKind.UserBreak && event_kind != EventKind.Breakpoint, - StepKind.Over); - context.IsSteppingThroughMethod = false; - if (shouldReturn) + if (await ShouldSkipMethod(sessionId, context, event_kind, j, method, token)) return true; - if (j == 0 && method?.Info.DebuggerAttrInfo.DoAttributesAffectCallStack(JustMyCode) == true) - { - if (method.Info.DebuggerAttrInfo.ShouldStepOut(event_kind)) - { - if (event_kind == EventKind.Step) - context.IsSkippingHiddenMethod = true; - if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) - return true; - } - if (!method.Info.DebuggerAttrInfo.HasStepperBoundary) - { - if (event_kind == EventKind.Step || - (JustMyCode && (event_kind == EventKind.Breakpoint || event_kind == EventKind.UserBreak))) - { - if (context.IsResumedAfterBp) - context.IsResumedAfterBp = false; - else if (event_kind != EventKind.UserBreak) - context.IsSteppingThroughMethod = true; - if (await SkipMethod(isSkippable: true, shouldBeSkipped: true, StepKind.Out)) - return true; - } - if (event_kind == EventKind.Breakpoint) - context.IsResumedAfterBp = true; - } - } - SourceLocation location = method?.Info.GetLocationByIl(il_pos); // When hitting a breakpoint on the "IncrementCount" method in the standard @@ -991,7 +1017,6 @@ private async Task SendCallStack(SessionId sessionId, ExecutionContext con }); context.CallStack = frames; - context.ThreadId = thread_id; } string[] bp_list = new string[bp == null ? 0 : 1]; if (bp != null) @@ -1019,25 +1044,14 @@ private async Task SendCallStack(SessionId sessionId, ExecutionContext con if (!await EvaluateCondition(sessionId, context, context.CallStack.First(), bp, token)) { context.ClearState(); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); return true; } await SendEvent(sessionId, "Debugger.paused", o, token); return true; - - async Task SkipMethod(bool isSkippable, bool shouldBeSkipped, StepKind stepKind) - { - if (isSkippable && shouldBeSkipped) - { - await context.SdbAgent.Step(context.ThreadId, stepKind, token); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); - return true; - } - return false; - } } - private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObject args, CancellationToken token) + internal async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObject args, CancellationToken token) { Result res = await SendMonoCommand(sessionId, MonoCommands.GetDebuggerAgentBufferReceived(RuntimeId), token); if (!res.IsOk) @@ -1055,18 +1069,19 @@ private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObjec if (event_kind == EventKind.Step) await context.SdbAgent.ClearSingleStep(request_id, token); int thread_id = retDebuggerCmdReader.ReadInt32(); + context.ThreadId = thread_id; switch (event_kind) { case EventKind.MethodUpdate: { var ret = await SendBreakpointsOfMethodUpdated(sessionId, context, retDebuggerCmdReader, token); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); return ret; } case EventKind.EnC: { var ret = await ProcessEnC(sessionId, context, retDebuggerCmdReader, token); - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); return ret; } case EventKind.Exception: @@ -1086,13 +1101,17 @@ private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObjec objectId = $"dotnet:object:{object_id}" }); - var ret = await SendCallStack(sessionId, context, reason, thread_id, null, data, args?["callFrames"]?.Values(), event_kind, token); + var ret = await SendCallStack(sessionId, context, reason, thread_id, null, data, args, event_kind, token); return ret; } case EventKind.UserBreak: case EventKind.Step: case EventKind.Breakpoint: { + if (event_kind == EventKind.Step) + context.PauseKind = "resumeLimit"; + else if (event_kind == EventKind.Breakpoint) + context.PauseKind = "breakpoint"; Breakpoint bp = context.BreakpointRequests.Values.SelectMany(v => v.Locations).FirstOrDefault(b => b.RemoteId == request_id); if (request_id == context.TempBreakpointForSetNextIP) { @@ -1103,7 +1122,7 @@ private async Task OnReceiveDebuggerAgentEvent(SessionId sessionId, JObjec int methodId = 0; if (event_kind != EventKind.UserBreak) methodId = retDebuggerCmdReader.ReadInt32(); - var ret = await SendCallStack(sessionId, context, reason, thread_id, bp, null, args?["callFrames"]?.Values(), event_kind, token); + var ret = await SendCallStack(sessionId, context, reason, thread_id, bp, null, args, event_kind, token); return ret; } } @@ -1154,9 +1173,8 @@ internal async Task LoadSymbolsOnDemand(AssemblyInfo asm, int method return null; } - private async Task OnDefaultContext(SessionId sessionId, ExecutionContext context, CancellationToken token) + protected void OnDefaultContextUpdate(SessionId sessionId, ExecutionContext context) { - Log("verbose", "Default context created, clearing state and sending events"); if (UpdateContext(sessionId, context, out ExecutionContext previousContext)) { foreach (KeyValuePair kvp in previousContext.BreakpointRequests) @@ -1165,12 +1183,17 @@ private async Task OnDefaultContext(SessionId sessionId, ExecutionContext contex } context.PauseOnExceptions = previousContext.PauseOnExceptions; } + } + protected async Task OnDefaultContext(SessionId sessionId, ExecutionContext context, CancellationToken token) + { + Log("verbose", "Default context created, clearing state and sending events"); + OnDefaultContextUpdate(sessionId, context); if (await IsRuntimeAlreadyReadyAlready(sessionId, token)) await RuntimeReady(sessionId, token); } - private async Task OnResume(MessageId msg_id, CancellationToken token) + protected async Task OnResume(MessageId msg_id, CancellationToken token) { ExecutionContext context = GetContext(msg_id); if (context.CallStack != null) @@ -1183,7 +1206,8 @@ private async Task OnResume(MessageId msg_id, CancellationToken token) GetContext(msg_id).ClearState(); } - private async Task Step(MessageId msgId, StepKind kind, CancellationToken token) + + protected async Task Step(MessageId msgId, StepKind kind, CancellationToken token) { ExecutionContext context = GetContext(msgId); if (context.CallStack == null) @@ -1203,7 +1227,7 @@ private async Task Step(MessageId msgId, StepKind kind, CancellationToken context.ClearState(); - await SendCommand(msgId, "Debugger.resume", new JObject(), token); + await SendResume(msgId, token); return true; } @@ -1370,10 +1394,10 @@ private async Task SetMonoBreakpoint(SessionId sessionId, string req return bp; } - private async Task OnSourceFileAdded(SessionId sessionId, SourceFile source, ExecutionContext context, CancellationToken token) + internal virtual async Task OnSourceFileAdded(SessionId sessionId, SourceFile source, ExecutionContext context, CancellationToken token) { JObject scriptSource = JObject.FromObject(source.ToScriptSource(context.Id, context.AuxData)); - Log("debug", $"sending {source.Url} {context.Id} {sessionId.sessionId}"); + // Log("debug", $"sending {source.Url} {context.Id} {sessionId.sessionId}"); await SendEvent(sessionId, "Debugger.scriptParsed", scriptSource, token); foreach (var req in context.BreakpointRequests.Values) @@ -1435,7 +1459,7 @@ async Task GetLoadedFiles(SessionId sessionId, ExecutionContext contex } } - private async Task RuntimeReady(SessionId sessionId, CancellationToken token) + protected async Task RuntimeReady(SessionId sessionId, CancellationToken token) { ExecutionContext context = GetContext(sessionId); if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource(), null) != null) @@ -1472,7 +1496,7 @@ private async Task ResetBreakpoint(SessionId msg_id, MethodInfo method, Cancella } } - private async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnCReset, CancellationToken token) + protected async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnCReset, CancellationToken token) { string bpid = args?["breakpointId"]?.Value(); @@ -1498,7 +1522,7 @@ private async Task RemoveBreakpoint(SessionId msg_id, JObject args, bool isEnCRe breakpointRequest.Locations = new List(); } - private async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token) + protected async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token) { ExecutionContext context = GetContext(sessionId); if (req.Locations.Any()) @@ -1612,11 +1636,11 @@ private async Task OnSetNextIP(MessageId sessionId, SourceLocation targetL var breakpointId = await context.SdbAgent.SetBreakpoint(scope.Method.DebugId, ilOffset.Offset, token); context.TempBreakpointForSetNextIP = breakpointId; - await SendCommand(sessionId, "Debugger.resume", new JObject(), token); + await SendResume(sessionId, token); return true; } - private async Task OnGetScriptSource(MessageId msg_id, string script_id, CancellationToken token) + internal virtual async Task OnGetScriptSource(MessageId msg_id, string script_id, CancellationToken token) { if (!SourceId.TryParse(script_id, out SourceId id)) return false; diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs index 23e2e29563db53..a5a6000692b553 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs @@ -17,7 +17,6 @@ using System.Text; using System.Runtime.CompilerServices; using System.Diagnostics; -using System.Reflection.Metadata; namespace Microsoft.WebAssembly.Diagnostics { @@ -986,7 +985,7 @@ internal async Task SendDebuggerAgentCommand(T command, Mon { Result res = await proxy.SendMonoCommand(sessionId, MonoCommands.SendDebuggerAgentCommand(proxy.RuntimeId, GetNewId(), (int)GetCommandSetForCommand(command), (int)(object)command, arguments?.ToBase64().data ?? string.Empty), token); return !res.IsOk && throwOnError - ? throw new DebuggerAgentException($"SendDebuggerAgentCommand failed for {command}") + ? throw new DebuggerAgentException($"SendDebuggerAgentCommand failed for {command}: {res}") : MonoBinaryReader.From(res); } @@ -1848,7 +1847,7 @@ public async Task CreateJObjectForPtr(ElementType etype, MonoBinaryRead else className = "(" + await GetTypeName(typeId, token) + ")"; - int pointerId = 0; + int pointerId = -1; if (valueAddress != 0 && className != "(void*)") { pointerId = Interlocked.Increment(ref debuggerObjectId); diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/RunLoopExitState.cs b/src/mono/wasm/debugger/BrowserDebugProxy/RunLoopExitState.cs new file mode 100644 index 00000000000000..5184ae2e49ce76 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/RunLoopExitState.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.WebAssembly.Diagnostics; + +public record RunLoopExitState(RunLoopStopReason reason, Exception? exception) +{ +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/RunLoopStopReason.cs b/src/mono/wasm/debugger/BrowserDebugProxy/RunLoopStopReason.cs new file mode 100644 index 00000000000000..bf94a2f62ea15d --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/RunLoopStopReason.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.WebAssembly.Diagnostics; + +public enum RunLoopStopReason +{ + Shutdown, + Cancelled, + Exception, + ProxyConnectionClosed, + IDEConnectionClosed, + HostConnectionClosed + +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/WasmHost.cs b/src/mono/wasm/debugger/BrowserDebugProxy/WasmHost.cs new file mode 100644 index 00000000000000..1b487c6b5683d3 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/WasmHost.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.WebAssembly.Diagnostics; + +public enum WasmHost +{ + Chrome, + Firefox +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs index a9e87746bd89ac..2132daa1846625 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs @@ -9,7 +9,7 @@ namespace DebuggerTests { - public class ArrayTests : DebuggerTestBase + public class ArrayTests : DebuggerTests { [Theory] @@ -218,6 +218,15 @@ async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, st string local_var_name_prefix, object[] array, object[] array_elem_props, bool test_prev_frame = false, int frame_idx = 0, bool use_cfo = false) { + // FIXME: + if (!RunningOnChrome) + { + if (use_cfo) + { + await Task.CompletedTask; + return; + } + } var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs"; UseCallFunctionOnBeforeGetProperties = use_cfo; @@ -282,7 +291,7 @@ async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, st await CheckProps(props, new object[0], "${local_var_name_prefix}_arr_empty"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectObjectArrayMembers(bool use_cfo) @@ -470,7 +479,7 @@ await CompareObjectPropertiesFor(frame_locals, "point", TPoint(45, 51, "point#Id", "Green")); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectValueTypeArrayLocalsInAsyncStaticStructMethod(bool use_cfo) @@ -502,7 +511,7 @@ public async Task InspectValueTypeArrayLocalsInAsyncStaticStructMethod(bool use_ }, "InspectValueTypeArrayLocalsInAsyncStaticStructMethod#locals"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectValueTypeArrayLocalsInAsyncInstanceStructMethod(bool use_cfo) @@ -551,7 +560,7 @@ await CompareObjectPropertiesFor(frame_locals, "this", label: "this#0"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] [Trait("Category", "windows-failing")] // https://github.com/dotnet/runtime/issues/65742 [Trait("Category", "linux-failing")] // https://github.com/dotnet/runtime/issues/65742 public async Task InvalidArrayId() => await CheckInspectLocalsAtBreakpointSite( @@ -575,7 +584,7 @@ public async Task InvalidArrayId() => await CheckInspectLocalsAtBreakpointSite( await GetProperties($"dotnet:array:{id.Value}", expect_ok: false); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InvalidAccessors() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.Container", "PlaceholderMethod", 1, "PlaceholderMethod", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers'); }, 1);", @@ -607,7 +616,7 @@ public async Task InvalidAccessors() => await CheckInspectLocalsAtBreakpointSite } }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectPrimitiveTypeMultiArrayLocals(bool use_cfo) @@ -623,13 +632,13 @@ public async Task InspectPrimitiveTypeMultiArrayLocals(bool use_cfo) var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); Assert.Equal(3, locals.Count()); var int_arr_1 = !use_cfo ? - await GetProperties(locals[0]["value"]["objectId"].Value()) : + await GetProperties(locals[0]["value"]["objectId"].Value()) : await GetObjectWithCFO((locals[0]["value"]["objectId"].Value())); CheckNumber(int_arr_1, "0", 0); CheckNumber(int_arr_1, "1", 1); var int_arr_2 = !use_cfo ? - await GetProperties(locals[1]["value"]["objectId"].Value()) : + await GetProperties(locals[1]["value"]["objectId"].Value()) : await GetObjectWithCFO((locals[1]["value"]["objectId"].Value())); CheckNumber(int_arr_2, "0, 0", 0); CheckNumber(int_arr_2, "0, 1", 1); @@ -639,7 +648,7 @@ await GetProperties(locals[1]["value"]["objectId"].Value()) : CheckNumber(int_arr_2, "1, 2", 12); var int_arr_3 = !use_cfo ? - await GetProperties(locals[2]["value"]["objectId"].Value()) : + await GetProperties(locals[2]["value"]["objectId"].Value()) : await GetObjectWithCFO((locals[2]["value"]["objectId"].Value())); CheckNumber(int_arr_3, "0, 0, 0", 0); CheckNumber(int_arr_3, "0, 0, 1", 1); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/AssignmentTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/AssignmentTests.cs index 9e1aec0a65d25a..c2396b6291ff3e 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/AssignmentTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/AssignmentTests.cs @@ -9,7 +9,7 @@ namespace DebuggerTests { - public class AssignmentTests : DebuggerTestBase + public class AssignmentTests : DebuggerTests { public static TheoryData GetTestData => new TheoryData { @@ -40,7 +40,7 @@ public class AssignmentTests : DebuggerTestBase { "MONO_TYPE_R8", TNumber(0), TNumber("3.1415") }, }; - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData("GetTestData")] async Task InspectVariableBeforeAndAfterAssignment(string clazz, JObject checkDefault, JObject checkValue) { diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs index 9a261369d5b948..af14acdbf83568 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs @@ -10,7 +10,7 @@ namespace DebuggerTests { - public class AsyncTests : DebuggerTestBase + public class AsyncTests : DebuggerTests { // FIXME: method with multiple async blocks - so that we have two separate classes for that method! @@ -19,7 +19,7 @@ public class AsyncTests : DebuggerTestBase // FIXME: check object properties.. //FIXME: function name - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("ContinueWithStaticAsync", "b__3_0")] [InlineData("ContinueWithInstanceAsync", "b__5_0")] public async Task AsyncLocalsInContinueWith(string method_name, string expected_method_name) => await CheckInspectLocalsAtBreakpointSite( @@ -40,7 +40,7 @@ public async Task AsyncLocalsInContinueWith(string method_name, string expected_ await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status"); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task AsyncLocalsInContinueWithInstanceUsingThisBlock() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithInstanceUsingThisAsync", 5, "b__6_0", "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })", diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/BadHarnessInitTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/BadHarnessInitTests.cs index 87f66c2ca615db..b32069db1274b5 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/BadHarnessInitTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/BadHarnessInitTests.cs @@ -12,11 +12,11 @@ namespace DebuggerTests { - public class BadHarnessInitTests : DebuggerTestBase + public class BadHarnessInitTests : DebuggerTests { public override async Task InitializeAsync() => await Task.CompletedTask; - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InvalidInitCommands() { var bad_cmd_name = "non-existant.command"; diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs index fde102009379a1..312cc2c79d6659 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/BreakpointTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.Threading.Tasks; using Microsoft.WebAssembly.Diagnostics; using Newtonsoft.Json.Linq; @@ -12,9 +13,9 @@ namespace DebuggerTests { - public class BreakpointTests : DebuggerTestBase + public class BreakpointTests : DebuggerTests { - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateGoodBreakpoint() { var bp1_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -30,7 +31,7 @@ public async Task CreateGoodBreakpoint() Assert.Equal(8, (int)loc["columnNumber"]); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateJSBreakpoint() { // Test that js breakpoints get set correctly @@ -59,7 +60,7 @@ public async Task CreateJSBreakpoint() Assert.Equal(53, (int)loc2["columnNumber"]); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateJS0Breakpoint() { // 13 24 @@ -87,7 +88,7 @@ public async Task CreateJS0Breakpoint() Assert.Equal(53, (int)loc2["columnNumber"]); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(0)] [InlineData(50)] public async Task CheckMultipleBreakpointsOnSameLine(int col) @@ -109,7 +110,7 @@ public async Task CheckMultipleBreakpointsOnSameLine(int col) CheckLocation("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 55, scripts, loc2); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateBadBreakpoint() { var bp1_req = JObject.FromObject(new @@ -152,6 +153,7 @@ await EvaluateAndCheck( Assert.Equal("IntAdd", scope["name"]); Assert.Equal("object", scope["object"]["type"]); + CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, scope["startLocation"]); CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 14, 4, scripts, scope["endLocation"]); @@ -175,14 +177,14 @@ await EvaluateAndCheck( public static TheoryData TrueConditions = new TheoryData { { "invoke_add()", "IntAdd", "c == 30", true }, - { "invoke_add()", "IntAdd", "true", true }, + /*{ "invoke_add()", "IntAdd", "true", true }, { "invoke_add()", "IntAdd", "5", true }, { "invoke_add()", "IntAdd", "c < 40", true }, { "invoke_use_complex()", "UseComplex", "complex.A == 10", true }, { "invoke_add()", "IntAdd", "1.0", true }, { "invoke_add()", "IntAdd", "\"foo\"", true }, { "invoke_add()", "IntAdd", "\"true\"", true }, - { "invoke_add()", "IntAdd", "\"false\"", true }, + { "invoke_add()", "IntAdd", "\"false\"", true },*/ }; public static TheoryData InvalidConditions = new TheoryData @@ -196,10 +198,10 @@ await EvaluateAndCheck( }; [Theory] - [MemberData(nameof(FalseConditions))] + //[MemberData(nameof(FalseConditions))] [MemberData(nameof(TrueConditions))] - [MemberData(nameof(InvalidConditions))] - public async Task ConditionalBreakpoint(string function_to_call, string method_to_stop, string condition, bool bp_stop_expected) + //[MemberData(nameof(InvalidConditions))] + public async Task ConditionalBreakpoint2(string function_to_call, string method_to_stop, string condition, bool bp_stop_expected) { Result [] bps = new Result[2]; bps[0] = await SetBreakpointInMethod("debugger-test.dll", "Math", method_to_stop, 3, condition:condition); @@ -212,7 +214,7 @@ await EvaluateAndCheck( method_to_stop); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("c == 15", 79, 3, 79, 11)] [InlineData("c == 17", 79, 3, 80, 11)] [InlineData("g == 17", 79, 3, 80, 11)] @@ -255,7 +257,7 @@ await SendCommandAndCheck(null, "Debugger.resume", method_to_stop); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task BreakOnDebuggerBreak() { await EvaluateAndCheck( @@ -298,7 +300,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test2.cs" ); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task BreakpointInAssemblyUsingTypeFromAnotherAssembly_BothDynamicallyLoaded() { int line = 7; @@ -327,7 +329,7 @@ await LoadAssemblyDynamically( CheckNumber(locals, "b", 10); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodChangedUserBreak() { var pause_location = await LoadAssemblyAndTestHotReload( @@ -345,7 +347,7 @@ public async Task DebugHotReloadMethodChangedUserBreak() await CheckBool(locals, "c", true); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodUnchanged() { var pause_location = await LoadAssemblyAndTestHotReload( @@ -363,7 +365,7 @@ public async Task DebugHotReloadMethodUnchanged() CheckNumber(locals, "a", 10); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodAddBreakpoint() { int line = 30; @@ -411,7 +413,7 @@ await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/Me } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodEmpty() { int line = 38; @@ -605,7 +607,7 @@ await SendCommandAndCheck(null, "Debugger.resume", }); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateGoodBreakpointAndHitGoToNonWasmPageComeBackAndHitAgain() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -675,7 +677,7 @@ await EvaluateAndCheck( } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("RunDebuggerHidden", "HiddenMethod")] [InlineData("RunStepThroughWithHidden", "StepThroughWithHiddenBp")] // debuggerHidden shadows the effect of stepThrough [InlineData("RunNonUserCodeWithHidden", "NonUserCodeWithHiddenBp")] // and nonUserCode @@ -693,7 +695,7 @@ await EvaluateAndCheck( ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("RunDebuggerHidden")] [InlineData("RunStepThroughWithHidden")] // debuggerHidden shadows the effect of stepThrough [InlineData("RunNonUserCodeWithHidden")] // and nonUserCode @@ -720,7 +722,7 @@ await SendCommandAndCheck(null, "Debugger.resume", evalFunName); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodChangedUserBreakUsingSDB() { string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); @@ -752,7 +754,7 @@ public async Task DebugHotReloadMethodChangedUserBreakUsingSDB() await CheckBool(locals, "c", true); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodUnchangedUsingSDB() { string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); @@ -781,7 +783,7 @@ public async Task DebugHotReloadMethodUnchangedUsingSDB() CheckLocation("dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 21, 12, scripts, top_frame["location"]); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodAddBreakpointUsingSDB() { string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); @@ -845,7 +847,7 @@ await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/Me } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethodEmptyUsingSDB() { string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); @@ -903,7 +905,7 @@ await StepAndCheck(StepKind.Over, "dotnet://ApplyUpdateReferencedAssembly.dll/Me //pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://ApplyUpdateReferencedAssembly.dll/MethodBody1.cs", 38, 8, "StaticMethod4"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false, "RunStepThrough", 847, 8)] [InlineData(true, "RunStepThrough", 847, 8)] [InlineData(false, "RunNonUserCode", 852, 4, "NonUserCodeBp")] @@ -926,7 +928,7 @@ public async Task StepThroughOrNonUserCodeAttributeStepInNoBp(bool justMyCodeEna await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", line, col, funcName); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false, "RunStepThrough", "StepThrougBp", "", 846, 8)] [InlineData(true, "RunStepThrough", "StepThrougBp", "RunStepThrough", 847, 8)] [InlineData(false, "RunNonUserCode", "NonUserCodeBp", "NonUserCodeBp", 852, 4)] @@ -960,7 +962,7 @@ public async Task StepThroughOrNonUserCodeAttributeStepInWithBp( await SendCommandAndCheck(null, "Debugger.stepInto", "dotnet://debugger-test.dll/debugger-test.cs", line, col, funName); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false, "RunStepThrough", "StepThrougBp")] [InlineData(true, "RunStepThrough", "StepThrougBp")] [InlineData(true, "RunNonUserCode", "NonUserCodeBp")] @@ -990,7 +992,7 @@ public async Task StepThroughOrNonUserCodeAttributeResumeWithBp(bool justMyCodeE await SendCommandAndCheck(null, "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", line2, 8, evalFunName); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false, "Debugger.stepInto", "RunStepThrough", "StepThrougUserBp", 841, 8, "RunStepThrough", 848, 4)] [InlineData(true, "Debugger.stepInto", "RunStepThrough", "RunStepThrough", -1, 8, "RunStepThrough", -1, 4)] [InlineData(false, "Debugger.resume", "RunStepThrough", "StepThrougUserBp", 841, 8, "RunStepThrough", 848, 4)] @@ -1029,7 +1031,7 @@ public async Task StepThroughOrNonUserCodeAttributeWithUserBp( await SendCommandAndCheck(null, debuggingFunction, "dotnet://debugger-test.dll/debugger-test.cs", line2, col2, functionNameCheck2); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("Debugger.stepInto", 1, 2, false)] [InlineData("Debugger.stepInto", 1, 2, true)] [InlineData("Debugger.resume", 1, 2, true)] @@ -1064,7 +1066,7 @@ public async Task StepperBoundary(string debuggingAction, int lineBpInit, int li await SendCommandAndCheck(null, debuggingAction, "dotnet://debugger-test.dll/debugger-test.cs", line, col, "RunNoBoundary"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateGoodBreakpointAndHitGoToWasmPageWithoutAssetsComeBackAndHitAgain() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -1134,7 +1136,7 @@ await EvaluateAndCheck( ); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugHotReloadMethod_CheckBreakpointLineUpdated_ByVS_Simulated() { string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs index 03819fe99c00e2..4723417e719830 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs @@ -11,12 +11,12 @@ namespace DebuggerTests { - public class CallFunctionOnTests : DebuggerTestBase + public class CallFunctionOnTests : DebuggerTests { // This tests `callFunctionOn` with a function that the vscode-js-debug extension uses // Using this here as a non-trivial test case - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, 10, false)] [InlineData("big_array_js_test (0);", "/other.js", 10, 1, 0, true)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 10, false)] @@ -67,7 +67,7 @@ void CheckJFunction(JToken actual, string className, string label) // This tests `callFunctionOn` with a function that the vscode-js-debug extension uses // Using this here as a non-trivial test case - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, 10)] [InlineData("big_array_js_test (0);", "/other.js", 10, 1, 0)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 10)] @@ -119,7 +119,7 @@ await RunCallFunctionOn(eval_fn, vscode_fn1, "big", bp_loc, line, col, }); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, false)] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, true)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)] @@ -164,7 +164,7 @@ await RunCallFunctionOn(eval_fn, }); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, false)] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, true)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)] @@ -217,7 +217,7 @@ await RunCallFunctionOn(eval_fn, }); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task RunOnVTArray(bool roundtrip) => await RunCallFunctionOn( @@ -280,7 +280,7 @@ public async Task RunOnVTArray(bool roundtrip) => await RunCallFunctionOn( } }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task RunOnCFOValueTypeResult(bool roundtrip) => await RunCallFunctionOn( @@ -326,7 +326,7 @@ public async Task RunOnCFOValueTypeResult(bool roundtrip) => await RunCallFuncti }, "simple_struct.gs-props"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task RunOnJSObject(bool roundtrip) => await RunCallFunctionOn( @@ -365,7 +365,7 @@ public async Task RunOnJSObject(bool roundtrip) => await RunCallFunctionOn( }, "obj_own"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, false)] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, true)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)] @@ -402,7 +402,7 @@ await RunCallFunctionOn(eval_fn, }); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, false)] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, true)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)] @@ -429,7 +429,7 @@ public async Task RunOnArrayReturnArrayByValue(string eval_fn, string bp_loc, in await Task.CompletedTask; }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, false)] [InlineData("big_array_js_test (10);", "/other.js", 10, 1, true)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)] @@ -502,7 +502,7 @@ public async Task RunOnArrayReturnPrimitive(string eval_fn, string bp_loc, int l { "big_array_js_test (10);", "/other.js", 10, 1, silent } }; - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(SilentErrorsTestData), null)] [MemberData(nameof(SilentErrorsTestData), false)] [MemberData(nameof(SilentErrorsTestData), true)] @@ -586,7 +586,7 @@ public async Task CFOWithSilentReturnsErrors(string eval_fn, string bp_loc, int } }; - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(GettersTestData), "ptd", false)] [MemberData(nameof(GettersTestData), "ptd", true)] [MemberData(nameof(GettersTestData), "swp", false)] @@ -672,7 +672,7 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li } }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InvokeInheritedAndPrivateGetters() => await CheckInspectLocalsAtBreakpointSite( $"DebuggerTests.GetPropertiesTests.DerivedClass", "InstanceMethod", 1, "InstanceMethod", $"window.setTimeout(function() {{ invoke_static_method_async ('[debugger-test] DebuggerTests.GetPropertiesTests.DerivedClass:run'); }})", @@ -704,7 +704,7 @@ public async Task InvokeInheritedAndPrivateGetters() => await CheckInspectLocals }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, true)] [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, false)] [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, true)] @@ -768,7 +768,7 @@ async Task GetPropertiesAndCheckAccessors(JObject get_prop_req, int num_ { "negative_cfo_test ();", "/other.js", 64, 1, use_cfo } }; - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(NegativeTestsData), false)] public async Task RunOnInvalidCfoId(string eval_fn, string bp_loc, int line, int col, bool use_cfo) => await RunCallFunctionOn( eval_fn, "function() { return this; }", "ptd", @@ -787,7 +787,7 @@ public async Task RunOnInvalidCfoId(string eval_fn, string bp_loc, int line, int Assert.False(res.IsOk); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(NegativeTestsData), false)] public async Task RunOnInvalidThirdSegmentOfObjectId(string eval_fn, string bp_loc, int line, int col, bool use_cfo) { @@ -813,7 +813,7 @@ public async Task RunOnInvalidThirdSegmentOfObjectId(string eval_fn, string bp_l Assert.False(res.IsOk); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(NegativeTestsData), false)] [MemberData(nameof(NegativeTestsData), true)] public async Task InvalidPropertyGetters(string eval_fn, string bp_loc, int line, int col, bool use_cfo) @@ -838,7 +838,7 @@ public async Task InvalidPropertyGetters(string eval_fn, string bp_loc, int line } } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(NegativeTestsData), false)] public async Task ReturnNullFromCFO(string eval_fn, string bp_loc, int line, int col, bool use_cfo) => await RunCallFunctionOn( eval_fn, "function() { return this; }", "ptd", diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs new file mode 100644 index 00000000000000..42c2b812d289d0 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ChromeProvider.cs @@ -0,0 +1,180 @@ +// 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.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Net.WebSockets; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using Microsoft.WebAssembly.Diagnostics; +using System.Threading; +using System.Collections.Generic; + +#nullable enable + +namespace DebuggerTests; + +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 s_browserPath = new(() => GetBrowserPath(GetPathsToProbe())); + + public ChromeProvider(string id, ILogger logger) : base(id, logger) + { + } + + public async Task StartBrowserAndProxyAsync(HttpContext context, + string targetUrl, + int remoteDebuggingPort, + string messagePrefix, + ILoggerFactory loggerFactory, + CancellationTokenSource cts, + int browserReadyTimeoutMs = 20000) + { + string? line; + try + { + ProcessStartInfo psi = GetProcessStartInfo(s_browserPath.Value, GetInitParms(remoteDebuggingPort), targetUrl); + line = await LaunchHostAsync( + psi, + context, + str => + { + if (string.IsNullOrEmpty(str)) + return null; + + Match match = s_parseConnection.Match(str); + return match.Success + ? match.Groups[1].Captures[0].Value + : null; + }, + messagePrefix, + browserReadyTimeoutMs, + cts.Token).ConfigureAwait(false); + + if (_process is null || line is null) + throw new Exception($"Failed to launch chrome"); + } + catch (Exception ex) + { + TestHarnessProxy.RegisterProxyExitState(Id, new(RunLoopStopReason.Exception, ex)); + throw; + } + + string con_str = await ExtractConnUrl(line, _logger); + + _logger.LogInformation($"{messagePrefix} launching proxy for {con_str}"); + + _debuggerProxy = new DebuggerProxy(loggerFactory, null, loggerId: Id); + TestHarnessProxy.RegisterNewProxy(Id, _debuggerProxy); + var browserUri = new Uri(con_str); + WebSocket? ideSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + await _debuggerProxy.Run(browserUri, ideSocket, cts).ConfigureAwait(false); + } + + public override void Dispose() + { + if (_isDisposed || _isDisposing) + return; + + _isDisposing = true; + _debuggerProxy?.Shutdown(); + base.Dispose(); + + if (_ideWebSocket is not null) + { + _ideWebSocket.Abort(); + _ideWebSocket.Dispose(); + _ideWebSocket = null; + } + + _isDisposed = true; + _isDisposing = false; + } + + private async Task ExtractConnUrl (string str, ILogger logger) + { + var client = new HttpClient(); + var start = DateTime.Now; + JArray? obj = null; + + while (true) + { + // Unfortunately it does look like we have to wait + // for a bit after getting the response but before + // making the list request. We get an empty result + // if we make the request too soon. + await Task.Delay(100); + + var res = await client.GetStringAsync(new Uri(new Uri(str), "/json/list")); + logger.LogInformation("res is {0}", res); + + if (!string.IsNullOrEmpty(res)) + { + // Sometimes we seem to get an empty array `[ ]` + obj = JArray.Parse(res); + if (obj != null && obj.Count >= 1) + break; + } + + var elapsed = DateTime.Now - start; + if (elapsed.Milliseconds > 5000) + { + string message = $"Unable to get DevTools /json/list response in {elapsed.Seconds} seconds, stopping"; + logger.LogError(message); + throw new Exception(message); + } + } + + string? wsURl = obj[0]?["webSocketDebuggerUrl"]?.Value(); + if (wsURl is null) + throw new Exception($"Could not get the webSocketDebuggerUrl in {obj}"); + + logger.LogTrace(">>> {0}", wsURl); + + return wsURl; + } + + private static string GetInitParms(int port) + { + string str = $"--headless --disable-gpu --lang=en-US --incognito --remote-debugging-port={port}"; + if (File.Exists("/.dockerenv")) + { + Console.WriteLine ("Detected a container, disabling sandboxing for debugger tests."); + str = "--no-sandbox " + str; + } + return str; + } + + private static IEnumerable GetPathsToProbe() + { + List 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; + } + +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs index e3ec38820cd64e..02023e7d383714 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CustomViewTests.cs @@ -13,9 +13,9 @@ namespace DebuggerTests { - public class CustomViewTests : DebuggerTestBase + public class CustomViewTests : DebuggerTests { - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task UsingDebuggerDisplay() { var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 15); @@ -34,7 +34,7 @@ public async Task UsingDebuggerDisplay() await CheckObject(locals, "person2", "DebuggerTests.Person", description: "FirstName: Lisa, SurName: Müller, Age: 41"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task UsingDebuggerTypeProxy() { var bp = await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.DebuggerCustomViewTest", "run", 15); @@ -66,7 +66,7 @@ await EvaluateOnCallFrameAndCheck(frame["callFrameId"].Value(), } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task UsingDebuggerDisplayConcurrent() { async Task CheckProperties(JObject pause_location) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs index 8115604204a81d..0396aa38190d42 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs @@ -8,7 +8,7 @@ namespace DebuggerTests { - public class DateTimeTests : DebuggerTestBase + public class DateTimeTests : DebuggerTests { [Theory] diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs index 89a8e822a9b6d4..758171d5deae18 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestBase.cs @@ -12,13 +12,30 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; -using Xunit.Abstractions; using Xunit.Sdk; namespace DebuggerTests { + public class DebuggerTests : +#if RUN_IN_CHROME + DebuggerTestBase +#else + DebuggerTestFirefox +#endif + {} + public class DebuggerTestBase : IAsyncLifetime { + public static WasmHost RunningOn +#if RUN_IN_CHROME + => WasmHost.Chrome; +#else + => WasmHost.Firefox; +#endif + public static bool RunningOnChrome => RunningOn == WasmHost.Chrome; + + public const int FirefoxProxyPort = 6002; + internal InspectorClient cli; internal Inspector insp; protected CancellationToken token; @@ -35,7 +52,7 @@ public class DebuggerTestBase : IAsyncLifetime public int Id { get; init; } - protected static string DebuggerTestAppPath + public static string DebuggerTestAppPath { get { @@ -74,55 +91,7 @@ static protected string FindTestPath() throw new Exception($"Cannot find 'debugger-driver.html' in {test_app_path}"); } - static string[] PROBE_LIST = { - "/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", - }; - static string chrome_path; - - static string GetChromePath() - { - if (string.IsNullOrEmpty(chrome_path)) - { - chrome_path = FindChromePath(); - if (!string.IsNullOrEmpty(chrome_path)) - { - chrome_path = Path.GetFullPath(chrome_path); - Console.WriteLine ($"** Using chrome from {chrome_path}"); - } - else - throw new Exception("Could not find an installed Chrome to use"); - } - - return chrome_path; - - static string FindChromePath() - { - string chrome_path_env_var = Environment.GetEnvironmentVariable("CHROME_PATH_FOR_DEBUGGER_TESTS"); - if (!string.IsNullOrEmpty(chrome_path_env_var)) - { - if (File.Exists(chrome_path_env_var)) - return chrome_path_env_var; - - Console.WriteLine ($"warning: Could not find CHROME_PATH_FOR_DEBUGGER_TESTS={chrome_path_env_var}"); - } - - // Look for a chrome installed in artifacts, for local runs - string baseDir = Path.Combine(Path.GetDirectoryName(typeof(DebuggerTestBase).Assembly.Location), "..", ".."); - string path = Path.Combine(baseDir, "chrome", "chrome-linux", "chrome"); - if (File.Exists(path)) - return path; - path = Path.Combine(baseDir, "chrome", "chrome-win", "chrome.exe"); - if (File.Exists(path)) - return path; - - return PROBE_LIST.FirstOrDefault(p => File.Exists(p)); - } - } + internal virtual string UrlToRemoteDebugging() => "http://localhost:0"; static string s_testLogPath = null; public static string TestLogPath @@ -151,8 +120,7 @@ public DebuggerTestBase(string driver = "debugger-driver.html") insp = new Inspector(Id); cli = insp.Client; scripts = SubscribeToScripts(insp); - - startTask = TestHarnessProxy.Start(GetChromePath(), DebuggerTestAppPath, driver); + startTask = TestHarnessProxy.Start(DebuggerTestAppPath, driver, UrlToRemoteDebugging()); } public virtual async Task InitializeAsync() @@ -181,7 +149,7 @@ public virtual async Task InitializeAsync() internal Dictionary dicScriptsIdToUrl; internal Dictionary dicFileToUrl; - internal Dictionary SubscribeToScripts(Inspector insp) + internal virtual Dictionary SubscribeToScripts(Inspector insp) { dicScriptsIdToUrl = new Dictionary(); dicFileToUrl = new Dictionary(); @@ -241,6 +209,19 @@ await EvaluateAndCheck( ); } + internal virtual string EvaluateCommand() + { + return "Runtime.evaluate"; + } + + internal virtual JObject CreateEvaluateArgs(string expression) + => JObject.FromObject(new { expression }); + + internal virtual async Task WaitFor(string what) + { + return await insp.WaitFor(what); + } + // sets breakpoint by method name and line offset internal async Task CheckInspectLocalsAtBreakpointSite(string type, string method, int line_offset, string bp_function_name, string eval_expression, Func locals_fn = null, Func wait_for_event_fn = null, bool use_cfo = false, string assembly = "debugger-test.dll", int col = 0) @@ -248,16 +229,13 @@ internal async Task CheckInspectLocalsAtBreakpointSite(string type, string metho UseCallFunctionOnBeforeGetProperties = use_cfo; var bp = await SetBreakpointInMethod(assembly, type, method, line_offset, col); - - var args = JObject.FromObject(new { expression = eval_expression }); - var res = await cli.SendCommand("Runtime.evaluate", args, token); + var res = await cli.SendCommand(EvaluateCommand(), CreateEvaluateArgs(eval_expression), token); if (!res.IsOk) { - Console.WriteLine($"Failed to run command {method} with args: {args?.ToString()}\nresult: {res.Error.ToString()}"); + Console.WriteLine($"Failed to run command {method} with args: {CreateEvaluateArgs(eval_expression)?.ToString()}\nresult: {res.Error.ToString()}"); Assert.True(false, $"SendCommand for {method} failed with {res.Error.ToString()}"); } - - var pause_location = await insp.WaitFor(Inspector.PAUSE); + var pause_location = await WaitFor(Inspector.PAUSE); if (bp_function_name != null) Assert.Equal(bp_function_name, pause_location["callFrames"]?[0]?["functionName"]?.Value()); @@ -278,7 +256,7 @@ internal async Task CheckInspectLocalsAtBreakpointSite(string type, string metho } } - internal void CheckLocation(string script_loc, int line, int column, Dictionary scripts, JToken location) + internal virtual void CheckLocation(string script_loc, int line, int column, Dictionary scripts, JToken location) { var loc_str = $"{ scripts[location["scriptId"].Value()] }" + $"#{ location["lineNumber"].Value() }" + @@ -366,30 +344,33 @@ internal async Task CheckDateTime(JToken locals, string name, DateTime expected, await CheckDateTimeValue(obj["value"], expected, label); } - internal async Task CheckDateTimeValue(JToken value, DateTime expected, string label = "") + async Task CheckDateTimeMembers(JToken v, DateTime exp_dt, string label = "") { - await CheckDateTimeMembers(value, expected, label); + AssertEqual("System.DateTime", v["className"]?.Value(), $"{label}#className"); + AssertEqual(exp_dt.ToString(), v["description"]?.Value(), $"{label}#description"); + + var members = await GetProperties(v["objectId"]?.Value()); + // not checking everything + CheckNumber(members, "Year", exp_dt.Year); + CheckNumber(members, "Month", exp_dt.Month); + CheckNumber(members, "Day", exp_dt.Day); + CheckNumber(members, "Hour", exp_dt.Hour); + CheckNumber(members, "Minute", exp_dt.Minute); + CheckNumber(members, "Second", exp_dt.Second); + } + + internal virtual async Task CheckDateTimeGetter(JToken value, DateTime expected, string label = "") + { var res = await InvokeGetter(JObject.FromObject(new { value = value }), "Date"); await CheckDateTimeMembers(res.Value["result"], expected.Date, label); + } - // FIXME: check some float properties too + internal async Task CheckDateTimeValue(JToken value, DateTime expected, string label = "") + { + await CheckDateTimeMembers(value, expected, label); - async Task CheckDateTimeMembers(JToken v, DateTime exp_dt, string label = "") - { - AssertEqual("System.DateTime", v["className"]?.Value(), $"{label}#className"); - AssertEqual(exp_dt.ToString(), v["description"]?.Value(), $"{label}#description"); - - var members = await GetProperties(v["objectId"]?.Value()); - - // not checking everything - CheckNumber(members, "Year", exp_dt.Year); - CheckNumber(members, "Month", exp_dt.Month); - CheckNumber(members, "Day", exp_dt.Day); - CheckNumber(members, "Hour", exp_dt.Hour); - CheckNumber(members, "Minute", exp_dt.Minute); - CheckNumber(members, "Second", exp_dt.Second); - } + await CheckDateTimeGetter(value, expected, label); } internal async Task CheckBool(JToken locals, string name, bool expected) @@ -445,7 +426,7 @@ internal async Task SendCommand(string method, JObject args) internal async Task Evaluate(string expression) { - return await SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expression })); + return await SendCommand(EvaluateCommand(), CreateEvaluateArgs(expression)); } internal void AssertLocation(JObject args, string methodName) @@ -460,7 +441,7 @@ internal async Task RunUntil(string methodName) await SetBreakpointInMethod("debugger-test", "DebuggerTest", methodName); // This will run all the tests until it hits the bp await Evaluate("window.setTimeout(function() { invoke_run_all (); }, 1);"); - var wait_res = await insp.WaitFor(Inspector.PAUSE); + var wait_res = await WaitFor(Inspector.PAUSE); AssertLocation(wait_res, "locals_inner"); return wait_res; } @@ -497,7 +478,7 @@ internal async Task InvokeGetter(JToken obj, object arguments, string fn return res; } - internal async Task StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name, + internal virtual async Task StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name, Func wait_for_event_fn = null, Func locals_fn = null, int times = 1) { string method = (kind == StepKind.Resume ? "Debugger.resume" : $"Debugger.step{kind}"); @@ -536,16 +517,16 @@ internal async Task SetNextIPAndCheck(string script_id, string script_l return JObject.FromObject(res); } - internal async Task EvaluateAndCheck( + internal virtual async Task EvaluateAndCheck( string expression, string script_loc, int line, int column, string function_name, Func wait_for_event_fn = null, Func locals_fn = null) => await SendCommandAndCheck( - JObject.FromObject(new { expression = expression }), + CreateEvaluateArgs(expression), "Runtime.evaluate", script_loc, line, column, function_name, wait_for_event_fn: wait_for_event_fn, locals_fn: locals_fn); - internal async Task SendCommandAndCheck(JObject args, string method, string script_loc, int line, int column, string function_name, + internal virtual async Task SendCommandAndCheck(JObject args, string method, string script_loc, int line, int column, string function_name, Func wait_for_event_fn = null, Func locals_fn = null, string waitForEvent = Inspector.PAUSE) { var res = await cli.SendCommand(method, args, token); @@ -555,7 +536,7 @@ internal async Task SendCommandAndCheck(JObject args, string method, st Assert.True(false, $"SendCommand for {method} failed with {res.Error.ToString()}"); } - var wait_res = await insp.WaitFor(waitForEvent); + var wait_res = await WaitFor(waitForEvent); JToken top_frame = wait_res["callFrames"]?[0]; if (function_name != null) { @@ -775,6 +756,11 @@ internal async Task CheckProps(JToken actual, object exp_o, string label, int nu } } + internal virtual bool SkipProperty(string propertyName) + { + return false; + } + internal async Task CheckValue(JToken actual_val, JToken exp_val, string label) { if (exp_val["__custom_type"] != null) @@ -794,6 +780,8 @@ internal async Task CheckValue(JToken actual_val, JToken exp_val, string label) { foreach (var jp in exp_val.Values()) { + if (SkipProperty(jp.Name)) + continue; if (jp.Value.Type == JTokenType.Object) { var new_val = await GetProperties(actual_val["objectId"].Value()); @@ -862,7 +850,7 @@ internal async Task GetObjectOnLocals(JToken locals, string name) } /* @fn_args is for use with `Runtime.callFunctionOn` only */ - internal async Task GetProperties(string id, JToken fn_args = null, bool? own_properties = null, bool? accessors_only = null, bool expect_ok = true) + internal virtual async Task GetProperties(string id, JToken fn_args = null, bool? own_properties = null, bool? accessors_only = null, bool expect_ok = true) { if (UseCallFunctionOnBeforeGetProperties && !id.StartsWith("dotnet:scope:")) { @@ -921,7 +909,6 @@ internal async Task GetProperties(string id, JToken fn_args = null, bool } } } - return locals; } @@ -984,7 +971,7 @@ internal async Task GetProperties(string id, JToken fn_args = null, bool return (locals, locals_internal, locals_private); } - internal async Task<(JToken, Result)> EvaluateOnCallFrame(string id, string expression, bool expect_ok = true) + internal virtual async Task<(JToken, Result)> EvaluateOnCallFrame(string id, string expression, bool expect_ok = true) { var evaluate_req = JObject.FromObject(new { @@ -1054,7 +1041,7 @@ internal async Task RemoveBreakpoint(string id, bool expect_ok = true) return res; } - internal async Task SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false, string condition = "") + internal virtual async Task SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false, string condition = "") { var bp1_req = !use_regex ? JObject.FromObject(new { lineNumber = line, columnNumber = column, url = dicFileToUrl[url_key], condition }) : @@ -1072,7 +1059,7 @@ internal async Task SetPauseOnException(string state) return exc_res; } - internal async Task SetBreakpointInMethod(string assembly, string type, string method, int lineOffset = 0, int col = 0, string condition = "") + internal virtual async Task SetBreakpointInMethod(string assembly, string type, string method, int lineOffset = 0, int col = 0, string condition = "") { var req = JObject.FromObject(new { assemblyName = assembly, typeName = type, methodName = method, lineOffset = lineOffset }); @@ -1093,7 +1080,6 @@ internal async Task SetBreakpointInMethod(string assembly, string type, res = await cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, token); Assert.True(res.IsOk); - return res; } @@ -1101,7 +1087,7 @@ internal async Task EvaluateOnCallFrameAndCheck(string call_frame_id, params (st { foreach (var arg in args) { - var (eval_val, _) = await EvaluateOnCallFrame(call_frame_id, arg.expression); + var (eval_val, _) = await EvaluateOnCallFrame(call_frame_id, arg.expression).ConfigureAwait(false); try { await CheckValue(eval_val, arg.expected, arg.expression); @@ -1174,7 +1160,7 @@ internal static JObject TObject(string className, string description = null, boo internal static JObject TBool(bool value) => JObject.FromObject(new { type = "boolean", value = @value, description = @value ? "true" : "false" }); internal static JObject TSymbol(string value) => JObject.FromObject(new { type = "symbol", value = @value, description = @value }); - + internal static JObject TChar(char value) => JObject.FromObject(new { type = "symbol", value = @value, description = $"{(int)value} '{@value}'" }); /* @@ -1252,7 +1238,7 @@ internal async Task LoadAssemblyDynamicallyALCAndRunMethod(string asm_f }); await cli.SendCommand("Runtime.evaluate", run_method, token); - return await insp.WaitFor(Inspector.PAUSE); + return await WaitFor(Inspector.PAUSE); } internal async Task LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges(string asm_file, string pdb_file, string class_name, string method_name) @@ -1278,7 +1264,7 @@ internal async Task LoadAssemblyAndTestHotReloadUsingSDBWithoutChanges( }); await cli.SendCommand("Runtime.evaluate", run_method, token); - return await insp.WaitFor(Inspector.PAUSE); + return await WaitFor(Inspector.PAUSE); } internal async Task LoadAssemblyAndTestHotReloadUsingSDB(string asm_file_hot_reload, string class_name, string method_name, int id, Func rebindBreakpoint = null) @@ -1320,7 +1306,7 @@ internal async Task LoadAssemblyAndTestHotReloadUsingSDB(string asm_fil expression = "window.setTimeout(function() { invoke_static_method('[debugger-test] TestHotReloadUsingSDB:RunMethod', '" + class_name + "', '" + method_name + "'); }, 1);" }); await cli.SendCommand("Runtime.evaluate", run_method, token); - return await insp.WaitFor(Inspector.PAUSE); + return await WaitFor(Inspector.PAUSE); } internal async Task LoadAssemblyAndTestHotReload(string asm_file, string pdb_file, string asm_file_hot_reload, string class_name, string method_name) @@ -1368,7 +1354,7 @@ internal async Task LoadAssemblyAndTestHotReload(string asm_file, strin }); await cli.SendCommand("Runtime.evaluate", run_method, token); - return await insp.WaitFor(Inspector.PAUSE); + return await WaitFor(Inspector.PAUSE); } public async Task WaitForBreakpointResolvedEvent() diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs new file mode 100644 index 00000000000000..884054c78963d1 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestFirefox.cs @@ -0,0 +1,470 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.WebAssembly.Diagnostics; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace DebuggerTests; + +public class DebuggerTestFirefox : DebuggerTestBase +{ + internal FirefoxInspectorClient _client; + public DebuggerTestFirefox(string driver = "debugger-driver.html"):base(driver) + { + if (insp.Client is not FirefoxInspectorClient) + throw new Exception($"Bug: client should be {nameof(FirefoxInspectorClient)} for use with {nameof(DebuggerTestFirefox)}"); + + _client = (FirefoxInspectorClient)insp.Client; + } + + public override async Task InitializeAsync() + { + Func)>> fn = (client, token) => + { + Func)> getInitCmdFn = (cmd, args) => (cmd, client.SendCommand(cmd, args, token)); + var init_cmds = new List<(string, Task)> + { + getInitCmdFn("listTabs", JObject.FromObject(new { type = "listTabs", to = "root"})) + }; + + return init_cmds; + }; + + await Ready(); + await insp.OpenSessionAsync(fn, TestTimeout); + } + + internal override Dictionary SubscribeToScripts(Inspector insp) + { + dicScriptsIdToUrl = new Dictionary(); + dicFileToUrl = new Dictionary(); + insp.On("newSource", async (args, c) => + { + var script_id = args?["source"]?["actor"].Value(); + var url = args?["source"]?["sourceMapBaseURL"]?.Value(); + /*Console.WriteLine(script_id); + Console.WriteLine(args);*/ + if (script_id.StartsWith("dotnet://")) + { + var dbgUrl = args?["source"]?["dotNetUrl"]?.Value(); + var arrStr = dbgUrl.Split("/"); + dbgUrl = arrStr[0] + "/" + arrStr[1] + "/" + arrStr[2] + "/" + arrStr[arrStr.Length - 1]; + dicScriptsIdToUrl[script_id] = dbgUrl; + dicFileToUrl[dbgUrl] = args?["source"]?["url"]?.Value(); + } + else if (!String.IsNullOrEmpty(url)) + { + var dbgUrl = args?["source"]?["sourceMapBaseURL"]?.Value(); + var arrStr = dbgUrl.Split("/"); + dicScriptsIdToUrl[script_id] = arrStr[arrStr.Length - 1]; + dicFileToUrl[new Uri(url).AbsolutePath] = url; + } + await Task.FromResult(0); + }); + insp.On("resource-available-form", async (args, c) => + { + var script_id = args?["resources"]?[0]?["actor"].Value(); + var url = args?["resources"]?[0]?["url"]?.Value(); + if (script_id.StartsWith("dotnet://")) + { + var dbgUrl = args?["resources"]?[0]?["dotNetUrl"]?.Value(); + var arrStr = dbgUrl.Split("/"); + dbgUrl = arrStr[0] + "/" + arrStr[1] + "/" + arrStr[2] + "/" + arrStr[arrStr.Length - 1]; + dicScriptsIdToUrl[script_id] = dbgUrl; + dicFileToUrl[dbgUrl] = args?["resources"]?[0]?["url"]?.Value(); + } + else if (!String.IsNullOrEmpty(url)) + { + var dbgUrl = args?["resources"]?[0]?["url"]?.Value(); + var arrStr = dbgUrl.Split("/"); + dicScriptsIdToUrl[script_id] = arrStr[arrStr.Length - 1]; + dicFileToUrl[new Uri(url).AbsolutePath] = url; + } + await Task.FromResult(0); + }); + return dicScriptsIdToUrl; + } + + internal override async Task SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false, string condition = "") + { + var bp1_req = JObject.FromObject(new { + type = "setBreakpoint", + location = JObject.FromObject(new { + line = line + 1, + column, + sourceUrl = dicFileToUrl[url_key] + }), + to = _client.BreakpointActorId + }); + + var bp1_res = await cli.SendCommand("setBreakpoint", bp1_req, token); + Assert.True(expect_ok == bp1_res.IsOk); + return bp1_res; + } + internal override async Task EvaluateAndCheck( + string expression, string script_loc, int line, int column, string function_name, + Func wait_for_event_fn = null, Func locals_fn = null) + { + return await SendCommandAndCheck( + CreateEvaluateArgs(expression), + "evaluateJSAsync", script_loc, line, column, function_name, + wait_for_event_fn: wait_for_event_fn, + locals_fn: locals_fn); + } + + + internal override void CheckLocation(string script_loc, int line, int column, Dictionary scripts, JToken location) + { + if (location == null) //probably trying to check startLocation endLocation or functionLocation which are not available on Firefox + return; + int column_from_stack = -1; + if (column != -1) + column_from_stack = location["columnNumber"].Value(); + + var loc_str = $"{ scripts[location["scriptId"].Value()] }" + + $"#{ location["lineNumber"].Value()}" + + $"#{ column_from_stack }"; + + var expected_loc_str = $"{script_loc}#{line+1}#{column}"; + Assert.Equal(expected_loc_str, loc_str); + } + + private JObject ConvertFirefoxToDefaultFormat(JArray frames, JObject wait_res) + { + var callFrames = new JArray(); + foreach (var frame in frames) + { + var callFrame = JObject.FromObject(new + { + functionName = frame["displayName"].Value(), + callFrameId = frame["actor"].Value(), + //functionLocation = 0, + location = JObject.FromObject(new + { + scriptId = frame["where"]["actor"].Value(), + lineNumber = frame["where"]["line"].Value(), + columnNumber = frame["where"]["column"].Value() + }), + url = scripts[frame["where"]["actor"].Value()], + scopeChain = new JArray(JObject.FromObject(new + { + type = "local", + name = frame["displayName"].Value(), + @object = JObject.FromObject(new + { + type = "object", + className = "Object", + description = "Object", + objectId = frame["actor"].Value() + }) + })) + }); + callFrames.Add(callFrame); + } + return JObject.FromObject(new + { + callFrames, + reason = "other" + }); + } + + internal override async Task SendCommandAndCheck(JObject args, string method, string script_loc, int line, int column, string function_name, + Func wait_for_event_fn = null, Func locals_fn = null, string waitForEvent = Inspector.PAUSE) + { + switch (method) + { + case "Debugger.resume": + return await StepAndCheck(StepKind.Resume, script_loc, line, column, function_name, wait_for_event_fn, locals_fn); + case "Debugger.stepInto": + return await StepAndCheck(StepKind.Into, script_loc, line, column, function_name, wait_for_event_fn, locals_fn); + } + var res = await cli.SendCommand(method, args, token); + if (!res.IsOk) + { + Console.WriteLine($"Failed to run command {method} with args: {args?.ToString()}\nresult: {res.Error.ToString()}"); + Assert.True(false, $"SendCommand for {method} failed with {res.Error.ToString()}"); + } + var wait_res = await WaitFor(waitForEvent); + if (function_name != null) + { + AssertEqual(function_name, wait_res["callFrames"]?[0]?["functionName"]?.Value(), wait_res["callFrames"]?[0]?["functionName"]?.ToString()); + } + + if (script_loc != null && line >= 0) + CheckLocation(script_loc, line, column, scripts, wait_res["callFrames"]?[0]?["location"]); + + if (wait_for_event_fn != null) + { + await wait_for_event_fn(wait_res); + } + + if (locals_fn != null) + { + var locals = await GetProperties(wait_res["callFrames"][0]["callFrameId"].Value()); + try + { + await locals_fn(locals); + } + catch (System.AggregateException ex) + { + throw new AggregateException(ex.Message + " \n" + locals.ToString(), ex); + } + } + + return wait_res; + } + + internal JObject ConvertFromFirefoxToDefaultFormat(KeyValuePair variable) + { + string name = variable.Key; + JToken value = variable.Value; + JObject variableValue = null; + string valueType = "value"; + if (value?["type"] == null || value["type"].Value() == "object") + { + var actor = value["value"]?["actor"]?.Value(); + if (value["value"]["type"].Value() == "null") + { + variableValue = JObject.FromObject(new + { + type = "object", + subtype = "null", + className = value["value"]["class"].Value(), + description = value["value"]["class"].Value() + }); + if (actor != null && actor.StartsWith("dotnet:pointer:")) + variableValue["type"] = "symbol"; + } + else if (value?["value"]?["type"].Value() == "function") + { + variableValue = JObject.FromObject(new + { + type = "function", + objectId = value["value"]["actor"].Value(), + className = "Function", + description = $"get {name} ()" + }); + valueType = "get"; + } + else { + variableValue = JObject.FromObject(new + { + type = value["value"]["type"], + value = (string)null, + description = value["value"]?["value"]?.Value() == null ? value["value"]["class"].Value() : value["value"]?["value"]?.Value(), + className = value["value"]["class"].Value(), + objectId = actor, + }); + if (actor.StartsWith("dotnet:valuetype:")) + variableValue["isValueType"] = true; + if (actor.StartsWith("dotnet:array:")) + variableValue["subtype"] = "array"; + if (actor.StartsWith("dotnet:pointer:")) + variableValue["type"] = "object"; + if (actor.StartsWith("dotnet:pointer:-1")) + { + variableValue["type"] = "symbol"; + variableValue["value"] = value["value"]?["value"]?.Value(); + } + } + } + else + { + var description = value["value"].ToString(); + if (value["type"].Value() == "boolean") + description = description.ToLower(); + variableValue = JObject.FromObject(new + { + type = value["type"], + value = value["value"], + description + }); + } + var ret = JObject.FromObject(new + { + name, + writable = value["writable"] != null ? value["writable"] : false + }); + ret[valueType] = variableValue; + return ret; + } + + /* @fn_args is for use with `Runtime.callFunctionOn` only */ + internal override async Task GetProperties(string id, JToken fn_args = null, bool? own_properties = null, bool? accessors_only = null, bool expect_ok = true) + { + if (id.StartsWith("dotnet:scope:")) + { + JArray ret = new (); + var o = JObject.FromObject(new + { + to = id, + type = "getEnvironment" + }); + var frame_props = await cli.SendCommand("getEnvironment", o, token); + foreach (var variable in frame_props.Value["result"]["value"]["bindings"]["variables"].Value()) + { + var varToAdd = ConvertFromFirefoxToDefaultFormat(variable); + ret.Add(varToAdd); + } + return ret; + } + if (id.StartsWith("dotnet:valuetype:") || id.StartsWith("dotnet:object:") || id.StartsWith("dotnet:array:") || id.StartsWith("dotnet:pointer:")) + { + JArray ret = new (); + var o = JObject.FromObject(new + { + to = id, + type = "enumProperties" + }); + var propertyIterator = await cli.SendCommand("enumProperties", o, token); + o = JObject.FromObject(new + { + to = propertyIterator.Value["result"]["value"]?["iterator"]?["actor"].Value().Replace("propertyIterator", ""), + type = "prototypeAndProperties" + }); + var objProps = await cli.SendCommand("prototypeAndProperties", o, token); + foreach (var prop in objProps.Value["result"]["value"]["ownProperties"].Value()) + { + var varToAdd = ConvertFromFirefoxToDefaultFormat(prop); + ret.Add(varToAdd); + } + return ret; + } + return null; + } + + internal override async Task StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name, + Func wait_for_event_fn = null, Func locals_fn = null, int times = 1) + { + JObject resumeLimit = null; + + if (kind != StepKind.Resume) + { + resumeLimit = JObject.FromObject(new + { + type = kind == StepKind.Over ? "next" : kind == StepKind.Out ? "finish" : "step" + }); + } + var o = JObject.FromObject(new + { + to = _client.ThreadActorId, + type = "resume", + resumeLimit + }); + + for (int i = 0; i < times - 1; i++) + { + await SendCommandAndCheck(o, "resume", null, -1, -1, null); + } + + // Check for method/line etc only at the last step + return await SendCommandAndCheck( + o, "resume", script_loc, line, column, function_name, + wait_for_event_fn: wait_for_event_fn, + locals_fn: locals_fn); + } + + internal override async Task SetBreakpointInMethod(string assembly, string type, string method, int lineOffset = 0, int col = 0, string condition = "") + { + var req = JObject.FromObject(new { assemblyName = assembly, type = "DotnetDebugger.getMethodLocation", typeName = type, methodName = method, lineOffset = lineOffset, to = "internal" }); + + // Protocol extension + var res = await cli.SendCommand("DotnetDebugger.getMethodLocation", req, token); + Assert.True(res.IsOk); + var m_url = res.Value["result"]["value"]["url"].Value(); + var m_line = res.Value["result"]["value"]["line"].Value(); + var m_column = res.Value["result"]["value"]["column"].Value(); + + + var bp1_req = JObject.FromObject(new { + type = "setBreakpoint", + location = JObject.FromObject(new { + line = m_line + lineOffset + 1, + column = col, + sourceUrl = m_url + }), + to = _client.BreakpointActorId + }); + + if (condition != "") + bp1_req["options"] = JObject.FromObject(new { condition }); + + var bp1_res = await cli.SendCommand("setBreakpoint", bp1_req, token); + Assert.True(bp1_res.IsOk); + + var arr = new JArray(JObject.FromObject(new { + lineNumber = m_line + lineOffset, + columnNumber = -1 + })); + + bp1_res.Value["locations"] = arr; + return bp1_res; + } + + internal override async Task<(JToken, Result)> EvaluateOnCallFrame(string id, string expression, bool expect_ok = true) + { + var o = CreateEvaluateArgs(expression); + var res = await cli.SendCommand("evaluateJSAsync", o, token); + if (res.IsOk) + { + if (res.Value["result"]["value"] is JObject) + { + var actor = res.Value["result"]["value"]["actor"].Value(); + var resObj = JObject.FromObject(new + { + type = res.Value["result"]["value"]["type"], + className = res.Value["result"]["value"]["class"], + description = res.Value["result"]["value"]["description"], + objectId = actor + }); + if (actor.StartsWith("dotnet:valuetype:")) + resObj["isValueType"] = true; + return (resObj, res); + } + return (res.Value["result"], res); + } + + return (null, res); + } + + internal override bool SkipProperty(string propertyName) => propertyName == "isEnum"; + + internal override async Task CheckDateTimeGetter(JToken value, DateTime expected, string label = "") => await Task.CompletedTask; + + internal override string EvaluateCommand() => "evaluateJSAsync"; + + internal override JObject CreateEvaluateArgs(string expression) + { + if (string.IsNullOrEmpty(_client.ConsoleActorId)) + throw new Exception($"Cannot create evaluate request because consoleActorId is '{_client.ConsoleActorId}"); + return JObject.FromObject(new + { + to = _client.ConsoleActorId, + type = "evaluateJSAsync", + text = expression, + options = new { eager = true, mapped = new { @await = true } } + }); + } + + internal override async Task WaitFor(string what) + { + var wait_res = await insp.WaitFor(what); + var frames = await cli.SendCommand("frames", JObject.FromObject(new + { + to = wait_res["from"].Value(), + type = "frames", + start = 0, + count = 1000 + }), token); + + if (frames.Value["result"]?["value"]?["frames"] is not JArray frames_arr) + throw new Exception($"Tried to get frames after waiting for '{what}', but got unexpected result: {frames}"); + + return ConvertFirefoxToDefaultFormat(frames_arr, wait_res); + } +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj index 8f981735595b23..67ae17bc43ed5f 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj @@ -5,23 +5,28 @@ true false true + chrome + $(DefineConstants);RUN_IN_CHROME $(MSBuildThisFileDirectory)..\..\BrowsersForTesting.props windows - true + true + true $(ArtifactsBinDir)DebuggerTestSuite\chrome\ - $(ArtifactsBinDir)DebuggerTestSuite\ - $(ChromeStampDir).install-chrome-$(ChromiumRevision).stamp + $(ArtifactsBinDir)DebuggerTestSuite\ + $(BrowserStampDir).install-chrome-$(ChromiumRevision).stamp + $(ArtifactsBinDir)DebuggerTestSuite\firefox\ + $(BrowserStampDir).install-firefox-$(FirefoxRevision).stamp - + @@ -51,7 +56,7 @@ Condition="!Exists($(ChromeStampFile)) and '$(InstallChromeForDebuggerTests)' == 'true'"> - <_StampFile Include="$(ChromeStampDir).install-chrome*.stamp" /> + <_StampFile Include="$(BrowserStampDir).install-chrome*.stamp" /> @@ -73,4 +78,32 @@ + + + + <_StampFile Include="$(BrowserStampDir).install-firefox*.stamp" /> + + + + + + + + + + + + + <_FirefoxBinaryPath>$([MSBuild]::NormalizePath($(FirefoxDir), $(FirefoxBinaryName))) + + + + + + + + diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs index cdbee2079e72aa..3e84e02d40a103 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs @@ -11,10 +11,10 @@ namespace DebuggerTests { - public class DelegateTests : DebuggerTestBase + public class DelegateTests : DebuggerTests { - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(0, 53, 8, "DelegatesTest", false)] [InlineData(0, 53, 8, "DelegatesTest", true)] [InlineData(2, 99, 8, "InnerMethod2", false)] @@ -80,7 +80,7 @@ await CompareObjectPropertiesFor(locals, "fn_del_arr_unused", new[] } ); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(0, 202, 8, "DelegatesSignatureTest", false)] [InlineData(0, 202, 8, "DelegatesSignatureTest", true)] [InlineData(2, 99, 8, "InnerMethod2", false)] @@ -151,7 +151,7 @@ await CompareObjectPropertiesFor(locals, "fn_void_del_arr", new[] }, "locals#fn_void_del_arr"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(0, 224, 8, "ActionTSignatureTest", false)] [InlineData(0, 224, 8, "ActionTSignatureTest", true)] [InlineData(2, 99, 8, "InnerMethod2", false)] @@ -193,7 +193,7 @@ await CompareObjectPropertiesFor(locals, "fn_action_arr", new[] }, "locals#fn_action_arr"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(0, 242, 8, "NestedDelegatesTest", false)] [InlineData(0, 242, 8, "NestedDelegatesTest", true)] [InlineData(2, 99, 8, "InnerMethod2", false)] @@ -236,7 +236,7 @@ await CompareObjectPropertiesFor(locals, "fn_del_arr", new[] }, "locals#fn_del_arr"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(0, 262, 8, "MethodWithDelegateArgs", false)] [InlineData(0, 262, 8, "MethodWithDelegateArgs", true)] [InlineData(2, 99, 8, "InnerMethod2", false)] @@ -269,7 +269,7 @@ await CompareObjectPropertiesFor(locals, "dst_arr", new[] }, "locals#dst_arr"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task MethodWithDelegatesAsyncTest(bool use_cfo) => await CheckInspectLocalsAtBreakpointSite( diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs index 6eed6e177f9a9c..576fa763e11784 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs @@ -3,26 +3,25 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Linq; using System.Net.WebSockets; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.WebAssembly.Diagnostics; -namespace Microsoft.WebAssembly.Diagnostics +namespace DebuggerTests { internal class DevToolsClient : IDisposable { DevToolsQueue _queue; - ClientWebSocket socket; - TaskCompletionSource _clientInitiatedClose = new TaskCompletionSource(); + protected WasmDebuggerConnection _conn; TaskCompletionSource _shutdownRequested = new TaskCompletionSource(); readonly TaskCompletionSource _failRequested = new(); TaskCompletionSource _newSendTaskAvailable = new (); protected readonly ILogger logger; - public event EventHandler<(RunLoopStopReason reason, Exception ex)> RunLoopStopped; + public event EventHandler RunLoopStopped; public DevToolsClient(ILogger logger) { @@ -42,29 +41,20 @@ public void Dispose() protected virtual void Dispose(bool disposing) { if (disposing) - socket.Dispose(); + _conn.Dispose(); } public async Task Shutdown(CancellationToken cancellationToken) { if (_shutdownRequested.Task.IsCompleted) { - logger.LogDebug($"Shutdown was already requested once. socket: {socket.State}. Ignoring"); + logger.LogDebug($"Shutdown was already requested once. Ignoring"); return; } - try - { - _shutdownRequested.SetResult(); - - if (!cancellationToken.IsCancellationRequested && socket.State == WebSocketState.Open) - await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken); - } - catch (Exception ex) when (ex is IOException || ex is WebSocketException || ex is OperationCanceledException) - { - logger.LogDebug($"DevToolsClient.Shutdown: Close failed, but ignoring: {ex}"); - } - } + await _conn.ShutdownAsync(cancellationToken); + _shutdownRequested.TrySetResult(); + } public void Fail(Exception exception) { @@ -74,47 +64,6 @@ public void Fail(Exception exception) _failRequested.TrySetResult(exception); } - async Task ReadOne(CancellationToken token) - { - byte[] buff = new byte[4000]; - var mem = new MemoryStream(); - while (true) - { - if (socket.State != WebSocketState.Open) - { - logger.LogDebug($"Socket is no longer open"); - _clientInitiatedClose.TrySetResult(); - return null; - } - - WebSocketReceiveResult result; - try - { - result = await socket.ReceiveAsync(new ArraySegment(buff), token).ConfigureAwait(false); - } - catch (Exception ex) - { - if (token.IsCancellationRequested || _shutdownRequested.Task.IsCompletedSuccessfully) - return null; - - logger.LogDebug($"DevToolsClient.ReadOne threw {ex.Message}, token: {token.IsCancellationRequested}, _shutdown: {_shutdownRequested.Task.Status}, clientInitiated: {_clientInitiatedClose.Task.Status}"); - throw; - } - - if (result.MessageType == WebSocketMessageType.Close) - { - _clientInitiatedClose.TrySetResult(); - return null; - } - - mem.Write(buff, 0, result.Count); - if (result.EndOfMessage) - { - return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int)mem.Length); - } - } - } - protected void Send(byte[] bytes, CancellationToken token) { Task sendTask = _queue.Send(bytes, token); @@ -122,88 +71,107 @@ protected void Send(byte[] bytes, CancellationToken token) _newSendTaskAvailable.TrySetResult(); } + protected async Task ConnectToWebServer(Uri uri, CancellationToken token) + { + // connects to the webserver to start the proxy + ClientWebSocket clientSocket = new (); + clientSocket.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan; + logger.LogDebug("Client connecting to {0}", uri); + await clientSocket.ConnectAsync(uri, token); + return clientSocket; + } + + protected virtual Task SetupConnection(Uri webserverUri, CancellationToken token) + => throw new NotImplementedException(); + protected async Task ConnectWithMainLoops( Uri uri, Func receive, - CancellationToken token) + CancellationTokenSource cts) { - logger.LogDebug("connecting to {0}", uri); - this.socket = new ClientWebSocket(); - this.socket.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan; - - await this.socket.ConnectAsync(uri, token); - _queue = new DevToolsQueue(socket); - - var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token); + CancellationToken token = cts.Token; + _conn = await SetupConnection(uri, token); + _queue = new DevToolsQueue(_conn); _ = Task.Run(async () => { try { - RunLoopStopReason reason; - Exception exception; + RunLoopExitState exitState; try { - (reason, exception) = await RunLoop(receive, linkedCts); + exitState = await RunLoop(receive, cts); } catch (Exception ex) { - logger.LogDebug($"RunLoop threw an exception. (parentToken: {token.IsCancellationRequested}, linked: {linkedCts.IsCancellationRequested}): {ex} "); - RunLoopStopped?.Invoke(this, (RunLoopStopReason.Exception, ex)); + logger.LogDebug($"RunLoop threw an exception. (parentToken: {token.IsCancellationRequested}, linked: {cts.IsCancellationRequested}): {ex} "); + RunLoopStopped?.Invoke(this, new(RunLoopStopReason.Exception, ex)); return; } try { - logger.LogDebug($"RunLoop stopped, reason: {reason}. (parentToken: {token.IsCancellationRequested}, linked: {linkedCts.IsCancellationRequested}): {exception?.Message}"); - RunLoopStopped?.Invoke(this, (reason, exception)); + logger.LogDebug($"RunLoop stopped, reason: {exitState}. (parentToken: {token.IsCancellationRequested}, linked: {cts.IsCancellationRequested}): {exitState.exception?.Message}"); + RunLoopStopped?.Invoke(this, exitState); } catch (Exception ex) { - logger.LogError(ex, $"Invoking RunLoopStopped event failed for (reason: {reason}, exception: {exception})"); + logger.LogError(ex, $"Invoking RunLoopStopped event failed for ({exitState}) with {ex}"); } } finally { - logger.LogDebug($"Loop ended with socket: {socket.State}"); - linkedCts.Cancel(); + cts.Cancel(); + _conn?.Dispose(); + if (_conn is DevToolsDebuggerConnection wsc) + logger.LogDebug($"Loop ended with socket: {wsc.WebSocket.State}"); + else + logger.LogDebug($"Loop ended"); } }); } - private async Task<(RunLoopStopReason, Exception)> RunLoop( + private async Task RunLoop( Func receive, - CancellationTokenSource linkedCts) + CancellationTokenSource cts) { var pending_ops = new List { - ReadOne(linkedCts.Token), + _conn.ReadOneAsync(cts.Token), _newSendTaskAvailable.Task, - _clientInitiatedClose.Task, _shutdownRequested.Task, _failRequested.Task }; // In case we had a Send called already - if (_queue.TryPumpIfCurrentCompleted(linkedCts.Token, out Task sendTask)) + if (_queue.TryPumpIfCurrentCompleted(cts.Token, out Task sendTask)) pending_ops.Add(sendTask); - while (!linkedCts.IsCancellationRequested) + while (!cts.IsCancellationRequested) { var task = await Task.WhenAny(pending_ops).ConfigureAwait(false); - if (task.IsCanceled && linkedCts.IsCancellationRequested) - return (RunLoopStopReason.Cancelled, null); - if (_shutdownRequested.Task.IsCompleted) - return (RunLoopStopReason.Shutdown, null); + return new(RunLoopStopReason.Shutdown, null); - if (_clientInitiatedClose.Task.IsCompleted) - return (RunLoopStopReason.ClientInitiatedClose, new TaskCanceledException("Proxy or the browser closed the connection")); + if (task.IsCanceled && cts.IsCancellationRequested) + return new(RunLoopStopReason.Cancelled, null); + + if (task.IsFaulted) + { + if (task == pending_ops[0] && !_conn.IsConnected) + return new(RunLoopStopReason.ProxyConnectionClosed, task.Exception); + return new(RunLoopStopReason.Exception, task.Exception); + } if (_failRequested.Task.IsCompleted) - return (RunLoopStopReason.Exception, _failRequested.Task.Result); + return new(RunLoopStopReason.Exception, _failRequested.Task.Result); + + // FIXME: instead of this, iterate through pending_ops, and clear it + // out every time we wake up + if (pending_ops.Where(t => t.IsFaulted).FirstOrDefault() is Task faultedTask) + return new(RunLoopStopReason.Exception, faultedTask.Exception); if (_newSendTaskAvailable.Task.IsCompleted) { @@ -212,7 +180,7 @@ protected async Task ConnectWithMainLoops( _newSendTaskAvailable = new (); pending_ops[1] = _newSendTaskAvailable.Task; - _queue.TryPumpIfCurrentCompleted(linkedCts.Token, out _); + _queue.TryPumpIfCurrentCompleted(cts.Token, out _); if (_queue.CurrentSend != null) pending_ops.Add(_queue.CurrentSend); } @@ -220,11 +188,11 @@ protected async Task ConnectWithMainLoops( if (task == pending_ops[0]) { var msg = await (Task)pending_ops[0]; - pending_ops[0] = ReadOne(linkedCts.Token); + pending_ops[0] = _conn.ReadOneAsync(cts.Token); if (msg != null) { - Task tsk = receive(msg, linkedCts.Token); + Task tsk = receive(msg, cts.Token); if (tsk != null) pending_ops.Add(tsk); } @@ -233,23 +201,15 @@ protected async Task ConnectWithMainLoops( { //must be a background task pending_ops.Remove(task); - if (task == _queue.CurrentSend && _queue.TryPumpIfCurrentCompleted(linkedCts.Token, out sendTask)) + if (task == _queue.CurrentSend && _queue.TryPumpIfCurrentCompleted(cts.Token, out sendTask)) pending_ops.Add(sendTask); } } - if (linkedCts.IsCancellationRequested) - return (RunLoopStopReason.Cancelled, null); + if (cts.IsCancellationRequested) + return new(RunLoopStopReason.Cancelled, null); - return (RunLoopStopReason.Exception, new InvalidOperationException($"This shouldn't ever get thrown. Unsure why the loop stopped")); + return new(RunLoopStopReason.Exception, new InvalidOperationException($"This shouldn't ever get thrown. Unsure why the loop stopped")); } } - - internal enum RunLoopStopReason - { - Shutdown, - Cancelled, - Exception, - ClientInitiatedClose - } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 6512d3ab9aaae8..ce77119109ebbc 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -13,7 +13,7 @@ namespace DebuggerTests { // TODO: static async, static method args - public class EvaluateOnCallFrameTests : DebuggerTestBase + public class EvaluateOnCallFrameTests : DebuggerTests { public static IEnumerable InstanceMethodsTestData(string type_name) { @@ -42,7 +42,7 @@ public static IEnumerable EvaluateStaticClassFromStaticMethodTestData( yield return new object[] { type_name, "EvaluateMethods", "EvaluateMethods", false }; } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] public async Task EvaluateTypeInstanceMembers(string prefix, int bias, string type, string method, string bp_function_name, bool is_async) @@ -88,6 +88,7 @@ public async Task EvaluateInstanceMethodArguments(string type, string method, st { var id = pause_location["callFrames"][0]["callFrameId"].Value(); var DTProp = new DateTime(2010, 9, 8, 7, 6, 5).AddMinutes(10); + Console.WriteLine ($"------- test running the bits.."); await EvaluateOnCallFrameAndCheck(id, ("g", TNumber(400)), ("h", TNumber(123)), @@ -97,7 +98,10 @@ await EvaluateOnCallFrameAndCheck(id, // property on method arg ("me.DTProp", TDateTime(DTProp)), ("me.DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)), - ("me.DTProp.Second + (me.IntProp - 5)", TNumber(DTProp.Second + 4))); + ("me.DTProp.Second + (me.IntProp - 5)", TNumber(DTProp.Second + 4))) + .ConfigureAwait(false); + + Console.WriteLine ($"------- test done!"); }); [Theory] @@ -126,7 +130,7 @@ await EvaluateOnCallFrameAndCheck(id, (" local_dt.Date", TDateTime(dt.Date))); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateStaticLocalsWithDeepMemberAccess() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", @@ -143,7 +147,7 @@ await EvaluateOnCallFrameAndCheck(id, ("f_s.dateTime.Date", TDateTime(dt.Date))); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateLocalsAsync() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.Point", "AsyncInstanceMethod", 1, "MoveNext", "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })", @@ -192,7 +196,7 @@ await EvaluateOnCallFrameAndCheck(id, } }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] public async Task EvaluateExpressionsWithDeepMemberAccesses(string prefix, int bias, string type, string method, string bp_function_name, bool _) @@ -317,7 +321,7 @@ await EvaluateOnCallFrameAndCheck(id, } }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("DebuggerTests.EvaluateTestsStructWithProperties", true)] [InlineData("DebuggerTests.EvaluateTestsClassWithProperties", false)] public async Task EvaluateOnPreviousFrames(string type_name, bool is_valuetype) => await CheckInspectLocalsAtBreakpointSite( @@ -408,7 +412,7 @@ await EvaluateOnCallFrameFail(id2, } }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task JSEvaluate() { var bp_loc = "/other.js"; @@ -430,7 +434,7 @@ await EvaluateOnCallFrameFail(id, await EvaluateOnCallFrame(id, "obj.foo", expect_ok: true); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task NegativeTestsInInstanceMethod() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", @@ -457,7 +461,7 @@ await EvaluateOnCallFrameFail(id, ("NullIfAIsNotZero.foo", "ReferenceError")); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task NegativeTestsInStaticMethod() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", @@ -493,7 +497,7 @@ async Task EvaluateOnCallFrameFail(string call_frame_id, params (string expressi } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateSimpleMethodCallsError() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", @@ -643,7 +647,7 @@ await EvaluateOnCallFrameAndCheck(id, }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateExpressionsWithElementAccessMultidimentional() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateLocalsWithElementAccessTests", "EvaluateLocals", 5, "EvaluateLocals", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithElementAccessTests:EvaluateLocals'); })", @@ -669,7 +673,7 @@ await EvaluateOnCallFrameAndCheck(id, }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateSimpleMethodCallsCheckChangedValue() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", @@ -783,7 +787,7 @@ await EvaluateOnCallFrameAndCheck(id, ("NoNamespaceClass.NestedClass1.NestedClass2.NestedClass3.StaticPropertyWithError", TString("System.Exception: not implemented 30"))); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateStaticClassesFromDifferentNamespaceInDifferentFrames() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTestsV2.EvaluateStaticClass", "Run", 1, "Run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", @@ -805,7 +809,7 @@ await EvaluateOnCallFrameAndCheck(id_second, ("EvaluateStaticClass.StaticPropertyWithError", TString("System.Exception: not implemented"))); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateStaticClassInvalidField() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateMethodTestsClass.TestEvaluate", "run", 9, "run", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateMethodTestsClass:EvaluateMethods'); })", @@ -822,7 +826,7 @@ public async Task EvaluateStaticClassInvalidField() => await CheckInspectLocalsA AssertEqual("Failed to resolve member access for DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", res.Error["result"]?["description"]?.Value(), "wrong error message"); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task AsyncLocalsInContinueWithBlock() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithStaticAsync", 4, "b__3_0", "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })", @@ -842,7 +846,7 @@ await EvaluateOnCallFrameFail(id, ); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateConstantValueUsingRuntimeEvaluate() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", @@ -856,7 +860,7 @@ await RuntimeEvaluateAndCheck( ("\"15\"", TString("15"))); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("EvaluateBrowsableProperties", "TestEvaluateFieldsNone", "testFieldsNone", 10)] [InlineData("EvaluateBrowsableProperties", "TestEvaluatePropertiesNone", "testPropertiesNone", 10)] [InlineData("EvaluateBrowsableCustomProperties", "TestEvaluatePropertiesNone", "testPropertiesNone", 5, true)] @@ -908,7 +912,7 @@ public async Task EvaluateBrowsableNever(string outerClassName, string className }, "testNeverProps#1"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("EvaluateBrowsableProperties", "TestEvaluateFieldsCollapsed", "testFieldsCollapsed", 10)] [InlineData("EvaluateBrowsableProperties", "TestEvaluatePropertiesCollapsed", "testPropertiesCollapsed", 10)] [InlineData("EvaluateBrowsableStaticProperties", "TestEvaluateFieldsCollapsed", "testFieldsCollapsed", 10)] @@ -976,7 +980,7 @@ public async Task EvaluateBrowsableRootHidden(string outerClassName, string clas Assert.Equal(mergedRefItems, testRootHiddenProps); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateStaticAttributeInAssemblyNotRelatedButLoaded() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", @@ -986,7 +990,7 @@ await RuntimeEvaluateAndCheck( ("ClassToBreak.valueToCheck", TNumber(10))); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateLocalObjectFromAssemblyNotRelatedButLoaded() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.EvaluateTestsClass", "EvaluateLocalsFromAnotherAssembly", 5, "EvaluateLocalsFromAnotherAssembly", @@ -997,7 +1001,7 @@ await RuntimeEvaluateAndCheck( ("a.valueToCheck", TNumber(20))); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateProtectionLevels() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.GetPropertiesTests.DerivedClass", "InstanceMethod", 1, "InstanceMethod", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.GetPropertiesTests.DerivedClass:run'); })", @@ -1037,7 +1041,7 @@ public async Task EvaluateProtectionLevels() => await CheckInspectLocalsAtBreak }, "private"); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task StructureGetters() => await CheckInspectLocalsAtBreakpointSite( "DebuggerTests.StructureGetters", "Evaluate", 2, "Evaluate", "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.StructureGetters:Evaluate'); })", @@ -1052,7 +1056,7 @@ public async Task StructureGetters() => await CheckInspectLocalsAtBreakpointSit }, "s#1"); }); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task EvaluateMethodWithDefaultParam() => await CheckInspectLocalsAtBreakpointSite( $"DebuggerTests.DefaultParamMethods", "Evaluate", 2, "Evaluate", $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] DebuggerTests.DefaultParamMethods:Evaluate'); 1 }})", diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs index 082a4a8a04cc97..5d483d6687baca 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs @@ -11,9 +11,9 @@ namespace DebuggerTests { - public class ExceptionTests : DebuggerTestBase + public class ExceptionTests : DebuggerTests { - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task ExceptionTestAll() { string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions"; @@ -60,7 +60,7 @@ await CheckValue(pause_location["data"], JObject.FromObject(new await CheckString(exception_members, "message", "not implemented uncaught"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task JSExceptionTestAll() { await SetPauseOnException("all"); @@ -127,7 +127,7 @@ async Task WaitForJSException(JObject pause_location, string exp_fn_nam // FIXME? BUG? We seem to get the stack trace for Runtime.exceptionThrown at `call_method`, // but JS shows the original error type, and original trace - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task ExceptionTestNone() { //Collect events @@ -162,7 +162,7 @@ await CheckValue(eo["exceptionDetails"]?["exception"], JObject.FromObject(new Assert.True(false, "Expected to get an ArgumentException from the uncaught user exception"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task JSExceptionTestNone() { await SetPauseOnException("none"); @@ -195,7 +195,7 @@ await CheckValue(eo["exceptionDetails"]?["exception"], JObject.FromObject(new Assert.True(false, "Expected to get an ArgumentException from the uncaught user exception"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("function () { exceptions_test (); }", null, 0, 0, "exception_uncaught_test", "RangeError", "exception uncaught")] [InlineData("function () { invoke_static_method ('[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions'); }", "dotnet://debugger-test.dll/debugger-exception-test.cs", 28, 16, "run", @@ -221,7 +221,7 @@ await CheckValue(pause_location["data"], JObject.FromObject(new await CheckString(exception_members, "message", exception_message); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task ExceptionTestUncaughtWithReload() { string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions"; @@ -261,7 +261,7 @@ await CheckValue(pause_location["data"], JObject.FromObject(new await CheckString(exception_members, "message", "not implemented uncaught"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("[debugger-test] DebuggerTests.ExceptionTestsClassDefault:TestExceptions", "System.Exception", 76)] [InlineData("[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions", "DebuggerTests.CustomException", 28)] public async Task ExceptionTestAllWithReload(string entry_method_name, string class_name, int line_number) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/FirefoxInspectorClient.cs b/src/mono/wasm/debugger/DebuggerTestSuite/FirefoxInspectorClient.cs new file mode 100644 index 00000000000000..b3e9fb6a6d03d5 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/FirefoxInspectorClient.cs @@ -0,0 +1,230 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using Microsoft.WebAssembly.Diagnostics; +using System; +using System.Net.WebSockets; +using System.Net.Sockets; +using System.Net; + +#nullable enable + +namespace DebuggerTests; + +class FirefoxInspectorClient : InspectorClient +{ + internal string? BreakpointActorId {get; set;} + internal string? ConsoleActorId {get; set;} + internal string? ThreadActorId {get; set;} + private ClientWebSocket? _clientSocket; + + public FirefoxInspectorClient(ILogger logger) : base(logger) + { + } + + protected override async Task SetupConnection(Uri webserverUri, CancellationToken token) + { + _clientSocket = await ConnectToWebServer(webserverUri, token); + + ArraySegment buff = new(new byte[10]); + _ = _clientSocket.ReceiveAsync(buff, token) + .ContinueWith(async t => + { + if (token.IsCancellationRequested) + return; + + logger.LogTrace($"** client socket closed, so stopping the client loop too"); + // Webserver connection is closed + // So, stop the loop here too + // _clientInitiatedClose.TrySetResult(); + await Shutdown(token); + }, TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.RunContinuationsAsynchronously) + .ConfigureAwait(false); + + RunLoopStopped += (_, _) => + { + logger.LogDebug($"RunLoop stopped, closing the websocket, state: {_clientSocket.State}"); + if (_clientSocket.State == WebSocketState.Open) + { + _clientSocket.Abort(); + } + }; + + IPEndPoint endpoint = new (IPAddress.Parse("127.0.0.1"), DebuggerTestBase.FirefoxProxyPort); + try + { + TcpClient tcpClient = new(); + + logger.LogDebug($"Connecting to the proxy at tcp://{endpoint} .."); + await tcpClient.ConnectAsync(endpoint, token); + logger.LogDebug($".. connected to the proxy!"); + return new FirefoxDebuggerConnection(tcpClient, "client", logger); + } + catch (SocketException se) + { + throw new Exception($"Failed to connect to the proxy at {endpoint}", se); + } + } + + public override async Task ProcessCommand(Result command, CancellationToken token) + { + if (command.Value?["result"]?["value"]?["tabs"] != null) + { + var toCmd = command.Value?["result"]?["value"]?["tabs"]?[0]?["actor"]?.Value(); + var res = await SendCommand("getWatcher", JObject.FromObject(new { type = "getWatcher", isServerTargetSwitchingEnabled = true, to = toCmd}), token); + var watcherId = res.Value?["result"]?["value"]?["actor"]?.Value(); + res = await SendCommand("watchResources", JObject.FromObject(new { type = "watchResources", resourceTypes = new JArray("console-message"), to = watcherId}), token); + res = await SendCommand("watchTargets", JObject.FromObject(new { type = "watchTargets", targetType = "frame", to = watcherId}), token); + UpdateTarget(res.Value?["result"]?["value"]?["target"] as JObject); + await SendCommand("attach", JObject.FromObject(new + { + type = "attach", + options = JObject.FromObject(new + { + pauseOnExceptions = false, + ignoreCaughtExceptions = true, + shouldShowOverlay = true, + shouldIncludeSavedFrames = true, + shouldIncludeAsyncLiveFrames = false, + skipBreakpoints = false, + logEventBreakpoints = false, + observeAsmJS = true, + breakpoints = new JArray(), + eventBreakpoints = new JArray() + }), + to = ThreadActorId + }), token); + res = await SendCommand("getBreakpointListActor", JObject.FromObject(new { type = "getBreakpointListActor", to = watcherId}), token); + BreakpointActorId = res.Value?["result"]?["value"]?["breakpointList"]?["actor"]?.Value(); + } + } + + protected override Task? HandleMessage(string msg, CancellationToken token) + { + var res = JObject.Parse(msg); + if (res["type"]?.Value() == "newSource") + { + var method = res["type"]?.Value(); + return onEvent(method, res, token); + } + + if (res["type"]?.Value() == "target-available-form" && res["target"] is JObject target) + { + UpdateTarget(target); + return Task.CompletedTask; + } + if (res["applicationType"] != null) + return null; + if (res["resultID"] != null) + { + if (res["type"]?.Value() == "evaluationResult") + { + if (res["from"]?.Value() is not string from_str) + return null; + + var messageId = new FirefoxMessageId("", 0, from_str); + if (pending_cmds.Remove(messageId, out var item)) + item.SetResult(Result.FromJsonFirefox(res)); + else + logger.LogDebug($"HandleMessage: Could not find any pending cmd for {messageId}. msg: {msg}"); + } + return null; + } + if (res["from"] is not null) + { + if (res["from"]?.Value() is not string from_str) + return null; + + var messageId = new FirefoxMessageId("", 0, from_str); + if (pending_cmds.Remove(messageId, out var item)) + { + item.SetResult(Result.FromJsonFirefox(res)); + return null; + } + } + if (res["type"] != null) + { + var method = res["type"]?.Value(); + switch (method) + { + case "paused": + { + method = "Debugger.paused"; + break; + } + case "resource-available-form": + { + if (res["resources"]?[0]?["resourceType"]?.Value() == "console-message" /*&& res["resources"][0]["arguments"] != null*/) + { + method = "Runtime.consoleAPICalled"; + var args = new JArray(); + // FIXME: unncessary alloc + foreach (JToken? argument in res["resources"]?[0]?["message"]?["arguments"]?.Value() ?? new JArray()) + { + args.Add(JObject.FromObject(new { value = argument.Value()})); + } + res = JObject.FromObject(new + { + type = res["resources"]?[0]?["message"]?["level"]?.Value(), + args + }); + } + break; + } + } + return onEvent(method, res, token); + } + return null; + } + + public override Task SendCommand(SessionId sessionId, string method, JObject? args, CancellationToken token) + { + if (args == null) + args = new JObject(); + + var tcs = new TaskCompletionSource(); + MessageId msgId; + if (args["to"]?.Value() is not string to_str) + throw new Exception($"No 'to' field found in '{args}'"); + + msgId = new FirefoxMessageId("", 0, to_str); + pending_cmds[msgId] = tcs; + logger.LogTrace($"SendCommand: to: {args}"); + + var msg = args.ToString(Formatting.None); + var bytes = Encoding.UTF8.GetBytes(msg); + Send(bytes, token); + + return tcs.Task; + } + + private void UpdateTarget(JObject? target) + { + if (target?["threadActor"]?.Value() is string threadActorId) + { + ThreadActorId = threadActorId; + logger.LogTrace($"Updated threadActorId to {threadActorId}"); + } + if (target?["consoleActor"]?.Value() is string consoleActorId) + { + ConsoleActorId = consoleActorId; + logger.LogTrace($"Updated consoleActorId to {consoleActorId}"); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing && _clientSocket?.State == WebSocketState.Open) + { + _clientSocket?.Abort(); + _clientSocket?.Dispose(); + } + } +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/FirefoxProvider.cs b/src/mono/wasm/debugger/DebuggerTestSuite/FirefoxProvider.cs new file mode 100644 index 00000000000000..efa3efd6a2d89b --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/FirefoxProvider.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.WebAssembly.Diagnostics; + +#nullable enable + +namespace DebuggerTests; + +internal class FirefoxProvider : WasmHostProvider +{ + private WebSocket? _ideWebSocket; + private FirefoxDebuggerProxy? _firefoxDebuggerProxy; + private static readonly Lazy s_browserPath = new(() => GetBrowserPath(GetPathsToProbe())); + + public FirefoxProvider(string id, ILogger logger) : base(id, logger) + { + } + + public async Task StartBrowserAndProxyAsync(HttpContext context, + string targetUrl, + int remoteDebuggingPort, + int proxyPort, + string messagePrefix, + ILoggerFactory loggerFactory, + CancellationTokenSource cts, + int browserReadyTimeoutMs = 20000) + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(FirefoxProvider)); + + try + { + string args = $"-profile {GetProfilePath(Id)} -headless -new-instance -private -start-debugger-server {remoteDebuggingPort}"; + ProcessStartInfo? psi = GetProcessStartInfo(s_browserPath.Value, args, targetUrl); + string? line = await LaunchHostAsync( + psi, + context, + str => + { + // FIXME: instead of this, we can wait for the port to open + //for running debugger tests on firefox + if (str?.Contains("[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer, no dt") == true) + return $"http://localhost:{remoteDebuggingPort}"; + + return null; + }, + messagePrefix, + browserReadyTimeoutMs, + cts.Token).ConfigureAwait(false); + + if (_process is null || line is null) + throw new Exception($"Failed to launch firefox"); + } + catch (Exception ex) + { + TestHarnessProxy.RegisterProxyExitState(Id, new(RunLoopStopReason.Exception, ex)); + throw; + } + + /* + * Firefox uses a plain tcp connection, so we use that for communicating + * with the browser. But the tests connect to the webserver via a websocket, + * so we *accept* that here to complete the connection. + * + * Normally, when the tests are closing down, they close that webserver + * connection, and the proxy would shutdown too. But in this case, we need + * to explicitly trigger the proxy/browser shutdown when the websocket + * is closed. + */ + _ideWebSocket = await context.WebSockets.AcceptWebSocketAsync(); + + ArraySegment buff = new(new byte[10]); + _ = _ideWebSocket.ReceiveAsync(buff, cts.Token) + .ContinueWith(t => + { + // client has closed the webserver connection, Or + // it has been cancelled. + // so, we should kill the proxy, and firefox + Dispose(); + }, TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.RunContinuationsAsynchronously) + .ConfigureAwait(false); + + _firefoxDebuggerProxy = new FirefoxDebuggerProxy(); + TestHarnessProxy.RegisterNewProxy(Id, _firefoxDebuggerProxy); + await _firefoxDebuggerProxy + .RunForTests(remoteDebuggingPort, proxyPort, Id, loggerFactory, _logger, cts) + .ConfigureAwait(false); + } + + public override void Dispose() + { + if (_isDisposed || _isDisposing) + return; + + _isDisposing = true; + if (_process?.HasExited == true) + _firefoxDebuggerProxy?.Fail(new Exception($"Firefox unexpectedly exited with code {_process.ExitCode}")); + else + _firefoxDebuggerProxy?.Shutdown(); + + base.Dispose(); + + _logger.LogDebug($"[test_id: {Id}] {nameof(FirefoxProvider)} Dispose"); + + if (_ideWebSocket is not null) + { + _ideWebSocket.Abort(); + _ideWebSocket.Dispose(); + _ideWebSocket = null; + } + + _logger.LogDebug($"[test_id: {Id}] {nameof(FirefoxProvider)} Dispose done"); + _isDisposed = true; + _isDisposing = false; + } + + private static string GetProfilePath(string Id) + { + string prefs = """ + user_pref("devtools.chrome.enabled", true); + user_pref("devtools.debugger.remote-enabled", true); + user_pref("devtools.debugger.prompt-connection", false); + """; + + string profilePath = Path.GetFullPath(Path.Combine(DebuggerTestBase.DebuggerTestAppPath, $"test-profile-{Id}")); + if (Directory.Exists(profilePath)) + Directory.Delete(profilePath, recursive: true); + + Directory.CreateDirectory(profilePath); + File.WriteAllText(Path.Combine(profilePath, "prefs.js"), prefs); + + return profilePath; + } + + private static IEnumerable GetPathsToProbe() + { + List 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; + } +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs index f91ba3c012effb..cdd091414f3674 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs @@ -11,7 +11,7 @@ namespace DebuggerTests { - public class GetPropertiesTests : DebuggerTestBase + public class GetPropertiesTests : DebuggerTests { public static TheoryData, bool> ClassGetPropertiesTestData(bool is_async) { @@ -161,7 +161,7 @@ public class GetPropertiesTests : DebuggerTestBase return data; } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(ClassGetPropertiesTestData), parameters: true)] [MemberData(nameof(ClassGetPropertiesTestData), parameters: false)] [MemberData(nameof(StructGetPropertiesTestData), parameters: true)] @@ -190,7 +190,7 @@ public async Task InspectTypeInheritedMembers(string type_name, bool? own_proper public static IEnumerable MembersForLocalNestedStructData(bool is_async) => StructGetPropertiesTestData(false).Select(datum => datum[1..]); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(MembersForLocalNestedStructData), parameters: false)] [MemberData(nameof(MembersForLocalNestedStructData), parameters: true)] public async Task MembersForLocalNestedStruct(bool? own_properties, bool? accessors_only, string[] expected_names, Dictionary all_props, bool is_async) => await CheckInspectLocalsAtBreakpointSite( @@ -275,7 +275,7 @@ public async Task MembersForLocalNestedStruct(bool? own_properties, bool? access } }; - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberData(nameof(JSGetPropertiesTestData), parameters: true)] // Note: Disabled because we don't match JS's behavior here! // We return inherited members too for `ownProperties:true` @@ -335,7 +335,7 @@ await CheckExpectedProperties( //AssertEqual(expected_names.Length, filtered_props.Count(), $"expected number of properties"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task GetObjectValueWithInheritance() { var pause_location = await EvaluateAndCheck( diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/HarnessTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/HarnessTests.cs index d164b9663887af..603d0f9c86f0bc 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/HarnessTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/HarnessTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Net.WebSockets; using System.Threading.Tasks; using Microsoft.WebAssembly.Diagnostics; using Newtonsoft.Json.Linq; @@ -12,9 +11,9 @@ namespace DebuggerTests { - public class HarnessTests : DebuggerTestBase + public class HarnessTests : DebuggerTests { - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task TimedOutWaitingForInvalidBreakpoint() { await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 100, 0); @@ -23,7 +22,7 @@ public async Task TimedOutWaitingForInvalidBreakpoint() Assert.Contains("timed out", tce.Message); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task ExceptionThrown() { var ae = await Assert.ThrowsAsync( @@ -31,11 +30,27 @@ public async Task ExceptionThrown() Assert.Contains("non_existant_fn is not defined", ae.Message); } - [Fact] - public async Task BrowserCrash() => await Assert.ThrowsAsync(async () => - await SendCommandAndCheck(null, "Browser.crash", null, -1, -1, null)); + [ConditionalFact(nameof(RunningOnChrome))] + public async Task BrowserCrash() + { + TaskCompletionSource clientRunLoopStopped = new(); + insp.Client.RunLoopStopped += (_, args) => clientRunLoopStopped.TrySetResult(args); + try + { + await SendCommandAndCheck(null, "Browser.crash", null, -1, -1, null); + } + catch (Exception ex) + { + Task t = await Task.WhenAny(clientRunLoopStopped.Task, Task.Delay(10000)); + if (t != clientRunLoopStopped.Task) + Assert.Fail($"Proxy did not stop, as expected"); + RunLoopExitState? state = await clientRunLoopStopped.Task; + if (state.reason != RunLoopStopReason.ProxyConnectionClosed) + Assert.Fail($"Client runloop did not stop with ProxyConnectionClosed. state: {state}.{Environment.NewLine}SendCommand had failed with {ex}"); + } + } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InspectorWaitForAfterMessageAlreadyReceived() { Result res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -53,7 +68,7 @@ public async Task InspectorWaitForAfterMessageAlreadyReceived() await insp.WaitFor(Inspector.PAUSE); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InspectorWaitForMessageThatNeverArrives() { var tce = await Assert.ThrowsAsync(async () => await insp.WaitFor("Message.that.never.arrives")); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs index 91ae31bfa5eabf..58f36667cc54b1 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs @@ -29,34 +29,39 @@ class Inspector public const string READY = "ready"; public CancellationToken Token { get; } public InspectorClient Client { get; } + public DebuggerProxyBase? Proxy { get; } public bool DetectAndFailOnAssertions { get; set; } = true; private CancellationTokenSource _cancellationTokenSource; + private Exception? _isFailingWithException; - protected ILoggerFactory _loggerFactory; - protected ILogger _logger; - public int Id { get; init; } - - public Inspector(int testId) + protected static Lazy s_loggerFactory = new(() => { - Id = testId; - _cancellationTokenSource = new CancellationTokenSource(); - Token = _cancellationTokenSource.Token; - - string logFilePath = Path.Combine(DebuggerTestBase.TestLogPath, $"{Id}-test.log"); - File.Delete(logFilePath); - _loggerFactory = LoggerFactory.Create(builder => + return LoggerFactory.Create(builder => builder - .AddFile(logFilePath, minimumLevel: LogLevel.Debug) + // .AddFile(logFilePath, minimumLevel: LogLevel.Debug) .AddSimpleConsole(options => { options.SingleLine = true; options.TimestampFormat = "[HH:mm:ss] "; }) .AddFilter(null, LogLevel.Trace)); + }); + + protected ILogger _logger; + public int Id { get; init; } - Client = new InspectorClient(_loggerFactory.CreateLogger($"{nameof(InspectorClient)}-{Id}")); - _logger = _loggerFactory.CreateLogger($"{nameof(Inspector)}-{Id}"); + public Inspector(int testId) + { + Id = testId; + _cancellationTokenSource = new CancellationTokenSource(); + Token = _cancellationTokenSource.Token; + + _logger = s_loggerFactory.Value.CreateLogger($"{nameof(Inspector)}-{Id}"); + if (DebuggerTestBase.RunningOnChrome) + Client = new InspectorClient(_logger); + else + Client = new FirefoxInspectorClient(_logger); } public Task WaitFor(string what) @@ -129,6 +134,7 @@ void FailAllWaiters(Exception? exception = null) if (exception != null) { + _isFailingWithException = exception; foreach (var tcs in notifications.Values) tcs.TrySetException(exception); } @@ -149,6 +155,9 @@ private static string FormatConsoleAPICalled(JObject args) consoleArgs.Add(arg!["value"]!.ToString()); } + if (consoleArgs.Count == 0) + return "console: "; + int position = 1; string first = consoleArgs[0]; string output = _consoleArgsRegex.Replace(first, (_) => $"{consoleArgs[position++]}"); @@ -193,6 +202,7 @@ async Task OnMessage(string method, JObject args, CancellationToken token) { args["__forMethod"] = method; Client.Fail(new ArgumentException($"Unexpected runtime error/warning message detected: {line}{Environment.NewLine}{args}")); + TestHarnessProxy.ShutdownProxy(Id.ToString()); return; } @@ -219,33 +229,59 @@ async Task OnMessage(string method, JObject args, CancellationToken token) } } - public async Task OpenSessionAsync(Func)>> getInitCmds, TimeSpan span) + public async Task LaunchBrowser(DateTime start, TimeSpan span) { - var start = DateTime.Now; - try + _cancellationTokenSource.CancelAfter(span); + string uriStr = $"ws://{TestHarnessProxy.Endpoint.Authority}/launch-host-and-connect/?test_id={Id}"; + if (!DebuggerTestBase.RunningOnChrome) { - _cancellationTokenSource.CancelAfter(span); - - var uri = new Uri($"ws://{TestHarnessProxy.Endpoint.Authority}/launch-chrome-and-connect/?test_id={Id}"); + uriStr += "&host=firefox&firefox-proxy-port=6002"; + // Ensure the listener is running early, so trying to + // connect to that does not race with the starting of it + FirefoxDebuggerProxy.StartListener(6002, _logger); + } - await Client.Connect(uri, OnMessage, _cancellationTokenSource.Token); - Client.RunLoopStopped += (_, args) => + await Client.Connect(new Uri(uriStr), OnMessage, _cancellationTokenSource); + Client.RunLoopStopped += (_, args) => + { + switch (args.reason) { - switch (args.reason) - { - case RunLoopStopReason.Exception: - FailAllWaiters(args.ex); - break; - - case RunLoopStopReason.Cancelled when Token.IsCancellationRequested: + case RunLoopStopReason.Exception: + if (TestHarnessProxy.TryGetProxyExitState(Id.ToString(), out var state)) + { + Console.WriteLine ($"client exiting with exception, and proxy has: {state}"); + } + FailAllWaiters(args.exception); + break; + + case RunLoopStopReason.Cancelled when Token.IsCancellationRequested: + if (_isFailingWithException is null) FailAllWaiters(new TaskCanceledException($"Test timed out (elapsed time: {(DateTime.Now - start).TotalSeconds})")); - break; + break; - default: + default: + if (_isFailingWithException is null) FailAllWaiters(); - break; - }; + break; }; + }; + + TestHarnessProxy.RegisterExitHandler(Id.ToString(), state => + { + if (_isFailingWithException is null && state.reason == RunLoopStopReason.Exception) + { + Client.Fail(state.exception); + FailAllWaiters(state.exception); + } + }); + } + + public async Task OpenSessionAsync(Func)>> getInitCmds, TimeSpan span) + { + var start = DateTime.Now; + try + { + await LaunchBrowser(start, span); var init_cmds = getInitCmds(Client, _cancellationTokenSource.Token); @@ -261,6 +297,9 @@ public async Task OpenSessionAsync(Func ct.Item2 == completedTask); string cmd_name = init_cmds[cmdIdx].Item1; + if (_isFailingWithException is not null) + throw _isFailingWithException; + if (completedTask.IsCanceled) { throw new TaskCanceledException( @@ -274,7 +313,7 @@ public async Task OpenSessionAsync(Func> pending_cmds = new Dictionary>(); - Func onEvent; - int next_cmd_id; + protected Dictionary> pending_cmds = new Dictionary>(); + protected Func onEvent; + protected int next_cmd_id; public InspectorClient(ILogger logger) : base(logger) { } - Task HandleMessage(string msg, CancellationToken token) + protected override async Task SetupConnection(Uri webserverUri, CancellationToken token) + => new DevToolsDebuggerConnection( + await ConnectToWebServer(webserverUri, token), + "client", + logger); + + protected virtual Task HandleMessage(string msg, CancellationToken token) { var res = JObject.Parse(msg); @@ -34,10 +44,15 @@ Task HandleMessage(string msg, CancellationToken token) return null; } - public async Task Connect( + public virtual async Task ProcessCommand(Result command, CancellationToken token) + { + await Task.FromResult(true); + } + + public virtual async Task Connect( Uri uri, Func onEvent, - CancellationToken token) + CancellationTokenSource cts) { this.onEvent = onEvent; @@ -47,7 +62,7 @@ public async Task Connect( if (args.reason == RunLoopStopReason.Exception) { foreach (var cmd in pending_cmds.Values) - cmd.SetException(args.ex); + cmd.SetException(args.exception); } else { @@ -56,13 +71,13 @@ public async Task Connect( } }; - await ConnectWithMainLoops(uri, HandleMessage, token); + await ConnectWithMainLoops(uri, HandleMessage, cts); } public Task SendCommand(string method, JObject args, CancellationToken token) => SendCommand(new SessionId(null), method, args, token); - public Task SendCommand(SessionId sessionId, string method, JObject args, CancellationToken token) + public virtual Task SendCommand(SessionId sessionId, string method, JObject args, CancellationToken token) { int id = ++next_cmd_id; if (args == null) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs index ece8701b61cb71..7880dc95eca57d 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/MiscTests.cs @@ -14,7 +14,7 @@ namespace DebuggerTests { - public class MiscTests : DebuggerTestBase + public class MiscTests : DebuggerTests { [Fact] @@ -25,7 +25,7 @@ public void CheckThatAllSourcesAreSent() Assert.Contains("dotnet://debugger-test.dll/dependency.cs", scripts.Values); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task ExceptionThrownInJS() { var eval_req = JObject.FromObject(new @@ -38,7 +38,7 @@ public async Task ExceptionThrownInJS() Assert.Equal("Uncaught", eval_res.Error["exceptionDetails"]?["text"]?.Value()); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task ExceptionThrownInJSOutOfBand() { await SetBreakpoint("/debugger-driver.html", 27, 2); @@ -77,7 +77,7 @@ await CheckInspectLocalsAtBreakpointSite( } ); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InspectPrimitiveTypeLocalsAtBreakpointSite() => await CheckInspectLocalsAtBreakpointSite( "dotnet://debugger-test.dll/debugger-test.cs", 154, 8, "PrimitiveTypesTest", @@ -157,7 +157,7 @@ await CheckProps(strings_arr, new[] } ); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("TestNullableLocal", false)] [InlineData("TestNullableLocalAsync", true)] public async Task InspectNullableLocals(string method_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( @@ -220,7 +220,7 @@ await CheckInspectLocalsAtBreakpointSite( } ); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task RuntimeGetPropertiesWithInvalidScopeIdTest() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 49, 8); @@ -250,7 +250,7 @@ await EvaluateAndCheck( ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectLocalsWithStructs(bool use_cfo) @@ -433,7 +433,6 @@ public async Task InspectBoxedAsClassLocals(string method_name, bool is_async) = { var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); var dt = new DateTime(2310, 1, 2, 3, 4, 5); - Console.WriteLine(locals); await CheckProps(locals, new { @@ -444,7 +443,7 @@ public async Task InspectBoxedAsClassLocals(string method_name, bool is_async) = }, "locals"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectLocalsWithStructsStaticAsync(bool use_cfo) @@ -510,9 +509,9 @@ await CompareObjectPropertiesFor(ss_local_props, "gs", [Theory] [InlineData(137, 12, "MethodWithLocalsForToStringTest", false, false)] - [InlineData(147, 12, "MethodWithArgumentsForToStringTest", true, false)] + /*[InlineData(147, 12, "MethodWithArgumentsForToStringTest", true, false)] [InlineData(192, 12, "MethodWithArgumentsForToStringTestAsync", true, true)] - [InlineData(182, 12, "MethodWithArgumentsForToStringTestAsync", false, true)] + [InlineData(182, 12, "MethodWithArgumentsForToStringTestAsync", false, true)]*/ public async Task InspectLocalsForToStringDescriptions(int line, int col, string method_name, bool call_other, bool invoke_async) { string entry_method_name = $"[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalsForToStringTest{(invoke_async ? "Async" : String.Empty)}"; @@ -606,7 +605,7 @@ public async Task InspectLocals() var locals = await GetProperties(wait_res["callFrames"][1]["callFrameId"].Value()); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectLocalsForStructInstanceMethod(bool use_cfo) => await CheckInspectLocalsAtBreakpointSite( @@ -676,7 +675,7 @@ public async Task EmptyTypeWithNoLocalsOrParams(string type_name, bool is_async) AssertEqual(0, frame_locals.Values().Count(), "locals"); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task StaticMethodWithLocalEmptyStructThatWillGetExpanded(bool is_async) => await CheckInspectLocalsAtBreakpointSite( @@ -734,7 +733,7 @@ JObject FindFrame(JObject pause_location, string function_name) ?.Where(f => f["functionName"]?.Value() == function_name) ?.FirstOrDefault(); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugLazyLoadedAssemblyWithPdb() { Task bpResolved = WaitForBreakpointResolvedEvent(); @@ -758,7 +757,7 @@ await LoadAssemblyDynamically( CheckNumber(locals, "b", 10); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugLazyLoadedAssemblyWithEmbeddedPdb() { Task bpResolved = WaitForBreakpointResolvedEvent(); @@ -782,7 +781,7 @@ await LoadAssemblyDynamically( CheckNumber(locals, "b", 10); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebugLazyLoadedAssemblyWithEmbeddedPdbALC() { int line = 9; @@ -799,7 +798,7 @@ public async Task DebugLazyLoadedAssemblyWithEmbeddedPdbALC() CheckNumber(locals, "b", 10); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CannotDebugLazyLoadedAssemblyWithoutPdb() { int line = 9; @@ -815,7 +814,7 @@ await LoadAssemblyDynamically( Assert.DoesNotContain(source_location, scripts.Values); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task GetSourceUsingSourceLink() { var bp = await SetBreakpointInMethod("debugger-test-with-source-link.dll", "DebuggerTests.ClassToBreak", "TestBreakpoint", 0); @@ -835,7 +834,7 @@ public async Task GetSourceUsingSourceLink() Assert.True(source.IsOk, $"Failed to getScriptSource: {source}"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task GetSourceEmbeddedSource() { string asm_file = Path.Combine(DebuggerTestAppPath, "ApplyUpdateReferencedAssembly.dll"); @@ -855,7 +854,7 @@ public async Task GetSourceEmbeddedSource() Assert.False(source.Value["scriptSource"].Value().Contains("// Unable to read document")); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task InspectTaskAtLocals() => await CheckInspectLocalsAtBreakpointSite( "InspectTask", "RunInspectTask", @@ -895,7 +894,7 @@ public async Task MallocUntilReallocate() //https://github.com/xamarin/xamarin-a { string eval_expr = "window.setTimeout(function() { malloc_to_reallocate_test (); }, 1)"; - var result = await cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), token); + var result = await Evaluate(eval_expr); var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -957,10 +956,10 @@ await EvaluateAndCheck( [Theory] [InlineData( - "DebuggerTests.CheckSpecialCharactersInPath", + "DebuggerTests.CheckSpecialCharactersInPath", "dotnet://debugger-test-special-char-in-path.dll/test#.cs")] [InlineData( - "DebuggerTests.CheckSNonAsciiCharactersInPath", + "DebuggerTests.CheckSNonAsciiCharactersInPath", "dotnet://debugger-test-special-char-in-path.dll/non-ascii-test-ął.cs")] public async Task SetBreakpointInProjectWithSpecialCharactersInPath( string classWithNamespace, string expectedFileLocation) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs index eacdda9f8b803f..90f80da350f807 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs @@ -11,9 +11,9 @@ namespace DebuggerTests { - public class MonoJsTests : DebuggerTestBase + public class MonoJsTests : DebuggerTests { - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task BadRaiseDebugEventsTest() { var bad_expressions = new[] @@ -38,7 +38,7 @@ public async Task BadRaiseDebugEventsTest() } } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(true)] [InlineData(false)] [InlineData(null)] @@ -70,7 +70,7 @@ public async Task RaiseDebugEventTraceTest(bool? trace) Assert.False(tcs.Task == t, "Event should not have been logged"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(true, 1)] [InlineData(false, 0)] public async Task DuplicateAssemblyLoadedEventNotLoadedFromBundle(bool load_pdb, int expected_count) @@ -82,7 +82,7 @@ public async Task DuplicateAssemblyLoadedEventNotLoadedFromBundle(bool load_pdb, expected_count ); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(true, 1)] [InlineData(false, 1)] // Since it's being loaded from the bundle, it will have the pdb even if we don't provide one public async Task DuplicateAssemblyLoadedEventForAssemblyFromBundle(bool load_pdb, int expected_count) @@ -94,7 +94,7 @@ public async Task DuplicateAssemblyLoadedEventForAssemblyFromBundle(bool load_pd expected_count ); - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DuplicateAssemblyLoadedEventWithEmbeddedPdbNotLoadedFromBundle() => await AssemblyLoadedEventTest( "lazy-debugger-test-embedded", diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs index e559c88afb6c9a..1767f56e50ebb0 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs @@ -11,7 +11,7 @@ namespace DebuggerTests { - public class PointerTests : DebuggerTestBase + public class PointerTests : DebuggerTests { public static TheoryData PointersTestData => @@ -22,7 +22,7 @@ public class PointerTests : DebuggerTestBase { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "LocalPointersAsync", true } }; - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [MemberDataAttribute(nameof(PointersTestData))] public async Task InspectLocalPointersToPrimitiveTypes(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite( type, method, line_offset, bp_function_name, @@ -517,7 +517,7 @@ public async Task InspectValueTypePointersAsMethodArgs(string eval_fn, string ty await CheckArrayElements(dtppa_elems, exp_elems); }); - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("invoke_static_method ('[debugger-test] Math:UseComplex', 0, 0);", "Math", "UseComplex", 3, "UseComplex", false)] [InlineData("invoke_static_method ('[debugger-test] Math:UseComplex', 0, 0);", "Math", "UseComplex", 3, "UseComplex", true)] public async Task DerefNonPointerObject(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite( diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/SetNextIpTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/SetNextIpTests.cs index 3e6e117aad762f..e1f28568109f24 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/SetNextIpTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/SetNextIpTests.cs @@ -12,9 +12,9 @@ namespace DebuggerTests; -public class SetNextIpTests : DebuggerTestBase +public class SetNextIpTests : DebuggerTests { - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task SetAndCheck() { async Task CheckLocalsAsync(JToken locals, int c, int d, int e, bool f) @@ -53,7 +53,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", "IntAdd"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task OutsideTheCurrentMethod() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 9, 8); @@ -75,7 +75,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", }); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task AsyncMethod() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs"; @@ -109,7 +109,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", }); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task Lambda() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-async-test.cs"; @@ -155,7 +155,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-async-tes }); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task Lambda_InvalidLocation() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-async-test.cs"; @@ -184,7 +184,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-async-tes times: 2); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task Lambda_ToNestedLambda() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-async-test.cs"; @@ -214,7 +214,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-async-tes times: 2); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task Lambda_ToNestedSingleLineLambda_Invalid() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-async-test.cs"; @@ -244,7 +244,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-async-tes times: 2); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task Lambda_ToNestedSingleLineLambda_Valid() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-async-test.cs"; diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/SetVariableValueTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/SetVariableValueTests.cs index 6ff6966b77b5f4..b8b152f512e1a4 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/SetVariableValueTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/SetVariableValueTests.cs @@ -12,9 +12,9 @@ namespace DebuggerTests { - public class SetVariableValueTests : DebuggerTestBase + public class SetVariableValueTests : DebuggerTests { - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("a", 1, 30, 130)] [InlineData("a", 1, -30, -130)] [InlineData("a1", 1, 20, -1)] @@ -63,7 +63,7 @@ public async Task SetLocalPrimitiveTypeVariableOutOfRange(string variableName, l ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("f", 9, 150.15616, 0.4564)] [InlineData("f", 9, -454.54654, -0.5648)] public async Task SetLocalFloatVariable(string variableName, float originalValue, float newValue, float newValue2) { @@ -102,7 +102,7 @@ public async Task SetLocalFloatVariable(string variableName, float originalValue ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("g", 10, 150.15615844726562, 0.4564000070095062)] [InlineData("g", 10, -454.5465393066406, -0.5648000240325928)] public async Task SetLocalDoubleVariable(string variableName, double originalValue, double newValue, double newValue2) { @@ -141,7 +141,7 @@ public async Task SetLocalDoubleVariable(string variableName, double originalVal ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("a", "1", "30", "127")] [InlineData("a", "1", "-30", "-128")] [InlineData("a1", "1", "20", "0")] @@ -191,7 +191,7 @@ public async Task SetLocalPrimitiveTypeVariableValid(string variableName, string ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(1, "a", 10, 30)] [InlineData(1, "a", 10, -1)] [InlineData(1, "b", 20, 30)] @@ -221,7 +221,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(1, "a", 10, "wrongValue")] [InlineData(1, "b", 20, "wrongValue")] [InlineData(2, "c", 30, "wrongValue")] @@ -251,7 +251,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(5, "f", true, false)] [InlineData(5, "f", true, true)] public async Task SetLocalBoolTypeVariable(int offset, string variableName, bool originalValue, bool newValue){ @@ -275,7 +275,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", } ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("A", 10, "20", true)] [InlineData("A", 10, "error", false)] [InlineData("d", 15, "20", true)] diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs index 45f26a74c84a3a..40c1a4079f0d04 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/SteppingTests.cs @@ -9,7 +9,7 @@ namespace DebuggerTests { - public class SteppingTests : DebuggerTestBase + public class SteppingTests : DebuggerTests { [Fact] public async Task TrivalStepping() @@ -87,7 +87,7 @@ await StepAndCheck(StepKind.Over, debugger_test_loc, 12, 8, "IntAdd", ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectLocalsInPreviousFramesDuringSteppingIn2(bool use_cfo) @@ -154,7 +154,7 @@ public async Task InspectLocalsInPreviousFramesDuringSteppingIn2(bool use_cfo) await CheckString(props, "c", "20_xx"); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectLocalsInPreviousFramesDuringSteppingIn(bool use_cfo) @@ -319,7 +319,7 @@ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectLocalsInAsyncMethods(bool use_cfo) @@ -376,7 +376,7 @@ public async Task InspectLocalsInAsyncMethods(bool use_cfo) // TODO: Check `this` properties } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData(false)] [InlineData(true)] public async Task InspectValueTypeMethodArgsWhileStepping(bool use_cfo) @@ -498,7 +498,7 @@ public async Task InspectValueTypeMethodArgsWhileStepping(bool use_cfo) // FIXME: check ss_local.gs.List's members } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CheckUpdatedValueTypeFieldsOnResume() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs"; @@ -544,7 +544,7 @@ async Task CheckLocals(JToken pause_location, DateTime obj_dt, DateTime vt_dt) } } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CheckUpdatedValueTypeLocalsOnResumeAsync() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs"; @@ -568,7 +568,7 @@ public async Task CheckUpdatedValueTypeLocalsOnResumeAsync() await CheckDateTime(locals, "dt", dt); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CheckUpdatedVTArrayMembersOnResume() { var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs"; @@ -605,7 +605,7 @@ async Task CheckArrayElements(JToken pause_location, DateTime dt) } } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task SteppingIntoMscorlib() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 83, 8); @@ -625,7 +625,7 @@ public async Task SteppingIntoMscorlib() Assert.Matches("^dotnet://(mscorlib|System\\.Console)\\.dll/Console.cs", scripts[script_id]); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateGoodBreakpointAndHitAndRemoveAndDontHit() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -643,7 +643,7 @@ public async Task CreateGoodBreakpointAndHitAndRemoveAndDontHit() await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 12, 8, "IntAdd"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateGoodBreakpointAndHitAndRemoveTwice() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -660,7 +660,7 @@ public async Task CreateGoodBreakpointAndHitAndRemoveTwice() await RemoveBreakpoint(bp.Value["breakpointId"]?.ToString()); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CreateGoodBreakpointAndHitAndRemoveAndDontHitAndCreateAgainAndHit() { var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); @@ -680,7 +680,7 @@ public async Task CreateGoodBreakpointAndHitAndRemoveAndDontHitAndCreateAgainAnd await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, "IntAdd"); } - // [Fact] + // [ConditionalFact(nameof(RunningOnChrome))] //https://github.com/dotnet/runtime/issues/42421 public async Task BreakAfterAwaitThenStepOverTillBackToCaller() { @@ -697,7 +697,7 @@ await EvaluateAndCheck( await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-async-step.cs", 15, 12, "MoveNext"); } - // [Fact] + // [ConditionalFact(nameof(RunningOnChrome))] //[ActiveIssue("https://github.com/dotnet/runtime/issues/42421")] public async Task StepOutOfAsyncMethod() { @@ -802,7 +802,7 @@ await EvaluateAndCheck( await StepAndCheck(StepKind.Resume, source_file, 56, 12, "MoveNext"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task BreakOnMethodCalledFromHiddenLine() { await SetBreakpointInMethod("debugger-test.dll", "HiddenSequencePointTest", "StepOverHiddenSP2", 0); @@ -820,7 +820,7 @@ public async Task BreakOnMethodCalledFromHiddenLine() CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 537, 8, scripts, top_frame["location"]); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task StepOverHiddenLinesShouldResumeAtNextAvailableLineInTheMethod() { string source_loc = "dotnet://debugger-test.dll/debugger-test.cs"; @@ -834,7 +834,7 @@ await EvaluateAndCheck( await StepAndCheck(StepKind.Over, source_loc, 542, 8, "StepOverHiddenSP"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] async Task StepOverHiddenLinesInMethodWithNoNextAvailableLineShouldResumeAtCallSite() { string source_loc = "dotnet://debugger-test.dll/debugger-test.cs"; @@ -848,7 +848,7 @@ await EvaluateAndCheck( await StepAndCheck(StepKind.Over, source_loc, 544, 4, "StepOverHiddenSP"); } - // [Fact] + // [ConditionalFact(nameof(RunningOnChrome))] // Issue: https://github.com/dotnet/runtime/issues/42704 async Task BreakpointOnHiddenLineShouldStopAtEarliestNextAvailableLine() { @@ -859,7 +859,7 @@ await EvaluateAndCheck( "StepOverHiddenSP2"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task BreakpointOnHiddenLineOfMethodWithNoNextVisibleLineShouldNotPause() { await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 554, 12); @@ -886,7 +886,7 @@ await EvaluateAndCheck( await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 678, 4, "Bart"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task StepAndEvaluateExpression() { await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 682, 0); @@ -938,7 +938,7 @@ await EvaluateAndCheck( await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 720, 4, "MoveNext"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task CheckResetFrameNumberForEachStep() { var bp_conditional = await SetBreakpointInMethod("debugger-test.dll", "SteppingInto", "MethodToStep", 1); @@ -958,7 +958,7 @@ await EvaluateAndCheck( Assert.Equal(pause_location["callFrames"][0]["callFrameId"], "dotnet:scope:1"); } - [Fact] + [ConditionalFact(nameof(RunningOnChrome))] public async Task DebuggerHiddenIgnoreStepInto() { var pause_location = await SetBreakpointInMethod("debugger-test.dll", "DebuggerAttribute", "RunDebuggerHidden", 1); @@ -976,7 +976,7 @@ await EvaluateAndCheck( ); } - [Theory] + [ConditionalTheory(nameof(RunningOnChrome))] [InlineData("Debugger.stepInto")] [InlineData("Debugger.stepOver")] public async Task DebuggerHiddenIgnoreStepUserBreakpoint(string steppingFunction) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessOptions.cs b/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessOptions.cs index 96a6c13eb97d35..c3b9bc6c588801 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessOptions.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessOptions.cs @@ -2,14 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.WebAssembly.Diagnostics; -namespace Microsoft.WebAssembly.Diagnostics +namespace DebuggerTests { public class TestHarnessOptions : ProxyOptions { - public string ChromePath { get; set; } public string AppPath { get; set; } public string PagePath { get; set; } public string NodeApp { get; set; } + public string BrowserParms { get; set; } + public Func, Task> ExtractConnUrl { get; set; } } -} \ No newline at end of file +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessProxy.cs b/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessProxy.cs index 728cff7881609d..7b73195c000c73 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessProxy.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessProxy.cs @@ -2,6 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore; @@ -9,23 +14,32 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.WebAssembly.Diagnostics; -namespace Microsoft.WebAssembly.Diagnostics +#nullable enable + +namespace DebuggerTests { public class TestHarnessProxy { - static IWebHost host; - static Task hostTask; + static IWebHost? host; + static Task? hostTask; static CancellationTokenSource cts = new CancellationTokenSource(); static object proxyLock = new object(); public static readonly Uri Endpoint = new Uri("http://localhost:9400"); - public static Task Start(string chromePath, string appPath, string pagePath) + // FIXME: use concurrentdictionary? + // And remove the "used" proxy entries + private static readonly ConcurrentBag<(string id, DebuggerProxyBase proxy)> s_proxyTable = new(); + private static readonly ConcurrentBag<(string id, Action handler)> s_exitHandlers = new(); + private static readonly ConcurrentBag<(string id, RunLoopExitState state)> s_statusTable = new(); + + public static Task Start(string appPath, string pagePath, string url) { lock (proxyLock) { - if (host != null) + if (hostTask != null) return hostTask; host = WebHost.CreateDefaultBuilder() @@ -41,17 +55,24 @@ public static Task Start(string chromePath, string appPath, string pagePath) options.SingleLine = true; options.TimestampFormat = "[HH:mm:ss] "; }) - .AddFilter(null, LogLevel.Information); + .AddFilter("DevToolsProxy", LogLevel.Debug) + .AddFile(Path.Combine(DebuggerTestBase.TestLogPath, "proxy.log"), + minimumLevel: LogLevel.Trace, + levelOverrides: new Dictionary + { + ["Microsoft.AspNetCore"] = LogLevel.Warning + }, + outputTemplate: "{Timestamp:o} [{Level:u3}] {SourceContext}: {Message}{NewLine}{Exception}") + .AddFilter(null, LogLevel.Information); }) - .ConfigureServices((ctx, services) => + .ConfigureServices((ctx, services) => { services.Configure(ctx.Configuration); services.Configure(options => { - options.ChromePath = options.ChromePath ?? chromePath; options.AppPath = appPath; options.PagePath = pagePath; - options.DevToolsUrl = new Uri("http://localhost:0"); + options.DevToolsUrl = new Uri(url); }); }) .UseStartup() @@ -63,5 +84,77 @@ public static Task Start(string chromePath, string appPath, string pagePath) Console.WriteLine("WebServer Ready!"); return hostTask; } + + public static void RegisterNewProxy(string id, DebuggerProxyBase proxy) + { + if (s_proxyTable.Where(t => t.id == id).Any()) + throw new ArgumentException($"Proxy with id {id} already exists"); + + s_proxyTable.Add((id, proxy)); + } + + private static bool TryGetProxyById(string id, [NotNullWhen(true)] out DebuggerProxyBase? proxy) + { + proxy = null; + IEnumerable<(string id, DebuggerProxyBase proxy)> found = s_proxyTable.Where(t => t.id == id); + if (found.Any()) + proxy = found.First().proxy; + + return proxy != null; + } + + public static void RegisterExitHandler(string id, Action handler) + { + if (s_exitHandlers.Any(t => t.id == id)) + throw new Exception($"Cannot register a duplicate exit handler for {id}"); + + s_exitHandlers.Add(new(id, handler)); + } + + public static void RegisterProxyExitState(string id, RunLoopExitState status) + { + Console.WriteLine ($"[{id}] RegisterProxyExitState: {status}"); + s_statusTable.Add((id, status)); + (string id, Action handler)[]? found = s_exitHandlers.Where(e => e.id == id).ToArray(); + if (found.Length > 0) + found[0].handler.Invoke(status); + } + + // FIXME: remove + public static bool TryGetProxyExitState(string id, [NotNullWhen(true)] out RunLoopExitState? state) + { + state = new(RunLoopStopReason.Cancelled, null); + + if (!TryGetProxyById(id, out DebuggerProxyBase? proxy)) + { + (string id, RunLoopExitState state)[]? found = s_statusTable.Where(t => t.id == id).ToArray(); + if (found.Length == 0) + { + Console.WriteLine($"[{id}] Cannot find exit proxy for {id}"); + return false; + } + + state = found[0].state; + return true; + } + + state = proxy.ExitState; + return state is not null; + } + + public static DebuggerProxyBase? ShutdownProxy(string id) + { + if (!string.IsNullOrEmpty(id)) + { + (_, DebuggerProxyBase? proxy) = s_proxyTable.FirstOrDefault(t => t.id == id); + if (proxy is not null) + { + proxy.Shutdown(); + return proxy; + } + } + return null; + } } + } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessStartup.cs b/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessStartup.cs index 93b92d71aa9037..7bacdfa496e0dc 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessStartup.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/TestHarnessStartup.cs @@ -4,8 +4,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Net.Http; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -18,13 +16,13 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.WebAssembly.Diagnostics; using Newtonsoft.Json.Linq; -namespace Microsoft.WebAssembly.Diagnostics +namespace DebuggerTests { public class TestHarnessStartup { - static Regex parseConnection = new Regex(@"listening on (ws?s://[^\s]*)"); public TestHarnessStartup(IConfiguration configuration) { Configuration = configuration; @@ -76,89 +74,6 @@ async Task SendNodeList(HttpContext context) catch (Exception e) { Logger.LogError(e, "webserver: SendNodeList failed"); } } - public async Task LaunchAndServe(ProcessStartInfo psi, - HttpContext context, - Func> extract_conn_url, - string test_id, - string message_prefix, - int get_con_url_timeout_ms=20000) - { - - if (!context.WebSockets.IsWebSocketRequest) - { - context.Response.StatusCode = 400; - return; - } - - var tcs = new TaskCompletionSource(); - - var proc = Process.Start(psi); - try - { - proc.ErrorDataReceived += (sender, e) => - { - var str = e.Data; - Logger.LogTrace($"{message_prefix} browser-stderr: {str}"); - - if (tcs.Task.IsCompleted) - return; - - if (!string.IsNullOrEmpty(str)) - { - var match = parseConnection.Match(str); - if (match.Success) - { - tcs.TrySetResult(match.Groups[1].Captures[0].Value); - } - } - }; - - proc.OutputDataReceived += (sender, e) => - { - Logger.LogTrace($"{message_prefix} browser-stdout: {e.Data}"); - }; - - proc.BeginErrorReadLine(); - proc.BeginOutputReadLine(); - - if (await Task.WhenAny(tcs.Task, Task.Delay(get_con_url_timeout_ms)) != tcs.Task) - { - Logger.LogError($"{message_prefix} Timed out after {get_con_url_timeout_ms/1000}s waiting for a connection string from {psi.FileName}"); - return; - } - var line = await tcs.Task; - var con_str = extract_conn_url != null ? await extract_conn_url(line) : line; - - Logger.LogInformation($"{message_prefix} launching proxy for {con_str}"); - - string logFilePath = Path.Combine(DebuggerTests.DebuggerTestBase.TestLogPath, $"{test_id}-proxy.log"); - File.Delete(logFilePath); - - var proxyLoggerFactory = LoggerFactory.Create( - builder => builder - .AddFile(logFilePath, minimumLevel: LogLevel.Debug) - .AddFilter(null, LogLevel.Trace)); - - var proxy = new DebuggerProxy(proxyLoggerFactory, null, loggerId: test_id); - var browserUri = new Uri(con_str); - var ideSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - - await proxy.Run(browserUri, ideSocket).ConfigureAwait(false); - } - catch (Exception e) - { - Logger.LogError($"{message_prefix} got exception {e}"); - } - finally - { - proc.CancelErrorRead(); - proc.CancelOutputRead(); - proc.Kill(); - proc.WaitForExit(); - proc.Close(); - } - } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IOptionsMonitor optionsAccessor, IWebHostEnvironment env, ILogger logger, ILoggerFactory loggerFactory) { @@ -184,7 +99,7 @@ public void Configure(IApplicationBuilder app, IOptionsMonitor { - router.MapGet("launch-chrome-and-connect", async context => + router.MapGet("launch-host-and-connect", async context => { string test_id; if (context.Request.Query.TryGetValue("test_id", out var value) && value.Count == 1) @@ -192,65 +107,60 @@ public void Configure(IApplicationBuilder app, IOptionsMonitor + else if (host == WasmHost.Firefox) { - var start = DateTime.Now; - JArray obj = null; - - while (true) - { - // Unfortunately it does look like we have to wait - // for a bit after getting the response but before - // making the list request. We get an empty result - // if we make the request too soon. - await Task.Delay(100); - - var res = await client.GetStringAsync(new Uri(new Uri(str), "/json/list")); - Logger.LogTrace($"{message_prefix}res is {res}"); - - if (!String.IsNullOrEmpty(res)) - { - // Sometimes we seem to get an empty array `[ ]` - obj = JArray.Parse(res); - if (obj != null && obj.Count >= 1) - break; - } - - var elapsed = DateTime.Now - start; - if (elapsed.Milliseconds > 5000) - { - Logger.LogError($"{message_prefix} Unable to get DevTools /json/list response in {elapsed.Seconds} seconds, stopping"); - return null; - } - } - - var wsURl = obj[0]?["webSocketDebuggerUrl"]?.Value(); - Logger.LogTrace($"{message_prefix} >>> {wsURl}"); - - return wsURl; - }, test_id, message_prefix).ConfigureAwait(false); + using var provider = new FirefoxProvider(test_id, Logger); + browserPort = 6500 + int.Parse(test_id); + await provider.StartBrowserAndProxyAsync(context, + $"http://{TestHarnessProxy.Endpoint.Authority}/{options.PagePath}", + browserPort, + firefox_proxy_port, + message_prefix, + _loggerFactory, + cts).ConfigureAwait(false); + } + Logger.LogDebug($"{message_prefix} TestHarnessStartup done"); } catch (Exception ex) { - Logger.LogError($"{message_prefix} launch-chrome-and-connect failed with {ex.ToString()}"); + Logger.LogError($"{message_prefix} launch-host-and-connect failed with {ex}"); + TestHarnessProxy.RegisterProxyExitState(test_id, new(RunLoopStopReason.Exception, ex)); + } + finally + { + Logger.LogDebug($"TestHarnessStartup: closing for {test_id}"); + cts.Cancel(); } }); }); @@ -277,7 +187,8 @@ await LaunchAndServe(psi, context, async (str) => router.MapGet("json/version", SendNodeVersion); router.MapGet("launch-done-and-connect", async context => { - await LaunchAndServe(psi, context, null, null, null); + await Task.CompletedTask; + // await LaunchAndServe(psi, context, null, null, null, null); }); }); } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/WasmHostProvider.cs b/src/mono/wasm/debugger/DebuggerTestSuite/WasmHostProvider.cs new file mode 100644 index 00000000000000..03c36b278a2808 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/WasmHostProvider.cs @@ -0,0 +1,143 @@ + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +#nullable enable + +namespace DebuggerTests; + +internal abstract class WasmHostProvider : IDisposable +{ + protected ILogger _logger; + public string Id { get; init; } + protected Process? _process; + protected bool _isDisposed; + protected bool _isDisposing; + + public WasmHostProvider(string id, ILogger logger) + { + Id = id; + _logger = logger; + } + + protected ProcessStartInfo GetProcessStartInfo(string browserPath, string arguments, string url) + => new() + { + Arguments = $"{arguments} {url}", + UseShellExecute = false, + FileName = browserPath, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + + protected async Task LaunchHostAsync(ProcessStartInfo psi, + HttpContext context, + Func checkBrowserReady, + string messagePrefix, + int hostReadyTimeoutMs, + CancellationToken token) + { + ArgumentNullException.ThrowIfNull(psi); + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(checkBrowserReady); + + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = 400; + return null; + } + + var browserReadyTCS = new TaskCompletionSource(); + + _logger.LogDebug($"[{Id}] Starting {psi.FileName} with {psi.Arguments}"); + _process = Process.Start(psi); + if (_process is null) + return null; + + Task waitForExitTask = _process.WaitForExitAsync(token); + _process.ErrorDataReceived += (sender, e) => ProcessOutput($"{messagePrefix} browser-stderr ", e?.Data); + _process.OutputDataReceived += (sender, e) => ProcessOutput($"{messagePrefix} browser-stdout ", e?.Data); + + _process.BeginErrorReadLine(); + _process.BeginOutputReadLine(); + + Task completedTask = await Task.WhenAny(browserReadyTCS.Task, waitForExitTask, Task.Delay(hostReadyTimeoutMs)) + .ConfigureAwait(false); + if (_process.HasExited) + throw new IOException($"Process for {psi.FileName} unexpectedly exited with {_process.ExitCode} during startup."); + + if (completedTask == browserReadyTCS.Task) + { + _process.Exited += (_, _) => + { + Console.WriteLine ($"**Browser died!**"); + Dispose(); + }; + + return await browserReadyTCS.Task; + } + + // FIXME: use custom exception types + throw new IOException($"{messagePrefix} Timed out after {hostReadyTimeoutMs/1000}s waiting for the browser to be ready: {psi.FileName}"); + + void ProcessOutput(string prefix, string? msg) + { + _logger.LogDebug($"{prefix}{msg}"); + + if (string.IsNullOrEmpty(msg) || browserReadyTCS.Task.IsCompleted) + return; + + string? result = checkBrowserReady(msg); + if (result is not null) + browserReadyTCS.TrySetResult(result); + } + } + + public virtual void Dispose() + { + if (_process is not null && !_process.HasExited) + { + _process.CancelErrorRead(); + _process.CancelOutputRead(); + _process.Kill(entireProcessTree: true); + _process.WaitForExit(); + _process.Close(); + + _process = null; + } + } + + protected static string GetBrowserPath(IEnumerable 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)); + } + } +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json b/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json index fb29f9aebfd612..6e8868d50b4664 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json +++ b/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json @@ -4,9 +4,12 @@ "Default": "Error", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.WebAssembly.Diagnostics.TestHarnessProxy": "Information", + "DebuggerTests.TestHarnessProxy": "Debug", "Microsoft.WebAssembly.Diagnostics.DevToolsProxy": "Information", - "Inspector": "Information" + "Inspector": "Debug", + "InspectorClient": "Debug", + "DevToolsProxy": "Information", + "DebuggerTests": "Debug" } } } diff --git a/src/mono/wasm/debugger/Directory.Build.props b/src/mono/wasm/debugger/Directory.Build.props new file mode 100644 index 00000000000000..c9cccf897a6b98 --- /dev/null +++ b/src/mono/wasm/debugger/Directory.Build.props @@ -0,0 +1,6 @@ + + + Release + + + diff --git a/src/mono/wasm/debugger/tests/Directory.Build.props b/src/mono/wasm/debugger/tests/Directory.Build.props index c1d2850617cfaf..2a3c5f49fdb856 100644 --- a/src/mono/wasm/debugger/tests/Directory.Build.props +++ b/src/mono/wasm/debugger/tests/Directory.Build.props @@ -1,5 +1,5 @@ - + $(AspNetCoreAppCurrent) diff --git a/src/mono/wasm/runtime/debug.ts b/src/mono/wasm/runtime/debug.ts index a6115177040283..1b57eb4c3bedb7 100644 --- a/src/mono/wasm/runtime/debug.ts +++ b/src/mono/wasm/runtime/debug.ts @@ -105,7 +105,7 @@ export function mono_wasm_send_dbg_command(id: number, command_set: number, comm } export function mono_wasm_get_dbg_command_info(): CommandResponseResult { - const { res_ok, res } = commands_received.remove(0); + const { res_ok, res } = commands_received.get(0); if (!res_ok) throw new Error("Failed on mono_wasm_get_dbg_command_info"); diff --git a/src/tests/BuildWasmApps/Wasm.Debugger.Tests/Wasm.Debugger.Tests.csproj b/src/tests/BuildWasmApps/Wasm.Debugger.Tests/Wasm.Debugger.Tests.csproj index 5de651dbd6b7ce..830456ded05918 100644 --- a/src/tests/BuildWasmApps/Wasm.Debugger.Tests/Wasm.Debugger.Tests.csproj +++ b/src/tests/BuildWasmApps/Wasm.Debugger.Tests/Wasm.Debugger.Tests.csproj @@ -10,6 +10,7 @@ true true xunit + chrome BundleDebuggerTestsForHelix @@ -36,11 +37,13 @@ - - - + + + + + + +