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