run-tests-%:
PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test /p:TargetOS=Browser /p:TargetArchitecture=wasm /p:Configuration=$(CONFIG) $(MSBUILD_ARGS)
+
+build-debugger-test-app:
+ $(DOTNET) build --configuration debug --nologo /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Debug /p:RuntimeConfiguration=$(CONFIG) $(TOP)/src/mono/wasm/debugger/tests
+ cp $(TOP)/src/mono/wasm/debugger/tests/debugger-driver.html $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+ cp $(TOP)/src/mono/wasm/debugger/tests/other.js $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+ cp $(TOP)/src/mono/wasm/debugger/tests/runtime-debugger.js $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+
+run-debugger-tests: build-debugger-test-app build-dbg-testsuite
+ if [ ! -z "$(TEST_FILTER)" ]; then \
+ export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish; \
+ export LC_ALL=en_US.UTF-8; \
+ $(DOTNET) test $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite --filter FullyQualifiedName~$(TEST_FILTER); \
+ unset TEST_SUITE_PATH LC_ALL; \
+ else \
+ export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish; \
+ export LC_ALL=en_US.UTF-8; \
+ $(DOTNET) test $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite $(TEST_ARGS); \
+ unset TEST_SUITE_PATH LC_ALL; \
+ fi
+
+build-dbg-proxy:
+ $(DOTNET) build $(TOP)/src/mono/wasm/debugger/BrowserDebugHost
+build-dbg-testsuite:
+ $(DOTNET) build $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\BrowserDebugProxy\BrowserDebugProxy.csproj" />
+ </ItemGroup>
+
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ public class ProxyOptions
+ {
+ public Uri DevToolsUrl { get; set; } = new Uri("http://localhost:9222");
+ }
+
+ 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 class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseSetting("UseIISIntegration", false.ToString())
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseStartup<Startup>()
+ .ConfigureAppConfiguration((hostingContext, config) =>
+ {
+ config.AddCommandLine(args);
+ })
+ .UseUrls("http://localhost:9300")
+ .Build();
+
+ host.Run();
+ }
+ }
+
+ public class TestHarnessProxy
+ {
+ 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)
+ {
+ lock(proxyLock)
+ {
+ if (host != null)
+ return hostTask;
+
+ host = WebHost.CreateDefaultBuilder()
+ .UseSetting("UseIISIntegration", false.ToString())
+ .ConfigureAppConfiguration((hostingContext, config) =>
+ {
+ config.AddEnvironmentVariables(prefix: "WASM_TESTS_");
+ })
+ .ConfigureServices((ctx, services) =>
+ {
+ services.Configure<TestHarnessOptions>(ctx.Configuration);
+ services.Configure<TestHarnessOptions>(options =>
+ {
+ options.ChromePath = options.ChromePath ?? chromePath;
+ options.AppPath = appPath;
+ options.PagePath = pagePath;
+ options.DevToolsUrl = new Uri("http://localhost:0");
+ });
+ })
+ .UseStartup<TestHarnessStartup>()
+ .UseUrls(Endpoint.ToString())
+ .Build();
+ hostTask = host.StartAsync(cts.Token);
+ }
+
+ Console.WriteLine("WebServer Ready!");
+ return hostTask;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ internal class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services) =>
+ services.AddRouting()
+ .Configure<ProxyOptions>(Configuration);
+
+ public Startup(IConfiguration configuration) =>
+ Configuration = configuration;
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IOptionsMonitor<ProxyOptions> optionsAccessor, IWebHostEnvironment env)
+ {
+ var options = optionsAccessor.CurrentValue;
+ app.UseDeveloperExceptionPage()
+ .UseWebSockets()
+ .UseDebugProxy(options);
+ }
+ }
+
+ static class DebugExtensions
+ {
+ public static Dictionary<string, string> MapValues(Dictionary<string, string> response, HttpContext context, Uri debuggerHost)
+ {
+ var filtered = new Dictionary<string, string>();
+ var request = context.Request;
+
+ foreach (var key in response.Keys)
+ {
+ switch (key)
+ {
+ case "devtoolsFrontendUrl":
+ var front = response[key];
+ filtered[key] = $"{debuggerHost.Scheme}://{debuggerHost.Authority}{front.Replace ($"ws={debuggerHost.Authority}", $"ws={request.Host}")}";
+ break;
+ case "webSocketDebuggerUrl":
+ var page = new Uri(response[key]);
+ filtered[key] = $"{page.Scheme}://{request.Host}{page.PathAndQuery}";
+ break;
+ default:
+ filtered[key] = response[key];
+ break;
+ }
+ }
+ return filtered;
+ }
+
+ public static IApplicationBuilder UseDebugProxy(this IApplicationBuilder app, ProxyOptions options) =>
+ UseDebugProxy(app, options, MapValues);
+
+ public static IApplicationBuilder UseDebugProxy(
+ this IApplicationBuilder app,
+ ProxyOptions options,
+ Func<Dictionary<string, string>, HttpContext, Uri, Dictionary<string, string>> mapFunc)
+ {
+ var devToolsHost = options.DevToolsUrl;
+ app.UseRouter(router =>
+ {
+ router.MapGet("/", Copy);
+ router.MapGet("/favicon.ico", Copy);
+ router.MapGet("json", RewriteArray);
+ router.MapGet("json/list", RewriteArray);
+ router.MapGet("json/version", RewriteSingle);
+ router.MapGet("json/new", RewriteSingle);
+ router.MapGet("devtools/page/{pageId}", ConnectProxy);
+ router.MapGet("devtools/browser/{pageId}", ConnectProxy);
+
+ string GetEndpoint(HttpContext context)
+ {
+ var request = context.Request;
+ var requestPath = request.Path;
+ return $"{devToolsHost.Scheme}://{devToolsHost.Authority}{request.Path}{request.QueryString}";
+ }
+
+ async Task Copy(HttpContext context)
+ {
+ using(var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) })
+ {
+ var response = await httpClient.GetAsync(GetEndpoint(context));
+ context.Response.ContentType = response.Content.Headers.ContentType.ToString();
+ if ((response.Content.Headers.ContentLength ?? 0) > 0)
+ context.Response.ContentLength = response.Content.Headers.ContentLength;
+ var bytes = await response.Content.ReadAsByteArrayAsync();
+ await context.Response.Body.WriteAsync(bytes);
+
+ }
+ }
+
+ async Task RewriteSingle(HttpContext context)
+ {
+ var version = await ProxyGetJsonAsync<Dictionary<string, string>>(GetEndpoint(context));
+ context.Response.ContentType = "application/json";
+ await context.Response.WriteAsync(
+ JsonSerializer.Serialize(mapFunc(version, context, devToolsHost)));
+ }
+
+ async Task RewriteArray(HttpContext context)
+ {
+ var tabs = await ProxyGetJsonAsync<Dictionary<string, string>[]>(GetEndpoint(context));
+ var alteredTabs = tabs.Select(t => mapFunc(t, context, devToolsHost)).ToArray();
+ context.Response.ContentType = "application/json";
+ await context.Response.WriteAsync(JsonSerializer.Serialize(alteredTabs));
+ }
+
+ async Task ConnectProxy(HttpContext context)
+ {
+ if (!context.WebSockets.IsWebSocketRequest)
+ {
+ context.Response.StatusCode = 400;
+ return;
+ }
+
+ var endpoint = new Uri($"ws://{devToolsHost.Authority}{context.Request.Path.ToString ()}");
+ try
+ {
+ using var loggerFactory = LoggerFactory.Create(
+ builder => builder.AddConsole().AddFilter(null, LogLevel.Information));
+ var proxy = new DebuggerProxy(loggerFactory);
+ var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
+
+ await proxy.Run(endpoint, ideSocket);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("got exception {0}", e);
+ }
+ }
+ });
+ return app;
+ }
+
+ static async Task<T> ProxyGetJsonAsync<T>(string url)
+ {
+ using(var httpClient = new HttpClient())
+ {
+ var response = await httpClient.GetAsync(url);
+ return await JsonSerializer.DeserializeAsync<T>(await response.Content.ReadAsStreamAsync());
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ public class TestHarnessStartup
+ {
+ static Regex parseConnection = new Regex(@"listening on (ws?s://[^\s]*)");
+ public TestHarnessStartup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; set; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddRouting()
+ .Configure<TestHarnessOptions>(Configuration);
+ }
+
+ async Task SendNodeVersion(HttpContext context)
+ {
+ Console.WriteLine("hello chrome! json/version");
+ var resp_obj = new JObject();
+ resp_obj["Browser"] = "node.js/v9.11.1";
+ resp_obj["Protocol-Version"] = "1.1";
+
+ var response = resp_obj.ToString();
+ await context.Response.WriteAsync(response, new CancellationTokenSource().Token);
+ }
+
+ async Task SendNodeList(HttpContext context)
+ {
+ Console.WriteLine("hello chrome! json/list");
+ try
+ {
+ var response = new JArray(JObject.FromObject(new
+ {
+ description = "node.js instance",
+ devtoolsFrontendUrl = "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=localhost:9300/91d87807-8a81-4f49-878c-a5604103b0a4",
+ faviconUrl = "https://nodejs.org/static/favicon.ico",
+ id = "91d87807-8a81-4f49-878c-a5604103b0a4",
+ title = "foo.js",
+ type = "node",
+ webSocketDebuggerUrl = "ws://localhost:9300/91d87807-8a81-4f49-878c-a5604103b0a4"
+ })).ToString();
+
+ Console.WriteLine($"sending: {response}");
+ await context.Response.WriteAsync(response, new CancellationTokenSource().Token);
+ }
+ catch (Exception e) { Console.WriteLine(e); }
+ }
+
+ public async Task LaunchAndServe(ProcessStartInfo psi, HttpContext context, Func<string, Task<string>> extract_conn_url)
+ {
+
+ if (!context.WebSockets.IsWebSocketRequest)
+ {
+ context.Response.StatusCode = 400;
+ return;
+ }
+
+ var tcs = new TaskCompletionSource<string>();
+
+ var proc = Process.Start(psi);
+ try
+ {
+ proc.ErrorDataReceived += (sender, e) =>
+ {
+ var str = e.Data;
+ Console.WriteLine($"stderr: {str}");
+
+ if (tcs.Task.IsCompleted)
+ return;
+
+ var match = parseConnection.Match(str);
+ if (match.Success)
+ {
+ tcs.TrySetResult(match.Groups[1].Captures[0].Value);
+ }
+ };
+
+ proc.OutputDataReceived += (sender, e) =>
+ {
+ Console.WriteLine($"stdout: {e.Data}");
+ };
+
+ proc.BeginErrorReadLine();
+ proc.BeginOutputReadLine();
+
+ if (await Task.WhenAny(tcs.Task, Task.Delay(5000)) != tcs.Task)
+ {
+ Console.WriteLine("Didnt get the con string after 5s.");
+ throw new Exception("node.js timedout");
+ }
+ var line = await tcs.Task;
+ var con_str = extract_conn_url != null ? await extract_conn_url(line) : line;
+
+ Console.WriteLine($"launching proxy for {con_str}");
+
+ using var loggerFactory = LoggerFactory.Create(
+ builder => builder.AddConsole().AddFilter(null, LogLevel.Information));
+ var proxy = new DebuggerProxy(loggerFactory);
+ var browserUri = new Uri(con_str);
+ var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
+
+ await proxy.Run(browserUri, ideSocket);
+ Console.WriteLine("Proxy done");
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("got exception {0}", 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<TestHarnessOptions> optionsAccessor, IWebHostEnvironment env)
+ {
+ app.UseWebSockets();
+ app.UseStaticFiles();
+
+ TestHarnessOptions options = optionsAccessor.CurrentValue;
+
+ var provider = new FileExtensionContentTypeProvider();
+ provider.Mappings[".wasm"] = "application/wasm";
+
+ app.UseStaticFiles(new StaticFileOptions
+ {
+ FileProvider = new PhysicalFileProvider(options.AppPath),
+ ServeUnknownFileTypes = true, //Cuz .wasm is not a known file type :cry:
+ RequestPath = "",
+ ContentTypeProvider = provider
+ });
+
+ var devToolsUrl = options.DevToolsUrl;
+ app.UseRouter(router =>
+ {
+ router.MapGet("launch-chrome-and-connect", async context =>
+ {
+ Console.WriteLine("New test request");
+ try
+ {
+ var client = new HttpClient();
+ var psi = new ProcessStartInfo();
+
+ psi.Arguments = $"--headless --disable-gpu --lang=en-US --incognito --remote-debugging-port={devToolsUrl.Port} http://{TestHarnessProxy.Endpoint.Authority}/{options.PagePath}";
+ psi.UseShellExecute = false;
+ psi.FileName = options.ChromePath;
+ psi.RedirectStandardError = true;
+ psi.RedirectStandardOutput = true;
+
+ await LaunchAndServe(psi, context, async(str) =>
+ {
+ 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"));
+ Console.WriteLine("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)
+ {
+ Console.WriteLine($"Unable to get DevTools /json/list response in {elapsed.Seconds} seconds, stopping");
+ return null;
+ }
+ }
+
+ var wsURl = obj[0] ? ["webSocketDebuggerUrl"]?.Value<string>();
+ Console.WriteLine(">>> {0}", wsURl);
+
+ return wsURl;
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"launch-chrome-and-connect failed with {ex.ToString ()}");
+ }
+ });
+ });
+
+ if (options.NodeApp != null)
+ {
+ Console.WriteLine($"Doing the nodejs: {options.NodeApp}");
+ var nodeFullPath = Path.GetFullPath(options.NodeApp);
+ Console.WriteLine(nodeFullPath);
+ var psi = new ProcessStartInfo();
+
+ psi.UseShellExecute = false;
+ psi.RedirectStandardError = true;
+ psi.RedirectStandardOutput = true;
+
+ psi.Arguments = $"--inspect-brk=localhost:0 {nodeFullPath}";
+ psi.FileName = "node";
+
+ app.UseRouter(router =>
+ {
+ //Inspector API for using chrome devtools directly
+ router.MapGet("json", SendNodeList);
+ router.MapGet("json/list", SendNodeList);
+ router.MapGet("json/version", SendNodeVersion);
+ router.MapGet("launch-done-and-connect", async context =>
+ {
+ await LaunchAndServe(psi, context, null);
+ });
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
\ No newline at end of file
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.1</TargetFramework>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.2" />
+ <PackageReference Include="Mono.Cecil" Version="0.11.2" />
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.5.0" />
+ </ItemGroup>
+
+</Project>
--- /dev/null
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28407.52
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrowserDebugHost", "..\BrowserDebugHost\BrowserDebugHost.csproj", "{954F768A-23E6-4B14-90E0-27EA6B41FBCC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrowserDebugProxy", "BrowserDebugProxy.csproj", "{490128B6-9F21-46CA-878A-F22BCF51EF3C}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F8BA2C2D-8F28-4F9E-9C54-51E394EF941E}
+ EndGlobalSection
+EndGlobal
--- /dev/null
+// 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.Http;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Pdb;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ internal 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 MethodInfo Method { get; private set; }
+
+ JObject request;
+
+ public bool IsResolved => Assembly != null;
+ public List<Breakpoint> Locations { get; } = new List<Breakpoint>();
+
+ public override string ToString() => $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
+
+ public object AsSetBreakpointByUrlResponse(IEnumerable<object> jsloc) => new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation()).Concat(jsloc) };
+
+ public BreakpointRequest()
+ { }
+
+ public BreakpointRequest(string id, MethodInfo method)
+ {
+ Id = id;
+ Method = method;
+ }
+
+ public BreakpointRequest(string id, JObject request)
+ {
+ Id = id;
+ this.request = request;
+ }
+
+ public static BreakpointRequest Parse(string id, JObject args)
+ {
+ return new BreakpointRequest(id, args);
+ }
+
+ public BreakpointRequest Clone() => new BreakpointRequest { Id = Id, request = request };
+
+ public bool IsMatch(SourceFile sourceFile)
+ {
+ var url = request?["url"]?.Value<string>();
+ if (url == null)
+ {
+ var urlRegex = request?["urlRegex"].Value<string>();
+ var regex = new Regex(urlRegex);
+ return regex.IsMatch(sourceFile.Url.ToString()) || regex.IsMatch(sourceFile.DocUrl);
+ }
+
+ return sourceFile.Url.ToString() == url || sourceFile.DotNetUrl == url;
+ }
+
+ public bool TryResolve(SourceFile sourceFile)
+ {
+ if (!IsMatch(sourceFile))
+ return false;
+
+ var line = request?["lineNumber"]?.Value<int>();
+ var column = request?["columnNumber"]?.Value<int>();
+
+ if (line == null || column == null)
+ return false;
+
+ Assembly = sourceFile.AssemblyName;
+ File = sourceFile.DebuggerFileName;
+ Line = line.Value;
+ Column = column.Value;
+ return true;
+ }
+
+ public bool TryResolve(DebugStore store)
+ {
+ if (request == null || store == null)
+ return false;
+
+ return store.AllSources().FirstOrDefault(source => TryResolve(source)) != null;
+ }
+ }
+
+ internal class VarInfo
+ {
+ public VarInfo(VariableDebugInformation v)
+ {
+ this.Name = v.Name;
+ this.Index = v.Index;
+ }
+
+ public VarInfo(ParameterDefinition p)
+ {
+ this.Name = p.Name;
+ this.Index = (p.Index + 1) * -1;
+ }
+
+ public string Name { get; }
+ public int Index { get; }
+
+ public override string ToString() => $"(var-info [{Index}] '{Name}')";
+ }
+
+ internal class CliLocation
+ {
+ public CliLocation(MethodInfo method, int offset)
+ {
+ Method = method;
+ Offset = offset;
+ }
+
+ public MethodInfo Method { get; }
+ public int Offset { get; }
+ }
+
+ internal class SourceLocation
+ {
+ SourceId id;
+ int line;
+ int column;
+ CliLocation cliLoc;
+
+ public SourceLocation(SourceId id, int line, int column)
+ {
+ this.id = id;
+ this.line = line;
+ this.column = column;
+ }
+
+ public SourceLocation(MethodInfo mi, SequencePoint sp)
+ {
+ this.id = mi.SourceId;
+ this.line = sp.StartLine - 1;
+ this.column = sp.StartColumn - 1;
+ this.cliLoc = new CliLocation(mi, sp.Offset);
+ }
+
+ public SourceId Id { get => id; }
+ public int Line { get => line; }
+ public int Column { get => column; }
+ public CliLocation CliLocation => this.cliLoc;
+
+ public override string ToString() => $"{id}:{Line}:{Column}";
+
+ public static SourceLocation Parse(JObject obj)
+ {
+ if (obj == null)
+ return null;
+
+ if (!SourceId.TryParse(obj["scriptId"]?.Value<string>(), out var id))
+ return null;
+
+ var line = obj["lineNumber"]?.Value<int>();
+ var column = obj["columnNumber"]?.Value<int>();
+ if (id == null || line == null || column == null)
+ return null;
+
+ return new SourceLocation(id, line.Value, column.Value);
+ }
+
+ internal class LocationComparer : EqualityComparer<SourceLocation>
+ {
+ public override bool Equals(SourceLocation l1, SourceLocation l2)
+ {
+ if (l1 == null && l2 == null)
+ return true;
+ else if (l1 == null || l2 == null)
+ return false;
+
+ return (l1.Line == l2.Line &&
+ l1.Column == l2.Column &&
+ l1.Id == l2.Id);
+ }
+
+ public override int GetHashCode(SourceLocation loc)
+ {
+ int hCode = loc.Line ^ loc.Column;
+ return loc.Id.GetHashCode() ^ hCode.GetHashCode();
+ }
+ }
+
+ internal object AsLocation() => new
+ {
+ scriptId = id.ToString(),
+ lineNumber = line,
+ columnNumber = column
+ };
+ }
+
+ internal class SourceId
+ {
+ const string Scheme = "dotnet://";
+
+ readonly int assembly, document;
+
+ public int Assembly => assembly;
+ public int Document => document;
+
+ internal SourceId(int assembly, int document)
+ {
+ this.assembly = assembly;
+ this.document = document;
+ }
+
+ public SourceId(string id)
+ {
+ if (!TryParse(id, out assembly, out document))
+ throw new ArgumentException("invalid source identifier", nameof(id));
+ }
+
+ public static bool TryParse(string id, out SourceId source)
+ {
+ source = null;
+ if (!TryParse(id, out var assembly, out var document))
+ return false;
+
+ source = new SourceId(assembly, document);
+ return true;
+ }
+
+ static bool TryParse(string id, out int assembly, out int document)
+ {
+ assembly = document = 0;
+ if (id == null || !id.StartsWith(Scheme, StringComparison.Ordinal))
+ return false;
+
+ var sp = id.Substring(Scheme.Length).Split('_');
+ if (sp.Length != 2)
+ return false;
+
+ if (!int.TryParse(sp[0], out assembly))
+ return false;
+
+ if (!int.TryParse(sp[1], out document))
+ return false;
+
+ return true;
+ }
+
+ public override string ToString() => $"{Scheme}{assembly}_{document}";
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ return false;
+ SourceId that = obj as SourceId;
+ return that.assembly == this.assembly && that.document == this.document;
+ }
+
+ public override int GetHashCode() => assembly.GetHashCode() ^ document.GetHashCode();
+
+ public static bool operator ==(SourceId a, SourceId b) => ((object) a == null) ? (object) b == null : a.Equals(b);
+
+ public static bool operator !=(SourceId a, SourceId b) => !a.Equals(b);
+ }
+
+ internal class MethodInfo
+ {
+ MethodDefinition methodDef;
+ SourceFile source;
+
+ public SourceId SourceId => source.SourceId;
+
+ public string Name => methodDef.Name;
+ public MethodDebugInformation DebugInformation => methodDef.DebugInformation;
+
+ public SourceLocation StartLocation { get; }
+ public SourceLocation EndLocation { get; }
+ public AssemblyInfo Assembly { get; }
+ public uint Token => methodDef.MetadataToken.RID;
+
+ public MethodInfo(AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
+ {
+ this.Assembly = assembly;
+ this.methodDef = methodDef;
+ this.source = source;
+
+ var sps = DebugInformation.SequencePoints;
+ if (sps == null || sps.Count() < 1)
+ return;
+
+ SequencePoint start = sps[0];
+ SequencePoint end = sps[0];
+
+ foreach (var sp in sps)
+ {
+ if (sp.StartLine < start.StartLine)
+ start = sp;
+ else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn)
+ start = sp;
+
+ if (sp.EndLine > end.EndLine)
+ end = sp;
+ else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn)
+ end = sp;
+ }
+
+ StartLocation = new SourceLocation(this, start);
+ EndLocation = new SourceLocation(this, end);
+ }
+
+ public SourceLocation GetLocationByIl(int pos)
+ {
+ SequencePoint prev = null;
+ foreach (var sp in DebugInformation.SequencePoints)
+ {
+ if (sp.Offset > pos)
+ break;
+ prev = sp;
+ }
+
+ if (prev != null)
+ return new SourceLocation(this, prev);
+
+ return null;
+ }
+
+ public VarInfo[] GetLiveVarsAt(int offset)
+ {
+ var res = new List<VarInfo>();
+
+ res.AddRange(methodDef.Parameters.Select(p => new VarInfo(p)));
+ res.AddRange(methodDef.DebugInformation.GetScopes()
+ .Where(s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
+ .SelectMany(s => s.Variables)
+ .Where(v => !v.IsDebuggerHidden)
+ .Select(v => new VarInfo(v)));
+
+ return res.ToArray();
+ }
+
+ public override string ToString() => "MethodInfo(" + methodDef.FullName + ")";
+ }
+
+ internal class TypeInfo
+ {
+ AssemblyInfo assembly;
+ TypeDefinition type;
+ List<MethodInfo> methods;
+
+ public TypeInfo(AssemblyInfo assembly, TypeDefinition type)
+ {
+ this.assembly = assembly;
+ this.type = type;
+ methods = new List<MethodInfo>();
+ }
+
+ public string Name => type.Name;
+ public string FullName => type.FullName;
+ public List<MethodInfo> Methods => methods;
+
+ public override string ToString() => "TypeInfo('" + FullName + "')";
+ }
+
+ class AssemblyInfo
+ {
+ static int next_id;
+ ModuleDefinition image;
+ readonly int id;
+ readonly ILogger logger;
+ Dictionary<uint, MethodInfo> methods = new Dictionary<uint, MethodInfo>();
+ Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
+ Dictionary<string, TypeInfo> typesByName = new Dictionary<string, TypeInfo>();
+ readonly List<SourceFile> sources = new List<SourceFile>();
+ internal string Url { get; }
+
+ public AssemblyInfo(IAssemblyResolver resolver, string url, byte[] assembly, byte[] pdb)
+ {
+ this.id = Interlocked.Increment(ref next_id);
+
+ try
+ {
+ Url = url;
+ ReaderParameters rp = new ReaderParameters( /*ReadingMode.Immediate*/ );
+ rp.AssemblyResolver = resolver;
+ // set ReadSymbols = true unconditionally in case there
+ // is an embedded pdb then handle ArgumentException
+ // and assume that if pdb == null that is the cause
+ rp.ReadSymbols = true;
+ rp.SymbolReaderProvider = new PdbReaderProvider();
+ if (pdb != null)
+ rp.SymbolStream = new MemoryStream(pdb);
+ rp.ReadingMode = ReadingMode.Immediate;
+
+ this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
+ }
+ catch (BadImageFormatException ex)
+ {
+ logger.LogWarning($"Failed to read assembly as portable PDB: {ex.Message}");
+ }
+ catch (ArgumentException)
+ {
+ // if pdb == null this is expected and we
+ // read the assembly without symbols below
+ if (pdb != null)
+ throw;
+ }
+
+ if (this.image == null)
+ {
+ ReaderParameters rp = new ReaderParameters( /*ReadingMode.Immediate*/ );
+ rp.AssemblyResolver = resolver;
+ if (pdb != null)
+ {
+ rp.ReadSymbols = true;
+ rp.SymbolReaderProvider = new PdbReaderProvider();
+ rp.SymbolStream = new MemoryStream(pdb);
+ }
+
+ rp.ReadingMode = ReadingMode.Immediate;
+
+ this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
+ }
+
+ Populate();
+ }
+
+ public AssemblyInfo(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ void Populate()
+ {
+ ProcessSourceLink();
+
+ var d2s = new Dictionary<Document, SourceFile>();
+
+ SourceFile FindSource(Document doc)
+ {
+ if (doc == null)
+ return null;
+
+ if (d2s.TryGetValue(doc, out var source))
+ return source;
+
+ var src = new SourceFile(this, sources.Count, doc, GetSourceLinkUrl(doc.Url));
+ sources.Add(src);
+ d2s[doc] = src;
+ return src;
+ };
+
+ foreach (var type in image.GetTypes())
+ {
+ var typeInfo = new TypeInfo(this, type);
+ typesByName[type.FullName] = typeInfo;
+
+ foreach (var method in type.Methods)
+ {
+ foreach (var sp in method.DebugInformation.SequencePoints)
+ {
+ var source = FindSource(sp.Document);
+ var methodInfo = new MethodInfo(this, method, source);
+ methods[method.MetadataToken.RID] = methodInfo;
+ if (source != null)
+ source.AddMethod(methodInfo);
+
+ typeInfo.Methods.Add(methodInfo);
+ }
+ }
+ }
+ }
+
+ private void ProcessSourceLink()
+ {
+ var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault(i => i.Kind == CustomDebugInformationKind.SourceLink);
+
+ if (sourceLinkDebugInfo != null)
+ {
+ var sourceLinkContent = ((SourceLinkDebugInformation) sourceLinkDebugInfo).Content;
+
+ if (sourceLinkContent != null)
+ {
+ var jObject = JObject.Parse(sourceLinkContent) ["documents"];
+ sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
+ }
+ }
+ }
+
+ private Uri GetSourceLinkUrl(string document)
+ {
+ if (sourceLinkMappings.TryGetValue(document, out string url))
+ return new Uri(url);
+
+ foreach (var sourceLinkDocument in sourceLinkMappings)
+ {
+ string key = sourceLinkDocument.Key;
+
+ if (Path.GetFileName(key) != "*")
+ {
+ continue;
+ }
+
+ var keyTrim = key.TrimEnd('*');
+
+ if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase))
+ {
+ var docUrlPart = document.Replace(keyTrim, "");
+ return new Uri(sourceLinkDocument.Value.TrimEnd('*') + docUrlPart);
+ }
+ }
+
+ return null;
+ }
+
+ public IEnumerable<SourceFile> Sources => this.sources;
+
+ public Dictionary<string, TypeInfo> TypesByName => this.typesByName;
+ public int Id => id;
+ public string Name => image.Name;
+
+ public SourceFile GetDocById(int document)
+ {
+ return sources.FirstOrDefault(s => s.SourceId.Document == document);
+ }
+
+ public MethodInfo GetMethodByToken(uint token)
+ {
+ methods.TryGetValue(token, out var value);
+ return value;
+ }
+
+ public TypeInfo GetTypeByName(string name)
+ {
+ typesByName.TryGetValue(name, out var res);
+ return res;
+ }
+ }
+
+ internal class SourceFile
+ {
+ Dictionary<uint, MethodInfo> methods;
+ AssemblyInfo assembly;
+ int id;
+ Document doc;
+
+ internal SourceFile(AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
+ {
+ this.methods = new Dictionary<uint, MethodInfo>();
+ this.SourceLinkUri = sourceLinkUri;
+ this.assembly = assembly;
+ this.id = id;
+ this.doc = doc;
+ this.DebuggerFileName = doc.Url.Replace("\\", "/").Replace(":", "");
+
+ this.SourceUri = new Uri((Path.IsPathRooted(doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute);
+ if (SourceUri.IsFile && File.Exists(SourceUri.LocalPath))
+ {
+ this.Url = this.SourceUri.ToString();
+ }
+ else
+ {
+ this.Url = DotNetUrl;
+ }
+ }
+
+ internal void AddMethod(MethodInfo mi)
+ {
+ if (!this.methods.ContainsKey(mi.Token))
+ this.methods[mi.Token] = mi;
+ }
+
+ public string DebuggerFileName { get; }
+ public string Url { get; }
+ public string AssemblyName => assembly.Name;
+ public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
+
+ public SourceId SourceId => new SourceId(assembly.Id, this.id);
+ public Uri SourceLinkUri { get; }
+ public Uri SourceUri { get; }
+
+ public IEnumerable<MethodInfo> Methods => this.methods.Values;
+
+ public string DocUrl => doc.Url;
+
+ public(int startLine, int startColumn, int endLine, int endColumn) GetExtents()
+ {
+ var start = Methods.OrderBy(m => m.StartLocation.Line).ThenBy(m => m.StartLocation.Column).First();
+ var end = Methods.OrderByDescending(m => m.EndLocation.Line).ThenByDescending(m => m.EndLocation.Column).First();
+ return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column);
+ }
+
+ async Task<MemoryStream> GetDataAsync(Uri uri, CancellationToken token)
+ {
+ var mem = new MemoryStream();
+ try
+ {
+ if (uri.IsFile && File.Exists(uri.LocalPath))
+ {
+ using(var file = File.Open(SourceUri.LocalPath, FileMode.Open))
+ {
+ await file.CopyToAsync(mem, token).ConfigureAwait(false);
+ mem.Position = 0;
+ }
+ }
+ else if (uri.Scheme == "http" || uri.Scheme == "https")
+ {
+ var client = new HttpClient();
+ using(var stream = await client.GetStreamAsync(uri))
+ {
+ await stream.CopyToAsync(mem, token).ConfigureAwait(false);
+ mem.Position = 0;
+ }
+ }
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ return mem;
+ }
+
+ static HashAlgorithm GetHashAlgorithm(DocumentHashAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case DocumentHashAlgorithm.SHA1:
+ return SHA1.Create();
+ case DocumentHashAlgorithm.SHA256:
+ return SHA256.Create();
+ case DocumentHashAlgorithm.MD5:
+ return MD5.Create();
+ }
+ return null;
+ }
+
+ bool CheckPdbHash(byte[] computedHash)
+ {
+ if (computedHash.Length != doc.Hash.Length)
+ return false;
+
+ for (var i = 0; i < computedHash.Length; i++)
+ if (computedHash[i] != doc.Hash[i])
+ return false;
+
+ return true;
+ }
+
+ byte[] ComputePdbHash(Stream sourceStream)
+ {
+ var algorithm = GetHashAlgorithm(doc.HashAlgorithm);
+ if (algorithm != null)
+ using(algorithm)
+ return algorithm.ComputeHash(sourceStream);
+
+ return Array.Empty<byte>();
+ }
+
+ public async Task<Stream> GetSourceAsync(bool checkHash, CancellationToken token = default(CancellationToken))
+ {
+ if (doc.EmbeddedSource.Length > 0)
+ return new MemoryStream(doc.EmbeddedSource, false);
+
+ foreach (var url in new [] { SourceUri, SourceLinkUri })
+ {
+ var mem = await GetDataAsync(url, token).ConfigureAwait(false);
+ if (mem != null && (!checkHash || CheckPdbHash(ComputePdbHash(mem))))
+ {
+ mem.Position = 0;
+ return mem;
+ }
+ }
+
+ return MemoryStream.Null;
+ }
+
+ public object ToScriptSource(int executionContextId, object executionContextAuxData)
+ {
+ return new
+ {
+ scriptId = SourceId.ToString(),
+ url = Url,
+ executionContextId,
+ executionContextAuxData,
+ //hash: should be the v8 hash algo, managed implementation is pending
+ dotNetUrl = DotNetUrl,
+ };
+ }
+ }
+
+ internal class DebugStore
+ {
+ List<AssemblyInfo> assemblies = new List<AssemblyInfo>();
+ readonly HttpClient client;
+ readonly ILogger logger;
+
+ public DebugStore(ILogger logger, HttpClient client)
+ {
+ this.client = client;
+ this.logger = logger;
+ }
+
+ public DebugStore(ILogger logger) : this(logger, new HttpClient())
+ { }
+
+ class DebugItem
+ {
+ public string Url { get; set; }
+ public Task<byte[][]> Data { get; set; }
+ }
+
+ public async IAsyncEnumerable<SourceFile> Load(SessionId sessionId, string[] loaded_files, [EnumeratorCancellation] CancellationToken token)
+ {
+ static bool MatchPdb(string asm, string pdb) => Path.ChangeExtension(asm, "pdb") == pdb;
+
+ var asm_files = new List<string>();
+ var pdb_files = new List<string>();
+ foreach (var file_name in loaded_files)
+ {
+ if (file_name.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase))
+ pdb_files.Add(file_name);
+ else
+ asm_files.Add(file_name);
+ }
+
+ List<DebugItem> steps = new List<DebugItem>();
+ foreach (var url in asm_files)
+ {
+ try
+ {
+ var pdb = pdb_files.FirstOrDefault(n => MatchPdb(url, n));
+ steps.Add(
+ new DebugItem
+ {
+ Url = url,
+ Data = Task.WhenAll(client.GetByteArrayAsync(url), pdb != null ? client.GetByteArrayAsync(pdb) : Task.FromResult<byte[]>(null))
+ });
+ }
+ catch (Exception e)
+ {
+ logger.LogDebug($"Failed to read {url} ({e.Message})");
+ }
+ }
+
+ var resolver = new DefaultAssemblyResolver();
+ foreach (var step in steps)
+ {
+ AssemblyInfo assembly = null;
+ try
+ {
+ var bytes = await step.Data.ConfigureAwait(false);
+ assembly = new AssemblyInfo(resolver, step.Url, bytes[0], bytes[1]);
+ }
+ catch (Exception e)
+ {
+ logger.LogDebug($"Failed to load {step.Url} ({e.Message})");
+ }
+ if (assembly == null)
+ continue;
+
+ assemblies.Add(assembly);
+ foreach (var source in assembly.Sources)
+ yield return source;
+ }
+ }
+
+ public IEnumerable<SourceFile> AllSources() => assemblies.SelectMany(a => a.Sources);
+
+ public SourceFile GetFileById(SourceId id) => AllSources().SingleOrDefault(f => f.SourceId.Equals(id));
+
+ public AssemblyInfo GetAssemblyByName(string name) => assemblies.FirstOrDefault(a => a.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
+
+ /*
+ V8 uses zero based indexing for both line and column.
+ PPDBs uses one based indexing for both line and column.
+ */
+ static bool Match(SequencePoint sp, SourceLocation start, SourceLocation end)
+ {
+ var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
+ var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
+
+ if (start.Line > spEnd.Line)
+ return false;
+
+ if (start.Column > spEnd.Column && start.Line == spEnd.Line)
+ return false;
+
+ if (end.Line < spStart.Line)
+ return false;
+
+ if (end.Column < spStart.Column && end.Line == spStart.Line)
+ return false;
+
+ return true;
+ }
+
+ public List<SourceLocation> FindPossibleBreakpoints(SourceLocation start, SourceLocation end)
+ {
+ //XXX FIXME no idea what todo with locations on different files
+ if (start.Id != end.Id)
+ {
+ logger.LogDebug($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}");
+ return null;
+ }
+
+ var sourceId = start.Id;
+
+ var doc = GetFileById(sourceId);
+
+ var res = new List<SourceLocation>();
+ if (doc == null)
+ {
+ logger.LogDebug($"Could not find document {sourceId}");
+ return res;
+ }
+
+ foreach (var method in doc.Methods)
+ {
+ foreach (var sequencePoint in method.DebugInformation.SequencePoints)
+ {
+ if (!sequencePoint.IsHidden && Match(sequencePoint, start, end))
+ res.Add(new SourceLocation(method, sequencePoint));
+ }
+ }
+ return res;
+ }
+
+ /*
+ V8 uses zero based indexing for both line and column.
+ PPDBs uses one based indexing for both line and column.
+ */
+ static bool Match(SequencePoint sp, int line, int column)
+ {
+ var bp = (line: line + 1, column: column + 1);
+
+ if (sp.StartLine > bp.line || sp.EndLine < bp.line)
+ return false;
+
+ //Chrome sends a zero column even if getPossibleBreakpoints say something else
+ if (column == 0)
+ return true;
+
+ if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
+ return false;
+
+ if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
+ return false;
+
+ return true;
+ }
+
+ public IEnumerable<SourceLocation> FindBreakpointLocations(BreakpointRequest request)
+ {
+ request.TryResolve(this);
+
+ var asm = assemblies.FirstOrDefault(a => a.Name.Equals(request.Assembly, StringComparison.OrdinalIgnoreCase));
+ var sourceFile = asm?.Sources?.SingleOrDefault(s => s.DebuggerFileName.Equals(request.File, StringComparison.OrdinalIgnoreCase));
+
+ if (sourceFile == null)
+ yield break;
+
+ foreach (var method in sourceFile.Methods)
+ {
+ foreach (var sequencePoint in method.DebugInformation.SequencePoints)
+ {
+ if (!sequencePoint.IsHidden && Match(sequencePoint, request.Line, request.Column))
+ yield return new SourceLocation(method, sequencePoint);
+ }
+ }
+ }
+
+ public string ToUrl(SourceLocation location) => location != null ? GetFileById(location.Id).Url : "";
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Net.WebSockets;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+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
+ {
+ private readonly MonoProxy proxy;
+
+ public DebuggerProxy(ILoggerFactory loggerFactory)
+ {
+ proxy = new MonoProxy(loggerFactory);
+ }
+
+ public Task Run(Uri browserUri, WebSocket ideSocket)
+ {
+ return proxy.Run(browserUri, ideSocket);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+ public struct SessionId
+ {
+ public readonly string sessionId;
+
+ public SessionId(string sessionId)
+ {
+ this.sessionId = sessionId;
+ }
+
+ // hashset treats 0 as unset
+ public override int GetHashCode() => sessionId?.GetHashCode() ?? -1;
+
+ public override bool Equals(object obj) => (obj is SessionId) ? ((SessionId) obj).sessionId == sessionId : false;
+
+ public static bool operator ==(SessionId a, SessionId b) => a.sessionId == b.sessionId;
+
+ public static bool operator !=(SessionId a, SessionId b) => a.sessionId != b.sessionId;
+
+ public static SessionId Null { get; } = new SessionId();
+
+ public override string ToString() => $"session-{sessionId}";
+ }
+
+ public struct MessageId
+ {
+ public readonly string sessionId;
+ public readonly int id;
+
+ public MessageId(string sessionId, int id)
+ {
+ this.sessionId = sessionId;
+ this.id = id;
+ }
+
+ public static implicit operator SessionId(MessageId id) => new SessionId(id.sessionId);
+
+ public override string ToString() => $"msg-{sessionId}:::{id}";
+
+ public override int GetHashCode() => (sessionId?.GetHashCode() ?? 0) ^ id.GetHashCode();
+
+ public override bool Equals(object obj) => (obj is MessageId) ? ((MessageId) obj).sessionId == sessionId && ((MessageId) obj).id == id : false;
+ }
+
+ internal class DotnetObjectId
+ {
+ public string Scheme { get; }
+ public string Value { get; }
+
+ public static bool TryParse(JToken jToken, out DotnetObjectId objectId) => TryParse(jToken?.Value<string>(), out objectId);
+
+ public static bool TryParse(string id, out DotnetObjectId objectId)
+ {
+ objectId = null;
+ if (id == null)
+ return false;
+
+ if (!id.StartsWith("dotnet:"))
+ return false;
+
+ var parts = id.Split(":", 3);
+
+ if (parts.Length < 3)
+ return false;
+
+ objectId = new DotnetObjectId(parts[1], parts[2]);
+
+ return true;
+ }
+
+ public DotnetObjectId(string scheme, string value)
+ {
+ Scheme = scheme;
+ Value = value;
+ }
+
+ public override string ToString() => $"dotnet:{Scheme}:{Value}";
+ }
+
+ public struct Result
+ {
+ public JObject Value { get; private set; }
+ public JObject Error { get; private set; }
+
+ public bool IsOk => Value != null;
+ public bool IsErr => Error != null;
+
+ Result(JObject result, JObject error)
+ {
+ if (result != null && error != null)
+ throw new ArgumentException($"Both {nameof(result)} and {nameof(error)} arguments cannot be non-null.");
+
+ bool resultHasError = String.Compare((result?["result"] as JObject) ? ["subtype"]?.Value<string>(), "error") == 0;
+ if (result != null && resultHasError)
+ {
+ this.Value = null;
+ this.Error = result;
+ }
+ else
+ {
+ this.Value = result;
+ this.Error = error;
+ }
+ }
+
+ public static Result FromJson(JObject obj)
+ {
+ //Log ("protocol", $"from result: {obj}");
+ return new Result(obj["result"] as JObject, obj["error"] as JObject);
+ }
+
+ public static Result Ok(JObject ok) => new Result(ok, null);
+
+ public static Result OkFromObject(object ok) => Ok(JObject.FromObject(ok));
+
+ public static Result Err(JObject err) => new Result(null, err);
+
+ public static Result Err(string msg) => new Result(null, JObject.FromObject(new { message = msg }));
+
+ public static Result Exception(Exception e) => new Result(null, JObject.FromObject(new { message = e.Message }));
+
+ public JObject ToJObject(MessageId target)
+ {
+ if (IsOk)
+ {
+ return JObject.FromObject(new
+ {
+ target.id,
+ target.sessionId,
+ result = Value
+ });
+ }
+ else
+ {
+ return JObject.FromObject(new
+ {
+ target.id,
+ target.sessionId,
+ error = Error
+ });
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"[Result: IsOk: {IsOk}, IsErr: {IsErr}, Value: {Value?.ToString ()}, Error: {Error?.ToString ()} ]";
+ }
+ }
+
+ internal class MonoCommands
+ {
+ public string expression { get; set; }
+ public string objectGroup { get; set; } = "mono-debugger";
+ public bool includeCommandLineAPI { get; set; } = false;
+ public bool silent { get; set; } = false;
+ public bool returnByValue { get; set; } = true;
+
+ public MonoCommands(string expression) => this.expression = expression;
+
+ public static MonoCommands GetCallStack() => new MonoCommands("MONO.mono_wasm_get_call_stack()");
+
+ public static MonoCommands IsRuntimeReady() => new MonoCommands("MONO.mono_wasm_runtime_is_ready");
+
+ public static MonoCommands StartSingleStepping(StepKind kind) => new MonoCommands($"MONO.mono_wasm_start_single_stepping ({(int)kind})");
+
+ public static MonoCommands GetLoadedFiles() => new MonoCommands("MONO.mono_wasm_get_loaded_files()");
+
+ public static MonoCommands ClearAllBreakpoints() => new MonoCommands("MONO.mono_wasm_clear_all_breakpoints()");
+
+ public static MonoCommands GetDetails(DotnetObjectId objectId, JToken args = null) => new MonoCommands($"MONO.mono_wasm_get_details ('{objectId}', {(args ?? "{ }")})");
+
+ public static MonoCommands GetScopeVariables(int scopeId, params VarInfo[] vars)
+ {
+ var var_ids = vars.Select(v => new { index = v.Index, name = v.Name }).ToArray();
+ return new MonoCommands($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject (var_ids)})");
+ }
+
+ public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");
+
+ public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
+
+ public static MonoCommands ReleaseObject(DotnetObjectId objectId) => new MonoCommands($"MONO.mono_wasm_release_object('{objectId}')");
+
+ public static MonoCommands CallFunctionOn(JToken args) => new MonoCommands($"MONO.mono_wasm_call_function_on ({args.ToString ()})");
+
+ public static MonoCommands Resume() => new MonoCommands($"MONO.mono_wasm_debugger_resume ()");
+ }
+
+ internal enum MonoErrorCodes
+ {
+ BpNotFound = 100000,
+ }
+
+ internal class MonoConstants
+ {
+ public const string RUNTIME_IS_READY = "mono_wasm_runtime_ready";
+ }
+
+ class Frame
+ {
+ public Frame(MethodInfo method, SourceLocation location, int id)
+ {
+ this.Method = method;
+ this.Location = location;
+ this.Id = id;
+ }
+
+ public MethodInfo Method { get; private set; }
+ public SourceLocation Location { get; private set; }
+ public int Id { get; private set; }
+ }
+
+ class Breakpoint
+ {
+ public SourceLocation Location { get; private set; }
+ public int RemoteId { get; set; }
+ public BreakpointState State { get; set; }
+ public string StackId { get; private set; }
+
+ public static bool TryParseId(string stackId, out int id)
+ {
+ id = -1;
+ if (stackId?.StartsWith("dotnet:", StringComparison.Ordinal) != true)
+ return false;
+
+ return int.TryParse(stackId.Substring("dotnet:".Length), out id);
+ }
+
+ public Breakpoint(string stackId, SourceLocation loc, BreakpointState state)
+ {
+ this.StackId = stackId;
+ this.Location = loc;
+ this.State = state;
+ }
+ }
+
+ enum BreakpointState
+ {
+ Active,
+ Disabled,
+ Pending
+ }
+
+ enum StepKind
+ {
+ Into,
+ Out,
+ Over
+ }
+
+ internal class ExecutionContext
+ {
+ public string DebuggerId { get; set; }
+ public Dictionary<string, BreakpointRequest> BreakpointRequests { get; } = new Dictionary<string, BreakpointRequest>();
+
+ public TaskCompletionSource<DebugStore> ready = null;
+ public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted;
+
+ public int Id { get; set; }
+ public object AuxData { get; set; }
+
+ public List<Frame> CallStack { get; set; }
+
+ public string[] LoadedFiles { get; set; }
+ internal DebugStore store;
+ public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore>();
+
+ public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken>();
+
+ public DebugStore Store
+ {
+ get
+ {
+ if (store == null || !Source.Task.IsCompleted)
+ return null;
+
+ return store;
+ }
+ }
+
+ public void ClearState()
+ {
+ CallStack = null;
+ LocalsCache.Clear();
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+ class DevToolsQueue
+ {
+ Task current_send;
+ List<byte[]> pending;
+
+ public WebSocket Ws { get; private set; }
+ public Task CurrentSend { get { return current_send; } }
+ public DevToolsQueue(WebSocket sock)
+ {
+ this.Ws = sock;
+ pending = new List<byte[]>();
+ }
+
+ public Task Send(byte[] bytes, CancellationToken token)
+ {
+ pending.Add(bytes);
+ if (pending.Count == 1)
+ {
+ if (current_send != null)
+ throw new Exception("current_send MUST BE NULL IF THERE'S no pending send");
+ //logger.LogTrace ("sending {0} bytes", bytes.Length);
+ current_send = Ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, token);
+ return current_send;
+ }
+ return null;
+ }
+
+ public Task Pump(CancellationToken token)
+ {
+ current_send = null;
+ pending.RemoveAt(0);
+
+ if (pending.Count > 0)
+ {
+ if (current_send != null)
+ throw new Exception("current_send MUST BE NULL IF THERE'S no pending send");
+
+ current_send = Ws.SendAsync(new ArraySegment<byte>(pending[0]), WebSocketMessageType.Text, true, token);
+ return current_send;
+ }
+ return null;
+ }
+ }
+
+ internal class DevToolsProxy
+ {
+ TaskCompletionSource<bool> side_exception = new TaskCompletionSource<bool>();
+ TaskCompletionSource<bool> client_initiated_close = new TaskCompletionSource<bool>();
+ Dictionary<MessageId, TaskCompletionSource<Result>> pending_cmds = new Dictionary<MessageId, TaskCompletionSource<Result>>();
+ ClientWebSocket browser;
+ WebSocket ide;
+ int next_cmd_id;
+ List<Task> pending_ops = new List<Task>();
+ List<DevToolsQueue> queues = new List<DevToolsQueue>();
+
+ protected readonly ILogger logger;
+
+ public DevToolsProxy(ILoggerFactory loggerFactory)
+ {
+ logger = loggerFactory.CreateLogger<DevToolsProxy>();
+ }
+
+ protected virtual Task<bool> AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+ {
+ return Task.FromResult(false);
+ }
+
+ protected virtual Task<bool> AcceptCommand(MessageId id, string method, JObject args, CancellationToken token)
+ {
+ return Task.FromResult(false);
+ }
+
+ async Task<string> ReadOne(WebSocket socket, CancellationToken token)
+ {
+ byte[] buff = new byte[4000];
+ var mem = new MemoryStream();
+ while (true)
+ {
+
+ if (socket.State != WebSocketState.Open)
+ {
+ Log("error", $"DevToolsProxy: Socket is no longer open.");
+ client_initiated_close.TrySetResult(true);
+ return null;
+ }
+
+ var result = await socket.ReceiveAsync(new ArraySegment<byte>(buff), token);
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ client_initiated_close.TrySetResult(true);
+ return null;
+ }
+
+ mem.Write(buff, 0, result.Count);
+
+ if (result.EndOfMessage)
+ return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int) mem.Length);
+ }
+ }
+
+ DevToolsQueue GetQueueForSocket(WebSocket ws)
+ {
+ return queues.FirstOrDefault(q => q.Ws == ws);
+ }
+
+ DevToolsQueue GetQueueForTask(Task task)
+ {
+ return queues.FirstOrDefault(q => q.CurrentSend == task);
+ }
+
+ void Send(WebSocket to, JObject o, CancellationToken token)
+ {
+ var sender = browser == to ? "Send-browser" : "Send-ide";
+
+ var method = o["method"]?.ToString();
+ //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
+ Log("protocol", $"{sender}: " + JsonConvert.SerializeObject(o));
+ var bytes = Encoding.UTF8.GetBytes(o.ToString());
+
+ var queue = GetQueueForSocket(to);
+
+ var task = queue.Send(bytes, token);
+ if (task != null)
+ pending_ops.Add(task);
+ }
+
+ async Task OnEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+ {
+ try
+ {
+ if (!await AcceptEvent(sessionId, method, args, token))
+ {
+ //logger.LogDebug ("proxy browser: {0}::{1}",method, args);
+ SendEventInternal(sessionId, method, args, token);
+ }
+ }
+ catch (Exception e)
+ {
+ side_exception.TrySetException(e);
+ }
+ }
+
+ async Task OnCommand(MessageId id, string method, JObject args, CancellationToken token)
+ {
+ try
+ {
+ if (!await AcceptCommand(id, method, args, token))
+ {
+ var res = await SendCommandInternal(id, method, args, token);
+ SendResponseInternal(id, res, token);
+ }
+ }
+ catch (Exception e)
+ {
+ side_exception.TrySetException(e);
+ }
+ }
+
+ void OnResponse(MessageId id, Result result)
+ {
+ //logger.LogTrace ("got id {0} res {1}", id, result);
+ // Fixme
+ if (pending_cmds.Remove(id, out var task))
+ {
+ task.SetResult(result);
+ return;
+ }
+ logger.LogError("Cannot respond to command: {id} with result: {result} - command is not pending", id, result);
+ }
+
+ void ProcessBrowserMessage(string msg, CancellationToken token)
+ {
+ var res = JObject.Parse(msg);
+
+ var method = res["method"]?.ToString();
+ //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
+ Log("protocol", $"browser: {msg}");
+
+ if (res["id"] == null)
+ pending_ops.Add(OnEvent(new SessionId(res["sessionId"]?.Value<string>()), res["method"].Value<string>(), res["params"] as JObject, token));
+ else
+ OnResponse(new MessageId(res["sessionId"]?.Value<string>(), res["id"].Value<int>()), Result.FromJson(res));
+ }
+
+ void ProcessIdeMessage(string msg, CancellationToken token)
+ {
+ Log("protocol", $"ide: {msg}");
+ if (!string.IsNullOrEmpty(msg))
+ {
+ var res = JObject.Parse(msg);
+ pending_ops.Add(OnCommand(
+ new MessageId(res["sessionId"]?.Value<string>(), res["id"].Value<int>()),
+ res["method"].Value<string>(),
+ res["params"] as JObject, token));
+ }
+ }
+
+ internal async Task<Result> SendCommand(SessionId id, string method, JObject args, CancellationToken token)
+ {
+ //Log ("verbose", $"sending command {method}: {args}");
+ return await SendCommandInternal(id, method, args, token);
+ }
+
+ Task<Result> SendCommandInternal(SessionId sessionId, string method, JObject args, CancellationToken token)
+ {
+ int id = Interlocked.Increment(ref next_cmd_id);
+
+ var o = JObject.FromObject(new
+ {
+ id,
+ method,
+ @params = args
+ });
+ if (sessionId.sessionId != null)
+ o["sessionId"] = sessionId.sessionId;
+ var tcs = new TaskCompletionSource<Result>();
+
+ var msgId = new MessageId(sessionId.sessionId, id);
+ //Log ("verbose", $"add cmd id {sessionId}-{id}");
+ pending_cmds[msgId] = tcs;
+
+ Send(this.browser, o, token);
+ return tcs.Task;
+ }
+
+ public void SendEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+ {
+ //Log ("verbose", $"sending event {method}: {args}");
+ SendEventInternal(sessionId, method, args, token);
+ }
+
+ void SendEventInternal(SessionId sessionId, string method, JObject args, CancellationToken token)
+ {
+ var o = JObject.FromObject(new
+ {
+ method,
+ @params = args
+ });
+ if (sessionId.sessionId != null)
+ o["sessionId"] = sessionId.sessionId;
+
+ Send(this.ide, o, token);
+ }
+
+ internal void SendResponse(MessageId id, Result result, CancellationToken token)
+ {
+ SendResponseInternal(id, result, token);
+ }
+
+ void SendResponseInternal(MessageId id, Result result, CancellationToken token)
+ {
+ JObject o = result.ToJObject(id);
+ if (result.IsErr)
+ logger.LogError($"sending error response for id: {id} -> {result}");
+
+ Send(this.ide, o, token);
+ }
+
+ // , HttpContext context)
+ public async Task Run(Uri browserUri, WebSocket ideSocket)
+ {
+ Log("info", $"DevToolsProxy: Starting on {browserUri}");
+ using(this.ide = ideSocket)
+ {
+ Log("verbose", $"DevToolsProxy: IDE waiting for connection on {browserUri}");
+ queues.Add(new DevToolsQueue(this.ide));
+ using(this.browser = new ClientWebSocket())
+ {
+ this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
+ await this.browser.ConnectAsync(browserUri, CancellationToken.None);
+ queues.Add(new DevToolsQueue(this.browser));
+
+ Log("verbose", $"DevToolsProxy: Client connected on {browserUri}");
+ var x = new CancellationTokenSource();
+
+ pending_ops.Add(ReadOne(browser, x.Token));
+ pending_ops.Add(ReadOne(ide, x.Token));
+ pending_ops.Add(side_exception.Task);
+ pending_ops.Add(client_initiated_close.Task);
+
+ try
+ {
+ while (!x.IsCancellationRequested)
+ {
+ var task = await Task.WhenAny(pending_ops.ToArray());
+ //logger.LogTrace ("pump {0} {1}", task, pending_ops.IndexOf (task));
+ if (task == pending_ops[0])
+ {
+ var msg = ((Task<string>) task).Result;
+ if (msg != null)
+ {
+ pending_ops[0] = ReadOne(browser, x.Token); //queue next read
+ ProcessBrowserMessage(msg, x.Token);
+ }
+ }
+ else if (task == pending_ops[1])
+ {
+ var msg = ((Task<string>) task).Result;
+ if (msg != null)
+ {
+ pending_ops[1] = ReadOne(ide, x.Token); //queue next read
+ ProcessIdeMessage(msg, x.Token);
+ }
+ }
+ else if (task == pending_ops[2])
+ {
+ var res = ((Task<bool>) task).Result;
+ throw new Exception("side task must always complete with an exception, what's going on???");
+ }
+ else if (task == pending_ops[3])
+ {
+ var res = ((Task<bool>) task).Result;
+ Log("verbose", $"DevToolsProxy: Client initiated close from {browserUri}");
+ x.Cancel();
+ }
+ else
+ {
+ //must be a background task
+ pending_ops.Remove(task);
+ var queue = GetQueueForTask(task);
+ if (queue != null)
+ {
+ var tsk = queue.Pump(x.Token);
+ if (tsk != null)
+ pending_ops.Add(tsk);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log("error", $"DevToolsProxy::Run: Exception {e}");
+ //throw;
+ }
+ finally
+ {
+ if (!x.IsCancellationRequested)
+ x.Cancel();
+ }
+ }
+ }
+ }
+
+ protected void Log(string priority, string msg)
+ {
+ switch (priority)
+ {
+ case "protocol":
+ logger.LogTrace(msg);
+ break;
+ case "verbose":
+ logger.LogDebug(msg);
+ break;
+ case "info":
+ case "warning":
+ case "error":
+ default:
+ logger.LogDebug(msg);
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Emit;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+ internal class EvaluateExpression
+ {
+
+ class FindThisExpression : CSharpSyntaxWalker
+ {
+ public List<string> thisExpressions = new List<string>();
+ public SyntaxTree syntaxTree;
+ public FindThisExpression(SyntaxTree syntax)
+ {
+ syntaxTree = syntax;
+ }
+ public override void Visit(SyntaxNode node)
+ {
+ if (node is ThisExpressionSyntax)
+ {
+ if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax)
+ {
+ IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax;
+ thisExpressions.Add(var.Identifier.Text);
+ var newRoot = syntaxTree.GetRoot().ReplaceNode(node.Parent, thisParent.Name);
+ syntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);
+ this.Visit(GetExpressionFromSyntaxTree(syntaxTree));
+ }
+ }
+ else
+ base.Visit(node);
+ }
+
+ public async Task CheckIfIsProperty(MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
+ {
+ foreach (var var in thisExpressions)
+ {
+ JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var, true, token);
+ if (value == null)
+ throw new Exception($"The property {var} does not exist in the current context");
+ }
+ }
+ }
+
+ class FindVariableNMethodCall : CSharpSyntaxWalker
+ {
+ public List<IdentifierNameSyntax> variables = new List<IdentifierNameSyntax>();
+ public List<ThisExpressionSyntax> thisList = new List<ThisExpressionSyntax>();
+ public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax>();
+ public List<object> values = new List<Object>();
+
+ public override void Visit(SyntaxNode node)
+ {
+ if (node is IdentifierNameSyntax identifier && !variables.Any(x => x.Identifier.Text == identifier.Identifier.Text))
+ variables.Add(identifier);
+ if (node is InvocationExpressionSyntax)
+ {
+ methodCall.Add(node as InvocationExpressionSyntax);
+ throw new Exception("Method Call is not implemented yet");
+ }
+ if (node is AssignmentExpressionSyntax)
+ throw new Exception("Assignment is not implemented yet");
+ base.Visit(node);
+ }
+ public async Task<SyntaxTree> ReplaceVars(SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
+ {
+ CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot();
+ foreach (var var in variables)
+ {
+ ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax;
+ MethodDeclarationSyntax method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
+
+ JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var.Identifier.Text, false, token);
+
+ if (value == null)
+ throw new Exception($"The name {var.Identifier.Text} does not exist in the current context");
+
+ values.Add(ConvertJSToCSharpType(value["value"]));
+
+ var updatedMethod = method.AddParameterListParameters(
+ SyntaxFactory.Parameter(
+ SyntaxFactory.Identifier(var.Identifier.Text))
+ .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value["value"]))));
+ root = root.ReplaceNode(method, updatedMethod);
+ }
+ syntaxTree = syntaxTree.WithRootAndOptions(root, syntaxTree.Options);
+ return syntaxTree;
+ }
+
+ private object ConvertJSToCSharpType(JToken variable)
+ {
+ var value = variable["value"];
+ var type = variable["type"].Value<string>();
+ var subType = variable["subtype"]?.Value<string>();
+
+ switch (type)
+ {
+ case "string":
+ return value?.Value<string>();
+ case "number":
+ return value?.Value<double>();
+ case "boolean":
+ return value?.Value<bool>();
+ case "object":
+ if (subType == "null")
+ return null;
+ break;
+ }
+ throw new Exception($"Evaluate of this datatype {type} not implemented yet");
+ }
+
+ private string GetTypeFullName(JToken variable)
+ {
+ var type = variable["type"].ToString();
+ var subType = variable["subtype"]?.Value<string>();
+ object value = ConvertJSToCSharpType(variable);
+
+ switch (type)
+ {
+ case "object":
+ {
+ if (subType == "null")
+ return variable["className"].Value<string>();
+ break;
+ }
+ default:
+ return value.GetType().FullName;
+ }
+ throw new Exception($"Evaluate of this datatype {type} not implemented yet");
+ }
+ }
+
+ static SyntaxNode GetExpressionFromSyntaxTree(SyntaxTree syntaxTree)
+ {
+ CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot();
+ ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax;
+ MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
+ BlockSyntax blockValue = methodDeclaration.Body;
+ ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt(0) as ReturnStatementSyntax;
+ InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax;
+ MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax;
+ ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax;
+ return expressionParenthesized.Expression;
+ }
+
+ internal static async Task<string> CompileAndRunTheExpression(MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token)
+ {
+ FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall();
+ string retString;
+ SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
+ using System;
+ public class CompileAndRunTheExpression
+ {
+ public string Evaluate()
+ {
+ return (" + expression + @").ToString();
+ }
+ }");
+
+ FindThisExpression findThisExpression = new FindThisExpression(syntaxTree);
+ var expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
+ findThisExpression.Visit(expressionTree);
+ await findThisExpression.CheckIfIsProperty(proxy, msg_id, scope_id, token);
+ syntaxTree = findThisExpression.syntaxTree;
+
+ expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
+ findVarNMethodCall.Visit(expressionTree);
+
+ syntaxTree = await findVarNMethodCall.ReplaceVars(syntaxTree, proxy, msg_id, scope_id, token);
+
+ MetadataReference[] references = new MetadataReference[]
+ {
+ MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
+ };
+
+ CSharpCompilation compilation = CSharpCompilation.Create(
+ "compileAndRunTheExpression",
+ syntaxTrees : new [] { syntaxTree },
+ references : references,
+ options : new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ using(var ms = new MemoryStream())
+ {
+ EmitResult result = compilation.Emit(ms);
+ ms.Seek(0, SeekOrigin.Begin);
+ Assembly assembly = Assembly.Load(ms.ToArray());
+ Type type = assembly.GetType("CompileAndRunTheExpression");
+ object obj = Activator.CreateInstance(type);
+ var ret = type.InvokeMember("Evaluate",
+ BindingFlags.Default | BindingFlags.InvokeMethod,
+ null,
+ obj,
+ findVarNMethodCall.values.ToArray());
+ retString = ret.ToString();
+ }
+ return retString;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+ internal class MonoProxy : DevToolsProxy
+ {
+ HashSet<SessionId> sessions = new HashSet<SessionId>();
+ Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext>();
+
+ public MonoProxy(ILoggerFactory loggerFactory, bool hideWebDriver = true) : base(loggerFactory) { hideWebDriver = true; }
+
+ readonly bool hideWebDriver;
+
+ internal ExecutionContext GetContext(SessionId sessionId)
+ {
+ if (contexts.TryGetValue(sessionId, out var context))
+ return context;
+
+ throw new ArgumentException($"Invalid Session: \"{sessionId}\"", nameof(sessionId));
+ }
+
+ bool UpdateContext(SessionId sessionId, ExecutionContext executionContext, out ExecutionContext previousExecutionContext)
+ {
+ var previous = contexts.TryGetValue(sessionId, out previousExecutionContext);
+ contexts[sessionId] = executionContext;
+ return previous;
+ }
+
+ internal Task<Result> SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token);
+
+ protected override async Task<bool> AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+ {
+ switch (method)
+ {
+ case "Runtime.consoleAPICalled":
+ {
+ var type = args["type"]?.ToString();
+ if (type == "debug")
+ {
+ var a = args["args"];
+ if (a?[0] ? ["value"]?.ToString() == MonoConstants.RUNTIME_IS_READY &&
+ a?[1] ? ["value"]?.ToString() == "fe00e07a-5519-4dfe-b35a-f867dbaf2e28")
+ {
+ if (a.Count() > 2)
+ {
+ try
+ {
+ // The optional 3rd argument is the stringified assembly
+ // list so that we don't have to make more round trips
+ var context = GetContext(sessionId);
+ var loaded = a?[2] ? ["value"]?.ToString();
+ if (loaded != null)
+ context.LoadedFiles = JToken.Parse(loaded).ToObject<string[]>();
+ }
+ catch (InvalidCastException ice)
+ {
+ Log("verbose", ice.ToString());
+ }
+ }
+ await RuntimeReady(sessionId, token);
+ }
+
+ }
+ break;
+ }
+
+ case "Runtime.executionContextCreated":
+ {
+ SendEvent(sessionId, method, args, token);
+ var ctx = args?["context"];
+ var aux_data = ctx?["auxData"] as JObject;
+ var id = ctx["id"].Value<int>();
+ if (aux_data != null)
+ {
+ var is_default = aux_data["isDefault"]?.Value<bool>();
+ if (is_default == true)
+ {
+ await OnDefaultContext(sessionId, new ExecutionContext { Id = id, AuxData = aux_data }, token);
+ }
+ }
+ return true;
+ }
+
+ case "Debugger.paused":
+ {
+ //TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack
+ var top_func = args?["callFrames"] ? [0] ? ["functionName"]?.Value<string>();
+
+ if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp")
+ {
+ return await OnBreakpointHit(sessionId, args, token);
+ }
+ break;
+ }
+
+ case "Debugger.breakpointResolved":
+ {
+ break;
+ }
+
+ case "Debugger.scriptParsed":
+ {
+ var url = args?["url"]?.Value<string>() ?? "";
+
+ switch (url)
+ {
+ case var _ when url == "":
+ case var _ when url.StartsWith("wasm://", StringComparison.Ordinal):
+ {
+ Log("verbose", $"ignoring wasm: Debugger.scriptParsed {url}");
+ return true;
+ }
+ }
+ Log("verbose", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}");
+ break;
+ }
+
+ case "Target.attachedToTarget":
+ {
+ if (args["targetInfo"]["type"]?.ToString() == "page")
+ await DeleteWebDriver(new SessionId(args["sessionId"]?.ToString()), token);
+ break;
+ }
+
+ }
+
+ return false;
+ }
+
+ async Task<bool> IsRuntimeAlreadyReadyAlready(SessionId sessionId, CancellationToken token)
+ {
+ if (contexts.TryGetValue(sessionId, out var context) && context.IsRuntimeReady)
+ return true;
+
+ var res = await SendMonoCommand(sessionId, MonoCommands.IsRuntimeReady(), token);
+ return res.Value?["result"] ? ["value"]?.Value<bool>() ?? false;
+ }
+
+ static int bpIdGenerator;
+
+ protected override async Task<bool> AcceptCommand(MessageId id, string method, JObject args, CancellationToken token)
+ {
+ // Inspector doesn't use the Target domain or sessions
+ // so we try to init immediately
+ if (hideWebDriver && id == SessionId.Null)
+ await DeleteWebDriver(id, token);
+
+ if (!contexts.TryGetValue(id, out var context))
+ return false;
+
+ switch (method)
+ {
+ case "Target.attachToTarget":
+ {
+ var resp = await SendCommand(id, method, args, token);
+ await DeleteWebDriver(new SessionId(resp.Value["sessionId"]?.ToString()), token);
+ break;
+ }
+
+ case "Debugger.enable":
+ {
+ System.Console.WriteLine("recebi o Debugger.enable");
+ var resp = await SendCommand(id, method, args, token);
+
+ context.DebuggerId = resp.Value["debuggerId"]?.ToString();
+
+ if (await IsRuntimeAlreadyReadyAlready(id, token))
+ await RuntimeReady(id, token);
+
+ SendResponse(id, resp, token);
+ return true;
+ }
+
+ case "Debugger.getScriptSource":
+ {
+ var script = args?["scriptId"]?.Value<string>();
+ return await OnGetScriptSource(id, script, token);
+ }
+
+ case "Runtime.compileScript":
+ {
+ var exp = args?["expression"]?.Value<string>();
+ if (exp.StartsWith("//dotnet:", StringComparison.Ordinal))
+ {
+ OnCompileDotnetScript(id, token);
+ return true;
+ }
+ break;
+ }
+
+ case "Debugger.getPossibleBreakpoints":
+ {
+ var resp = await SendCommand(id, method, args, token);
+ if (resp.IsOk && resp.Value["locations"].HasValues)
+ {
+ SendResponse(id, resp, token);
+ return true;
+ }
+
+ var start = SourceLocation.Parse(args?["start"] as JObject);
+ //FIXME support variant where restrictToFunction=true and end is omitted
+ var end = SourceLocation.Parse(args?["end"] as JObject);
+ if (start != null && end != null && await GetPossibleBreakpoints(id, start, end, token))
+ return true;
+
+ SendResponse(id, resp, token);
+ return true;
+ }
+
+ case "Debugger.setBreakpoint":
+ {
+ break;
+ }
+
+ case "Debugger.setBreakpointByUrl":
+ {
+ var resp = await SendCommand(id, method, args, token);
+ if (!resp.IsOk)
+ {
+ SendResponse(id, resp, token);
+ return true;
+ }
+
+ var bpid = resp.Value["breakpointId"]?.ToString();
+ var locations = resp.Value["locations"]?.Values<object>();
+ var request = BreakpointRequest.Parse(bpid, args);
+
+ // is the store done loading?
+ var loaded = context.Source.Task.IsCompleted;
+ if (!loaded)
+ {
+ // Send and empty response immediately if not
+ // and register the breakpoint for resolution
+ context.BreakpointRequests[bpid] = request;
+ SendResponse(id, resp, token);
+ }
+
+ if (await IsRuntimeAlreadyReadyAlready(id, token))
+ {
+ var store = await RuntimeReady(id, token);
+
+ Log("verbose", $"BP req {args}");
+ await SetBreakpoint(id, store, request, !loaded, token);
+ }
+
+ if (loaded)
+ {
+ // we were already loaded so we should send a response
+ // with the locations included and register the request
+ context.BreakpointRequests[bpid] = request;
+ var result = Result.OkFromObject(request.AsSetBreakpointByUrlResponse(locations));
+ SendResponse(id, result, token);
+
+ }
+ return true;
+ }
+
+ case "Debugger.removeBreakpoint":
+ {
+ await RemoveBreakpoint(id, args, token);
+ break;
+ }
+
+ case "Debugger.resume":
+ {
+ await OnResume(id, token);
+ break;
+ }
+
+ case "Debugger.stepInto":
+ {
+ return await Step(id, StepKind.Into, token);
+ }
+
+ case "Debugger.stepOut":
+ {
+ return await Step(id, StepKind.Out, token);
+ }
+
+ case "Debugger.stepOver":
+ {
+ return await Step(id, StepKind.Over, token);
+ }
+
+ case "Debugger.evaluateOnCallFrame":
+ {
+ if (!DotnetObjectId.TryParse(args?["callFrameId"], out var objectId))
+ return false;
+
+ switch (objectId.Scheme)
+ {
+ case "scope":
+ return await OnEvaluateOnCallFrame(id,
+ int.Parse(objectId.Value),
+ args?["expression"]?.Value<string>(), token);
+ default:
+ return false;
+ }
+ }
+
+ case "Runtime.getProperties":
+ {
+ if (!DotnetObjectId.TryParse(args?["objectId"], out var objectId))
+ break;
+
+ var result = await RuntimeGetProperties(id, objectId, args, token);
+ SendResponse(id, result, token);
+ return true;
+ }
+
+ case "Runtime.releaseObject":
+ {
+ if (!(DotnetObjectId.TryParse(args["objectId"], out var objectId) && objectId.Scheme == "cfo_res"))
+ break;
+
+ await SendMonoCommand(id, MonoCommands.ReleaseObject(objectId), token);
+ SendResponse(id, Result.OkFromObject(new { }), token);
+ return true;
+ }
+
+ // Protocol extensions
+ case "DotnetDebugger.getMethodLocation":
+ {
+ Console.WriteLine("set-breakpoint-by-method: " + id + " " + args);
+
+ var store = await RuntimeReady(id, token);
+ string aname = args["assemblyName"]?.Value<string>();
+ string typeName = args["typeName"]?.Value<string>();
+ string methodName = args["methodName"]?.Value<string>();
+ 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
+ var 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;
+ }
+
+ var type = assembly.GetTypeByName(typeName);
+ if (type == null)
+ {
+ SendResponse(id, Result.Err($"Type '{typeName}' not found."), token);
+ return true;
+ }
+
+ var 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}/<method_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;
+ }
+
+ var 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);
+
+ return true;
+ }
+ case "Runtime.callFunctionOn":
+ {
+ if (!DotnetObjectId.TryParse(args["objectId"], out var objectId))
+ return false;
+
+ if (objectId.Scheme == "scope")
+ {
+ SendResponse(id,
+ Result.Exception(new ArgumentException(
+ $"Runtime.callFunctionOn not supported with scope ({objectId}).")),
+ token);
+ return true;
+ }
+
+ var res = await SendMonoCommand(id, MonoCommands.CallFunctionOn(args), token);
+ var res_value_type = res.Value?["result"] ? ["value"]?.Type;
+
+ if (res.IsOk && res_value_type == JTokenType.Object || res_value_type == JTokenType.Object)
+ res = Result.OkFromObject(new { result = res.Value["result"]["value"] });
+
+ SendResponse(id, res, token);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ async Task<Result> RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
+ {
+ if (objectId.Scheme == "scope")
+ return await GetScopeProperties(id, int.Parse(objectId.Value), token);
+
+ var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token);
+ if (res.IsErr)
+ return res;
+
+ if (objectId.Scheme == "cfo_res")
+ {
+ // Runtime.callFunctionOn result object
+ var value_json_str = res.Value["result"] ? ["value"] ? ["__value_as_json_string__"]?.Value<string>();
+ if (value_json_str != null)
+ {
+ res = Result.OkFromObject(new
+ {
+ result = JArray.Parse(value_json_str)
+ });
+ }
+ else
+ {
+ res = Result.OkFromObject(new { result = new { } });
+ }
+ }
+ else
+ {
+ res = Result.Ok(JObject.FromObject(new { result = res.Value["result"]["value"] }));
+ }
+
+ return res;
+ }
+
+ //static int frame_id=0;
+ async Task<bool> OnBreakpointHit(SessionId sessionId, JObject args, CancellationToken token)
+ {
+ //FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime
+ var res = await SendMonoCommand(sessionId, MonoCommands.GetCallStack(), token);
+ var orig_callframes = args?["callFrames"]?.Values<JObject>();
+ var context = GetContext(sessionId);
+
+ if (res.IsErr)
+ {
+ //Give up and send the original call stack
+ return false;
+ }
+
+ //step one, figure out where did we hit
+ var res_value = res.Value?["result"] ? ["value"];
+ if (res_value == null || res_value is JValue)
+ {
+ //Give up and send the original call stack
+ return false;
+ }
+
+ Log("verbose", $"call stack (err is {res.Error} value is:\n{res.Value}");
+ var bp_id = res_value?["breakpoint_id"]?.Value<int>();
+ Log("verbose", $"We just hit bp {bp_id}");
+ if (!bp_id.HasValue)
+ {
+ //Give up and send the original call stack
+ return false;
+ }
+
+ var bp = context.BreakpointRequests.Values.SelectMany(v => v.Locations).FirstOrDefault(b => b.RemoteId == bp_id.Value);
+
+ var callFrames = new List<object>();
+ foreach (var frame in orig_callframes)
+ {
+ var function_name = frame["functionName"]?.Value<string>();
+ var url = frame["url"]?.Value<string>();
+ if ("mono_wasm_fire_bp" == function_name ||"_mono_wasm_fire_bp" == function_name)
+ {
+ var frames = new List<Frame>();
+ int frame_id = 0;
+ var the_mono_frames = res.Value?["result"] ? ["value"] ? ["frames"]?.Values<JObject>();
+
+ foreach (var mono_frame in the_mono_frames)
+ {
+ ++frame_id;
+ var il_pos = mono_frame["il_pos"].Value<int>();
+ var method_token = mono_frame["method_token"].Value<uint>();
+ var assembly_name = mono_frame["assembly_name"].Value<string>();
+
+ // This can be different than `method.Name`, like in case of generic methods
+ var method_name = mono_frame["method_name"]?.Value<string>();
+
+ var store = await LoadStore(sessionId, token);
+ var asm = store.GetAssemblyByName(assembly_name);
+ if (asm == null)
+ {
+ Log("info", $"Unable to find assembly: {assembly_name}");
+ continue;
+ }
+
+ var method = asm.GetMethodByToken(method_token);
+
+ if (method == null)
+ {
+ Log("info", $"Unable to find il offset: {il_pos} in method token: {method_token} assembly name: {assembly_name}");
+ continue;
+ }
+
+ var location = method?.GetLocationByIl(il_pos);
+
+ // When hitting a breakpoint on the "IncrementCount" method in the standard
+ // Blazor project template, one of the stack frames is inside mscorlib.dll
+ // and we get location==null for it. It will trigger a NullReferenceException
+ // if we don't skip over that stack frame.
+ if (location == null)
+ {
+ continue;
+ }
+
+ Log("info", $"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
+ Log("info", $"\tmethod {method_name} location: {location}");
+ frames.Add(new Frame(method, location, frame_id - 1));
+
+ callFrames.Add(new
+ {
+ functionName = method_name,
+ callFrameId = $"dotnet:scope:{frame_id-1}",
+ functionLocation = method.StartLocation.AsLocation(),
+
+ location = location.AsLocation(),
+
+ url = store.ToUrl(location),
+
+ scopeChain = new []
+ {
+ new
+ {
+ type = "local",
+ @object = new
+ {
+ @type = "object",
+ className = "Object",
+ description = "Object",
+ objectId = $"dotnet:scope:{frame_id-1}",
+ },
+ name = method_name,
+ startLocation = method.StartLocation.AsLocation(),
+ endLocation = method.EndLocation.AsLocation(),
+ }
+ }
+ });
+
+ context.CallStack = frames;
+
+ }
+ }
+ else if (!(function_name.StartsWith("wasm-function", StringComparison.Ordinal) ||
+ url.StartsWith("wasm://wasm/", StringComparison.Ordinal)))
+ {
+ callFrames.Add(frame);
+ }
+ }
+
+ var bp_list = new string[bp == null ? 0 : 1];
+ if (bp != null)
+ bp_list[0] = bp.StackId;
+
+ var o = JObject.FromObject(new
+ {
+ callFrames,
+ reason = "other", //other means breakpoint
+ hitBreakpoints = bp_list,
+ });
+
+ SendEvent(sessionId, "Debugger.paused", o, token);
+ return true;
+ }
+
+ async Task OnDefaultContext(SessionId sessionId, ExecutionContext context, CancellationToken token)
+ {
+ Log("verbose", "Default context created, clearing state and sending events");
+ if (UpdateContext(sessionId, context, out var previousContext))
+ {
+ foreach (var kvp in previousContext.BreakpointRequests)
+ {
+ context.BreakpointRequests[kvp.Key] = kvp.Value.Clone();
+ }
+ }
+
+ if (await IsRuntimeAlreadyReadyAlready(sessionId, token))
+ await RuntimeReady(sessionId, token);
+ }
+
+ async Task OnResume(MessageId msg_id, CancellationToken token)
+ {
+ var ctx = GetContext(msg_id);
+ if (ctx.CallStack != null)
+ {
+ // Stopped on managed code
+ await SendMonoCommand(msg_id, MonoCommands.Resume(), token);
+ }
+
+ //discard managed frames
+ GetContext(msg_id).ClearState();
+ }
+
+ async Task<bool> Step(MessageId msg_id, StepKind kind, CancellationToken token)
+ {
+ var context = GetContext(msg_id);
+ if (context.CallStack == null)
+ return false;
+
+ if (context.CallStack.Count <= 1 && kind == StepKind.Out)
+ return false;
+
+ var res = await SendMonoCommand(msg_id, MonoCommands.StartSingleStepping(kind), token);
+
+ var ret_code = res.Value?["result"] ? ["value"]?.Value<int>();
+
+ if (ret_code.HasValue && ret_code.Value == 0)
+ {
+ context.ClearState();
+ await SendCommand(msg_id, "Debugger.stepOut", new JObject(), token);
+ return false;
+ }
+
+ SendResponse(msg_id, Result.Ok(new JObject()), token);
+
+ context.ClearState();
+
+ await SendCommand(msg_id, "Debugger.resume", new JObject(), token);
+ return true;
+ }
+
+ internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
+ {
+ if (ctx.LocalsCache.TryGetValue(expression, out obj))
+ {
+ if (only_search_on_this && obj["fromThis"] == null)
+ return false;
+ return true;
+ }
+ return false;
+ }
+
+ internal async Task<JToken> TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
+ {
+ JToken thisValue = null;
+ var context = GetContext(msg_id);
+ if (context.CallStack == null)
+ return null;
+
+ if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
+ return obj;
+
+ var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id);
+ var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
+ //get_this
+ var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token);
+
+ var scope_values = res.Value?["result"] ? ["value"]?.Values<JObject>()?.ToArray();
+ thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value<string>() == "this");
+
+ if (!only_search_on_this)
+ {
+ if (thisValue != null && expression == "this")
+ return thisValue;
+
+ var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value<string>() == expression);
+ if (value != null)
+ return value;
+ }
+
+ //search in scope
+ if (thisValue != null)
+ {
+ if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId))
+ return null;
+
+ res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token);
+ scope_values = res.Value?["result"] ? ["value"]?.Values<JObject>().ToArray();
+ var foundValue = scope_values.FirstOrDefault(v => v["name"].Value<string>() == expression);
+ if (foundValue != null)
+ {
+ foundValue["fromThis"] = true;
+ context.LocalsCache[foundValue["name"].Value<string>()] = foundValue;
+ return foundValue;
+ }
+ }
+ return null;
+ }
+
+ async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
+ {
+ try
+ {
+ var context = GetContext(msg_id);
+ if (context.CallStack == null)
+ return false;
+
+ var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token);
+
+ if (varValue != null)
+ {
+ SendResponse(msg_id, Result.OkFromObject(new
+ {
+ result = varValue["value"]
+ }), token);
+ return true;
+ }
+
+ string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token);
+ SendResponse(msg_id, Result.OkFromObject(new
+ {
+ result = new
+ {
+ value = retValue
+ }
+ }), token);
+ return true;
+ }
+ catch (Exception e)
+ {
+ logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
+ }
+ return false;
+ }
+
+ async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
+ {
+ try
+ {
+ var ctx = GetContext(msg_id);
+ var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scope_id);
+ if (scope == null)
+ return Result.Err(JObject.FromObject(new { message = $"Could not find scope with id #{scope_id}" }));
+
+ var var_ids = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
+ var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, var_ids), token);
+
+ //if we fail we just buble that to the IDE (and let it panic over it)
+ if (res.IsErr)
+ return res;
+
+ var values = res.Value?["result"] ? ["value"]?.Values<JObject>().ToArray();
+
+ if (values == null || values.Length == 0)
+ return Result.OkFromObject(new { result = Array.Empty<object>() });
+
+ foreach (var value in values)
+ ctx.LocalsCache[value["name"]?.Value<string>()] = value;
+
+ return Result.OkFromObject(new { result = values });
+ }
+ catch (Exception exception)
+ {
+ Log("verbose", $"Error resolving scope properties {exception.Message}");
+ return Result.Exception(exception);
+ }
+ }
+
+ async Task<Breakpoint> SetMonoBreakpoint(SessionId sessionId, string reqId, SourceLocation location, CancellationToken token)
+ {
+ var bp = new Breakpoint(reqId, location, BreakpointState.Pending);
+ var asm_name = bp.Location.CliLocation.Method.Assembly.Name;
+ var method_token = bp.Location.CliLocation.Method.Token;
+ var il_offset = bp.Location.CliLocation.Offset;
+
+ var res = await SendMonoCommand(sessionId, MonoCommands.SetBreakpoint(asm_name, method_token, il_offset), token);
+ var ret_code = res.Value?["result"] ? ["value"]?.Value<int>();
+
+ if (ret_code.HasValue)
+ {
+ bp.RemoteId = ret_code.Value;
+ bp.State = BreakpointState.Active;
+ //Log ("verbose", $"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}");
+ }
+
+ return bp;
+ }
+
+ async Task<DebugStore> LoadStore(SessionId sessionId, CancellationToken token)
+ {
+ var context = GetContext(sessionId);
+
+ if (Interlocked.CompareExchange(ref context.store, new DebugStore(logger), null) != null)
+ return await context.Source.Task;
+
+ try
+ {
+ var loaded_files = context.LoadedFiles;
+
+ if (loaded_files == null)
+ {
+ var loaded = await SendMonoCommand(sessionId, MonoCommands.GetLoadedFiles(), token);
+ loaded_files = loaded.Value?["result"] ? ["value"]?.ToObject<string[]>();
+ }
+
+ await
+ foreach (var source in context.store.Load(sessionId, loaded_files, token).WithCancellation(token))
+ {
+ var scriptSource = JObject.FromObject(source.ToScriptSource(context.Id, context.AuxData));
+ Log("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}");
+
+ SendEvent(sessionId, "Debugger.scriptParsed", scriptSource, token);
+
+ foreach (var req in context.BreakpointRequests.Values)
+ {
+ if (req.TryResolve(source))
+ {
+ await SetBreakpoint(sessionId, context.store, req, true, token);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ context.Source.SetException(e);
+ }
+
+ if (!context.Source.Task.IsCompleted)
+ context.Source.SetResult(context.store);
+ return context.store;
+ }
+
+ async Task<DebugStore> RuntimeReady(SessionId sessionId, CancellationToken token)
+ {
+ var context = GetContext(sessionId);
+ if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource<DebugStore>(), null) != null)
+ return await context.ready.Task;
+
+ var clear_result = await SendMonoCommand(sessionId, MonoCommands.ClearAllBreakpoints(), token);
+ if (clear_result.IsErr)
+ {
+ Log("verbose", $"Failed to clear breakpoints due to {clear_result}");
+ }
+
+ var store = await LoadStore(sessionId, token);
+
+ context.ready.SetResult(store);
+ SendEvent(sessionId, "Mono.runtimeReady", new JObject(), token);
+ return store;
+ }
+
+ async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token)
+ {
+ var bpid = args?["breakpointId"]?.Value<string>();
+
+ var context = GetContext(msg_id);
+ if (!context.BreakpointRequests.TryGetValue(bpid, out var breakpointRequest))
+ return;
+
+ foreach (var bp in breakpointRequest.Locations)
+ {
+ var res = await SendMonoCommand(msg_id, MonoCommands.RemoveBreakpoint(bp.RemoteId), token);
+ var ret_code = res.Value?["result"] ? ["value"]?.Value<int>();
+
+ if (ret_code.HasValue)
+ {
+ bp.RemoteId = -1;
+ bp.State = BreakpointState.Disabled;
+ }
+ }
+ breakpointRequest.Locations.Clear();
+ }
+
+ async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token)
+ {
+ var context = GetContext(sessionId);
+ if (req.Locations.Any())
+ {
+ Log("debug", $"locations already loaded for {req.Id}");
+ return;
+ }
+
+ var comparer = new SourceLocation.LocationComparer();
+ // if column is specified the frontend wants the exact matches
+ // and will clear the bp if it isn't close enoug
+ var locations = store.FindBreakpointLocations(req)
+ .Distinct(comparer)
+ .Where(l => l.Line == req.Line && (req.Column == 0 || l.Column == req.Column))
+ .OrderBy(l => l.Column)
+ .GroupBy(l => l.Id);
+
+ logger.LogDebug("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext(sessionId).IsRuntimeReady);
+
+ var breakpoints = new List<Breakpoint>();
+
+ foreach (var sourceId in locations)
+ {
+ var loc = sourceId.First();
+ var bp = await SetMonoBreakpoint(sessionId, req.Id, loc, token);
+
+ // If we didn't successfully enable the breakpoint
+ // don't add it to the list of locations for this id
+ if (bp.State != BreakpointState.Active)
+ continue;
+
+ breakpoints.Add(bp);
+
+ var resolvedLocation = new
+ {
+ breakpointId = req.Id,
+ location = loc.AsLocation()
+ };
+
+ if (sendResolvedEvent)
+ SendEvent(sessionId, "Debugger.breakpointResolved", JObject.FromObject(resolvedLocation), token);
+ }
+
+ req.Locations.AddRange(breakpoints);
+ return;
+ }
+
+ async Task<bool> GetPossibleBreakpoints(MessageId msg, SourceLocation start, SourceLocation end, CancellationToken token)
+ {
+ var bps = (await RuntimeReady(msg, token)).FindPossibleBreakpoints(start, end);
+
+ if (bps == null)
+ return false;
+
+ var response = new { locations = bps.Select(b => b.AsLocation()) };
+
+ SendResponse(msg, Result.OkFromObject(response), token);
+ return true;
+ }
+
+ void OnCompileDotnetScript(MessageId msg_id, CancellationToken token)
+ {
+ SendResponse(msg_id, Result.OkFromObject(new { }), token);
+ }
+
+ async Task<bool> OnGetScriptSource(MessageId msg_id, string script_id, CancellationToken token)
+ {
+ if (!SourceId.TryParse(script_id, out var id))
+ return false;
+
+ var 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(var 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();
+ }
+ SendResponse(msg_id, Result.OkFromObject(new { scriptSource = source }), token);
+ }
+ catch (Exception e)
+ {
+ var o = new
+ {
+ scriptSource = $"// Unable to read document ({e.Message})\n" +
+ $"Local path: {src_file?.SourceUri}\n" +
+ $"SourceLink path: {src_file?.SourceLinkUri}\n"
+ };
+
+ SendResponse(msg_id, Result.OkFromObject(o), token);
+ }
+ return true;
+ }
+
+ async Task DeleteWebDriver(SessionId sessionId, CancellationToken token)
+ {
+ // see https://github.com/mono/mono/issues/19549 for background
+ if (hideWebDriver && sessions.Add(sessionId))
+ {
+ var res = await SendCommand(sessionId,
+ "Page.addScriptToEvaluateOnNewDocument",
+ JObject.FromObject(new { source = "delete navigator.constructor.prototype.webdriver" }),
+ token);
+
+ if (sessionId != SessionId.Null && !res.IsOk)
+ sessions.Remove(sessionId);
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+ public class ArrayTests : DebuggerTestBase
+ {
+
+ [Theory]
+ [InlineData(19, 8, "PrimitiveTypeLocals", false, 0, false)]
+ [InlineData(19, 8, "PrimitiveTypeLocals", false, 0, true)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+ public async Task InspectPrimitiveTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+ line, col,
+ entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:PrimitiveTypeLocals",
+ method_name : method_name,
+ etype_name: "int",
+ local_var_name_prefix: "int",
+ array : new [] { TNumber(4), TNumber(70), TNumber(1) },
+ array_elements : null,
+ test_prev_frame : test_prev_frame,
+ frame_idx : frame_idx,
+ use_cfo : use_cfo);
+
+ [Theory]
+ [InlineData(36, 8, "ValueTypeLocals", false, 0, false)]
+ [InlineData(36, 8, "ValueTypeLocals", false, 0, true)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+ public async Task InspectValueTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+ line, col,
+ entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:ValueTypeLocals",
+ method_name : method_name,
+ etype_name: "DebuggerTests.Point",
+ local_var_name_prefix: "point",
+ array : new []
+ {
+ TValueType("DebuggerTests.Point"),
+ TValueType("DebuggerTests.Point"),
+ },
+ array_elements : new []
+ {
+ TPoint(5, -2, "point_arr#Id#0", "Green"),
+ TPoint(123, 0, "point_arr#Id#1", "Blue")
+ },
+ test_prev_frame : test_prev_frame,
+ frame_idx : frame_idx,
+ use_cfo : use_cfo);
+
+ [Theory]
+ [InlineData(54, 8, "ObjectTypeLocals", false, 0, false)]
+ [InlineData(54, 8, "ObjectTypeLocals", false, 0, true)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+ public async Task InspectObjectArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+ line, col,
+ entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:ObjectTypeLocals",
+ method_name : method_name,
+ etype_name: "DebuggerTests.SimpleClass",
+ local_var_name_prefix: "class",
+ array : new []
+ {
+ TObject("DebuggerTests.SimpleClass"),
+ TObject("DebuggerTests.SimpleClass", is_null : true),
+ TObject("DebuggerTests.SimpleClass")
+ },
+ array_elements : new []
+ {
+ TSimpleClass(5, -2, "class_arr#Id#0", "Green"),
+ null, // Element is null
+ TSimpleClass(123, 0, "class_arr#Id#2", "Blue")
+ },
+ test_prev_frame : test_prev_frame,
+ frame_idx : frame_idx,
+ use_cfo : use_cfo);
+
+ [Theory]
+ [InlineData(72, 8, "GenericTypeLocals", false, 0, false)]
+ [InlineData(72, 8, "GenericTypeLocals", false, 0, true)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+ public async Task InspectGenericTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+ line, col,
+ entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:GenericTypeLocals",
+ method_name : method_name,
+ etype_name: "DebuggerTests.GenericClass<int>",
+ local_var_name_prefix: "gclass",
+ array : new []
+ {
+ TObject("DebuggerTests.GenericClass<int>", is_null : true),
+ TObject("DebuggerTests.GenericClass<int>"),
+ TObject("DebuggerTests.GenericClass<int>")
+ },
+ array_elements : new []
+ {
+ null, // Element is null
+ new
+ {
+ Id = TString("gclass_arr#1#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = TNumber(5)
+ },
+ new
+ {
+ Id = TString("gclass_arr#2#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Blue"),
+ Value = TNumber(-12)
+ }
+ },
+ test_prev_frame : test_prev_frame,
+ frame_idx : frame_idx,
+ use_cfo : use_cfo);
+
+ [Theory]
+ [InlineData(89, 8, "GenericValueTypeLocals", false, 0, false)]
+ [InlineData(89, 8, "GenericValueTypeLocals", false, 0, true)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+ public async Task InspectGenericValueTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+ line, col,
+ entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:GenericValueTypeLocals",
+ method_name : method_name,
+ etype_name: "DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>",
+ local_var_name_prefix: "gvclass",
+ array : new []
+ {
+ TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
+ TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>")
+ },
+ array_elements : new []
+ {
+ new
+ {
+ Id = TString("gvclass_arr#1#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = TPoint(100, 200, "gvclass_arr#1#Value#Id", "Red")
+ },
+ new
+ {
+ Id = TString("gvclass_arr#2#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Blue"),
+ Value = TPoint(10, 20, "gvclass_arr#2#Value#Id", "Green")
+ }
+ },
+ test_prev_frame : test_prev_frame,
+ frame_idx : frame_idx,
+ use_cfo : use_cfo);
+
+ [Theory]
+ [InlineData(213, 8, "GenericValueTypeLocals2", false, 0, false)]
+ [InlineData(213, 8, "GenericValueTypeLocals2", false, 0, true)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+ [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+ public async Task InspectGenericValueTypeArrayLocals2(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+ line, col,
+ entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:GenericValueTypeLocals2",
+ method_name : method_name,
+ etype_name: "DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>",
+ local_var_name_prefix: "gvclass",
+ array : new []
+ {
+ TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>"),
+ TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>")
+ },
+ array_elements : new []
+ {
+ new
+ {
+ Id = TString("gvclass_arr#0#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = new []
+ {
+ TPoint(100, 200, "gvclass_arr#0#0#Value#Id", "Red"),
+ TPoint(100, 200, "gvclass_arr#0#1#Value#Id", "Green")
+ }
+ },
+ new
+ {
+ Id = TString("gvclass_arr#1#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Blue"),
+ Value = new []
+ {
+ TPoint(100, 200, "gvclass_arr#1#0#Value#Id", "Green"),
+ TPoint(100, 200, "gvclass_arr#1#1#Value#Id", "Blue")
+ }
+ }
+ },
+ test_prev_frame : test_prev_frame,
+ frame_idx : frame_idx,
+ use_cfo : use_cfo);
+
+ async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, string method_name, string etype_name,
+ string local_var_name_prefix, object[] array, object[] array_elements,
+ bool test_prev_frame = false, int frame_idx = 0, bool use_cfo = false)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+ $"'{entry_method_name}', { (test_prev_frame ? "true" : "false") }" +
+ "); }, 1);";
+
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, method_name);
+
+ var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ Assert.Equal(4, locals.Count());
+ CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]");
+ CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]");
+ CheckObject(locals, $"{local_var_name_prefix}_arr_null", $"{etype_name}[]", is_null : true);
+ CheckBool(locals, "call_other", test_prev_frame);
+
+ var local_arr_name = $"{local_var_name_prefix}_arr";
+
+ JToken prefix_arr;
+ if (use_cfo)
+ { // Use `Runtime.callFunctionOn` to get the properties
+ var frame = pause_location["callFrames"][frame_idx];
+ var name = local_arr_name;
+ var fl = await GetProperties(frame["callFrameId"].Value<string>());
+ var l_obj = GetAndAssertObjectWithName(locals, name);
+ var l_objectId = l_obj["value"]["objectId"]?.Value<string>();
+
+ Assert.True(!String.IsNullOrEmpty(l_objectId), $"No objectId found for {name}");
+
+ prefix_arr = await GetObjectWithCFO(l_objectId);
+ }
+ else
+ {
+ prefix_arr = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], local_arr_name);
+ }
+
+ await CheckProps(prefix_arr, array, local_arr_name);
+
+ if (array_elements?.Length > 0)
+ {
+ for (int i = 0; i < array_elements.Length; i++)
+ {
+ var i_str = i.ToString();
+ var label = $"{local_var_name_prefix}_arr[{i}]";
+ if (array_elements[i] == null)
+ {
+ var act_i = prefix_arr.FirstOrDefault(jt => jt["name"]?.Value<string>() == i_str);
+ Assert.True(act_i != null, $"[{label}] Couldn't find array element [{i_str}]");
+
+ await CheckValue(act_i["value"], TObject(etype_name, is_null : true), label);
+ }
+ else
+ {
+ await CompareObjectPropertiesFor(prefix_arr, i_str, array_elements[i], label : label);
+ }
+ }
+ }
+
+ var props = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], $"{local_var_name_prefix}_arr_empty");
+ await CheckProps(props, new object[0], "${local_var_name_prefix}_arr_empty");
+ });
+
+ async Task<JToken> GetObjectWithCFO(string objectId, JObject fn_args = null)
+ {
+ var fn_decl = "function () { return this; }";
+ var cfo_args = JObject.FromObject(new
+ {
+ functionDeclaration = fn_decl,
+ objectId = objectId
+ });
+
+ if (fn_args != null)
+ cfo_args["arguments"] = fn_args;
+
+ // callFunctionOn
+ var result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+
+ return await GetProperties(result.Value["result"]["objectId"]?.Value<string>(), fn_args);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectObjectArrayMembers(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ int line = 227;
+ int col = 12;
+ string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers";
+ string method_name = "PlaceholderMethod";
+ int frame_idx = 1;
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+ $"'{entry_method_name}'" +
+ "); }, 1);";
+
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, method_name);
+ var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ Assert.Single(locals);
+ CheckObject(locals, "c", "DebuggerTests.Container");
+
+ var c_props = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], "c");
+ await CheckProps(c_props, new
+ {
+ id = TString("c#id"),
+ ClassArrayProperty = TArray("DebuggerTests.SimpleClass[]", 3),
+ ClassArrayField = TArray("DebuggerTests.SimpleClass[]", 3),
+ PointsProperty = TArray("DebuggerTests.Point[]", 2),
+ PointsField = TArray("DebuggerTests.Point[]", 2)
+ },
+ "c"
+ );
+
+ await CompareObjectPropertiesFor(c_props, "ClassArrayProperty",
+ new []
+ {
+ TSimpleClass(5, -2, "ClassArrayProperty#Id#0", "Green"),
+ TSimpleClass(30, 1293, "ClassArrayProperty#Id#1", "Green"),
+ TObject("DebuggerTests.SimpleClass", is_null : true)
+ },
+ label: "InspectLocalsWithStructsStaticAsync");
+
+ await CompareObjectPropertiesFor(c_props, "ClassArrayField",
+ new []
+ {
+ TObject("DebuggerTests.SimpleClass", is_null : true),
+ TSimpleClass(5, -2, "ClassArrayField#Id#1", "Blue"),
+ TSimpleClass(30, 1293, "ClassArrayField#Id#2", "Green")
+ },
+ label: "c#ClassArrayField");
+
+ await CompareObjectPropertiesFor(c_props, "PointsProperty",
+ new []
+ {
+ TPoint(5, -2, "PointsProperty#Id#0", "Green"),
+ TPoint(123, 0, "PointsProperty#Id#1", "Blue"),
+ },
+ label: "c#PointsProperty");
+
+ await CompareObjectPropertiesFor(c_props, "PointsField",
+ new []
+ {
+ TPoint(5, -2, "PointsField#Id#0", "Green"),
+ TPoint(123, 0, "PointsField#Id#1", "Blue"),
+ },
+ label: "c#PointsField");
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectValueTypeArrayLocalsStaticAsync(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ int line = 157;
+ int col = 12;
+ string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:ValueTypeLocalsAsync";
+ string method_name = "MoveNext"; // BUG: this should be ValueTypeLocalsAsync
+ int frame_idx = 0;
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+ $"'{entry_method_name}', false" // *false* here keeps us only in the static method
+ +
+ "); }, 1);";
+
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, method_name);
+ var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ await CheckProps(frame_locals, new
+ {
+ call_other = TBool(false),
+ gvclass_arr = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", 2),
+ gvclass_arr_empty = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]"),
+ gvclass_arr_null = TObject("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", is_null : true),
+ gvclass = TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
+ // BUG: this shouldn't be null!
+ points = TObject("DebuggerTests.Point[]", is_null : true)
+ }, "ValueTypeLocalsAsync#locals");
+
+ var local_var_name_prefix = "gvclass";
+ await CompareObjectPropertiesFor(frame_locals, local_var_name_prefix, new
+ {
+ Id = TString(null),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = TPoint(0, 0, null, "Red")
+ });
+
+ await CompareObjectPropertiesFor(frame_locals, $"{local_var_name_prefix}_arr",
+ new []
+ {
+ new
+ {
+ Id = TString("gvclass_arr#1#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = TPoint(100, 200, "gvclass_arr#1#Value#Id", "Red")
+ },
+ new
+ {
+ Id = TString("gvclass_arr#2#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Blue"),
+ Value = TPoint(10, 20, "gvclass_arr#2#Value#Id", "Green")
+ }
+ }
+ );
+ await CompareObjectPropertiesFor(frame_locals, $"{local_var_name_prefix}_arr_empty",
+ new object[0]);
+ });
+ }
+
+ // TODO: Check previous frame too
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectValueTypeArrayLocalsInstanceAsync(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ int line = 170;
+ int col = 12;
+ string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:ValueTypeLocalsAsync";
+ int frame_idx = 0;
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+ $"'{entry_method_name}', true" +
+ "); }, 1);";
+
+ // BUG: Should be InspectValueTypeArrayLocalsInstanceAsync
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, "MoveNext");
+
+ var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ await CheckProps(frame_locals, new
+ {
+ t1 = TObject("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
+ @this = TObject("DebuggerTests.ArrayTestsClass"),
+ point_arr = TArray("DebuggerTests.Point[]", 2),
+ point = TValueType("DebuggerTests.Point")
+ }, "InspectValueTypeArrayLocalsInstanceAsync#locals");
+
+ await CompareObjectPropertiesFor(frame_locals, "t1",
+ new
+ {
+ Id = TString("gvclass_arr#1#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = TPoint(100, 200, "gvclass_arr#1#Value#Id", "Red")
+ });
+
+ await CompareObjectPropertiesFor(frame_locals, "point_arr",
+ new []
+ {
+ TPoint(5, -2, "point_arr#Id#0", "Red"),
+ TPoint(123, 0, "point_arr#Id#1", "Blue"),
+ }
+ );
+
+ await CompareObjectPropertiesFor(frame_locals, "point",
+ TPoint(45, 51, "point#Id", "Green"));
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectValueTypeArrayLocalsInAsyncStaticStructMethod(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ int line = 244;
+ int col = 12;
+ string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod";
+ int frame_idx = 0;
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+ //await SetBreakpoint (debugger_test_loc, 143, 3);
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+ $"'{entry_method_name}', false" +
+ "); }, 1);";
+
+ // BUG: Should be InspectValueTypeArrayLocalsInstanceAsync
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, "MoveNext");
+
+ var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ await CheckProps(frame_locals, new
+ {
+ call_other = TBool(false),
+ local_i = TNumber(5),
+ sc = TSimpleClass(10, 45, "sc#Id", "Blue")
+ }, "InspectValueTypeArrayLocalsInAsyncStaticStructMethod#locals");
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectValueTypeArrayLocalsInAsyncInstanceStructMethod(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ int line = 251;
+ int col = 12;
+ string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod";
+ int frame_idx = 0;
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+
+ var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+ $"'{entry_method_name}', true" +
+ "); }, 1);";
+
+ // BUG: Should be InspectValueTypeArrayLocalsInstanceAsync
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, "MoveNext");
+
+ var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ await CheckProps(frame_locals, new
+ {
+ sc_arg = TObject("DebuggerTests.SimpleClass"),
+ @this = TValueType("DebuggerTests.Point"),
+ local_gs = TValueType("DebuggerTests.SimpleGenericStruct<int>")
+ },
+ "locals#0");
+
+ await CompareObjectPropertiesFor(frame_locals, "local_gs",
+ new
+ {
+ Id = TString("local_gs#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Green"),
+ Value = TNumber(4)
+ },
+ label: "local_gs#0");
+
+ await CompareObjectPropertiesFor(frame_locals, "sc_arg",
+ TSimpleClass(10, 45, "sc_arg#Id", "Blue"),
+ label: "sc_arg#0");
+
+ await CompareObjectPropertiesFor(frame_locals, "this",
+ TPoint(90, -4, "point#Id", "Green"),
+ label: "this#0");
+ });
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+ public class CallFunctionOnTests : DebuggerTestBase
+ {
+
+ // This tests `callFunctionOn` with a function that the vscode-js-debug extension uses
+ // Using this here as a non-trivial test case
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, 10, false)]
+ [InlineData("big_array_js_test (0);", "/other.js", 8, 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)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 0);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 0, true)]
+ public async Task CheckVSCodeTestFunction1(string eval_fn, string bp_loc, int line, int col, int len, bool roundtrip)
+ {
+ string vscode_fn0 = "function(){const e={__proto__:this.__proto__},t=Object.getOwnPropertyNames(this);for(let r=0;r<t.length;++r){const n=t[r],i=n>>>0;if(String(i>>>0)===n&&i>>>0!=4294967295)continue;const a=Object.getOwnPropertyDescriptor(this,n);a&&Object.defineProperty(e,n,a)}return e}";
+
+ await RunCallFunctionOn(eval_fn, vscode_fn0, "big", bp_loc, line, col, res_array_len : len, roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+
+ var is_js = bp_loc.EndsWith(".js", StringComparison.Ordinal);
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+ if (is_js)
+ await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+ else
+ AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+ // Check for a __proto__ object
+ // isOwn = true, accessorPropertiesOnly = false
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ await CheckProps(obj_own.Value["result"], new
+ {
+ length = TNumber(len),
+ // __proto__ = TArray (type, 0) // Is this one really required?
+ }, $"obj_own", num_fields : is_js ? 2 : 1);
+
+ });
+ }
+
+ void CheckJFunction(JToken actual, string className, string label)
+ {
+ AssertEqual("function", actual["type"]?.Value<string>(), $"{label}-type");
+ AssertEqual(className, actual["className"]?.Value<string>(), $"{label}-className");
+ }
+
+ // This tests `callFunctionOn` with a function that the vscode-js-debug extension uses
+ // Using this here as a non-trivial test case
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, 10)]
+ [InlineData("big_array_js_test (0);", "/other.js", 8, 1, 0)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 10)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 0);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 0)]
+ public async Task CheckVSCodeTestFunction2(string eval_fn, string bp_loc, int line, int col, int len)
+ {
+ var fetch_start_idx = 2;
+ var num_elems_fetch = 3;
+ string vscode_fn1 = "function(e,t){const r={},n=-1===e?0:e,i=-1===t?this.length:e+t;for(let e=n;e<i&&e<this.length;++e){const t=Object.getOwnPropertyDescriptor(this,e);t&&Object.defineProperty(r,e,t)}return r}";
+
+ await RunCallFunctionOn(eval_fn, vscode_fn1, "big", bp_loc, line, col,
+ fn_args : JArray.FromObject(new []
+ {
+ new { @value = fetch_start_idx },
+ new { @value = num_elems_fetch }
+ }),
+ test_fn : async(result) =>
+ {
+
+ var is_js = bp_loc.EndsWith(".js", StringComparison.Ordinal);
+
+ // isOwn = false, accessorPropertiesOnly = true
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+ if (is_js)
+ await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+ else
+ AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+ // Ignoring the __proto__ property
+
+ // isOwn = true, accessorPropertiesOnly = false
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ var obj_own_val = obj_own.Value["result"];
+ var num_elems_recd = len == 0 ? 0 : num_elems_fetch;
+ AssertEqual(is_js ? num_elems_recd + 1 : num_elems_recd, obj_own_val.Count(), $"obj_own-count");
+
+ if (is_js)
+ CheckObject(obj_own_val, "__proto__", "Object");
+
+ for (int i = fetch_start_idx; i < fetch_start_idx + num_elems_recd; i++)
+ CheckNumber(obj_own_val, i.ToString(), 1000 + i);
+ });
+ }
+
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+ public async Task RunOnArrayReturnEmptyArray(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+ {
+ var ret_len = 0;
+
+ await RunCallFunctionOn(eval_fn,
+ "function () { return []; }",
+ "big", bp_loc, line, col,
+ res_array_len : ret_len,
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+ var is_js = bp_loc.EndsWith(".js", StringComparison.Ordinal);
+
+ // getProperties (isOwn = false, accessorPropertiesOnly = true)
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+ if (is_js)
+ await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+ else
+ AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+ // getProperties (isOwn = true, accessorPropertiesOnly = false)
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ await CheckProps(obj_own.Value["result"], new
+ {
+ length = TNumber(ret_len),
+ // __proto__ returned by js
+ }, $"obj_own", num_fields : is_js ? 2 : 1);
+ });
+ }
+
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+ public async Task RunOnArrayReturnArray(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+ {
+ var ret_len = 5;
+ await RunCallFunctionOn(eval_fn,
+ "function (m) { return Object.values (this).filter ((k, i) => i%m == 0); }",
+ "big", bp_loc, line, col,
+ fn_args : JArray.FromObject(new [] { new { value = 2 } }),
+ res_array_len : ret_len,
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+ var is_js = bp_loc.EndsWith(".js");
+
+ // getProperties (own=false)
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+
+ if (is_js)
+ await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+ else
+ AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+ // getProperties (own=true)
+ // isOwn = true, accessorPropertiesOnly = false
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ // AssertEqual (2, obj_own.Value ["result"].Count (), $"{label}-obj_own.count");
+
+ var obj_own_val = obj_own.Value["result"];
+ await CheckProps(obj_own_val, new
+ {
+ length = TNumber(ret_len),
+ // __proto__ returned by JS
+ }, $"obj_own", num_fields: (is_js ? ret_len + 2 : ret_len + 1));
+
+ for (int i = 0; i < ret_len; i++)
+ CheckNumber(obj_own_val, i.ToString(), i * 2 + 1000);
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task RunOnVTArray(bool roundtrip) => await RunCallFunctionOn(
+ "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);",
+ "function (m) { return Object.values (this).filter ((k, i) => i%m == 0); }",
+ "ss_arr",
+ "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12,
+ fn_args : JArray.FromObject(new [] { new { value = 2 } }),
+ res_array_len : 5,
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+ var ret_len = 5;
+
+ // getProperties (own=false)
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+
+ AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+ // getProperties (own=true)
+ // isOwn = true, accessorPropertiesOnly = false
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ var obj_own_val = obj_own.Value["result"];
+ await CheckProps(obj_own_val, new
+ {
+ length = TNumber(ret_len),
+ // __proto__ returned by JS
+ }, "obj_own", num_fields : ret_len + 1);
+
+ for (int i = 0; i < ret_len; i++)
+ {
+ var act_i = CheckValueType(obj_own_val, i.ToString(), "Math.SimpleStruct");
+
+ // Valuetypes can get sent as part of the container's getProperties, so ensure that we can access it
+ var act_i_props = await GetProperties(act_i["value"]["objectId"]?.Value<string>());
+ await CheckProps(act_i_props, new
+ {
+ dt = TValueType("System.DateTime", new DateTime(2020 + (i * 2), 1, 2, 3, 4, 5).ToString()),
+ gs = TValueType("Math.GenericStruct<System.DateTime>")
+ }, "obj_own ss_arr[{i}]");
+
+ var gs_props = await GetObjectOnLocals(act_i_props, "gs");
+ await CheckProps(gs_props, new
+ {
+ List = TObject("System.Collections.Generic.List<System.DateTime>", is_null : true),
+ StringField = TString($"ss_arr # {i*2} # gs # StringField")
+ }, "obj_own ss_arr[{i}].gs");
+
+ }
+ });
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task RunOnCFOValueTypeResult(bool roundtrip) => await RunCallFunctionOn(
+ eval_fn: "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);",
+ fn_decl: "function () { return this; }",
+ local_name: "simple_struct",
+ bp_loc: "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12,
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+
+ // getProperties (own=false)
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+ AssertEqual(0, obj_accessors.Value["result"].Count(), "obj_accessors-count");
+
+ // getProperties (own=true)
+ // isOwn = true, accessorPropertiesOnly = false
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ var obj_own_val = obj_own.Value["result"];
+ var dt = new DateTime(2020, 1, 2, 3, 4, 5);
+ await CheckProps(obj_own_val, new
+ {
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("Math.GenericStruct<System.DateTime>")
+ }, $"obj_own-props");
+
+ await CheckDateTime(obj_own_val, "dt", dt);
+
+ var gs_props = await GetObjectOnLocals(obj_own_val, "gs");
+ await CheckProps(gs_props, new
+ {
+ List = TObject("System.Collections.Generic.List<System.DateTime>", is_null : true),
+ StringField = TString($"simple_struct # gs # StringField")
+ }, "simple_struct.gs-props");
+ });
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task RunOnJSObject(bool roundtrip) => await RunCallFunctionOn(
+ "object_js_test ();",
+ "function () { return this; }",
+ "obj", "/other.js", 17, 1,
+ fn_args : JArray.FromObject(new [] { new { value = 2 } }),
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+
+ // getProperties (own=false)
+ var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = true,
+ ownProperties = false
+ }), ctx.token);
+
+ await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+
+ // getProperties (own=true)
+ // isOwn = true, accessorPropertiesOnly = false
+ var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+ {
+ objectId = result.Value["result"]["objectId"].Value<string>(),
+ accessorPropertiesOnly = false,
+ ownProperties = true
+ }), ctx.token);
+
+ var obj_own_val = obj_own.Value["result"];
+ await CheckProps(obj_own_val, new
+ {
+ a_obj = TObject("Object"),
+ b_arr = TArray("Array", 2)
+ }, "obj_own", num_fields : 3);
+ });
+
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+ public async Task RunOnArrayReturnObjectArrayByValue(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+ {
+ var ret_len = 5;
+ await RunCallFunctionOn(eval_fn,
+ "function () { return Object.values (this).filter ((k, i) => i%2 == 0); }",
+ "big", bp_loc, line, col, returnByValue : true, roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+ // Check cfo result
+ AssertEqual(JTokenType.Object, result.Value["result"].Type, "cfo-result-jsontype");
+ AssertEqual("object", result.Value["result"]["type"]?.Value<string>(), "cfo-res-type");
+
+ AssertEqual(JTokenType.Array, result.Value["result"]["value"].Type, "cfo-res-value-jsontype");
+ var actual = result.Value["result"] ? ["value"].Values<JToken>().ToArray();
+ AssertEqual(ret_len, actual.Length, "cfo-res-value-length");
+
+ for (int i = 0; i < ret_len; i++)
+ {
+ var exp_num = i * 2 + 1000;
+ if (bp_loc.EndsWith(".js", StringComparison.Ordinal))
+ AssertEqual(exp_num, actual[i].Value<int>(), $"[{i}]");
+ else
+ {
+ AssertEqual("number", actual[i] ? ["type"]?.Value<string>(), $"[{i}]-type");
+ AssertEqual(exp_num.ToString(), actual[i] ? ["description"]?.Value<string>(), $"[{i}]-description");
+ AssertEqual(exp_num, actual[i] ? ["value"]?.Value<int>(), $"[{i}]-value");
+ }
+ }
+ await Task.CompletedTask;
+ });
+ }
+
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+ public async Task RunOnArrayReturnArrayByValue(string eval_fn, string bp_loc, int line, int col, bool roundtrip) => await RunCallFunctionOn(eval_fn,
+ "function () { return Object.getOwnPropertyNames (this); }",
+ "big", bp_loc, line, col, returnByValue : true,
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+ // Check cfo result
+ AssertEqual("object", result.Value["result"]["type"]?.Value<string>(), "cfo-res-type");
+
+ var exp = new JArray();
+ for (int i = 0; i < 10; i++)
+ exp.Add(i.ToString());
+ exp.Add("length");
+
+ var actual = result.Value["result"] ? ["value"];
+ if (!JObject.DeepEquals(exp, actual))
+ {
+ Assert.True(false, $"Results don't match.\nExpected: {exp}\nActual: {actual}");
+ }
+ await Task.CompletedTask;
+ });
+
+ [Theory]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+ [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+ public async Task RunOnArrayReturnPrimitive(string eval_fn, string bp_loc, int line, int col, bool return_by_val)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ await SetBreakpoint(bp_loc, line, col);
+
+ // callFunctionOn
+ var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);";
+ var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+ var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+ // Um for js we get "scriptId": "6"
+ // CheckLocation (bp_loc, line, col, ctx.scripts, pause_location ["callFrames"][0]["location"]);
+
+ // Check the object at the bp
+ var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+ var obj = GetAndAssertObjectWithName(frame_locals, "big");
+ var obj_id = obj["value"]["objectId"].Value<string>();
+
+ var cfo_args = JObject.FromObject(new
+ {
+ functionDeclaration = "function () { return 5; }",
+ objectId = obj_id
+ });
+
+ // value of @returnByValue doesn't matter, as the returned value
+ // is a primitive
+ if (return_by_val)
+ cfo_args["returnByValue"] = return_by_val;
+
+ // callFunctionOn
+ result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+ await CheckValue(result.Value["result"], TNumber(5), "cfo-res");
+ });
+ }
+
+ public static TheoryData<string, string, int, int, bool?> SilentErrorsTestData(bool? silent) => new TheoryData<string, string, int, int, bool?>
+ { { "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, silent },
+ { "big_array_js_test (10);", "/other.js", 8, 1, silent }
+ };
+
+ [Theory]
+ [MemberData(nameof(SilentErrorsTestData), null)]
+ [MemberData(nameof(SilentErrorsTestData), false)]
+ [MemberData(nameof(SilentErrorsTestData), true)]
+ public async Task CFOWithSilentReturnsErrors(string eval_fn, string bp_loc, int line, int col, bool? silent)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ await SetBreakpoint(bp_loc, line, col);
+
+ // callFunctionOn
+ var eval_expr = "window.setTimeout(function() { " + eval_fn + " }, 1);";
+ var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+ var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+ var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+ var obj = GetAndAssertObjectWithName(frame_locals, "big");
+ var big_obj_id = obj["value"]["objectId"].Value<string>();
+ var error_msg = "#This is an error message#";
+
+ // Check the object at the bp
+ var cfo_args = JObject.FromObject(new
+ {
+ functionDeclaration = $"function () {{ throw Error ('{error_msg}'); }}",
+ objectId = big_obj_id
+ });
+
+ if (silent.HasValue)
+ cfo_args["silent"] = silent;
+
+ // callFunctionOn, Silent does not change the result, except that the error
+ // doesn't get reported, and the execution is NOT paused even with setPauseOnException=true
+ result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+ Assert.False(result.IsOk, "result.IsOk");
+ Assert.True(result.IsErr, "result.IsErr");
+
+ var hasErrorMessage = result.Error["exceptionDetails"] ? ["exception"] ? ["description"]?.Value<string>()?.Contains(error_msg);
+ Assert.True((hasErrorMessage ?? false), "Exception message not found");
+ });
+ }
+
+ public static TheoryData<string, string, int, int, string, Func<string[], object>, bool> GettersTestData(bool use_cfo) => new TheoryData<string, string, int, int, string, Func<string[], object>, bool>
+ {
+ // Chrome sends this one
+ {
+ "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');",
+ "PropertyGettersTest",
+ 30,
+ 12,
+ "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i<n;++i){ result=result[properties[i]]; } return result; }",
+ (arg_strs) => JArray.FromObject(arg_strs).ToString(),
+ use_cfo
+ },
+ {
+ "invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');",
+ "MoveNext",
+ 38,
+ 12,
+ "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i<n;++i){ result=result[properties[i]]; } return result; }",
+ (arg_strs) => JArray.FromObject(arg_strs).ToString(),
+ use_cfo
+ },
+
+ // VSCode sends this one
+ {
+ "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');",
+ "PropertyGettersTest",
+ 30,
+ 12,
+ "function(e){return this[e]}",
+ (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty,
+ use_cfo
+ },
+ {
+ "invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');",
+ "MoveNext",
+ 38,
+ 12,
+ "function(e){return this[e]}",
+ (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty,
+ use_cfo
+ }
+ };
+
+ [Theory]
+ [MemberData(nameof(GettersTestData), parameters : false)]
+ [MemberData(nameof(GettersTestData), parameters : true)]
+ public async Task PropertyGettersOnObjectsTest(string eval_fn, string method_name, int line, int col, string cfo_fn, Func<string[], object> get_args_fn, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col,
+ method_name,
+ $"window.setTimeout(function() {{ {eval_fn} }}, 1);",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var dt = new DateTime(10, 9, 8, 7, 6, 5);
+
+ await CheckProps(frame_locals, new
+ {
+ ptd = TObject("DebuggerTests.ClassWithProperties"),
+ swp = TObject("DebuggerTests.StructWithProperties"),
+ }, "locals#0");
+
+ var ptd = GetAndAssertObjectWithName(frame_locals, "ptd");
+
+ var ptd_props = await GetProperties(ptd?["value"] ? ["objectId"]?.Value<string>());
+ await CheckProps(ptd_props, new
+ {
+ Int = TGetter("Int"),
+ String = TGetter("String"),
+ DT = TGetter("DT"),
+ IntArray = TGetter("IntArray"),
+ DTArray = TGetter("DTArray")
+ }, "ptd", num_fields : 7);
+
+ // Automatic properties don't have invokable getters, because we can get their
+ // value from the backing field directly
+ {
+ dt = new DateTime(4, 5, 6, 7, 8, 9);
+ var dt_auto_props = await GetObjectOnLocals(ptd_props, "DTAutoProperty");
+ await CheckDateTime(ptd_props, "DTAutoProperty", dt);
+ }
+
+ // Invoke getters, and check values
+
+ var res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "Int" }));
+ Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+ await CheckValue(res.Value["result"], JObject.FromObject(new { type = "number", value = 5 }), "ptd.Int");
+
+ res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "String" }));
+ Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+ await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = "foobar" }), "ptd.String");
+
+ dt = new DateTime(3, 4, 5, 6, 7, 8);
+ res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DT" }));
+ Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+ await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), "ptd.DT");
+ await CheckDateTimeValue(res.Value["result"], dt);
+
+ // Check arrays through getters
+
+ res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "IntArray" }));
+ Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+ await CheckValue(res.Value["result"], TArray("int[]", 2), "ptd.IntArray");
+ {
+ var arr_elems = await GetProperties(res.Value["result"] ? ["objectId"]?.Value<string>());
+ var exp_elems = new []
+ {
+ TNumber(10),
+ TNumber(20)
+ };
+
+ await CheckProps(arr_elems, exp_elems, "ptd.IntArray");
+ }
+
+ res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DTArray" }));
+ Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+ await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), "ptd.DTArray");
+ {
+ var dt0 = new DateTime(6, 7, 8, 9, 10, 11);
+ var dt1 = new DateTime(1, 2, 3, 4, 5, 6);
+
+ var arr_elems = await GetProperties(res.Value["result"] ? ["objectId"]?.Value<string>());
+ var exp_elems = new []
+ {
+ TValueType("System.DateTime", dt0.ToString()),
+ TValueType("System.DateTime", dt1.ToString()),
+ };
+
+ await CheckProps(arr_elems, exp_elems, "ptd.DTArray");
+ }
+ });
+
+ [Theory]
+ [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "MoveNext", 38, 12)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "PropertyGettersTest", 30, 12)]
+ public async Task PropertyGettersOnStructsTest(string eval_fn, string method_name, int line, int col) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col,
+ method_name,
+ $"window.setTimeout(function() {{ {eval_fn} }}, 1);",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckProps(frame_locals, new
+ {
+ ptd = TObject("DebuggerTests.ClassWithProperties"),
+ swp = TObject("DebuggerTests.StructWithProperties"),
+ }, "locals#0");
+
+ var swp = GetAndAssertObjectWithName(frame_locals, "swp");
+
+ var swp_props = await GetProperties(swp?["value"] ? ["objectId"]?.Value<string>());
+ await CheckProps(swp_props, new
+ {
+ Int = TSymbol("int { get; }"),
+ String = TSymbol("string { get; }"),
+ DT = TSymbol("System.DateTime { get; }"),
+ IntArray = TSymbol("int[] { get; }"),
+ DTArray = TSymbol("System.DateTime[] { get; }")
+ }, "swp");
+ });
+
+ [Theory]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, false)]
+ [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, true)]
+ [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, false)]
+ [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, true)]
+ public async Task CheckAccessorsOnObjectsWithCFO(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+ {
+ await RunCallFunctionOn(
+ eval_fn, "function() { return this; }", "ptd",
+ bp_loc, line, col,
+ roundtrip : roundtrip,
+ test_fn : async(result) =>
+ {
+
+ var is_js = bp_loc.EndsWith(".js");
+
+ // Check with `accessorPropertiesOnly=true`
+
+ var id = result.Value?["result"] ? ["objectId"]?.Value<string>();
+ var get_prop_req = JObject.FromObject(new
+ {
+ objectId = id,
+ accessorPropertiesOnly = true
+ });
+
+ var res = await GetPropertiesAndCheckAccessors(get_prop_req, is_js ? 6 : 5); // js returns extra `__proto__` member also
+ Assert.False(res.Value["result"].Any(jt => jt["name"]?.Value<string>() == "StringField"), "StringField shouldn't be returned for `accessorPropertiesOnly`");
+
+ // Check with `accessorPropertiesOnly` unset, == false
+ get_prop_req = JObject.FromObject(new
+ {
+ objectId = id,
+ });
+
+ res = await GetPropertiesAndCheckAccessors(get_prop_req, is_js ? 8 : 7); // js returns a `__proto__` member also
+ Assert.True(res.Value["result"].Any(jt => jt["name"]?.Value<string>() == "StringField"), "StringField should be returned for `accessorPropertiesOnly=false`");
+ });
+
+ async Task<Result> GetPropertiesAndCheckAccessors(JObject get_prop_req, int num_fields)
+ {
+ var res = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token);
+ if (!res.IsOk)
+ Assert.True(false, $"Runtime.getProperties failed for {get_prop_req.ToString ()}, with Result: {res}");
+
+ var accessors = new string[] { "Int", "String", "DT", "IntArray", "DTArray" };
+ foreach (var name in accessors)
+ {
+ var prop = GetAndAssertObjectWithName(res.Value["result"], name);
+ Assert.True(prop["value"] == null, $"{name} shouldn't have a `value`");
+
+ await CheckValue(prop, TGetter(name), $"{name}");
+ }
+
+ return res;
+ }
+ }
+
+ async Task<Result> InvokeGetter(JToken obj, string fn, object arguments) => await ctx.cli.SendCommand(
+ "Runtime.callFunctionOn",
+ JObject.FromObject(new
+ {
+ functionDeclaration = fn,
+ objectId = obj["value"] ? ["objectId"]?.Value<string>(),
+ arguments = new [] { new { value = arguments } }
+ }), ctx.token);
+
+ /*
+ * 1. runs `Runtime.callFunctionOn` on the objectId,
+ * if @roundtrip == false, then
+ * -> calls @test_fn for that result (new objectId)
+ * else
+ * -> runs it again on the *result's* objectId.
+ * -> calls @test_fn on the *new* result objectId
+ *
+ * Returns: result of `Runtime.callFunctionOn`
+ */
+ async Task RunCallFunctionOn(string eval_fn, string fn_decl, string local_name, string bp_loc, int line, int col, int res_array_len = -1,
+ Func<Result, Task> test_fn = null, bool returnByValue = false, JArray fn_args = null, bool roundtrip = false)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ await SetBreakpoint(bp_loc, line, col);
+
+ // callFunctionOn
+ var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);";
+ var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+ var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+ // Um for js we get "scriptId": "6"
+ // CheckLocation (bp_loc, line, col, ctx.scripts, pause_location ["callFrames"][0]["location"]);
+
+ // Check the object at the bp
+ var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+ var obj = GetAndAssertObjectWithName(frame_locals, local_name);
+ var obj_id = obj["value"]["objectId"].Value<string>();
+
+ var cfo_args = JObject.FromObject(new
+ {
+ functionDeclaration = fn_decl,
+ objectId = obj_id
+ });
+
+ if (fn_args != null)
+ cfo_args["arguments"] = fn_args;
+
+ if (returnByValue)
+ cfo_args["returnByValue"] = returnByValue;
+
+ // callFunctionOn
+ result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+ await CheckCFOResult(result);
+
+ // If it wasn't `returnByValue`, then try to run a new function
+ // on that *returned* object
+ // This second function, just returns the object as-is, so the same
+ // test_fn is re-usable.
+ if (!returnByValue && roundtrip)
+ {
+ cfo_args = JObject.FromObject(new
+ {
+ functionDeclaration = "function () { return this; }",
+ objectId = result.Value["result"]["objectId"]?.Value<string>()
+ });
+
+ if (fn_args != null)
+ cfo_args["arguments"] = fn_args;
+
+ result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+
+ await CheckCFOResult(result);
+ }
+
+ if (test_fn != null)
+ await test_fn(result);
+
+ return;
+
+ async Task CheckCFOResult(Result result)
+ {
+ if (returnByValue)
+ return;
+
+ if (res_array_len < 0)
+ await CheckValue(result.Value["result"], TObject("Object"), $"cfo-res");
+ else
+ await CheckValue(result.Value["result"], TArray("Array", res_array_len), $"cfo-res");
+ }
+ });
+ }
+ }
+
+}
\ No newline at end of file
--- /dev/null
+// 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.Globalization;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace DebuggerTests
+{
+ public class DateTimeList : DebuggerTestBase
+ {
+
+ [Theory]
+ [InlineData("en-US")]
+
+ // Currently not passing tests. Issue #19743
+ // [InlineData ("ja-JP")]
+ // [InlineData ("es-ES")]
+ //[InlineData ("de-DE")]
+ //[InlineData ("ka-GE")]
+ //[InlineData ("hu-HU")]
+ public async Task CheckDateTimeLocale(string locale)
+ {
+ var insp = new Inspector();
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-datetime-test.cs";
+
+ await SetBreakpointInMethod("debugger-test", "DebuggerTests.DateTimeTest", "LocaleTest", 15);
+
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.DateTimeTest:LocaleTest'," +
+ $"'{locale}'); }}, 1);",
+ debugger_test_loc, 25, 12, "LocaleTest",
+ locals_fn : async(locals) =>
+ {
+ DateTimeFormatInfo dtfi = CultureInfo.GetCultureInfo(locale).DateTimeFormat;
+ CultureInfo.CurrentCulture = new CultureInfo(locale, false);
+ DateTime dt = new DateTime(2020, 1, 2, 3, 4, 5);
+ string dt_str = dt.ToString();
+
+ var fdtp = dtfi.FullDateTimePattern;
+ var ldp = dtfi.LongDatePattern;
+ var ltp = dtfi.LongTimePattern;
+ var sdp = dtfi.ShortDatePattern;
+ var stp = dtfi.ShortTimePattern;
+
+ CheckString(locals, "fdtp", fdtp);
+ CheckString(locals, "ldp", ldp);
+ CheckString(locals, "ltp", ltp);
+ CheckString(locals, "sdp", sdp);
+ CheckString(locals, "stp", stp);
+ await CheckDateTime(locals, "dt", dt);
+ CheckString(locals, "dt_str", dt_str);
+ }
+ );
+
+ });
+ }
+
+ }
+}
\ No newline at end of file
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="xunit" Version="2.4.0" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
+
+ <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\BrowserDebugHost\BrowserDebugHost.csproj" />
+ <ProjectReference Include="..\BrowserDebugProxy\BrowserDebugProxy.csproj" />
+ </ItemGroup>
+
+</Project>
--- /dev/null
+// 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.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+ public class DelegateTests : DebuggerTestBase
+ {
+
+ [Theory]
+ [InlineData(0, 53, 8, "DelegatesTest", false)]
+ [InlineData(0, 53, 8, "DelegatesTest", true)]
+ [InlineData(2, 99, 8, "InnerMethod2", false)]
+ [InlineData(2, 99, 8, "InnerMethod2", true)]
+ public async Task InspectLocalsWithDelegatesAtBreakpointSite(int frame, int line, int col, string method_name, bool use_cfo) =>
+ await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", line, col, method_name,
+ "window.setTimeout(function() { invoke_delegates_test (); }, 1);",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+ await CheckProps(locals, new
+ {
+ fn_func = TDelegate("System.Func<Math, bool>", "bool <DelegatesTest>|(Math)"),
+ fn_func_null = TObject("System.Func<Math, bool>", is_null : true),
+ fn_func_arr = TArray("System.Func<Math, bool>[]", 1),
+ fn_del = TDelegate("Math.IsMathNull", "bool IsMathNullDelegateTarget (Math)"),
+ fn_del_null = TObject("Math.IsMathNull", is_null : true),
+ fn_del_arr = TArray("Math.IsMathNull[]", 1),
+
+ // Unused locals
+ fn_func_unused = TDelegate("System.Func<Math, bool>", "bool <DelegatesTest>|(Math)"),
+ fn_func_null_unused = TObject("System.Func<Math, bool>", is_null : true),
+ fn_func_arr_unused = TArray("System.Func<Math, bool>[]", 1),
+
+ fn_del_unused = TDelegate("Math.IsMathNull", "bool IsMathNullDelegateTarget (Math)"),
+ fn_del_null_unused = TObject("Math.IsMathNull", is_null : true),
+ fn_del_arr_unused = TArray("Math.IsMathNull[]", 1),
+
+ res = TBool(false),
+ m_obj = TObject("Math")
+ }, "locals");
+
+ await CompareObjectPropertiesFor(locals, "fn_func_arr", new []
+ {
+ TDelegate(
+ "System.Func<Math, bool>",
+ "bool <DelegatesTest>|(Math)")
+ }, "locals#fn_func_arr");
+
+ await CompareObjectPropertiesFor(locals, "fn_del_arr", new []
+ {
+ TDelegate(
+ "Math.IsMathNull",
+ "bool IsMathNullDelegateTarget (Math)")
+ }, "locals#fn_del_arr");
+
+ await CompareObjectPropertiesFor(locals, "fn_func_arr_unused", new []
+ {
+ TDelegate(
+ "System.Func<Math, bool>",
+ "bool <DelegatesTest>|(Math)")
+ }, "locals#fn_func_arr_unused");
+
+ await CompareObjectPropertiesFor(locals, "fn_del_arr_unused", new []
+ {
+ TDelegate(
+ "Math.IsMathNull",
+ "bool IsMathNullDelegateTarget (Math)")
+ }, "locals#fn_del_arr_unused");
+ }
+ );
+
+ [Theory]
+ [InlineData(0, 202, 8, "DelegatesSignatureTest", false)]
+ [InlineData(0, 202, 8, "DelegatesSignatureTest", true)]
+ [InlineData(2, 99, 8, "InnerMethod2", false)]
+ [InlineData(2, 99, 8, "InnerMethod2", true)]
+ public async Task InspectDelegateSignaturesWithFunc(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs",
+ line, col,
+ bp_method,
+ "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:DelegatesSignatureTest'); }, 1)",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+ await CheckProps(locals, new
+ {
+ fn_func = TDelegate("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>",
+ "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+ fn_func_del = TDelegate("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>",
+ "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+ fn_func_null = TObject("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>", is_null : true),
+ fn_func_only_ret = TDelegate("System.Func<bool>", "bool <DelegatesSignatureTest>|()"),
+ fn_func_arr = TArray("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>[]", 1),
+
+ fn_del = TDelegate("Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+ fn_del_l = TDelegate("Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+ fn_del_null = TObject("Math.DelegateForSignatureTest", is_null : true),
+ fn_del_arr = TArray("Math.DelegateForSignatureTest[]", 2),
+ m_obj = TObject("Math"),
+ gs_gs = TValueType("Math.GenericStruct<Math.GenericStruct<int[]>>"),
+ fn_void_del = TDelegate("Math.DelegateWithVoidReturn",
+ "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)"),
+
+ fn_void_del_arr = TArray("Math.DelegateWithVoidReturn[]", 1),
+ fn_void_del_null = TObject("Math.DelegateWithVoidReturn", is_null : true),
+ gs = TValueType("Math.GenericStruct<int[]>"),
+ rets = TArray("Math.GenericStruct<bool[]>[]", 6)
+ }, "locals");
+
+ await CompareObjectPropertiesFor(locals, "fn_func_arr", new []
+ {
+ TDelegate(
+ "System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>",
+ "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+ }, "locals#fn_func_arr");
+
+ await CompareObjectPropertiesFor(locals, "fn_del_arr", new []
+ {
+ TDelegate(
+ "Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+ TDelegate(
+ "Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)")
+ }, "locals#fn_del_arr");
+
+ await CompareObjectPropertiesFor(locals, "fn_void_del_arr", new []
+ {
+ TDelegate(
+ "Math.DelegateWithVoidReturn",
+ "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)")
+ }, "locals#fn_void_del_arr");
+ });
+
+ [Theory]
+ [InlineData(0, 224, 8, "ActionTSignatureTest", false)]
+ [InlineData(0, 224, 8, "ActionTSignatureTest", true)]
+ [InlineData(2, 99, 8, "InnerMethod2", false)]
+ [InlineData(2, 99, 8, "InnerMethod2", true)]
+ public async Task ActionTSignatureTest(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", line, col,
+ bp_method,
+ "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:ActionTSignatureTest'); }, 1)",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+ await CheckProps(locals, new
+ {
+ fn_action = TDelegate("System.Action<Math.GenericStruct<int[]>>",
+ "void <ActionTSignatureTest>|(Math.GenericStruct<int[]>)"),
+ fn_action_del = TDelegate("System.Action<Math.GenericStruct<int[]>>",
+ "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)"),
+ fn_action_bare = TDelegate("System.Action",
+ "void|()"),
+
+ fn_action_null = TObject("System.Action<Math.GenericStruct<int[]>>", is_null : true),
+
+ fn_action_arr = TArray("System.Action<Math.GenericStruct<int[]>>[]", 3),
+
+ gs = TValueType("Math.GenericStruct<int[]>"),
+ }, "locals");
+
+ await CompareObjectPropertiesFor(locals, "fn_action_arr", new []
+ {
+ TDelegate(
+ "System.Action<Math.GenericStruct<int[]>>",
+ "void <ActionTSignatureTest>|(Math.GenericStruct<int[]>)"),
+ TDelegate(
+ "System.Action<Math.GenericStruct<int[]>>",
+ "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)"),
+ TObject("System.Action<Math.GenericStruct<int[]>>", is_null : true)
+ }, "locals#fn_action_arr");
+ });
+
+ [Theory]
+ [InlineData(0, 242, 8, "NestedDelegatesTest", false)]
+ [InlineData(0, 242, 8, "NestedDelegatesTest", true)]
+ [InlineData(2, 99, 8, "InnerMethod2", false)]
+ [InlineData(2, 99, 8, "InnerMethod2", true)]
+ public async Task NestedDelegatesTest(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", line, col,
+ bp_method,
+ "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:NestedDelegatesTest'); }, 1)",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+ await CheckProps(locals, new
+ {
+ fn_func = TDelegate("System.Func<System.Func<int, bool>, bool>",
+ "bool <NestedDelegatesTest>|(Func<int, bool>)"),
+ fn_func_null = TObject("System.Func<System.Func<int, bool>, bool>", is_null : true),
+ fn_func_arr = TArray("System.Func<System.Func<int, bool>, bool>[]", 1),
+ fn_del_arr = TArray("System.Func<System.Func<int, bool>, bool>[]", 1),
+
+ m_obj = TObject("Math"),
+ fn_del_null = TObject("System.Func<System.Func<int, bool>, bool>", is_null : true),
+ fs = TDelegate("System.Func<int, bool>",
+ "bool <NestedDelegatesTest>|(int)")
+ }, "locals");
+
+ await CompareObjectPropertiesFor(locals, "fn_func_arr", new []
+ {
+ TDelegate(
+ "System.Func<System.Func<int, bool>, bool>",
+ "bool <NestedDelegatesTest>|(System.Func<int, bool>)")
+ }, "locals#fn_func_arr");
+
+ await CompareObjectPropertiesFor(locals, "fn_del_arr", new []
+ {
+ TDelegate(
+ "System.Func<System.Func<int, bool>, bool>",
+ "bool DelegateTargetForNestedFunc (Func<int, bool>)")
+ }, "locals#fn_del_arr");
+ });
+
+ [Theory]
+ [InlineData(0, 262, 8, "MethodWithDelegateArgs", false)]
+ [InlineData(0, 262, 8, "MethodWithDelegateArgs", true)]
+ [InlineData(2, 99, 8, "InnerMethod2", false)]
+ [InlineData(2, 99, 8, "InnerMethod2", true)]
+ public async Task DelegatesAsMethodArgsTest(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", line, col,
+ bp_method,
+ "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:DelegatesAsMethodArgsTest'); }, 1)",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+ await CheckProps(locals, new
+ {
+ @this = TObject("Math"),
+ dst_arr = TArray("Math.DelegateForSignatureTest[]", 2),
+ fn_func = TDelegate("System.Func<char[], bool>",
+ "bool <DelegatesAsMethodArgsTest>|(char[])"),
+ fn_action = TDelegate("System.Action<Math.GenericStruct<int>[]>",
+ "void <DelegatesAsMethodArgsTest>|(Math.GenericStruct<int>[])")
+ }, "locals");
+
+ await CompareObjectPropertiesFor(locals, "dst_arr", new []
+ {
+ TDelegate("Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+ TDelegate("Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> <DelegatesAsMethodArgsTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+ }, "locals#dst_arr");
+ });
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task MethodWithDelegatesAsyncTest(bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", 281, 8,
+ "MoveNext", //"DelegatesAsMethodArgsTestAsync"
+ "window.setTimeout (function () { invoke_static_method_async ('[debugger-test] Math:MethodWithDelegatesAsyncTest'); }, 1)",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ await CheckProps(locals, new
+ {
+ @this = TObject("Math"),
+ _dst_arr = TArray("Math.DelegateForSignatureTest[]", 2),
+ _fn_func = TDelegate("System.Func<char[], bool>",
+ "bool <MethodWithDelegatesAsync>|(char[])"),
+ _fn_action = TDelegate("System.Action<Math.GenericStruct<int>[]>",
+ "void <MethodWithDelegatesAsync>|(Math.GenericStruct<int>[])")
+ }, "locals");
+
+ await CompareObjectPropertiesFor(locals, "_dst_arr", new []
+ {
+ TDelegate(
+ "Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+ TDelegate(
+ "Math.DelegateForSignatureTest",
+ "Math.GenericStruct<bool[]> <MethodWithDelegatesAsync>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+ }, "locals#dst_arr");
+ });
+ }
+
+}
\ No newline at end of file
--- /dev/null
+// 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.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ internal class DevToolsClient : IDisposable
+ {
+ ClientWebSocket socket;
+ List<Task> pending_ops = new List<Task>();
+ TaskCompletionSource<bool> side_exit = new TaskCompletionSource<bool>();
+ List<byte[]> pending_writes = new List<byte[]>();
+ Task current_write;
+ readonly ILogger logger;
+
+ public DevToolsClient(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ ~DevToolsClient()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ public async Task Close(CancellationToken cancellationToken)
+ {
+ if (socket.State == WebSocketState.Open)
+ await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ socket.Dispose();
+ }
+
+ Task Pump(Task task, CancellationToken token)
+ {
+ if (task != current_write)
+ return null;
+ current_write = null;
+
+ pending_writes.RemoveAt(0);
+
+ if (pending_writes.Count > 0)
+ {
+ current_write = socket.SendAsync(new ArraySegment<byte>(pending_writes[0]), WebSocketMessageType.Text, true, token);
+ return current_write;
+ }
+ return null;
+ }
+
+ async Task<string> ReadOne(CancellationToken token)
+ {
+ byte[] buff = new byte[4000];
+ var mem = new MemoryStream();
+ while (true)
+ {
+ var result = await this.socket.ReceiveAsync(new ArraySegment<byte>(buff), token);
+ if (result.MessageType == WebSocketMessageType.Close)
+ {
+ return null;
+ }
+
+ if (result.EndOfMessage)
+ {
+ mem.Write(buff, 0, result.Count);
+ return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int) mem.Length);
+ }
+ else
+ {
+ mem.Write(buff, 0, result.Count);
+ }
+ }
+ }
+
+ protected void Send(byte[] bytes, CancellationToken token)
+ {
+ pending_writes.Add(bytes);
+ if (pending_writes.Count == 1)
+ {
+ if (current_write != null)
+ throw new Exception("Internal state is bad. current_write must be null if there are no pending writes");
+
+ current_write = socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, token);
+ pending_ops.Add(current_write);
+ }
+ }
+
+ async Task MarkCompleteAfterward(Func<CancellationToken, Task> send, CancellationToken token)
+ {
+ try
+ {
+ await send(token);
+ side_exit.SetResult(true);
+ }
+ catch (Exception e)
+ {
+ side_exit.SetException(e);
+ }
+ }
+
+ protected async Task<bool> ConnectWithMainLoops(
+ Uri uri,
+ Func<string, CancellationToken, Task> receive,
+ Func<CancellationToken, Task> send,
+ CancellationToken token)
+ {
+
+ logger.LogDebug("connecting to {0}", uri);
+ this.socket = new ClientWebSocket();
+ this.socket.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
+
+ await this.socket.ConnectAsync(uri, token);
+ pending_ops.Add(ReadOne(token));
+ pending_ops.Add(side_exit.Task);
+ pending_ops.Add(MarkCompleteAfterward(send, token));
+
+ while (!token.IsCancellationRequested)
+ {
+ var task = await Task.WhenAny(pending_ops);
+ if (task == pending_ops[0])
+ { //pending_ops[0] is for message reading
+ var msg = ((Task<string>) task).Result;
+ pending_ops[0] = ReadOne(token);
+ Task tsk = receive(msg, token);
+ if (tsk != null)
+ pending_ops.Add(tsk);
+ }
+ else if (task == pending_ops[1])
+ {
+ var res = ((Task<bool>) task).Result;
+ //it might not throw if exiting successfull
+ return res;
+ }
+ else
+ { //must be a background task
+ pending_ops.Remove(task);
+ var tsk = Pump(task, token);
+ if (tsk != null)
+ pending_ops.Add(tsk);
+ }
+ }
+
+ return false;
+ }
+
+ protected virtual void Log(string priority, string msg)
+ {
+ //
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+ public class EvaluateOnCallFrameTests : DebuggerTestBase
+ {
+
+ [Fact]
+ public async Task EvaluateThisProperties() => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+ "run",
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a");
+ CheckContentValue(evaluate, "1");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "b");
+ CheckContentValue(evaluate, "2");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "c");
+ CheckContentValue(evaluate, "3");
+
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "dt");
+ await CheckDateTimeValue(evaluate, new DateTime(2000, 5, 4, 3, 2, 1));
+ });
+
+ [Theory]
+ [InlineData(63, 12, "EvaluateTestsStructInstanceMethod")]
+ [InlineData(79, 12, "GenericInstanceMethodOnStruct<int>")]
+ [InlineData(102, 12, "EvaluateTestsGenericStructInstanceMethod")]
+ public async Task EvaluateThisPropertiesOnStruct(int line, int col, string method_name) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-evaluate-test.cs", line, col,
+ method_name,
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a");
+ CheckContentValue(evaluate, "1");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "b");
+ CheckContentValue(evaluate, "2");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "c");
+ CheckContentValue(evaluate, "3");
+
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "dateTime");
+ await CheckDateTimeValue(evaluate, new DateTime(2020, 1, 2, 3, 4, 5));
+ });
+
+ [Fact]
+ public async Task EvaluateParameters() => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+ "run",
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "g");
+ CheckContentValue(evaluate, "100");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "h");
+ CheckContentValue(evaluate, "200");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "valString");
+ CheckContentValue(evaluate, "test");
+ });
+
+ [Fact]
+ public async Task EvaluateLocals() => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+ "run",
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "d");
+ CheckContentValue(evaluate, "101");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "e");
+ CheckContentValue(evaluate, "102");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "f");
+ CheckContentValue(evaluate, "103");
+
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_dt");
+ await CheckDateTimeValue(evaluate, new DateTime(2010, 9, 8, 7, 6, 5));
+ });
+
+ [Fact]
+ public async Task EvaluateLocalsAsync()
+ {
+ var bp_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+ int line = 249;
+ int col = 12;
+ var function_name = "MoveNext";
+ await CheckInspectLocalsAtBreakpointSite(
+ bp_loc, line, col,
+ function_name,
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ // sc_arg
+ {
+ var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "sc_arg");
+ await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#1");
+
+ var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
+ await CheckProps(sc_arg_props, new
+ {
+ X = TNumber(10),
+ Y = TNumber(45),
+ Id = TString("sc#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Blue"),
+ PointWithCustomGetter = TGetter("PointWithCustomGetter")
+ }, "sc_arg_props#1");
+ }
+
+ // local_gs
+ {
+ var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_gs");
+ await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), "local_gs#1");
+
+ var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
+ await CheckProps(local_gs_props, new
+ {
+ Id = TObject("string", is_null : true),
+ Color = TEnum("DebuggerTests.RGB", "Red"),
+ Value = TNumber(0)
+ }, "local_gs_props#1");
+ }
+
+ // step, check local_gs
+ pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 1, col, function_name);
+ {
+ var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_gs");
+ await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), "local_gs#2");
+
+ var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
+ await CheckProps(local_gs_props, new
+ {
+ Id = TString("local_gs#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Green"),
+ Value = TNumber(4)
+ }, "local_gs_props#2");
+ }
+
+ // step check sc_arg.Id
+ pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 2, col, function_name);
+ {
+ var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "sc_arg");
+ await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#2");
+
+ var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
+ await CheckProps(sc_arg_props, new
+ {
+ X = TNumber(10),
+ Y = TNumber(45),
+ Id = TString("sc_arg#Id"), // <------- This changed
+ Color = TEnum("DebuggerTests.RGB", "Blue"),
+ PointWithCustomGetter = TGetter("PointWithCustomGetter")
+ }, "sc_arg_props#2");
+ }
+ });
+ }
+
+ [Fact]
+ public async Task EvaluateExpressions() => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+ "run",
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "d + e");
+ CheckContentValue(evaluate, "203");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "e + 10");
+ CheckContentValue(evaluate, "112");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a + a");
+ CheckContentValue(evaluate, "2");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.a + this.b");
+ CheckContentValue(evaluate, "3");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "\"test\" + \"test\"");
+ CheckContentValue(evaluate, "testtest");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "5 + 5");
+ CheckContentValue(evaluate, "10");
+ });
+
+ [Fact]
+ public async Task EvaluateThisExpressions() => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+ "run",
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.a");
+ CheckContentValue(evaluate, "1");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.b");
+ CheckContentValue(evaluate, "2");
+ evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.c");
+ CheckContentValue(evaluate, "3");
+
+ // FIXME: not supported yet
+ // evaluate = await EvaluateOnCallFrame (pause_location ["callFrames"][0] ["callFrameId"].Value<string> (), "this.dt");
+ // await CheckDateTimeValue (evaluate, new DateTime (2000, 5, 4, 3, 2, 1));
+ });
+ }
+
+}
\ No newline at end of file
--- /dev/null
+// 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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ internal class InspectorClient : DevToolsClient
+ {
+ List < (int, TaskCompletionSource<Result>) > pending_cmds = new List < (int, TaskCompletionSource<Result>) > ();
+ Func<string, JObject, CancellationToken, Task> onEvent;
+ int next_cmd_id;
+
+ public InspectorClient(ILogger logger) : base(logger) { }
+
+ Task HandleMessage(string msg, CancellationToken token)
+ {
+ var res = JObject.Parse(msg);
+ if (res["id"] == null)
+ DumpProtocol(string.Format("Event method: {0} params: {1}", res["method"], res["params"]));
+ else
+ DumpProtocol(string.Format("Response id: {0} res: {1}", res["id"], res));
+
+ if (res["id"] == null)
+ return onEvent(res["method"].Value<string>(), res["params"] as JObject, token);
+ var id = res["id"].Value<int>();
+ var idx = pending_cmds.FindIndex(e => e.Item1 == id);
+ var item = pending_cmds[idx];
+ pending_cmds.RemoveAt(idx);
+ item.Item2.SetResult(Result.FromJson(res));
+ return null;
+ }
+
+ public async Task Connect(
+ Uri uri,
+ Func<string, JObject, CancellationToken, Task> onEvent,
+ Func<CancellationToken, Task> send,
+ CancellationToken token)
+ {
+
+ this.onEvent = onEvent;
+ await ConnectWithMainLoops(uri, HandleMessage, send, token);
+ }
+
+ public Task<Result> SendCommand(string method, JObject args, CancellationToken token)
+ {
+ int id = ++next_cmd_id;
+ if (args == null)
+ args = new JObject();
+
+ var o = JObject.FromObject(new
+ {
+ id = id,
+ method = method,
+ @params = args
+ });
+
+ var tcs = new TaskCompletionSource<Result>();
+ pending_cmds.Add((id, tcs));
+
+ var str = o.ToString();
+ //Log ("protocol", $"SendCommand: id: {id} method: {method} params: {args}");
+
+ var bytes = Encoding.UTF8.GetBytes(str);
+ Send(bytes, token);
+ return tcs.Task;
+ }
+
+ protected virtual void DumpProtocol(string msg)
+ {
+ // Console.WriteLine (msg);
+ //XXX make logging not stupid
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+ public class PointerTests : DebuggerTestBase
+ {
+
+ public static TheoryData<string, string, string, int, string, bool> PointersTestData =>
+ new TheoryData<string, string, string, int, string, bool>
+ { { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "LocalPointers", 32, "LocalPointers", false },
+ { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "LocalPointers", 32, "LocalPointers", true },
+ { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "MoveNext", false },
+ { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "MoveNext", true }
+ };
+
+ [Theory]
+ [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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ ip = TPointer("int*"),
+ ip_null = TPointer("int*", is_null : true),
+ ipp = TPointer("int**"),
+ ipp_null = TPointer("int**"),
+
+ cvalue0 = TSymbol("113 'q'"),
+ cp = TPointer("char*"),
+
+ vp = TPointer("void*"),
+ vp_null = TPointer("void*", is_null : true),
+ }, "locals", num_fields : 26);
+
+ var props = await GetObjectOnLocals(locals, "ip");
+ await CheckPointerValue(props, "*ip", TNumber(5), "locals");
+
+ {
+ var ipp_props = await GetObjectOnLocals(locals, "ipp");
+ await CheckPointerValue(ipp_props, "*ipp", TPointer("int*"));
+
+ ipp_props = await GetObjectOnLocals(ipp_props, "*ipp");
+ await CheckPointerValue(ipp_props, "**ipp", TNumber(5));
+ }
+
+ {
+ var ipp_props = await GetObjectOnLocals(locals, "ipp_null");
+ await CheckPointerValue(ipp_props, "*ipp_null", TPointer("int*", is_null : true));
+ }
+
+ // *cp
+ props = await GetObjectOnLocals(locals, "cp");
+ await CheckPointerValue(props, "*cp", TSymbol("113 'q'"));
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalPointerArrays(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ ipa = TArray("int*[]", 3)
+ }, "locals", num_fields : 26);
+
+ var ipa_elems = await CompareObjectPropertiesFor(locals, "ipa", new []
+ {
+ TPointer("int*"),
+ TPointer("int*"),
+ TPointer("int*", is_null : true)
+ });
+
+ await CheckArrayElements(ipa_elems, new []
+ {
+ TNumber(5),
+ TNumber(10),
+ null
+ });
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalDoublePointerToPrimitiveTypeArrays(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ ippa = TArray("int**[]", 5)
+ }, "locals", num_fields : 26);
+
+ var ippa_elems = await CompareObjectPropertiesFor(locals, "ippa", new []
+ {
+ TPointer("int**"),
+ TPointer("int**"),
+ TPointer("int**"),
+ TPointer("int**"),
+ TPointer("int**", is_null : true)
+ });
+
+ {
+ var actual_elems = await CheckArrayElements(ippa_elems, new []
+ {
+ TPointer("int*"),
+ TPointer("int*", is_null : true),
+ TPointer("int*"),
+ TPointer("int*", is_null : true),
+ null
+ });
+
+ var val = await GetObjectOnLocals(actual_elems[0], "*[0]");
+ await CheckPointerValue(val, "**[0]", TNumber(5));
+
+ val = await GetObjectOnLocals(actual_elems[2], "*[2]");
+ await CheckPointerValue(val, "**[2]", TNumber(5));
+ }
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalPointersToValueTypes(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ dt = TValueType("System.DateTime", dt.ToString()),
+ dtp = TPointer("System.DateTime*"),
+ dtp_null = TPointer("System.DateTime*", is_null : true),
+
+ gsp = TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*"),
+ gsp_null = TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*")
+ }, "locals", num_fields : 26);
+
+ await CheckDateTime(locals, "dt", dt);
+
+ // *dtp
+ var props = await GetObjectOnLocals(locals, "dtp");
+ await CheckDateTime(props, "*dtp", dt);
+
+ var gsp_props = await GetObjectOnLocals(locals, "gsp");
+ await CheckPointerValue(gsp_props, "*gsp", TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>"), "locals#gsp");
+
+ {
+ var gs_dt = new DateTime(1, 2, 3, 4, 5, 6);
+
+ var gsp_deref_props = await GetObjectOnLocals(gsp_props, "*gsp");
+ await CheckProps(gsp_deref_props, new
+ {
+ Value = TValueType("System.DateTime", gs_dt.ToString()),
+ IntField = TNumber(4),
+ DTPP = TPointer("System.DateTime**")
+ }, "locals#gsp#deref");
+ {
+ var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+ await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*"), "locals#*gsp");
+
+ var dtpp_deref_props = await GetObjectOnLocals(dtpp_props, "*DTPP");
+ await CheckDateTime(dtpp_deref_props, "**DTPP", dt);
+ }
+ }
+
+ // gsp_null
+ var gsp_w_n_props = await GetObjectOnLocals(locals, "gsp_null");
+ await CheckPointerValue(gsp_w_n_props, "*gsp_null", TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>"), "locals#gsp");
+
+ {
+ var gs_dt = new DateTime(1, 2, 3, 4, 5, 6);
+
+ var gsp_deref_props = await GetObjectOnLocals(gsp_w_n_props, "*gsp_null");
+ await CheckProps(gsp_deref_props, new
+ {
+ Value = TValueType("System.DateTime", gs_dt.ToString()),
+ IntField = TNumber(4),
+ DTPP = TPointer("System.DateTime**")
+ }, "locals#gsp#deref");
+ {
+ var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+ await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*", is_null : true), "locals#*gsp");
+ }
+ }
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalPointersToValueTypeArrays(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ dtpa = TArray("System.DateTime*[]", 2)
+ }, "locals", num_fields : 26);
+
+ // dtpa
+ var dtpa_elems = (await CompareObjectPropertiesFor(locals, "dtpa", new []
+ {
+ TPointer("System.DateTime*"),
+ TPointer("System.DateTime*", is_null : true)
+ }));
+ {
+ var actual_elems = await CheckArrayElements(dtpa_elems, new []
+ {
+ TValueType("System.DateTime", dt.ToString()),
+ null
+ });
+
+ await CheckDateTime(actual_elems[0], "*[0]", dt);
+ }
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalPointersToGenericValueTypeArrays(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ gspa = TArray("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*[]", 3),
+ }, "locals", num_fields : 26);
+
+ // dtpa
+ var gspa_elems = await CompareObjectPropertiesFor(locals, "gspa", new []
+ {
+ TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*", is_null : true),
+ TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*"),
+ TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*"),
+ });
+ {
+ var gs_dt = new DateTime(1, 2, 3, 4, 5, 6);
+ var actual_elems = await CheckArrayElements(gspa_elems, new []
+ {
+ null,
+ TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>"),
+ TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>")
+ });
+
+ // *[1]
+ {
+ var gsp_deref_props = await GetObjectOnLocals(actual_elems[1], "*[1]");
+ await CheckProps(gsp_deref_props, new
+ {
+ Value = TValueType("System.DateTime", gs_dt.ToString()),
+ IntField = TNumber(4),
+ DTPP = TPointer("System.DateTime**")
+ }, "locals#gsp#deref");
+ {
+ var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+ await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*"), "locals#*gsp");
+
+ dtpp_props = await GetObjectOnLocals(dtpp_props, "*DTPP");
+ await CheckDateTime(dtpp_props, "**DTPP", dt);
+ }
+ }
+
+ // *[2]
+ {
+ var gsp_deref_props = await GetObjectOnLocals(actual_elems[2], "*[2]");
+ await CheckProps(gsp_deref_props, new
+ {
+ Value = TValueType("System.DateTime", gs_dt.ToString()),
+ IntField = TNumber(4),
+ DTPP = TPointer("System.DateTime**")
+ }, "locals#gsp#deref");
+ {
+ var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+ await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*", is_null : true), "locals#*gsp");
+ }
+ }
+ }
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalDoublePointersToValueTypeArrays(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ dtppa = TArray("System.DateTime**[]", 3),
+ }, "locals", num_fields : 26);
+
+ // DateTime**[] dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+ var dtppa_elems = (await CompareObjectPropertiesFor(locals, "dtppa", new []
+ {
+ TPointer("System.DateTime**"),
+ TPointer("System.DateTime**"),
+ TPointer("System.DateTime**", is_null : true)
+ }));
+
+ var exp_elems = new []
+ {
+ TPointer("System.DateTime*"),
+ TPointer("System.DateTime*", is_null : true),
+ null
+ };
+
+ var actual_elems = new JToken[exp_elems.Length];
+ for (int i = 0; i < exp_elems.Length; i++)
+ {
+ if (exp_elems[i] != null)
+ {
+ actual_elems[i] = await GetObjectOnLocals(dtppa_elems, i.ToString());
+ await CheckPointerValue(actual_elems[i], $"*[{i}]", exp_elems[i], $"dtppa->");
+ }
+ }
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersTestData))]
+ public async Task InspectLocalPointersInClasses(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ cwp = TObject("DebuggerTests.GenericClassWithPointers<System.DateTime>"),
+ cwp_null = TObject("DebuggerTests.GenericClassWithPointers<System.DateTime>")
+ }, "locals", num_fields : 26);
+
+ var cwp_props = await GetObjectOnLocals(locals, "cwp");
+ var ptr_props = await GetObjectOnLocals(cwp_props, "Ptr");
+ await CheckDateTime(ptr_props, "*Ptr", dt);
+ });
+
+ public static TheoryData<string, string, string, int, string, bool> PointersAsMethodArgsTestData =>
+ new TheoryData<string, string, string, int, string, bool>
+ { { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "PointersAsArgsTest", 2, "PointersAsArgsTest", false },
+ { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "PointersAsArgsTest", 2, "PointersAsArgsTest", true },
+ };
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersAsMethodArgsTestData))]
+ public async Task InspectPrimitiveTypePointersAsMethodArgs(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ ip = TPointer("int*"),
+ ipp = TPointer("int**"),
+ ipa = TArray("int*[]", 3),
+ ippa = TArray("int**[]", 5)
+ }, "locals", num_fields : 8);
+
+ // ip
+ var props = await GetObjectOnLocals(locals, "ip");
+ await CheckPointerValue(props, "*ip", TNumber(5), "locals");
+
+ // ipp
+ var ipp_props = await GetObjectOnLocals(locals, "ipp");
+ await CheckPointerValue(ipp_props, "*ipp", TPointer("int*"));
+
+ ipp_props = await GetObjectOnLocals(ipp_props, "*ipp");
+ await CheckPointerValue(ipp_props, "**ipp", TNumber(5));
+
+ // ipa
+ var ipa_elems = await CompareObjectPropertiesFor(locals, "ipa", new []
+ {
+ TPointer("int*"),
+ TPointer("int*"),
+ TPointer("int*", is_null : true)
+ });
+
+ await CheckArrayElements(ipa_elems, new []
+ {
+ TNumber(5),
+ TNumber(10),
+ null
+ });
+
+ // ippa
+ var ippa_elems = await CompareObjectPropertiesFor(locals, "ippa", new []
+ {
+ TPointer("int**"),
+ TPointer("int**"),
+ TPointer("int**"),
+ TPointer("int**"),
+ TPointer("int**", is_null : true)
+ });
+
+ {
+ var actual_elems = await CheckArrayElements(ippa_elems, new []
+ {
+ TPointer("int*"),
+ TPointer("int*", is_null : true),
+ TPointer("int*"),
+ TPointer("int*", is_null : true),
+ null
+ });
+
+ var val = await GetObjectOnLocals(actual_elems[0], "*[0]");
+ await CheckPointerValue(val, "**[0]", TNumber(5));
+
+ val = await GetObjectOnLocals(actual_elems[2], "*[2]");
+ await CheckPointerValue(val, "**[2]", TNumber(5));
+ }
+ });
+
+ [Theory]
+ [MemberDataAttribute(nameof(PointersAsMethodArgsTestData))]
+ public async Task InspectValueTypePointersAsMethodArgs(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,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ var dt = new DateTime(5, 6, 7, 8, 9, 10);
+ await CheckProps(locals, new
+ {
+ dtp = TPointer("System.DateTime*"),
+ dtpp = TPointer("System.DateTime**"),
+ dtpa = TArray("System.DateTime*[]", 2),
+ dtppa = TArray("System.DateTime**[]", 3)
+ }, "locals", num_fields : 8);
+
+ // *dtp
+ var dtp_props = await GetObjectOnLocals(locals, "dtp");
+ await CheckDateTime(dtp_props, "*dtp", dt);
+
+ // *dtpp
+ var dtpp_props = await GetObjectOnLocals(locals, "dtpp");
+ await CheckPointerValue(dtpp_props, "*dtpp", TPointer("System.DateTime*"), "locals");
+
+ dtpp_props = await GetObjectOnLocals(dtpp_props, "*dtpp");
+ await CheckDateTime(dtpp_props, "**dtpp", dt);
+
+ // dtpa
+ var dtpa_elems = (await CompareObjectPropertiesFor(locals, "dtpa", new []
+ {
+ TPointer("System.DateTime*"),
+ TPointer("System.DateTime*", is_null : true)
+ }));
+ {
+ var actual_elems = await CheckArrayElements(dtpa_elems, new []
+ {
+ TValueType("System.DateTime", dt.ToString()),
+ null
+ });
+
+ await CheckDateTime(actual_elems[0], "*[0]", dt);
+ }
+
+ // dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+ var dtppa_elems = (await CompareObjectPropertiesFor(locals, "dtppa", new []
+ {
+ TPointer("System.DateTime**"),
+ TPointer("System.DateTime**"),
+ TPointer("System.DateTime**", is_null : true)
+ }));
+
+ var exp_elems = new []
+ {
+ TPointer("System.DateTime*"),
+ TPointer("System.DateTime*", is_null : true),
+ null
+ };
+
+ await CheckArrayElements(dtppa_elems, exp_elems);
+ });
+
+ [Theory]
+ [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(
+ type, method, line_offset, bp_function_name,
+ "window.setTimeout(function() { " + eval_fn + " })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+
+ // this will generate the object ids
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ var complex = GetAndAssertObjectWithName(locals, "complex");
+
+ // try to deref the non-pointer object, as a pointer
+ var props = await GetProperties(complex["value"]["objectId"].Value<string>().Replace(":object:", ":pointer:"));
+ Assert.Empty(props.Values());
+
+ // try to deref an invalid pointer id
+ props = await GetProperties("dotnet:pointer:123897");
+ Assert.Empty(props.Values());
+ });
+
+ async Task<JToken[]> CheckArrayElements(JToken array, JToken[] exp_elems)
+ {
+ var actual_elems = new JToken[exp_elems.Length];
+ for (int i = 0; i < exp_elems.Length; i++)
+ {
+ if (exp_elems[i] != null)
+ {
+ actual_elems[i] = await GetObjectOnLocals(array, i.ToString());
+ await CheckPointerValue(actual_elems[i], $"*[{i}]", exp_elems[i], $"dtppa->");
+ }
+ }
+
+ return actual_elems;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+ class Inspector
+ {
+ // InspectorClient client;
+ Dictionary<string, TaskCompletionSource<JObject>> notifications = new Dictionary<string, TaskCompletionSource<JObject>>();
+ Dictionary<string, Func<JObject, CancellationToken, Task>> eventListeners = new Dictionary<string, Func<JObject, CancellationToken, Task>>();
+
+ public const string PAUSE = "pause";
+ public const string READY = "ready";
+
+ public Task<JObject> WaitFor(string what)
+ {
+ if (notifications.ContainsKey(what))
+ throw new Exception($"Invalid internal state, waiting for {what} while another wait is already setup");
+ var n = new TaskCompletionSource<JObject>();
+ notifications[what] = n;
+ return n.Task;
+ }
+
+ void NotifyOf(string what, JObject args)
+ {
+ if (!notifications.ContainsKey(what))
+ throw new Exception($"Invalid internal state, notifying of {what}, but nobody waiting");
+ notifications[what].SetResult(args);
+ notifications.Remove(what);
+ }
+
+ public void On(string evtName, Func<JObject, CancellationToken, Task> cb)
+ {
+ eventListeners[evtName] = cb;
+ }
+
+ void FailAllWaitersWithException(JObject exception)
+ {
+ foreach (var tcs in notifications.Values)
+ tcs.SetException(new ArgumentException(exception.ToString()));
+ }
+
+ async Task OnMessage(string method, JObject args, CancellationToken token)
+ {
+ //System.Console.WriteLine("OnMessage " + method + args);
+ switch (method)
+ {
+ case "Debugger.paused":
+ NotifyOf(PAUSE, args);
+ break;
+ case "Mono.runtimeReady":
+ NotifyOf(READY, args);
+ break;
+ case "Runtime.consoleAPICalled":
+ Console.WriteLine("CWL: {0}", args?["args"] ? [0] ? ["value"]);
+ break;
+ }
+ if (eventListeners.ContainsKey(method))
+ await eventListeners[method](args, token);
+ else if (String.Compare(method, "Runtime.exceptionThrown") == 0)
+ FailAllWaitersWithException(args);
+ }
+
+ public async Task Ready(Func<InspectorClient, CancellationToken, Task> cb = null, TimeSpan? span = null)
+ {
+ using(var cts = new CancellationTokenSource())
+ {
+ cts.CancelAfter(span?.Milliseconds ?? 60 * 1000); //tests have 1 minute to complete by default
+ var uri = new Uri($"ws://{TestHarnessProxy.Endpoint.Authority}/launch-chrome-and-connect");
+ using var loggerFactory = LoggerFactory.Create(
+ builder => builder.AddConsole().AddFilter(null, LogLevel.Information));
+ using(var client = new InspectorClient(loggerFactory.CreateLogger<Inspector>()))
+ {
+ await client.Connect(uri, OnMessage, async token =>
+ {
+ Task[] init_cmds = {
+ client.SendCommand("Profiler.enable", null, token),
+ client.SendCommand("Runtime.enable", null, token),
+ client.SendCommand("Debugger.enable", null, token),
+ client.SendCommand("Runtime.runIfWaitingForDebugger", null, token),
+ WaitFor(READY),
+ };
+ // await Task.WhenAll (init_cmds);
+ Console.WriteLine("waiting for the runtime to be ready");
+ await init_cmds[4];
+ Console.WriteLine("runtime ready, TEST TIME");
+ if (cb != null)
+ {
+ Console.WriteLine("await cb(client, token)");
+ await cb(client, token);
+ }
+
+ }, cts.Token);
+ await client.Close(cts.Token);
+ }
+ }
+ }
+ }
+
+ public class DebuggerTestBase
+ {
+ protected Task startTask;
+
+ static string FindTestPath()
+ {
+ //FIXME how would I locate it otherwise?
+ var test_path = Environment.GetEnvironmentVariable("TEST_SUITE_PATH");
+ //Lets try to guest
+ if (test_path != null && Directory.Exists(test_path))
+ return test_path;
+
+ var cwd = Environment.CurrentDirectory;
+ Console.WriteLine("guessing from {0}", cwd);
+ //tests run from DebuggerTestSuite/bin/Debug/netcoreapp2.1
+ var new_path = Path.Combine(cwd, "../../../../bin/debugger-test-suite");
+ if (File.Exists(Path.Combine(new_path, "debugger-driver.html")))
+ return new_path;
+
+ throw new Exception("Missing TEST_SUITE_PATH env var and could not guess path from CWD");
+ }
+
+ 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",
+ "/usr/bin/chromium-browser",
+ };
+ static string chrome_path;
+
+ static string FindChromePath()
+ {
+ if (chrome_path != null)
+ return chrome_path;
+
+ foreach (var s in PROBE_LIST)
+ {
+ if (File.Exists(s))
+ {
+ chrome_path = s;
+ Console.WriteLine($"Using chrome path: ${s}");
+ return s;
+ }
+ }
+ throw new Exception("Could not find an installed Chrome to use");
+ }
+
+ public DebuggerTestBase(string driver = "debugger-driver.html")
+ {
+ startTask = TestHarnessProxy.Start(FindChromePath(), FindTestPath(), driver);
+ }
+
+ public Task Ready() => startTask;
+
+ internal DebugTestContext ctx;
+ internal Dictionary<string, string> dicScriptsIdToUrl;
+ internal Dictionary<string, string> dicFileToUrl;
+ internal Dictionary<string, string> SubscribeToScripts(Inspector insp)
+ {
+ dicScriptsIdToUrl = new Dictionary<string, string>();
+ dicFileToUrl = new Dictionary<string, string>();
+ insp.On("Debugger.scriptParsed", async(args, c) =>
+ {
+ var script_id = args?["scriptId"]?.Value<string>();
+ var url = args["url"]?.Value<string>();
+ if (script_id.StartsWith("dotnet://"))
+ {
+ var dbgUrl = args["dotNetUrl"]?.Value<string>();
+ var arrStr = dbgUrl.Split("/");
+ dbgUrl = arrStr[0] + "/" + arrStr[1] + "/" + arrStr[2] + "/" + arrStr[arrStr.Length - 1];
+ dicScriptsIdToUrl[script_id] = dbgUrl;
+ dicFileToUrl[dbgUrl] = args["url"]?.Value<string>();
+ }
+ else if (!String.IsNullOrEmpty(url))
+ {
+ dicFileToUrl[new Uri(url).AbsolutePath] = url;
+ }
+ await Task.FromResult(0);
+ });
+ return dicScriptsIdToUrl;
+ }
+
+ internal async Task CheckInspectLocalsAtBreakpointSite(string url_key, int line, int column, string function_name, string eval_expression,
+ Action<JToken> test_fn = null, Func<JObject, Task> wait_for_event_fn = null, bool use_cfo = false)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+ var bp = await SetBreakpoint(url_key, line, column);
+
+ await EvaluateAndCheck(
+ eval_expression, url_key, line, column,
+ function_name,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ //make sure we're on the right bp
+
+ Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+ var top_frame = pause_location["callFrames"][0];
+
+ var scope = top_frame["scopeChain"][0];
+ Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+ if (wait_for_event_fn != null)
+ await wait_for_event_fn(pause_location);
+ else
+ await Task.CompletedTask;
+ },
+ locals_fn: (locals) =>
+ {
+ if (test_fn != null)
+ test_fn(locals);
+ }
+ );
+ });
+ }
+
+ // 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,
+ Action<JToken> locals_fn = null, Func<JObject, Task> wait_for_event_fn = null, bool use_cfo = false, string assembly = "debugger-test.dll", int col = 0)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+ var bp = await SetBreakpointInMethod(assembly, type, method, line_offset, col);
+
+ var args = JObject.FromObject(new { expression = eval_expression });
+ var res = await ctx.cli.SendCommand("Runtime.evaluate", args, ctx.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 pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+ if (bp_function_name != null)
+ Assert.Equal(bp_function_name, pause_location["callFrames"] ? [0] ? ["functionName"]?.Value<string>());
+
+ Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+ var top_frame = pause_location["callFrames"][0];
+
+ var scope = top_frame["scopeChain"][0];
+ Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+
+ if (wait_for_event_fn != null)
+ await wait_for_event_fn(pause_location);
+
+ if (locals_fn != null)
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ locals_fn(locals);
+ }
+ });
+ }
+
+ internal void CheckLocation(string script_loc, int line, int column, Dictionary<string, string> scripts, JToken location)
+ {
+ var loc_str = $"{ scripts[location["scriptId"].Value<string>()] }" +
+ $"#{ location ["lineNumber"].Value<int> () }" +
+ $"#{ location ["columnNumber"].Value<int> () }";
+
+ var expected_loc_str = $"{script_loc}#{line}#{column}";
+ Assert.Equal(expected_loc_str, loc_str);
+ }
+
+ internal void CheckNumber<T>(JToken locals, string name, T value)
+ {
+ foreach (var l in locals)
+ {
+ if (name != l["name"]?.Value<string>())
+ continue;
+ var val = l["value"];
+ Assert.Equal("number", val["type"]?.Value<string>());
+ Assert.Equal(value, val["value"].Value<T>());
+ return;
+ }
+ Assert.True(false, $"Could not find variable '{name}'");
+ }
+
+ internal void CheckString(JToken locals, string name, string value)
+ {
+ foreach (var l in locals)
+ {
+ if (name != l["name"]?.Value<string>())
+ continue;
+ var val = l["value"];
+ if (value == null)
+ {
+ Assert.Equal("object", val["type"]?.Value<string>());
+ Assert.Equal("null", val["subtype"]?.Value<string>());
+ }
+ else
+ {
+ Assert.Equal("string", val["type"]?.Value<string>());
+ Assert.Equal(value, val["value"]?.Value<string>());
+ }
+ return;
+ }
+ Assert.True(false, $"Could not find variable '{name}'");
+ }
+
+ internal JToken CheckSymbol(JToken locals, string name, string value)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ var val = l["value"];
+ Assert.Equal("symbol", val["type"]?.Value<string>());
+ Assert.Equal(value, val["value"]?.Value<string>());
+ return l;
+ }
+
+ internal JToken CheckObject(JToken locals, string name, string class_name, string subtype = null, bool is_null = false)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ var val = l["value"];
+ Assert.Equal("object", val["type"]?.Value<string>());
+ Assert.True(val["isValueType"] == null || !val["isValueType"].Value<bool>());
+ Assert.Equal(class_name, val["className"]?.Value<string>());
+
+ var has_null_subtype = val["subtype"] != null && val["subtype"]?.Value<string>() == "null";
+ Assert.Equal(is_null, has_null_subtype);
+ if (subtype != null)
+ Assert.Equal(subtype, val["subtype"]?.Value<string>());
+
+ return l;
+ }
+
+ internal async Task<JToken> CheckPointerValue(JToken locals, string name, JToken expected, string label = null)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ await CheckValue(l["value"], expected, $"{label ?? String.Empty}-{name}");
+ return l;
+ }
+
+ internal async Task CheckDateTime(JToken locals, string name, DateTime expected)
+ {
+ var obj = GetAndAssertObjectWithName(locals, name);
+ await CheckDateTimeValue(obj["value"], expected);
+ }
+
+ internal async Task CheckDateTimeValue(JToken value, DateTime expected)
+ {
+ AssertEqual("System.DateTime", value["className"]?.Value<string>(), "className");
+ AssertEqual(expected.ToString(), value["description"]?.Value<string>(), "description");
+
+ var members = await GetProperties(value["objectId"]?.Value<string>());
+
+ // not checking everything
+ CheckNumber(members, "Year", expected.Year);
+ CheckNumber(members, "Month", expected.Month);
+ CheckNumber(members, "Day", expected.Day);
+ CheckNumber(members, "Hour", expected.Hour);
+ CheckNumber(members, "Minute", expected.Minute);
+ CheckNumber(members, "Second", expected.Second);
+
+ // FIXME: check some float properties too
+ }
+
+ internal JToken CheckBool(JToken locals, string name, bool expected)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ var val = l["value"];
+ Assert.Equal("boolean", val["type"]?.Value<string>());
+ if (val["value"] == null)
+ Assert.True(false, "expected bool value not found for variable named {name}");
+ Assert.Equal(expected, val["value"]?.Value<bool>());
+
+ return l;
+ }
+
+ internal void CheckContentValue(JToken token, string value)
+ {
+ var val = token["value"].Value<string>();
+ Assert.Equal(value, val);
+ }
+
+ internal JToken CheckValueType(JToken locals, string name, string class_name)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ var val = l["value"];
+ Assert.Equal("object", val["type"]?.Value<string>());
+ Assert.True(val["isValueType"] != null && val["isValueType"].Value<bool>());
+ Assert.Equal(class_name, val["className"]?.Value<string>());
+ return l;
+ }
+
+ internal JToken CheckEnum(JToken locals, string name, string class_name, string descr)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ var val = l["value"];
+ Assert.Equal("object", val["type"]?.Value<string>());
+ Assert.True(val["isEnum"] != null && val["isEnum"].Value<bool>());
+ Assert.Equal(class_name, val["className"]?.Value<string>());
+ Assert.Equal(descr, val["description"]?.Value<string>());
+ return l;
+ }
+
+ internal void CheckArray(JToken locals, string name, string class_name)
+ {
+ foreach (var l in locals)
+ {
+ if (name != l["name"]?.Value<string>())
+ continue;
+
+ var val = l["value"];
+ Assert.Equal("object", val["type"]?.Value<string>());
+ Assert.Equal("array", val["subtype"]?.Value<string>());
+ Assert.Equal(class_name, val["className"]?.Value<string>());
+
+ //FIXME: elements?
+ return;
+ }
+ Assert.True(false, $"Could not find variable '{name}'");
+ }
+
+ internal JToken GetAndAssertObjectWithName(JToken obj, string name)
+ {
+ var l = obj.FirstOrDefault(jt => jt["name"]?.Value<string>() == name);
+ if (l == null)
+ Assert.True(false, $"Could not find variable '{name}'");
+ return l;
+ }
+
+ internal async Task<Result> SendCommand(string method, JObject args)
+ {
+ var res = await ctx.cli.SendCommand(method, args, ctx.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 ()}");
+ }
+ return res;
+ }
+
+ internal async Task<Result> Evaluate(string expression)
+ {
+ return await SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expression }));
+ }
+
+ internal void AssertLocation(JObject args, string methodName)
+ {
+ Assert.Equal(methodName, args["callFrames"] ? [0] ? ["functionName"]?.Value<string>());
+ }
+
+ // Place a breakpoint in the given method and run until its hit
+ // Return the Debugger.paused data
+ internal async Task<JObject> 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 ctx.insp.WaitFor(Inspector.PAUSE);
+ AssertLocation(wait_res, "locals_inner");
+ return wait_res;
+ }
+
+ internal async Task<JObject> StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name,
+ Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null, int times = 1)
+ {
+ for (int i = 0; i < times - 1; i++)
+ {
+ await SendCommandAndCheck(null, $"Debugger.step{kind.ToString ()}", null, -1, -1, null);
+ }
+
+ // Check for method/line etc only at the last step
+ return await SendCommandAndCheck(
+ null, $"Debugger.step{kind.ToString ()}", script_loc, line, column, function_name,
+ wait_for_event_fn : wait_for_event_fn,
+ locals_fn : locals_fn);
+ }
+
+ internal async Task<JObject> EvaluateAndCheck(string expression, string script_loc, int line, int column, string function_name,
+ Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null) => await SendCommandAndCheck(
+ JObject.FromObject(new { expression = expression }),
+ "Runtime.evaluate", script_loc, line, column, function_name,
+ wait_for_event_fn : wait_for_event_fn,
+ locals_fn : locals_fn);
+
+ internal async Task<JObject> SendCommandAndCheck(JObject args, string method, string script_loc, int line, int column, string function_name,
+ Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null, string waitForEvent = Inspector.PAUSE)
+ {
+ var res = await ctx.cli.SendCommand(method, args, ctx.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 ctx.insp.WaitFor(waitForEvent);
+
+ if (function_name != null)
+ Assert.Equal(function_name, wait_res["callFrames"] ? [0] ? ["functionName"]?.Value<string>());
+
+ if (script_loc != null)
+ CheckLocation(script_loc, line, column, ctx.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<string>());
+ locals_fn(locals);
+ }
+
+ return wait_res;
+ }
+
+ internal async Task CheckDelegate(JToken locals, string name, string className, string target)
+ {
+ var l = GetAndAssertObjectWithName(locals, name);
+ var val = l["value"];
+
+ await CheckDelegate(l, TDelegate(className, target), name);
+ }
+
+ internal async Task CheckDelegate(JToken actual_val, JToken exp_val, string label)
+ {
+ AssertEqual("object", actual_val["type"]?.Value<string>(), $"{label}-type");
+ AssertEqual(exp_val["className"]?.Value<string>(), actual_val["className"]?.Value<string>(), $"{label}-className");
+
+ var actual_target = actual_val["description"]?.Value<string>();
+ Assert.True(actual_target != null, $"${label}-description");
+ var exp_target = exp_val["target"].Value<string>();
+
+ CheckDelegateTarget(actual_target, exp_target);
+
+ var del_props = await GetProperties(actual_val["objectId"]?.Value<string>());
+ AssertEqual(1, del_props.Count(), $"${label}-delegate-properties-count");
+
+ var obj = del_props.Where(jt => jt["name"]?.Value<string>() == "Target").FirstOrDefault();
+ Assert.True(obj != null, $"[{label}] Property named 'Target' found found in delegate properties");
+
+ AssertEqual("symbol", obj["value"] ? ["type"]?.Value<string>(), $"{label}#Target#type");
+ CheckDelegateTarget(obj["value"] ? ["value"]?.Value<string>(), exp_target);
+
+ return;
+
+ void CheckDelegateTarget(string actual_target, string exp_target)
+ {
+ var parts = exp_target.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length == 1)
+ {
+ // not a generated method
+ AssertEqual(exp_target, actual_target, $"{label}-description");
+ }
+ else
+ {
+ bool prefix = actual_target.StartsWith(parts[0], StringComparison.Ordinal);
+ Assert.True(prefix, $"{label}-description, Expected target to start with '{parts[0]}'. Actual: '{actual_target}'");
+
+ var remaining = actual_target.Substring(parts[0].Length);
+ bool suffix = remaining.EndsWith(parts[1], StringComparison.Ordinal);
+ Assert.True(prefix, $"{label}-description, Expected target to end with '{parts[1]}'. Actual: '{remaining}'");
+ }
+ }
+ }
+
+ internal async Task CheckCustomType(JToken actual_val, JToken exp_val, string label)
+ {
+ var ctype = exp_val["__custom_type"].Value<string>();
+ switch (ctype)
+ {
+ case "delegate":
+ await CheckDelegate(actual_val, exp_val, label);
+ break;
+
+ case "pointer":
+ {
+
+ if (exp_val["is_null"]?.Value<bool>() == true)
+ {
+ AssertEqual("symbol", actual_val["type"]?.Value<string>(), $"{label}-type");
+
+ var exp_val_str = $"({exp_val ["type_name"]?.Value<string>()}) 0";
+ AssertEqual(exp_val_str, actual_val["value"]?.Value<string>(), $"{label}-value");
+ AssertEqual(exp_val_str, actual_val["description"]?.Value<string>(), $"{label}-description");
+ }
+ else if (exp_val["is_void"]?.Value<bool>() == true)
+ {
+ AssertEqual("symbol", actual_val["type"]?.Value<string>(), $"{label}-type");
+
+ var exp_val_str = $"({exp_val ["type_name"]?.Value<string>()})";
+ AssertStartsWith(exp_val_str, actual_val["value"]?.Value<string>(), $"{label}-value");
+ AssertStartsWith(exp_val_str, actual_val["description"]?.Value<string>(), $"{label}-description");
+ }
+ else
+ {
+ AssertEqual("object", actual_val["type"]?.Value<string>(), $"{label}-type");
+
+ var exp_prefix = $"({exp_val ["type_name"]?.Value<string>()})";
+ AssertStartsWith(exp_prefix, actual_val["className"]?.Value<string>(), $"{label}-className");
+ AssertStartsWith(exp_prefix, actual_val["description"]?.Value<string>(), $"{label}-description");
+ Assert.False(actual_val["className"]?.Value<string>() == $"{exp_prefix} 0", $"[{label}] Expected a non-null value, but got {actual_val}");
+ }
+ break;
+ }
+
+ case "getter":
+ {
+ // For getter, `actual_val` is not `.value`, instead it's the container object
+ // which has a `.get` instead of a `.value`
+ var get = actual_val["get"];
+ Assert.True(get != null, $"[{label}] No `get` found. {(actual_val != null ? "Make sure to pass the container object for testing getters, and not the ['value']": String.Empty)}");
+
+ AssertEqual("Function", get["className"]?.Value<string>(), $"{label}-className");
+ AssertStartsWith($"get {exp_val ["type_name"]?.Value<string> ()} ()", get["description"]?.Value<string>(), $"{label}-description");
+ AssertEqual("function", get["type"]?.Value<string>(), $"{label}-type");
+
+ break;
+ }
+
+ case "ignore_me":
+ // nothing to check ;)
+ break;
+
+ default:
+ throw new ArgumentException($"{ctype} not supported");
+ }
+ }
+
+ internal async Task CheckProps(JToken actual, object exp_o, string label, int num_fields = -1)
+ {
+ if (exp_o.GetType().IsArray || exp_o is JArray)
+ {
+ if (!(actual is JArray actual_arr))
+ {
+ Assert.True(false, $"[{label}] Expected to get an array here but got {actual}");
+ return;
+ }
+
+ var exp_v_arr = JArray.FromObject(exp_o);
+ AssertEqual(exp_v_arr.Count, actual_arr.Count(), $"{label}-count");
+
+ for (int i = 0; i < exp_v_arr.Count; i++)
+ {
+ var exp_i = exp_v_arr[i];
+ var act_i = actual_arr[i];
+
+ AssertEqual(i.ToString(), act_i["name"]?.Value<string>(), $"{label}-[{i}].name");
+ if (exp_i != null)
+ await CheckValue(act_i["value"], exp_i, $"{label}-{i}th value");
+ }
+
+ return;
+ }
+
+ // Not an array
+ var exp = exp_o as JObject;
+ if (exp == null)
+ exp = JObject.FromObject(exp_o);
+
+ num_fields = num_fields < 0 ? exp.Values<JToken>().Count() : num_fields;
+ Assert.True(num_fields == actual.Count(), $"[{label}] Number of fields don't match, Expected: {num_fields}, Actual: {actual.Count()}");
+
+ foreach (var kvp in exp)
+ {
+ var exp_name = kvp.Key;
+ var exp_val = kvp.Value;
+
+ var actual_obj = actual.FirstOrDefault(jt => jt["name"]?.Value<string>() == exp_name);
+ if (actual_obj == null)
+ {
+ Assert.True(actual_obj != null, $"[{label}] Could not find property named '{exp_name}'");
+ }
+
+ Assert.True(actual_obj != null, $"[{label}] not value found for property named '{exp_name}'");
+
+ var actual_val = actual_obj["value"];
+ if (exp_val.Type == JTokenType.Array)
+ {
+ var actual_props = await GetProperties(actual_val["objectId"]?.Value<string>());
+ await CheckProps(actual_props, exp_val, $"{label}-{exp_name}");
+ }
+ else if (exp_val["__custom_type"] != null && exp_val["__custom_type"]?.Value<string>() == "getter")
+ {
+ // hack: for getters, actual won't have a .value
+ await CheckCustomType(actual_obj, exp_val, $"{label}#{exp_name}");
+ }
+ else
+ {
+ await CheckValue(actual_val, exp_val, $"{label}#{exp_name}");
+ }
+ }
+ }
+
+ internal async Task CheckValue(JToken actual_val, JToken exp_val, string label)
+ {
+ if (exp_val["__custom_type"] != null)
+ {
+ await CheckCustomType(actual_val, exp_val, label);
+ return;
+ }
+
+ if (exp_val["type"] == null && actual_val["objectId"] != null)
+ {
+ var new_val = await GetProperties(actual_val["objectId"].Value<string>());
+ await CheckProps(new_val, exp_val, $"{label}-{actual_val["objectId"]?.Value<string>()}");
+ return;
+ }
+
+ foreach (var jp in exp_val.Values<JProperty>())
+ {
+ if (jp.Value.Type == JTokenType.Object)
+ {
+ var new_val = await GetProperties(actual_val["objectId"].Value<string>());
+ await CheckProps(new_val, jp.Value, $"{label}-{actual_val["objectId"]?.Value<string>()}");
+
+ continue;
+ }
+
+ var exp_val_str = jp.Value.Value<string>();
+ bool null_or_empty_exp_val = String.IsNullOrEmpty(exp_val_str);
+
+ var actual_field_val = actual_val.Values<JProperty>().FirstOrDefault(a_jp => a_jp.Name == jp.Name);
+ var actual_field_val_str = actual_field_val?.Value?.Value<string>();
+ if (null_or_empty_exp_val && String.IsNullOrEmpty(actual_field_val_str))
+ continue;
+
+ Assert.True(actual_field_val != null, $"[{label}] Could not find value field named {jp.Name}");
+
+ Assert.True(exp_val_str == actual_field_val_str,
+ $"[{label}] Value for json property named {jp.Name} didn't match.\n" +
+ $"Expected: {jp.Value.Value<string> ()}\n" +
+ $"Actual: {actual_field_val.Value.Value<string> ()}");
+ }
+ }
+
+ internal async Task<JToken> GetLocalsForFrame(JToken frame, string script_loc, int line, int column, string function_name)
+ {
+ CheckLocation(script_loc, line, column, ctx.scripts, frame["location"]);
+ Assert.Equal(function_name, frame["functionName"].Value<string>());
+
+ return await GetProperties(frame["callFrameId"].Value<string>());
+ }
+
+ internal async Task<JToken> GetObjectOnFrame(JToken frame, string name)
+ {
+ var locals = await GetProperties(frame["callFrameId"].Value<string>());
+ return await GetObjectOnLocals(locals, name);
+ }
+
+ // Find an object with @name, *fetch* the object, and check against @o
+ internal async Task<JToken> CompareObjectPropertiesFor(JToken locals, string name, object o, string label = null, int num_fields = -1)
+ {
+ if (label == null)
+ label = name;
+ var props = await GetObjectOnLocals(locals, name);
+ try
+ {
+ if (o != null)
+ await CheckProps(props, o, label, num_fields);
+ return props;
+ }
+ catch
+ {
+ throw;
+ }
+ }
+
+ internal async Task<JToken> GetObjectOnLocals(JToken locals, string name)
+ {
+ var obj = GetAndAssertObjectWithName(locals, name);
+ var objectId = obj["value"]["objectId"]?.Value<string>();
+ Assert.True(!String.IsNullOrEmpty(objectId), $"No objectId found for {name}");
+
+ return await GetProperties(objectId);
+ }
+
+ /* @fn_args is for use with `Runtime.callFunctionOn` only */
+ internal async Task<JToken> GetProperties(string id, JToken fn_args = null)
+ {
+ if (ctx.UseCallFunctionOnBeforeGetProperties && !id.StartsWith("dotnet:scope:"))
+ {
+ var fn_decl = "function () { return this; }";
+ var cfo_args = JObject.FromObject(new
+ {
+ functionDeclaration = fn_decl,
+ objectId = id
+ });
+ if (fn_args != null)
+ cfo_args["arguments"] = fn_args;
+
+ var result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+ AssertEqual(true, result.IsOk, $"Runtime.getProperties failed for {cfo_args.ToString ()}, with Result: {result}");
+ id = result.Value["result"] ? ["objectId"]?.Value<string>();
+ }
+
+ var get_prop_req = JObject.FromObject(new
+ {
+ objectId = id
+ });
+
+ var frame_props = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token);
+ if (!frame_props.IsOk)
+ Assert.True(false, $"Runtime.getProperties failed for {get_prop_req.ToString ()}, with Result: {frame_props}");
+
+ var locals = frame_props.Value["result"];
+ // FIXME: Should be done when generating the list in library_mono.js, but not sure yet
+ // whether to remove it, and how to do it correctly.
+ if (locals is JArray)
+ {
+ foreach (var p in locals)
+ {
+ if (p["name"]?.Value<string>() == "length" && p["enumerable"]?.Value<bool>() != true)
+ {
+ p.Remove();
+ break;
+ }
+ }
+ }
+
+ return locals;
+ }
+
+ internal async Task<JToken> EvaluateOnCallFrame(string id, string expression)
+ {
+ var evaluate_req = JObject.FromObject(new
+ {
+ callFrameId = id,
+ expression = expression
+ });
+
+ var frame_evaluate = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token);
+ if (!frame_evaluate.IsOk)
+ Assert.True(false, $"Debugger.evaluateOnCallFrame failed for {evaluate_req.ToString ()}, with Result: {frame_evaluate}");
+
+ var evaluate_result = frame_evaluate.Value["result"];
+ return evaluate_result;
+ }
+
+ internal async Task<Result> SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false)
+ {
+ var bp1_req = !use_regex ?
+ JObject.FromObject(new { lineNumber = line, columnNumber = column, url = dicFileToUrl[url_key], }) :
+ JObject.FromObject(new { lineNumber = line, columnNumber = column, urlRegex = url_key, });
+
+ var bp1_res = await ctx.cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, ctx.token);
+ Assert.True(expect_ok ? bp1_res.IsOk : bp1_res.IsErr);
+
+ return bp1_res;
+ }
+
+ internal async Task<Result> SetBreakpointInMethod(string assembly, string type, string method, int lineOffset = 0, int col = 0)
+ {
+ var req = JObject.FromObject(new { assemblyName = assembly, typeName = type, methodName = method, lineOffset = lineOffset });
+
+ // Protocol extension
+ var res = await ctx.cli.SendCommand("DotnetDebugger.getMethodLocation", req, ctx.token);
+ Assert.True(res.IsOk);
+
+ var m_url = res.Value["result"]["url"].Value<string>();
+ var m_line = res.Value["result"]["line"].Value<int>();
+
+ var bp1_req = JObject.FromObject(new
+ {
+ lineNumber = m_line + lineOffset,
+ columnNumber = col,
+ url = m_url
+ });
+
+ res = await ctx.cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, ctx.token);
+ Assert.True(res.IsOk);
+
+ return res;
+ }
+
+ internal void AssertEqual(object expected, object actual, string label) => Assert.True(expected?.Equals(actual),
+ $"[{label}]\n" +
+ $"Expected: {expected?.ToString()}\n" +
+ $"Actual: {actual?.ToString()}\n");
+
+ internal void AssertStartsWith(string expected, string actual, string label) => Assert.True(actual?.StartsWith(expected), $"[{label}] Does not start with the expected string\nExpected: {expected}\nActual: {actual}");
+
+ internal static Func<int, int, string, string, object> TSimpleClass = (X, Y, Id, Color) => new
+ {
+ X = TNumber(X),
+ Y = TNumber(Y),
+ Id = TString(Id),
+ Color = TEnum("DebuggerTests.RGB", Color),
+ PointWithCustomGetter = TGetter("PointWithCustomGetter")
+ };
+
+ internal static Func<int, int, string, string, object> TPoint = (X, Y, Id, Color) => new
+ {
+ X = TNumber(X),
+ Y = TNumber(Y),
+ Id = TString(Id),
+ Color = TEnum("DebuggerTests.RGB", Color),
+ };
+
+ //FIXME: um maybe we don't need to convert jobject right here!
+ internal static JObject TString(string value) =>
+ value == null ?
+ TObject("string", is_null : true) :
+ JObject.FromObject(new { type = "string", value = @value, description = @value });
+
+ internal static JObject TNumber(int value) =>
+ JObject.FromObject(new { type = "number", value = @value.ToString(), description = value.ToString() });
+
+ internal static JObject TValueType(string className, string description = null, object members = null) =>
+ JObject.FromObject(new { type = "object", isValueType = true, className = className, description = description ?? className });
+
+ internal static JObject TEnum(string className, string descr, object members = null) =>
+ JObject.FromObject(new { type = "object", isEnum = true, className = className, description = descr });
+
+ internal static JObject TObject(string className, string description = null, bool is_null = false) =>
+ is_null ?
+ JObject.FromObject(new { type = "object", className = className, description = description ?? className, subtype = is_null ? "null" : null }) :
+ JObject.FromObject(new { type = "object", className = className, description = description ?? className });
+
+ internal static JObject TArray(string className, int length = 0) => JObject.FromObject(new { type = "object", className = className, description = $"{className}({length})", subtype = "array" });
+
+ 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 });
+
+ /*
+ For target names with generated method names like
+ `void <ActionTSignatureTest>b__11_0 (Math.GenericStruct<int[]>)`
+
+ .. pass target "as `target: "void <ActionTSignatureTest>|(Math.GenericStruct<int[]>)"`
+ */
+ internal static JObject TDelegate(string className, string target) => JObject.FromObject(new
+ {
+ __custom_type = "delegate",
+ className = className,
+ target = target
+ });
+
+ internal static JObject TPointer(string type_name, bool is_null = false) => JObject.FromObject(new { __custom_type = "pointer", type_name = type_name, is_null = is_null, is_void = type_name.StartsWith("void*") });
+
+ internal static JObject TIgnore() => JObject.FromObject(new { __custom_type = "ignore_me" });
+
+ internal static JObject TGetter(string type) => JObject.FromObject(new { __custom_type = "getter", type_name = type });
+ }
+
+ class DebugTestContext
+ {
+ public InspectorClient cli;
+ public Inspector insp;
+ public CancellationToken token;
+ public Dictionary<string, string> scripts;
+
+ public bool UseCallFunctionOnBeforeGetProperties;
+
+ public DebugTestContext(InspectorClient cli, Inspector insp, CancellationToken token, Dictionary<string, string> scripts)
+ {
+ this.cli = cli;
+ this.insp = insp;
+ this.token = token;
+ this.scripts = scripts;
+ }
+ }
+
+ enum StepKind
+ {
+ Into,
+ Over,
+ Out
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+[assembly : CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
+
+namespace DebuggerTests
+{
+
+ public class SourceList : DebuggerTestBase
+ {
+
+ [Fact]
+ public async Task CheckThatAllSourcesAreSent()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ //all sources are sent before runtime ready is sent, nothing to check
+ await insp.Ready();
+ Assert.Contains("dotnet://debugger-test.dll/debugger-test.cs", scripts.Values);
+ Assert.Contains("dotnet://debugger-test.dll/debugger-test2.cs", scripts.Values);
+ Assert.Contains("dotnet://debugger-test.dll/dependency.cs", scripts.Values);
+ }
+
+ [Fact]
+ public async Task CreateGoodBreakpoint()
+ {
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var bp1_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8);
+
+ Assert.EndsWith("debugger-test.cs", bp1_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+ Assert.NotNull(loc["scriptId"]);
+ Assert.Equal("dotnet://debugger-test.dll/debugger-test.cs", scripts[loc["scriptId"]?.Value<string>()]);
+ Assert.Equal(10, loc["lineNumber"]);
+ Assert.Equal(8, loc["columnNumber"]);
+ });
+ }
+
+ [Fact]
+ public async Task CreateJSBreakpoint()
+ {
+ // Test that js breakpoints get set correctly
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ // 13 24
+ // 13 31
+ var bp1_res = await SetBreakpoint("/debugger-driver.html", 13, 24);
+
+ Assert.EndsWith("debugger-driver.html", bp1_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+ Assert.NotNull(loc["scriptId"]);
+ Assert.Equal(13, loc["lineNumber"]);
+ Assert.Equal(24, loc["columnNumber"]);
+
+ var bp2_res = await SetBreakpoint("/debugger-driver.html", 13, 31);
+
+ Assert.EndsWith("debugger-driver.html", bp2_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp2_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc2 = bp2_res.Value["locations"]?.Value<JArray>() [0];
+
+ Assert.NotNull(loc2["scriptId"]);
+ Assert.Equal(13, loc2["lineNumber"]);
+ Assert.Equal(31, loc2["columnNumber"]);
+ });
+ }
+
+ [Fact]
+ public async Task CreateJS0Breakpoint()
+ {
+ // Test that js column 0 does as expected
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ // 13 24
+ // 13 31
+ var bp1_res = await SetBreakpoint("/debugger-driver.html", 13, 0);
+
+ Assert.EndsWith("debugger-driver.html", bp1_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+ Assert.NotNull(loc["scriptId"]);
+ Assert.Equal(13, loc["lineNumber"]);
+ Assert.Equal(24, loc["columnNumber"]);
+
+ var bp2_res = await SetBreakpoint("/debugger-driver.html", 13, 31);
+
+ Assert.EndsWith("debugger-driver.html", bp2_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp2_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc2 = bp2_res.Value["locations"]?.Value<JArray>() [0];
+
+ Assert.NotNull(loc2["scriptId"]);
+ Assert.Equal(13, loc2["lineNumber"]);
+ Assert.Equal(31, loc2["columnNumber"]);
+ });
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(50)]
+ public async Task CheckMultipleBreakpointsOnSameLine(int col)
+ {
+ var insp = new Inspector();
+
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var bp1_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-array-test.cs", 219, col);
+ Assert.EndsWith("debugger-array-test.cs", bp1_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+ CheckLocation("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 50, scripts, loc);
+
+ var bp2_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 55);
+ Assert.EndsWith("debugger-array-test.cs", bp2_res.Value["breakpointId"].ToString());
+ Assert.Equal(1, bp2_res.Value["locations"]?.Value<JArray>()?.Count);
+
+ var loc2 = bp2_res.Value["locations"]?.Value<JArray>() [0];
+
+ CheckLocation("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 55, scripts, loc2);
+ });
+ }
+
+ [Fact]
+ public async Task CreateBadBreakpoint()
+ {
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ var bp1_req = JObject.FromObject(new
+ {
+ lineNumber = 8,
+ columnNumber = 2,
+ url = "dotnet://debugger-test.dll/this-file-doesnt-exist.cs",
+ });
+
+ var bp1_res = await cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, token);
+
+ Assert.True(bp1_res.IsOk);
+ Assert.Empty(bp1_res.Value["locations"].Values<object>());
+ //Assert.Equal ((int)MonoErrorCodes.BpNotFound, bp1_res.Error ["code"]?.Value<int> ());
+ });
+ }
+
+ [Fact]
+ public async Task CreateGoodBreakpointAndHit()
+ {
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8);
+
+ var eval_req = JObject.FromObject(new
+ {
+ expression = "window.setTimeout(function() { invoke_add(); }, 1);",
+ });
+
+ await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_add(); }, 1);",
+ "dotnet://debugger-test.dll/debugger-test.cs", 10, 8,
+ "IntAdd",
+ wait_for_event_fn: (pause_location) =>
+ {
+ Assert.Equal("other", pause_location["reason"]?.Value<string>());
+ Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+ var top_frame = pause_location["callFrames"][0];
+ Assert.Equal("IntAdd", top_frame["functionName"].Value<string>());
+ Assert.Contains("debugger-test.cs", top_frame["url"].Value<string>());
+
+ CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, top_frame["functionLocation"]);
+
+ //now check the scope
+ var scope = top_frame["scopeChain"][0];
+ Assert.Equal("local", scope["type"]);
+ Assert.Equal("IntAdd", scope["name"]);
+
+ Assert.Equal("object", scope["object"]["type"]);
+ Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+ 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"]);
+ return Task.CompletedTask;
+ }
+ );
+
+ });
+ }
+
+ [Fact]
+ public async Task ExceptionThrownInJS()
+ {
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ var eval_req = JObject.FromObject(new
+ {
+ expression = "invoke_bad_js_test();"
+ });
+
+ var eval_res = await cli.SendCommand("Runtime.evaluate", eval_req, token);
+ Assert.True(eval_res.IsErr);
+ Assert.Equal("Uncaught", eval_res.Error["exceptionDetails"] ? ["text"]?.Value<string>());
+ });
+ }
+
+ [Fact]
+ public async Task ExceptionThrownInJSOutOfBand()
+ {
+ var insp = new Inspector();
+
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ await SetBreakpoint("/debugger-driver.html", 27, 2);
+
+ var eval_req = JObject.FromObject(new
+ {
+ expression = "window.setTimeout(function() { invoke_bad_js_test(); }, 1);",
+ });
+
+ var eval_res = await cli.SendCommand("Runtime.evaluate", eval_req, token);
+ // Response here will be the id for the timer from JS!
+ Assert.True(eval_res.IsOk);
+
+ var ex = await Assert.ThrowsAsync<ArgumentException>(async() => await insp.WaitFor("Runtime.exceptionThrown"));
+ var ex_json = JObject.Parse(ex.Message);
+ Assert.Equal(dicFileToUrl["/debugger-driver.html"], ex_json["exceptionDetails"] ? ["url"]?.Value<string>());
+ });
+
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsAtBreakpointSite(bool use_cfo) =>
+ await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, "IntAdd",
+ "window.setTimeout(function() { invoke_add(); }, 1);",
+ use_cfo : use_cfo,
+ test_fn: (locals) =>
+ {
+ CheckNumber(locals, "a", 10);
+ CheckNumber(locals, "b", 20);
+ CheckNumber(locals, "c", 30);
+ CheckNumber(locals, "d", 0);
+ CheckNumber(locals, "e", 0);
+ }
+ );
+
+ [Fact]
+ public async Task InspectPrimitiveTypeLocalsAtBreakpointSite() =>
+ await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", 154, 8, "PrimitiveTypesTest",
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:PrimitiveTypesTest'); }, 1);",
+ test_fn: (locals) =>
+ {
+ CheckSymbol(locals, "c0", "8364 '€'");
+ CheckSymbol(locals, "c1", "65 'A'");
+ }
+ );
+
+ [Fact]
+ public async Task InspectLocalsTypesAtBreakpointSite() =>
+ await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test2.cs", 48, 8, "Types",
+ "window.setTimeout(function() { invoke_static_method (\"[debugger-test] Fancy:Types\")(); }, 1);",
+ use_cfo : false,
+ test_fn: (locals) =>
+ {
+ CheckNumber(locals, "dPI", Math.PI);
+ CheckNumber(locals, "fPI", (float) Math.PI);
+ CheckNumber(locals, "iMax", int.MaxValue);
+ CheckNumber(locals, "iMin", int.MinValue);
+ CheckNumber(locals, "uiMax", uint.MaxValue);
+ CheckNumber(locals, "uiMin", uint.MinValue);
+
+ CheckNumber(locals, "l", uint.MaxValue * (long) 2);
+ //CheckNumber (locals, "lMax", long.MaxValue); // cannot be represented as double
+ //CheckNumber (locals, "lMin", long.MinValue); // cannot be represented as double
+
+ CheckNumber(locals, "sbMax", sbyte.MaxValue);
+ CheckNumber(locals, "sbMin", sbyte.MinValue);
+ CheckNumber(locals, "bMax", byte.MaxValue);
+ CheckNumber(locals, "bMin", byte.MinValue);
+
+ CheckNumber(locals, "sMax", short.MaxValue);
+ CheckNumber(locals, "sMin", short.MinValue);
+ CheckNumber(locals, "usMin", ushort.MinValue);
+ CheckNumber(locals, "usMax", ushort.MaxValue);
+ }
+ );
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsWithGenericTypesAtBreakpointSite(bool use_cfo) =>
+ await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-test.cs", 74, 8, "GenericTypesTest",
+ "window.setTimeout(function() { invoke_generic_types_test (); }, 1);",
+ use_cfo : use_cfo,
+ test_fn: (locals) =>
+ {
+ CheckObject(locals, "list", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>");
+ CheckObject(locals, "list_null", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>", is_null : true);
+
+ CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]");
+ CheckObject(locals, "list_arr_null", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", is_null : true);
+
+ // Unused locals
+ CheckObject(locals, "list_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>");
+ CheckObject(locals, "list_null_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>", is_null : true);
+
+ CheckObject(locals, "list_arr_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]");
+ CheckObject(locals, "list_arr_null_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", is_null : true);
+ }
+ );
+
+ object TGenericStruct(string typearg, string stringField) => new
+ {
+ List = TObject($"System.Collections.Generic.List<{typearg}>"),
+ StringField = TString(stringField)
+ };
+
+ [Fact]
+ public async Task RuntimeGetPropertiesWithInvalidScopeIdTest()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 49, 8);
+
+ await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_delegates_test (); }, 1);",
+ "dotnet://debugger-test.dll/debugger-test.cs", 49, 8,
+ "DelegatesTest",
+ wait_for_event_fn : async(pause_location) =>
+ {
+ //make sure we're on the right bp
+ Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+ var top_frame = pause_location["callFrames"][0];
+
+ var scope = top_frame["scopeChain"][0];
+ Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+
+ // Try to get an invalid scope!
+ var get_prop_req = JObject.FromObject(new
+ {
+ objectId = "dotnet:scope:23490871",
+ });
+
+ var frame_props = await cli.SendCommand("Runtime.getProperties", get_prop_req, token);
+ Assert.True(frame_props.IsErr);
+ }
+ );
+ });
+ }
+
+ [Fact]
+ public async Task TrivalStepping()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8);
+
+ await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_add(); }, 1);",
+ "dotnet://debugger-test.dll/debugger-test.cs", 10, 8,
+ "IntAdd",
+ wait_for_event_fn: (pause_location) =>
+ {
+ //make sure we're on the right bp
+ Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+ var top_frame = pause_location["callFrames"][0];
+ CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, top_frame["functionLocation"]);
+ return Task.CompletedTask;
+ }
+ );
+
+ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 11, 8, "IntAdd",
+ wait_for_event_fn: (pause_location) =>
+ {
+ var top_frame = pause_location["callFrames"][0];
+ CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, top_frame["functionLocation"]);
+ return Task.CompletedTask;
+ }
+ );
+ });
+ }
+
+ [Fact]
+ public async Task InspectLocalsDuringStepping()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+ await SetBreakpoint(debugger_test_loc, 10, 8);
+
+ await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_add(); }, 1);",
+ debugger_test_loc, 10, 8, "IntAdd",
+ locals_fn: (locals) =>
+ {
+ CheckNumber(locals, "a", 10);
+ CheckNumber(locals, "b", 20);
+ CheckNumber(locals, "c", 30);
+ CheckNumber(locals, "d", 0);
+ CheckNumber(locals, "e", 0);
+ }
+ );
+
+ await StepAndCheck(StepKind.Over, debugger_test_loc, 11, 8, "IntAdd",
+ locals_fn: (locals) =>
+ {
+ CheckNumber(locals, "a", 10);
+ CheckNumber(locals, "b", 20);
+ CheckNumber(locals, "c", 30);
+ CheckNumber(locals, "d", 50);
+ CheckNumber(locals, "e", 0);
+ }
+ );
+
+ //step and get locals
+ await StepAndCheck(StepKind.Over, debugger_test_loc, 12, 8, "IntAdd",
+ locals_fn: (locals) =>
+ {
+ CheckNumber(locals, "a", 10);
+ CheckNumber(locals, "b", 20);
+ CheckNumber(locals, "c", 30);
+ CheckNumber(locals, "d", 50);
+ CheckNumber(locals, "e", 60);
+ }
+ );
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsInPreviousFramesDuringSteppingIn2(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+ var dep_cs_loc = "dotnet://debugger-test.dll/dependency.cs";
+ await SetBreakpoint(dep_cs_loc, 33, 8);
+
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+
+ // Will stop in Complex.DoEvenMoreStuff
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_use_complex (); }, 1);",
+ dep_cs_loc, 33, 8, "DoEvenMoreStuff",
+ locals_fn: (locals) =>
+ {
+ Assert.Single(locals);
+ CheckObject(locals, "this", "Simple.Complex");
+ }
+ );
+
+ var props = await GetObjectOnFrame(pause_location["callFrames"][0], "this");
+ Assert.Equal(3, props.Count());
+ CheckNumber(props, "A", 10);
+ CheckString(props, "B", "xx");
+ CheckObject(props, "c", "object");
+
+ // Check UseComplex frame
+ var locals_m1 = await GetLocalsForFrame(pause_location["callFrames"][3], debugger_test_loc, 23, 8, "UseComplex");
+ Assert.Equal(7, locals_m1.Count());
+
+ CheckNumber(locals_m1, "a", 10);
+ CheckNumber(locals_m1, "b", 20);
+ CheckObject(locals_m1, "complex", "Simple.Complex");
+ CheckNumber(locals_m1, "c", 30);
+ CheckNumber(locals_m1, "d", 50);
+ CheckNumber(locals_m1, "e", 60);
+ CheckNumber(locals_m1, "f", 0);
+
+ props = await GetObjectOnFrame(pause_location["callFrames"][3], "complex");
+ Assert.Equal(3, props.Count());
+ CheckNumber(props, "A", 10);
+ CheckString(props, "B", "xx");
+ CheckObject(props, "c", "object");
+
+ pause_location = await StepAndCheck(StepKind.Over, dep_cs_loc, 23, 8, "DoStuff", times : 2);
+ // Check UseComplex frame again
+ locals_m1 = await GetLocalsForFrame(pause_location["callFrames"][1], debugger_test_loc, 23, 8, "UseComplex");
+ Assert.Equal(7, locals_m1.Count());
+
+ CheckNumber(locals_m1, "a", 10);
+ CheckNumber(locals_m1, "b", 20);
+ CheckObject(locals_m1, "complex", "Simple.Complex");
+ CheckNumber(locals_m1, "c", 30);
+ CheckNumber(locals_m1, "d", 50);
+ CheckNumber(locals_m1, "e", 60);
+ CheckNumber(locals_m1, "f", 0);
+
+ props = await GetObjectOnFrame(pause_location["callFrames"][1], "complex");
+ Assert.Equal(3, props.Count());
+ CheckNumber(props, "A", 10);
+ CheckString(props, "B", "xx");
+ CheckObject(props, "c", "object");
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsInPreviousFramesDuringSteppingIn(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+ await SetBreakpoint(debugger_test_loc, 111, 12);
+
+ // Will stop in InnerMethod
+ var wait_res = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_outer_method(); }, 1);",
+ debugger_test_loc, 111, 12, "InnerMethod",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(4, locals.Count());
+ CheckNumber(locals, "i", 5);
+ CheckNumber(locals, "j", 24);
+ CheckString(locals, "foo_str", "foo");
+ CheckObject(locals, "this", "Math.NestedInMath");
+ }
+ );
+
+ var this_props = await GetObjectOnFrame(wait_res["callFrames"][0], "this");
+ Assert.Equal(2, this_props.Count());
+ CheckObject(this_props, "m", "Math");
+ CheckValueType(this_props, "SimpleStructProperty", "Math.SimpleStruct");
+
+ var ss_props = await GetObjectOnLocals(this_props, "SimpleStructProperty");
+ Assert.Equal(2, ss_props.Count());
+ CheckValueType(ss_props, "dt", "System.DateTime");
+ CheckValueType(ss_props, "gs", "Math.GenericStruct<System.DateTime>");
+
+ await CheckDateTime(ss_props, "dt", new DateTime(2020, 1, 2, 3, 4, 5));
+
+ // Check OuterMethod frame
+ var locals_m1 = await GetLocalsForFrame(wait_res["callFrames"][1], debugger_test_loc, 87, 8, "OuterMethod");
+ Assert.Equal(5, locals_m1.Count());
+ // FIXME: Failing test CheckNumber (locals_m1, "i", 5);
+ // FIXME: Failing test CheckString (locals_m1, "text", "Hello");
+ CheckNumber(locals_m1, "new_i", 0);
+ CheckNumber(locals_m1, "k", 0);
+ CheckObject(locals_m1, "nim", "Math.NestedInMath");
+
+ // step back into OuterMethod
+ await StepAndCheck(StepKind.Over, debugger_test_loc, 91, 8, "OuterMethod", times : 9,
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(5, locals.Count());
+
+ // FIXME: Failing test CheckNumber (locals_m1, "i", 5);
+ CheckString(locals, "text", "Hello");
+ // FIXME: Failing test CheckNumber (locals, "new_i", 24);
+ CheckNumber(locals, "k", 19);
+ CheckObject(locals, "nim", "Math.NestedInMath");
+ }
+ );
+
+ //await StepAndCheck (StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 81, 2, "OuterMethod", times: 2);
+
+ // step into InnerMethod2
+ await StepAndCheck(StepKind.Into, "dotnet://debugger-test.dll/debugger-test.cs", 96, 4, "InnerMethod2",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(3, locals.Count());
+
+ CheckString(locals, "s", "test string");
+ //out var: CheckNumber (locals, "k", 0);
+ CheckNumber(locals, "i", 24);
+ }
+ );
+
+ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 100, 4, "InnerMethod2", times : 4,
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(3, locals.Count());
+
+ CheckString(locals, "s", "test string");
+ // FIXME: Failing test CheckNumber (locals, "k", 34);
+ CheckNumber(locals, "i", 24);
+ }
+ );
+
+ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 92, 8, "OuterMethod", times : 2,
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(5, locals.Count());
+
+ CheckString(locals, "text", "Hello");
+ // FIXME: failing test CheckNumber (locals, "i", 5);
+ CheckNumber(locals, "new_i", 22);
+ CheckNumber(locals, "k", 34);
+ CheckObject(locals, "nim", "Math.NestedInMath");
+ }
+ );
+ });
+ }
+
+ [Fact]
+ public async Task InspectLocalsDuringSteppingIn()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 86, 8);
+
+ await EvaluateAndCheck("window.setTimeout(function() { invoke_outer_method(); }, 1);",
+ "dotnet://debugger-test.dll/debugger-test.cs", 86, 8, "OuterMethod",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(5, locals.Count());
+
+ CheckObject(locals, "nim", "Math.NestedInMath");
+ CheckNumber(locals, "i", 5);
+ CheckNumber(locals, "k", 0);
+ CheckNumber(locals, "new_i", 0);
+ CheckString(locals, "text", null);
+ }
+ );
+
+ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 87, 8, "OuterMethod",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(5, locals.Count());
+
+ CheckObject(locals, "nim", "Math.NestedInMath");
+ // FIXME: Failing test CheckNumber (locals, "i", 5);
+ CheckNumber(locals, "k", 0);
+ CheckNumber(locals, "new_i", 0);
+ CheckString(locals, "text", "Hello");
+ }
+ );
+
+ // Step into InnerMethod
+ await StepAndCheck(StepKind.Into, "dotnet://debugger-test.dll/debugger-test.cs", 105, 8, "InnerMethod");
+ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 109, 12, "InnerMethod", times : 5,
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(4, locals.Count());
+
+ CheckNumber(locals, "i", 5);
+ CheckNumber(locals, "j", 15);
+ CheckString(locals, "foo_str", "foo");
+ CheckObject(locals, "this", "Math.NestedInMath");
+ }
+ );
+
+ // Step back to OuterMethod
+ await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 88, 8, "OuterMethod", times : 6,
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(5, locals.Count());
+
+ CheckObject(locals, "nim", "Math.NestedInMath");
+ // FIXME: Failing test CheckNumber (locals, "i", 5);
+ CheckNumber(locals, "k", 0);
+ CheckNumber(locals, "new_i", 24);
+ CheckString(locals, "text", "Hello");
+ }
+ );
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsInAsyncMethods(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, 120, 12);
+ await SetBreakpoint(debugger_test_loc, 135, 12);
+
+ // Will stop in Asyncmethod0
+ var wait_res = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_async_method_with_await(); }, 1);",
+ debugger_test_loc, 120, 12, "MoveNext", //FIXME:
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(4, locals.Count());
+ CheckString(locals, "s", "string from js");
+ CheckNumber(locals, "i", 42);
+ CheckString(locals, "local0", "value0");
+ CheckObject(locals, "this", "Math.NestedInMath");
+ }
+ );
+ Console.WriteLine(wait_res);
+
+#if false // Disabled for now, as we don't have proper async traces
+ var locals = await GetProperties(wait_res["callFrames"][2]["callFrameId"].Value<string>());
+ Assert.Equal(4, locals.Count());
+ CheckString(locals, "ls", "string from jstest");
+ CheckNumber(locals, "li", 52);
+#endif
+
+ // TODO: previous frames have async machinery details, so no point checking that right now
+
+ var pause_loc = await SendCommandAndCheck(null, "Debugger.resume", debugger_test_loc, 135, 12, /*FIXME: "AsyncMethodNoReturn"*/ "MoveNext",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(4, locals.Count());
+ CheckString(locals, "str", "AsyncMethodNoReturn's local");
+ CheckObject(locals, "this", "Math.NestedInMath");
+ //FIXME: check fields
+ CheckValueType(locals, "ss", "Math.SimpleStruct");
+ CheckArray(locals, "ss_arr", "Math.SimpleStruct[]");
+ // TODO: struct fields
+ }
+ );
+
+ var this_props = await GetObjectOnFrame(pause_loc["callFrames"][0], "this");
+ Assert.Equal(2, this_props.Count());
+ CheckObject(this_props, "m", "Math");
+ CheckValueType(this_props, "SimpleStructProperty", "Math.SimpleStruct");
+
+ // TODO: Check `this` properties
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsWithStructs(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, 22, 8);
+
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_method_with_structs(); }, 1);",
+ debugger_test_loc, 22, 8, "MethodWithLocalStructs",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(3, locals.Count());
+
+ CheckValueType(locals, "ss_local", "DebuggerTests.ValueTypesTest.SimpleStruct");
+ CheckValueType(locals, "gs_local", "DebuggerTests.ValueTypesTest.GenericStruct<DebuggerTests.ValueTypesTest>");
+ CheckObject(locals, "vt_local", "DebuggerTests.ValueTypesTest");
+ }
+ );
+
+ var dt = new DateTime(2021, 2, 3, 4, 6, 7);
+ // Check ss_local's properties
+ var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
+ await CheckProps(ss_local_props, new
+ {
+ str_member = TString("set in MethodWithLocalStructs#SimpleStruct#str_member"),
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+ Kind = TEnum("System.DateTimeKind", "Utc")
+ }, "ss_local");
+
+ {
+ // Check ss_local.dt
+ await CheckDateTime(ss_local_props, "dt", dt);
+
+ // Check ss_local.gs
+ var gs_props = await GetObjectOnLocals(ss_local_props, "gs");
+ CheckString(gs_props, "StringField", "set in MethodWithLocalStructs#SimpleStruct#gs#StringField");
+ CheckObject(gs_props, "List", "System.Collections.Generic.List<System.DateTime>");
+ }
+
+ // Check gs_local's properties
+ var gs_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "gs_local");
+ await CheckProps(gs_local_props, new
+ {
+ StringField = TString("gs_local#GenericStruct<ValueTypesTest>#StringField"),
+ List = TObject("System.Collections.Generic.List<DebuggerTests.ValueTypesTest>", is_null : true),
+ Options = TEnum("DebuggerTests.Options", "None")
+ }, "gs_local");
+
+ // Check vt_local's properties
+ var vt_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "vt_local");
+ Assert.Equal(5, vt_local_props.Count());
+
+ CheckString(vt_local_props, "StringField", "string#0");
+ CheckValueType(vt_local_props, "SimpleStructField", "DebuggerTests.ValueTypesTest.SimpleStruct");
+ CheckValueType(vt_local_props, "SimpleStructProperty", "DebuggerTests.ValueTypesTest.SimpleStruct");
+ await CheckDateTime(vt_local_props, "DT", new DateTime(2020, 1, 2, 3, 4, 5));
+ CheckEnum(vt_local_props, "RGB", "DebuggerTests.RGB", "Blue");
+
+ {
+ // SimpleStructProperty
+ dt = new DateTime(2022, 3, 4, 5, 7, 8);
+ var ssp_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructProperty",
+ new
+ {
+ str_member = TString("SimpleStructProperty#string#0#SimpleStruct#str_member"),
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+ Kind = TEnum("System.DateTimeKind", "Utc")
+ },
+ label: "vt_local_props.SimpleStructProperty");
+
+ await CheckDateTime(ssp_props, "dt", dt);
+
+ // SimpleStructField
+ dt = new DateTime(2025, 6, 7, 8, 10, 11);
+ var ssf_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructField",
+ new
+ {
+ str_member = TString("SimpleStructField#string#0#SimpleStruct#str_member"),
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+ Kind = TEnum("System.DateTimeKind", "Local")
+ },
+ label: "vt_local_props.SimpleStructField");
+
+ await CheckDateTime(ssf_props, "dt", dt);
+ }
+
+ // FIXME: check ss_local.gs.List's members
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectValueTypeMethodArgs(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, 34, 12);
+
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:TestStructsAsMethodArgs'); }, 1);",
+ debugger_test_loc, 34, 12, "MethodWithStructArgs",
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(3, locals.Count());
+
+ CheckString(locals, "label", "TestStructsAsMethodArgs#label");
+ CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
+ CheckNumber(locals, "x", 3);
+ }
+ );
+
+ var dt = new DateTime(2025, 6, 7, 8, 10, 11);
+ var ss_local_as_ss_arg = new
+ {
+ str_member = TString("ss_local#SimpleStruct#string#0#SimpleStruct#str_member"),
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+ Kind = TEnum("System.DateTimeKind", "Local")
+ };
+ var ss_local_gs = new
+ {
+ StringField = TString("ss_local#SimpleStruct#string#0#SimpleStruct#gs#StringField"),
+ List = TObject("System.Collections.Generic.List<System.DateTime>"),
+ Options = TEnum("DebuggerTests.Options", "Option1")
+ };
+
+ // Check ss_arg's properties
+ var ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg");
+ await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_arg");
+
+ {
+ // Check ss_local.dt
+ await CheckDateTime(ss_arg_props, "dt", dt);
+
+ // Check ss_local.gs
+ await CompareObjectPropertiesFor(ss_arg_props, "gs", ss_local_gs);
+ }
+
+ pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 38, 8, "MethodWithStructArgs", times : 4,
+ locals_fn: (locals) =>
+ {
+ Assert.Equal(3, locals.Count());
+
+ CheckString(locals, "label", "TestStructsAsMethodArgs#label");
+ CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
+ CheckNumber(locals, "x", 3);
+
+ }
+ );
+
+ var ss_arg_updated = new
+ {
+ str_member = TString("ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member"),
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+ Kind = TEnum("System.DateTimeKind", "Utc")
+ };
+
+ ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg");
+ await CheckProps(ss_arg_props, ss_arg_updated, "ss_ar");
+
+ {
+ // Check ss_local.gs
+ await CompareObjectPropertiesFor(ss_arg_props, "gs", new
+ {
+ StringField = TString("ValueTypesTest#MethodWithStructArgs#updated#gs#StringField#3"),
+ List = TObject("System.Collections.Generic.List<System.DateTime>"),
+ Options = TEnum("DebuggerTests.Options", "Option1")
+ });
+
+ await CheckDateTime(ss_arg_props, "dt", dt);
+ }
+
+ // Check locals on previous frame, same as earlier in this test
+ ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][1], "ss_local");
+ await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_local");
+
+ {
+ // Check ss_local.dt
+ await CheckDateTime(ss_arg_props, "dt", dt);
+
+ // Check ss_local.gs
+ var gs_props = await GetObjectOnLocals(ss_arg_props, "gs");
+ CheckString(gs_props, "StringField", "ss_local#SimpleStruct#string#0#SimpleStruct#gs#StringField");
+ CheckObject(gs_props, "List", "System.Collections.Generic.List<System.DateTime>");
+ }
+
+ // ----------- Step back to the caller ---------
+
+ pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 28, 12, "TestStructsAsMethodArgs",
+ times : 2, locals_fn: (l) => { /* non-null to make sure that locals get fetched */ });
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckProps(locals, new
+ {
+ ss_local = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct"),
+ ss_ret = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct")
+ },
+ "locals#0");
+
+ ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
+ await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_local");
+
+ {
+ // Check ss_local.gs
+ await CompareObjectPropertiesFor(ss_arg_props, "gs", ss_local_gs, label: "ss_local_gs");
+ }
+
+ // FIXME: check ss_local.gs.List's members
+ });
+ }
+
+ [Fact]
+ public async Task CheckUpdatedValueTypeFieldsOnResume()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ var lines = new [] { 202, 205 };
+ await SetBreakpoint(debugger_test_loc, lines[0], 12);
+ await SetBreakpoint(debugger_test_loc, lines[1], 12);
+
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingValueTypeMembers'); }, 1);",
+ debugger_test_loc, lines[0], 12, "MethodUpdatingValueTypeMembers");
+
+ var dt = new DateTime(1, 2, 3, 4, 5, 6);
+ await CheckLocals(pause_location, dt);
+
+ // Resume
+ dt = new DateTime(9, 8, 7, 6, 5, 4);
+ pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MethodUpdatingValueTypeMembers");
+ await CheckLocals(pause_location, dt);
+ });
+
+ async Task CheckLocals(JToken pause_location, DateTime dt)
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckProps(locals, new
+ {
+ obj = TObject("DebuggerTests.ClassForToStringTests"),
+ vt = TObject("DebuggerTests.StructForToStringTests")
+ }, "locals");
+
+ var obj_props = await GetObjectOnLocals(locals, "obj");
+ {
+ await CheckProps(obj_props, new
+ {
+ DT = TValueType("System.DateTime", dt.ToString())
+ }, "locals#obj.DT", num_fields : 5);
+
+ await CheckDateTime(obj_props, "DT", dt);
+ }
+
+ var vt_props = await GetObjectOnLocals(locals, "obj");
+ {
+ await CheckProps(vt_props, new
+ {
+ DT = TValueType("System.DateTime", dt.ToString())
+ }, "locals#obj.DT", num_fields : 5);
+
+ await CheckDateTime(vt_props, "DT", dt);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task CheckUpdatedValueTypeLocalsOnResumeAsync()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ var lines = new [] { 211, 213 };
+ await SetBreakpoint(debugger_test_loc, lines[0], 12);
+ await SetBreakpoint(debugger_test_loc, lines[1], 12);
+
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingValueTypeLocalsAsync'); }, 1);",
+ debugger_test_loc, lines[0], 12, "MoveNext");
+
+ var dt = new DateTime(1, 2, 3, 4, 5, 6);
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckDateTime(locals, "dt", dt);
+
+ // Resume
+ dt = new DateTime(9, 8, 7, 6, 5, 4);
+ pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MoveNext");
+ locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckDateTime(locals, "dt", dt);
+ });
+ }
+
+ [Fact]
+ public async Task CheckUpdatedVTArrayMembersOnResume()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ var lines = new [] { 222, 224 };
+ await SetBreakpoint(debugger_test_loc, lines[0], 12);
+ await SetBreakpoint(debugger_test_loc, lines[1], 12);
+
+ var dt = new DateTime(1, 2, 3, 4, 5, 6);
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingVTArrayMembers'); }, 1);",
+ debugger_test_loc, lines[0], 12, "MethodUpdatingVTArrayMembers");
+ await CheckArrayElements(pause_location, dt);
+
+ // Resume
+ dt = new DateTime(9, 8, 7, 6, 5, 4);
+ pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MethodUpdatingVTArrayMembers");
+ await CheckArrayElements(pause_location, dt);
+ });
+
+ async Task CheckArrayElements(JToken pause_location, DateTime dt)
+ {
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckProps(locals, new
+ {
+ ssta = TArray("DebuggerTests.StructForToStringTests[]", 1)
+ }, "locals");
+
+ var ssta = await GetObjectOnLocals(locals, "ssta");
+ var sst0 = await GetObjectOnLocals(ssta, "0");
+ await CheckProps(sst0, new
+ {
+ DT = TValueType("System.DateTime", dt.ToString())
+ }, "dta [0]", num_fields : 5);
+
+ await CheckDateTime(sst0, "DT", dt);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsWithStructsStaticAsync(bool use_cfo)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, 54, 12);
+
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method_async (" +
+ "'[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalStructsStaticAsync'" +
+ "); }, 1);",
+ debugger_test_loc, 54, 12, "MoveNext"); //BUG: method name
+
+ var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+ await CheckProps(locals, new
+ {
+ ss_local = TObject("DebuggerTests.ValueTypesTest.SimpleStruct"),
+ gs_local = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<int>"),
+ result = TBool(true)
+ },
+ "locals#0");
+
+ var dt = new DateTime(2021, 2, 3, 4, 6, 7);
+ // Check ss_local's properties
+ var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
+ await CheckProps(ss_local_props, new
+ {
+ str_member = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#str_member"),
+ dt = TValueType("System.DateTime", dt.ToString()),
+ gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+ Kind = TEnum("System.DateTimeKind", "Utc")
+ }, "ss_local");
+
+ {
+ // Check ss_local.dt
+ await CheckDateTime(ss_local_props, "dt", dt);
+
+ // Check ss_local.gs
+ await CompareObjectPropertiesFor(ss_local_props, "gs",
+ new
+ {
+ StringField = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#gs#StringField"),
+ List = TObject("System.Collections.Generic.List<System.DateTime>"),
+ Options = TEnum("DebuggerTests.Options", "Option1")
+ }
+ );
+ }
+
+ // Check gs_local's properties
+ var gs_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "gs_local");
+ await CheckProps(gs_local_props, new
+ {
+ StringField = TString("gs_local#GenericStruct<ValueTypesTest>#StringField"),
+ List = TObject("System.Collections.Generic.List<int>"),
+ Options = TEnum("DebuggerTests.Options", "Option2")
+ }, "gs_local");
+
+ // FIXME: check ss_local.gs.List's members
+ });
+ }
+
+ [Theory]
+ [InlineData(134, 12, "MethodWithLocalsForToStringTest", false, false)]
+ [InlineData(144, 12, "MethodWithArgumentsForToStringTest", true, false)]
+ [InlineData(189, 12, "MethodWithArgumentsForToStringTestAsync", true, true)]
+ [InlineData(179, 12, "MethodWithArgumentsForToStringTestAsync", false, true)]
+ public async Task InspectLocalsForToStringDescriptions(int line, int col, string method_name, bool call_other, bool invoke_async)
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+ string entry_method_name = $"[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalsForToStringTest{(invoke_async ? "Async" : String.Empty)}";
+ int frame_idx = 0;
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+ var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+ await SetBreakpoint(debugger_test_loc, line, col);
+
+ var eval_expr = "window.setTimeout(function() {" +
+ (invoke_async ? "invoke_static_method_async (" : "invoke_static_method (") +
+ $"'{entry_method_name}'," +
+ (call_other ? "true" : "false") +
+ "); }, 1);";
+ Console.WriteLine($"{eval_expr}");
+
+ var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, invoke_async ? "MoveNext" : method_name);
+
+ var dt0 = new DateTime(2020, 1, 2, 3, 4, 5);
+ var dt1 = new DateTime(2010, 5, 4, 3, 2, 1);
+ var ts = dt0 - dt1;
+ var dto = new DateTimeOffset(dt0, new TimeSpan(4, 5, 0));
+
+ var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+ await CheckProps(frame_locals, new
+ {
+ call_other = TBool(call_other),
+ dt0 = TValueType("System.DateTime", dt0.ToString()),
+ dt1 = TValueType("System.DateTime", dt1.ToString()),
+ dto = TValueType("System.DateTimeOffset", dto.ToString()),
+ ts = TValueType("System.TimeSpan", ts.ToString()),
+ dec = TValueType("System.Decimal", "123987123"),
+ guid = TValueType("System.Guid", "3D36E07E-AC90-48C6-B7EC-A481E289D014"),
+ dts = TArray("System.DateTime[]", 2),
+ obj = TObject("DebuggerTests.ClassForToStringTests"),
+ sst = TObject("DebuggerTests.StructForToStringTests")
+ }, "locals#0");
+
+ var dts_0 = new DateTime(1983, 6, 7, 5, 6, 10);
+ var dts_1 = new DateTime(1999, 10, 15, 1, 2, 3);
+ var dts_elements = await GetObjectOnLocals(frame_locals, "dts");
+ await CheckDateTime(dts_elements, "0", dts_0);
+ await CheckDateTime(dts_elements, "1", dts_1);
+
+ // TimeSpan
+ await CompareObjectPropertiesFor(frame_locals, "ts",
+ new
+ {
+ Days = TNumber(3530),
+ Minutes = TNumber(2),
+ Seconds = TNumber(4),
+ }, "ts_props", num_fields : 12);
+
+ // DateTimeOffset
+ await CompareObjectPropertiesFor(frame_locals, "dto",
+ new
+ {
+ Day = TNumber(2),
+ Year = TNumber(2020),
+ DayOfWeek = TEnum("System.DayOfWeek", "Thursday")
+ }, "dto_props", num_fields : 22);
+
+ var DT = new DateTime(2004, 10, 15, 1, 2, 3);
+ var DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0));
+
+ var obj_props = await CompareObjectPropertiesFor(frame_locals, "obj",
+ new
+ {
+ DT = TValueType("System.DateTime", DT.ToString()),
+ DTO = TValueType("System.DateTimeOffset", DTO.ToString()),
+ TS = TValueType("System.TimeSpan", ts.ToString()),
+ Dec = TValueType("System.Decimal", "1239871"),
+ Guid = TValueType("System.Guid", "3D36E07E-AC90-48C6-B7EC-A481E289D014")
+ }, "obj_props");
+
+ DTO = new DateTimeOffset(dt0, new TimeSpan(3, 15, 0));
+ var sst_props = await CompareObjectPropertiesFor(frame_locals, "sst",
+ new
+ {
+ DT = TValueType("System.DateTime", DT.ToString()),
+ DTO = TValueType("System.DateTimeOffset", DTO.ToString()),
+ TS = TValueType("System.TimeSpan", ts.ToString()),
+ Dec = TValueType("System.Decimal", "1239871"),
+ Guid = TValueType("System.Guid", "3D36E07E-AC90-48C6-B7EC-A481E289D014")
+ }, "sst_props");
+ });
+ }
+
+ [Fact]
+ public async Task InspectLocals()
+ {
+ var insp = new Inspector();
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var wait_res = await RunUntil("locals_inner");
+ var locals = await GetProperties(wait_res["callFrames"][1]["callFrameId"].Value<string>());
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task InspectLocalsForStructInstanceMethod(bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+ "dotnet://debugger-test.dll/debugger-array-test.cs", 258, 12,
+ "GenericInstanceMethod<DebuggerTests.SimpleClass>",
+ "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EntryClass:run'); })",
+ use_cfo : use_cfo,
+ wait_for_event_fn : async(pause_location) =>
+ {
+ var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+ await CheckProps(frame_locals, new
+ {
+ sc_arg = TObject("DebuggerTests.SimpleClass"),
+ @this = TValueType("DebuggerTests.Point"),
+ local_gs = TValueType("DebuggerTests.SimpleGenericStruct<int>")
+ },
+ "locals#0");
+
+ await CompareObjectPropertiesFor(frame_locals, "local_gs",
+ new
+ {
+ Id = TString("local_gs#Id"),
+ Color = TEnum("DebuggerTests.RGB", "Green"),
+ Value = TNumber(4)
+ },
+ label: "local_gs#0");
+
+ await CompareObjectPropertiesFor(frame_locals, "sc_arg",
+ TSimpleClass(10, 45, "sc_arg#Id", "Blue"),
+ label: "sc_arg#0");
+
+ await CompareObjectPropertiesFor(frame_locals, "this",
+ TPoint(90, -4, "point#Id", "Green"),
+ label: "this#0");
+
+ });
+
+ [Fact]
+ public async Task SteppingIntoMscorlib()
+ {
+ var insp = new Inspector();
+ //Collect events
+ var scripts = SubscribeToScripts(insp);
+
+ await Ready();
+ await insp.Ready(async(cli, token) =>
+ {
+ ctx = new DebugTestContext(cli, insp, token, scripts);
+
+ var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 83, 8);
+ var pause_location = await EvaluateAndCheck(
+ "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:OuterMethod'); }, 1);",
+ "dotnet://debugger-test.dll/debugger-test.cs", 83, 8,
+ "OuterMethod");
+
+ //make sure we're on the right bp
+ Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+ pause_location = await SendCommandAndCheck(null, $"Debugger.stepInto", null, -1, -1, null);
+ var top_frame = pause_location["callFrames"][0];
+
+ AssertEqual("WriteLine", top_frame["functionName"]?.Value<string>(), "Expected to be in WriteLine method");
+ var script_id = top_frame["functionLocation"]["scriptId"].Value<string>();
+ AssertEqual("dotnet://System.Console.dll/Console.cs", scripts[script_id], "Expected to stopped in System.Console.WriteLine");
+ });
+ }
+
+ //TODO add tests covering basic stepping behavior as step in/out/over
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Error",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
--- /dev/null
+// 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.Tasks;
+namespace DebuggerTests
+{
+ public class ArrayTestsClass
+ {
+ public static void PrimitiveTypeLocals(bool call_other = false)
+ {
+ var int_arr = new int[] { 4, 70, 1 };
+ var int_arr_empty = new int[0];
+ int[] int_arr_null = null;
+
+ if (call_other)
+ OtherMethod();
+
+ Console.WriteLine($"int_arr: {int_arr.Length}, {int_arr_empty.Length}, {int_arr_null?.Length}");
+ }
+
+ public static void ValueTypeLocals(bool call_other = false)
+ {
+ var point_arr = new Point[]
+ {
+ new Point { X = 5, Y = -2, Id = "point_arr#Id#0", Color = RGB.Green },
+ new Point { X = 123, Y = 0, Id = "point_arr#Id#1", Color = RGB.Blue },
+ };
+
+ var point_arr_empty = new Point[0];
+ Point[] point_arr_null = null;
+
+ if (call_other)
+ OtherMethod();
+
+ Console.WriteLine($"point_arr: {point_arr.Length}, {point_arr_empty.Length}, {point_arr_null?.Length}");
+ }
+
+ public static void ObjectTypeLocals(bool call_other = false)
+ {
+ var class_arr = new SimpleClass[]
+ {
+ new SimpleClass { X = 5, Y = -2, Id = "class_arr#Id#0", Color = RGB.Green },
+ null,
+ new SimpleClass { X = 123, Y = 0, Id = "class_arr#Id#2", Color = RGB.Blue },
+ };
+
+ var class_arr_empty = new SimpleClass[0];
+ SimpleClass[] class_arr_null = null;
+
+ if (call_other)
+ OtherMethod();
+
+ Console.WriteLine($"class_arr: {class_arr.Length}, {class_arr_empty.Length}, {class_arr_null?.Length}");
+ }
+
+ public static void GenericTypeLocals(bool call_other = false)
+ {
+ var gclass_arr = new GenericClass<int>[]
+ {
+ null,
+ new GenericClass<int> { Id = "gclass_arr#1#Id", Color = RGB.Red, Value = 5 },
+ new GenericClass<int> { Id = "gclass_arr#2#Id", Color = RGB.Blue, Value = -12 },
+ };
+
+ var gclass_arr_empty = new GenericClass<int>[0];
+ GenericClass<int>[] gclass_arr_null = null;
+
+ if (call_other)
+ OtherMethod();
+
+ Console.WriteLine($"gclass_arr: {gclass_arr.Length}, {gclass_arr_empty.Length}, {gclass_arr_null?.Length}");
+ }
+
+ public static void GenericValueTypeLocals(bool call_other = false)
+ {
+ var gvclass_arr = new SimpleGenericStruct<Point>[]
+ {
+ new SimpleGenericStruct<Point> { Id = "gvclass_arr#1#Id", Color = RGB.Red, Value = new Point { X = 100, Y = 200, Id = "gvclass_arr#1#Value#Id", Color = RGB.Red } },
+ new SimpleGenericStruct<Point> { Id = "gvclass_arr#2#Id", Color = RGB.Blue, Value = new Point { X = 10, Y = 20, Id = "gvclass_arr#2#Value#Id", Color = RGB.Green } }
+ };
+
+ var gvclass_arr_empty = new SimpleGenericStruct<Point>[0];
+ SimpleGenericStruct<Point>[] gvclass_arr_null = null;
+
+ if (call_other)
+ OtherMethod();
+
+ Console.WriteLine($"gvclass_arr: {gvclass_arr.Length}, {gvclass_arr_empty.Length}, {gvclass_arr_null?.Length}");
+ }
+
+ static void OtherMethod()
+ {
+ YetAnotherMethod();
+ Console.WriteLine($"Just a placeholder for breakpoints");
+ }
+
+ static void YetAnotherMethod()
+ {
+ Console.WriteLine($"Just a placeholder for breakpoints");
+ }
+
+ public static void ObjectArrayMembers()
+ {
+ var c = new Container
+ {
+ id = "c#id",
+ ClassArrayProperty = new SimpleClass[]
+ {
+ new SimpleClass { X = 5, Y = -2, Id = "ClassArrayProperty#Id#0", Color = RGB.Green },
+ new SimpleClass { X = 30, Y = 1293, Id = "ClassArrayProperty#Id#1", Color = RGB.Green },
+ null
+ },
+ ClassArrayField = new SimpleClass[]
+ {
+ null,
+ new SimpleClass { X = 5, Y = -2, Id = "ClassArrayField#Id#1", Color = RGB.Blue },
+ new SimpleClass { X = 30, Y = 1293, Id = "ClassArrayField#Id#2", Color = RGB.Green },
+ },
+ PointsProperty = new Point[]
+ {
+ new Point { X = 5, Y = -2, Id = "PointsProperty#Id#0", Color = RGB.Green },
+ new Point { X = 123, Y = 0, Id = "PointsProperty#Id#1", Color = RGB.Blue },
+ },
+ PointsField = new Point[]
+ {
+ new Point { X = 5, Y = -2, Id = "PointsField#Id#0", Color = RGB.Green },
+ new Point { X = 123, Y = 0, Id = "PointsField#Id#1", Color = RGB.Blue },
+ }
+ };
+
+ Console.WriteLine($"Back from PlaceholderMethod, {c.ClassArrayProperty?.Length}");
+ c.PlaceholderMethod();
+ Console.WriteLine($"Back from PlaceholderMethod, {c.id}");
+ }
+
+ public static async Task<bool> ValueTypeLocalsAsync(bool call_other = false)
+ {
+ var gvclass_arr = new SimpleGenericStruct<Point>[]
+ {
+ new SimpleGenericStruct<Point> { Id = "gvclass_arr#1#Id", Color = RGB.Red, Value = new Point { X = 100, Y = 200, Id = "gvclass_arr#1#Value#Id", Color = RGB.Red } },
+ new SimpleGenericStruct<Point> { Id = "gvclass_arr#2#Id", Color = RGB.Blue, Value = new Point { X = 10, Y = 20, Id = "gvclass_arr#2#Value#Id", Color = RGB.Green } }
+ };
+
+ var gvclass_arr_empty = new SimpleGenericStruct<Point>[0];
+ SimpleGenericStruct<Point>[] gvclass_arr_null = null;
+ Console.WriteLine($"ValueTypeLocalsAsync: call_other: {call_other}");
+ SimpleGenericStruct<Point> gvclass;
+ Point[] points = null;
+
+ if (call_other)
+ {
+ (gvclass, points) = await new ArrayTestsClass().InstanceMethodValueTypeLocalsAsync<SimpleGenericStruct<Point>>(gvclass_arr[0]);
+ Console.WriteLine($"* gvclass: {gvclass}, points: {points.Length}");
+ }
+
+ Console.WriteLine($"gvclass_arr: {gvclass_arr.Length}, {gvclass_arr_empty.Length}, {gvclass_arr_null?.Length}");
+ return true;
+ }
+
+ public async Task < (T, Point[]) > InstanceMethodValueTypeLocalsAsync<T>(T t1)
+ {
+ var point_arr = new Point[]
+ {
+ new Point { X = 5, Y = -2, Id = "point_arr#Id#0", Color = RGB.Red },
+ new Point { X = 123, Y = 0, Id = "point_arr#Id#1", Color = RGB.Blue }
+ };
+ var point = new Point { X = 45, Y = 51, Id = "point#Id", Color = RGB.Green };
+
+ Console.WriteLine($"point_arr: {point_arr.Length}, T: {t1}, point: {point}");
+ return (t1, new Point[] { point_arr[0], point_arr[1], point });
+ }
+
+ // A workaround for method invocations on structs not working right now
+ public static async Task EntryPointForStructMethod(bool call_other = false)
+ {
+ await Point.AsyncMethod(call_other);
+ }
+
+ public static void GenericValueTypeLocals2(bool call_other = false)
+ {
+ var gvclass_arr = new SimpleGenericStruct<Point[]>[]
+ {
+ new SimpleGenericStruct<Point[]>
+ {
+ Id = "gvclass_arr#0#Id",
+ Color = RGB.Red,
+ Value = new Point[]
+ {
+ new Point { X = 100, Y = 200, Id = "gvclass_arr#0#0#Value#Id", Color = RGB.Red },
+ new Point { X = 100, Y = 200, Id = "gvclass_arr#0#1#Value#Id", Color = RGB.Green }
+ }
+ },
+
+ new SimpleGenericStruct<Point[]>
+ {
+ Id = "gvclass_arr#1#Id",
+ Color = RGB.Blue,
+ Value = new Point[]
+ {
+ new Point { X = 100, Y = 200, Id = "gvclass_arr#1#0#Value#Id", Color = RGB.Green },
+ new Point { X = 100, Y = 200, Id = "gvclass_arr#1#1#Value#Id", Color = RGB.Blue }
+ }
+ },
+ };
+
+ var gvclass_arr_empty = new SimpleGenericStruct<Point[]>[0];
+ SimpleGenericStruct<Point[]>[] gvclass_arr_null = null;
+
+ if (call_other)
+ OtherMethod();
+
+ Console.WriteLine($"gvclass_arr: {gvclass_arr.Length}, {gvclass_arr_empty.Length}, {gvclass_arr_null?.Length}");
+ }
+ }
+
+ public class Container
+ {
+ public string id;
+ public SimpleClass[] ClassArrayProperty { get; set; }
+ public SimpleClass[] ClassArrayField;
+
+ public Point[] PointsProperty { get; set; }
+ public Point[] PointsField;
+
+ public void PlaceholderMethod()
+ {
+ Console.WriteLine($"Container.PlaceholderMethod");
+ }
+ }
+
+ public struct Point
+ {
+ public int X, Y;
+ public string Id { get; set; }
+ public RGB Color { get; set; }
+
+ /* instance too */
+ public static async Task AsyncMethod(bool call_other)
+ {
+ int local_i = 5;
+ var sc = new SimpleClass { X = 10, Y = 45, Id = "sc#Id", Color = RGB.Blue };
+ if (call_other)
+ await new Point { X = 90, Y = -4, Id = "point#Id", Color = RGB.Green }.AsyncInstanceMethod(sc);
+ Console.WriteLine($"AsyncMethod local_i: {local_i}, sc: {sc.Id}");
+ }
+
+ public async Task AsyncInstanceMethod(SimpleClass sc_arg)
+ {
+ var local_gs = new SimpleGenericStruct<int> { Id = "local_gs#Id", Color = RGB.Green, Value = 4 };
+ sc_arg.Id = "sc_arg#Id";
+ Console.WriteLine($"AsyncInstanceMethod sc_arg: {sc_arg.Id}, local_gs: {local_gs.Id}");
+ }
+
+ public void GenericInstanceMethod<T>(T sc_arg) where T : SimpleClass
+ {
+ var local_gs = new SimpleGenericStruct<int> { Id = "local_gs#Id", Color = RGB.Green, Value = 4 };
+ sc_arg.Id = "sc_arg#Id";
+ Console.WriteLine($"AsyncInstanceMethod sc_arg: {sc_arg.Id}, local_gs: {local_gs.Id}");
+ }
+ }
+
+ public class SimpleClass
+ {
+ public int X, Y;
+ public string Id { get; set; }
+ public RGB Color { get; set; }
+
+ public Point PointWithCustomGetter { get { return new Point { X = 100, Y = 400, Id = "SimpleClass#Point#gen#Id", Color = RGB.Green }; } }
+ }
+
+ public class GenericClass<T>
+ {
+ public string Id { get; set; }
+ public RGB Color { get; set; }
+ public T Value { get; set; }
+ }
+
+ public struct SimpleGenericStruct<T>
+ {
+ public string Id { get; set; }
+ public RGB Color { get; set; }
+ public T Value { get; set; }
+ }
+
+ public class EntryClass
+ {
+ public static void run()
+ {
+ ArrayTestsClass.PrimitiveTypeLocals(true);
+ ArrayTestsClass.ValueTypeLocals(true);
+ ArrayTestsClass.ObjectTypeLocals(true);
+
+ ArrayTestsClass.GenericTypeLocals(true);
+ ArrayTestsClass.GenericValueTypeLocals(true);
+ ArrayTestsClass.GenericValueTypeLocals2(true);
+
+ ArrayTestsClass.ObjectArrayMembers();
+
+ ArrayTestsClass.ValueTypeLocalsAsync(true).Wait();
+
+ ArrayTestsClass.EntryPointForStructMethod(true).Wait();
+
+ var sc = new SimpleClass { X = 10, Y = 45, Id = "sc#Id", Color = RGB.Blue };
+ new Point { X = 90, Y = -4, Id = "point#Id", Color = RGB.Green }.GenericInstanceMethod(sc);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace DebuggerTests
+{
+ public class CallFunctionOnTest
+ {
+ public static void LocalsTest(int len)
+ {
+ var big = new int[len];
+ for (int i = 0; i < len; i++)
+ big[i] = i + 1000;
+
+ var simple_struct = new Math.SimpleStruct() { dt = new DateTime(2020, 1, 2, 3, 4, 5), gs = new Math.GenericStruct<DateTime> { StringField = $"simple_struct # gs # StringField" } };
+
+ var ss_arr = new Math.SimpleStruct[len];
+ for (int i = 0; i < len; i++)
+ ss_arr[i] = new Math.SimpleStruct() { dt = new DateTime(2020 + i, 1, 2, 3, 4, 5), gs = new Math.GenericStruct<DateTime> { StringField = $"ss_arr # {i} # gs # StringField" } };
+
+ var nim = new Math.NestedInMath { SimpleStructProperty = new Math.SimpleStruct() { dt = new DateTime(2010, 6, 7, 8, 9, 10) } };
+ Action<Math.GenericStruct<int[]>> action = Math.DelegateTargetWithVoidReturn;
+ Console.WriteLine("foo");
+ }
+
+ public static void PropertyGettersTest()
+ {
+ var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) };
+ var swp = new StructWithProperties();
+ System.Console.WriteLine("break here");
+ }
+
+ public static async System.Threading.Tasks.Task PropertyGettersTestAsync()
+ {
+ var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) };
+ var swp = new StructWithProperties();
+ System.Console.WriteLine("break here");
+ await System.Threading.Tasks.Task.CompletedTask;
+ }
+ }
+
+ class ClassWithProperties
+ {
+ public int Int { get { return 5; } }
+ public string String { get { return "foobar"; } }
+ public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } }
+
+ public int[] IntArray { get { return new int[] { 10, 20 }; } }
+ public DateTime[] DTArray { get { return new DateTime[] { new DateTime(6, 7, 8, 9, 10, 11), new DateTime(1, 2, 3, 4, 5, 6) }; } }
+ public DateTime DTAutoProperty { get; set; }
+ public string StringField;
+ }
+
+ struct StructWithProperties
+ {
+ public int Int { get { return 5; } }
+ public string String { get { return "foobar"; } }
+ public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } }
+
+ public int[] IntArray { get { return new int[] { 10, 20 }; } }
+ public DateTime[] DTArray { get { return new DateTime[] { new DateTime(6, 7, 8, 9, 10, 11), new DateTime(1, 2, 3, 4, 5, 6) }; } }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Globalization;
+namespace DebuggerTests
+{
+ public class DateTimeTest
+ {
+ public static string LocaleTest(string locale)
+ {
+ CultureInfo.CurrentCulture = new CultureInfo(locale, false);
+ Console.WriteLine("CurrentCulture is {0}", CultureInfo.CurrentCulture.Name);
+
+ DateTimeFormatInfo dtfi = CultureInfo.GetCultureInfo(locale).DateTimeFormat;
+ var fdtp = dtfi.FullDateTimePattern;
+ var ldp = dtfi.LongDatePattern;
+ var ltp = dtfi.LongTimePattern;
+ var sdp = dtfi.ShortDatePattern;
+ var stp = dtfi.ShortTimePattern;
+
+ DateTime dt = new DateTime(2020, 1, 2, 3, 4, 5);
+ string dt_str = dt.ToString();
+ Console.WriteLine("Current time is {0}", dt_str);
+
+ return dt_str;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<!doctype html>
+<html lang="en-us">
+ <head>
+ </head>
+ <body>
+ <script type='text/javascript'>
+ var App = {
+ init: function () {
+ this.int_add = Module.mono_bind_static_method ("[debugger-test] Math:IntAdd");
+ this.use_complex = Module.mono_bind_static_method ("[debugger-test] Math:UseComplex");
+ this.delegates_test = Module.mono_bind_static_method ("[debugger-test] Math:DelegatesTest");
+ this.generic_types_test = Module.mono_bind_static_method ("[debugger-test] Math:GenericTypesTest");
+ this.outer_method = Module.mono_bind_static_method ("[debugger-test] Math:OuterMethod");
+ this.async_method = Module.mono_bind_static_method ("[debugger-test] Math/NestedInMath:AsyncTest");
+ this.method_with_structs = Module.mono_bind_static_method ("[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalStructs");
+ this.run_all = Module.mono_bind_static_method ("[debugger-test] DebuggerTest:run_all");
+ this.static_method_table = {};
+ console.log ("ready");
+ },
+ };
+ function invoke_static_method (method_name, ...args) {
+ var method = App.static_method_table [method_name];
+ if (method == undefined)
+ method = App.static_method_table [method_name] = Module.mono_bind_static_method (method_name);
+
+ return method (...args);
+ }
+
+ async function invoke_static_method_async (method_name, ...args) {
+ var method = App.static_method_table [method_name];
+ if (method == undefined) {
+ method = App.static_method_table [method_name] = Module.mono_bind_static_method (method_name);
+ }
+
+ return await method (...args);
+ }
+
+ function invoke_big_array_js_test (len) {
+ big_array_js_test(len);
+ }
+
+ function invoke_getters_js_test () {
+ getters_js_test ();
+ }
+
+ function invoke_add () {
+ return App.int_add (10, 20);
+ }
+ function invoke_use_complex () {
+ return App.use_complex (10, 20);
+ }
+ function invoke_delegates_test () {
+ return App.delegates_test ();
+ }
+ function invoke_generic_types_test () {
+ return App.generic_types_test ();
+ }
+ function invoke_bad_js_test () {
+ console.log ("js: In invoke_bad_js_test");
+ App.non_existant ();
+ console.log ("js: After.. shouldn't reach here");
+ }
+ function invoke_outer_method () {
+ console.log('invoke_outer_method called');
+ return App.outer_method ();
+ }
+ async function invoke_async_method_with_await () {
+ return await App.async_method ("string from js", 42);
+ }
+ function invoke_method_with_structs () {
+ return App.method_with_structs ();
+ }
+ function invoke_run_all () {
+ return App.run_all ();
+ }
+ </script>
+ <script type="text/javascript" src="mono-config.js"></script>
+ <script type="text/javascript" src="runtime-debugger.js"></script>
+ <script type="text/javascript" src="other.js"></script>
+ <script async type="text/javascript" src="dotnet.js"></script>
+ Stuff goes here
+ </body>
+ </html>
+
+17 sdks/wasm/debugger-test.cs
+@@ -0,0 +1,17 @@
+using System;
+
+public class Math { //Only append content to this class as the test suite depends on line info
+ public static int IntAdd (int a, int b) {
+ int c = a + b;
+ int d = c + b;
+ int e = d + a;
+
+ return e;
+ }
+
+ public static int UseComplex () {
+ var complex = new Simple.Complex (10, "xx");
+ var res = complex.DoStuff ();
+ return res;
+ }
+}
+
+19 sdks/wasm/debugger-test2.cs
+@@ -0,0 +1,19 @@
+using System;
+
+public class Misc { //Only append content to this class as the test suite depends on line info
+ public static int CreateObject (int foo, int bar) {
+ var f = new Fancy () {
+ Foo = foo,
+ Bar = bar,
+ };
+
+ Console.WriteLine ($"{f.Foo} {f.Bar}");
+ return f.Foo + f.Bar;
+ }
+}
+
+public class Fancy {
+ public int Foo;
+ public int Bar { get ; set; }
+}
--- /dev/null
+// 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.Tasks;
+namespace DebuggerTests
+{
+ public class EvaluateTestsClass
+ {
+ public class TestEvaluate
+ {
+ public int a;
+ public int b;
+ public int c;
+ public DateTime dt = new DateTime(2000, 5, 4, 3, 2, 1);
+ public void run(int g, int h, string valString)
+ {
+ int d = g + 1;
+ int e = g + 2;
+ int f = g + 3;
+ int i = d + e + f;
+ var local_dt = new DateTime(2010, 9, 8, 7, 6, 5);
+ a = 1;
+ b = 2;
+ c = 3;
+ a = a + 1;
+ b = b + 1;
+ c = c + 1;
+ }
+ }
+
+ public static void EvaluateLocals()
+ {
+ TestEvaluate f = new TestEvaluate();
+ f.run(100, 200, "test");
+
+ var f_s = new EvaluateTestsStruct();
+ f_s.EvaluateTestsStructInstanceMethod(100, 200, "test");
+ f_s.GenericInstanceMethodOnStruct<int>(100, 200, "test");
+
+ var f_g_s = new EvaluateTestsGenericStruct<int>();
+ f_g_s.EvaluateTestsGenericStructInstanceMethod(100, 200, "test");
+ Console.WriteLine($"a: {f.a}, b: {f.b}, c: {f.c}");
+ }
+
+ }
+
+ public struct EvaluateTestsStruct
+ {
+ public int a;
+ public int b;
+ public int c;
+ DateTime dateTime;
+ public void EvaluateTestsStructInstanceMethod(int g, int h, string valString)
+ {
+ int d = g + 1;
+ int e = g + 2;
+ int f = g + 3;
+ int i = d + e + f;
+ a = 1;
+ b = 2;
+ c = 3;
+ dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+ a = a + 1;
+ b = b + 1;
+ c = c + 1;
+ }
+
+ public void GenericInstanceMethodOnStruct<T>(int g, int h, string valString)
+ {
+ int d = g + 1;
+ int e = g + 2;
+ int f = g + 3;
+ int i = d + e + f;
+ a = 1;
+ b = 2;
+ c = 3;
+ dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+ T t = default(T);
+ a = a + 1;
+ b = b + 1;
+ c = c + 1;
+ }
+ }
+
+ public struct EvaluateTestsGenericStruct<T>
+ {
+ public int a;
+ public int b;
+ public int c;
+ DateTime dateTime;
+ public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString)
+ {
+ int d = g + 1;
+ int e = g + 2;
+ int f = g + 3;
+ int i = d + e + f;
+ a = 1;
+ b = 2;
+ c = 3;
+ dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+ T t = default(T);
+ a = a + 1;
+ b = b + 2;
+ c = c + 3;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Tasks;
+
+namespace DebuggerTests
+{
+ public class PointerTests
+ {
+
+ public static unsafe void LocalPointers()
+ {
+ int ivalue0 = 5;
+ int ivalue1 = 10;
+
+ int* ip = &ivalue0;
+ int* ip_null = null;
+ int** ipp = &ip;
+ int** ipp_null = &ip_null;
+ int*[] ipa = new int*[] { &ivalue0, &ivalue1, null };
+ int**[] ippa = new int**[] { &ip, &ip_null, ipp, ipp_null, null };
+ char cvalue0 = 'q';
+ char* cp = &cvalue0;
+
+ DateTime dt = new DateTime(5, 6, 7, 8, 9, 10);
+ void* vp = &dt;
+ void* vp_null = null;
+ void** vpp = &vp;
+ void** vpp_null = &vp_null;
+
+ DateTime* dtp = &dt;
+ DateTime* dtp_null = null;
+ DateTime*[] dtpa = new DateTime*[] { dtp, dtp_null };
+ DateTime**[] dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+ Console.WriteLine($"-- break here: ip_null==null: {ip_null == null}, ipp_null: {ipp_null == null}, *ipp_null==ip_null: {*ipp_null == ip_null}, *ipp_null==null: {*ipp_null == null}");
+
+ var gs = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp };
+ var gs_null = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp_null };
+ var gsp = &gs;
+ var gsp_null = &gs_null;
+ var gspa = new GenericStructWithUnmanagedT<DateTime>*[] { null, gsp, gsp_null };
+
+ var cwp = new GenericClassWithPointers<DateTime> { Ptr = dtp };
+ var cwp_null = new GenericClassWithPointers<DateTime>();
+ Console.WriteLine($"{(int)*ip}, {(int)**ipp}, {ipp_null == null}, {ip_null == null}, {ippa == null}, {ipa}, {(char)*cp}, {(vp == null ? "null" : "not null")}, {dtp->Second}, {gsp->IntField}, {cwp}, {cwp_null}, {gs_null}");
+
+ PointersAsArgsTest(ip, ipp, ipa, ippa, &dt, &dtp, dtpa, dtppa);
+ }
+
+ static unsafe void PointersAsArgsTest(int* ip, int** ipp, int*[] ipa, int**[] ippa,
+ DateTime* dtp, DateTime** dtpp, DateTime*[] dtpa, DateTime**[] dtppa)
+ {
+ Console.WriteLine($"break here!");
+ if (ip == null)
+ Console.WriteLine($"ip is null");
+ Console.WriteLine($"done!");
+ }
+
+ public static unsafe async Task LocalPointersAsync()
+ {
+ int ivalue0 = 5;
+ int ivalue1 = 10;
+
+ int* ip = &ivalue0;
+ int* ip_null = null;
+ int** ipp = &ip;
+ int** ipp_null = &ip_null;
+ int*[] ipa = new int*[] { &ivalue0, &ivalue1, null };
+ int**[] ippa = new int**[] { &ip, &ip_null, ipp, ipp_null, null };
+ char cvalue0 = 'q';
+ char* cp = &cvalue0;
+
+ DateTime dt = new DateTime(5, 6, 7, 8, 9, 10);
+ void* vp = &dt;
+ void* vp_null = null;
+ void** vpp = &vp;
+ void** vpp_null = &vp_null;
+
+ DateTime* dtp = &dt;
+ DateTime* dtp_null = null;
+ DateTime*[] dtpa = new DateTime*[] { dtp, dtp_null };
+ DateTime**[] dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+ Console.WriteLine($"-- break here: ip_null==null: {ip_null == null}, ipp_null: {ipp_null == null}, *ipp_null==ip_null: {*ipp_null == ip_null}, *ipp_null==null: {*ipp_null == null}");
+
+ var gs = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp };
+ var gs_null = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp_null };
+ var gsp = &gs;
+ var gsp_null = &gs_null;
+ var gspa = new GenericStructWithUnmanagedT<DateTime>*[] { null, gsp, gsp_null };
+
+ var cwp = new GenericClassWithPointers<DateTime> { Ptr = dtp };
+ var cwp_null = new GenericClassWithPointers<DateTime>();
+ Console.WriteLine($"{(int)*ip}, {(int)**ipp}, {ipp_null == null}, {ip_null == null}, {ippa == null}, {ipa}, {(char)*cp}, {(vp == null ? "null" : "not null")}, {dtp->Second}, {gsp->IntField}, {cwp}, {cwp_null}, {gs_null}");
+ }
+
+ // async methods cannot have unsafe params, so no test for that
+ }
+
+ public unsafe struct GenericStructWithUnmanagedT<T> where T : unmanaged
+ {
+ public T Value;
+ public int IntField;
+
+ public DateTime** DTPP;
+ }
+
+ public unsafe class GenericClassWithPointers<T> where T : unmanaged
+ {
+ public unsafe T* Ptr;
+ }
+}
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public partial class Math
+{ //Only append content to this class as the test suite depends on line info
+ public static int IntAdd(int a, int b)
+ {
+ int c = a + b;
+ int d = c + b;
+ int e = d + a;
+ int f = 0;
+ return e;
+ }
+
+ public static int UseComplex(int a, int b)
+ {
+ var complex = new Simple.Complex(10, "xx");
+ int c = a + b;
+ int d = c + b;
+ int e = d + a;
+ int f = 0;
+ e += complex.DoStuff();
+ return e;
+ }
+
+ delegate bool IsMathNull(Math m);
+
+ public static int DelegatesTest()
+ {
+ Func<Math, bool> fn_func = (Math m) => m == null;
+ Func<Math, bool> fn_func_null = null;
+ Func<Math, bool>[] fn_func_arr = new Func<Math, bool>[] {
+ (Math m) => m == null };
+
+ Math.IsMathNull fn_del = Math.IsMathNullDelegateTarget;
+ var fn_del_arr = new Math.IsMathNull[] { Math.IsMathNullDelegateTarget };
+ var m_obj = new Math();
+ Math.IsMathNull fn_del_null = null;
+ bool res = fn_func(m_obj) && fn_del(m_obj) && fn_del_arr[0](m_obj) && fn_del_null == null && fn_func_null == null && fn_func_arr[0] != null;
+
+ // Unused locals
+
+ Func<Math, bool> fn_func_unused = (Math m) => m == null;
+ Func<Math, bool> fn_func_null_unused = null;
+ Func<Math, bool>[] fn_func_arr_unused = new Func<Math, bool>[] { (Math m) => m == null };
+
+ Math.IsMathNull fn_del_unused = Math.IsMathNullDelegateTarget;
+ Math.IsMathNull fn_del_null_unused = null;
+ var fn_del_arr_unused = new Math.IsMathNull[] { Math.IsMathNullDelegateTarget };
+ OuterMethod();
+ Console.WriteLine("Just a test message, ignore");
+ return res ? 0 : 1;
+ }
+
+ public static int GenericTypesTest()
+ {
+ var list = new System.Collections.Generic.Dictionary<Math[], IsMathNull>();
+ System.Collections.Generic.Dictionary<Math[], IsMathNull> list_null = null;
+
+ var list_arr = new System.Collections.Generic.Dictionary<Math[], IsMathNull>[] { new System.Collections.Generic.Dictionary<Math[], IsMathNull>() };
+ System.Collections.Generic.Dictionary<Math[], IsMathNull>[] list_arr_null = null;
+
+ Console.WriteLine($"list_arr.Length: {list_arr.Length}, list.Count: {list.Count}");
+
+ // Unused locals
+
+ var list_unused = new System.Collections.Generic.Dictionary<Math[], IsMathNull>();
+ System.Collections.Generic.Dictionary<Math[], IsMathNull> list_null_unused = null;
+
+ var list_arr_unused = new System.Collections.Generic.Dictionary<Math[], IsMathNull>[] { new System.Collections.Generic.Dictionary<Math[], IsMathNull>() };
+ System.Collections.Generic.Dictionary<Math[], IsMathNull>[] list_arr_null_unused = null;
+
+ OuterMethod();
+ Console.WriteLine("Just a test message, ignore");
+ return 0;
+ }
+
+ static bool IsMathNullDelegateTarget(Math m) => m == null;
+
+ public static void OuterMethod()
+ {
+ Console.WriteLine($"OuterMethod called");
+ var nim = new Math.NestedInMath();
+ var i = 5;
+ var text = "Hello";
+ var new_i = nim.InnerMethod(i);
+ Console.WriteLine($"i: {i}");
+ Console.WriteLine($"-- InnerMethod returned: {new_i}, nim: {nim}, text: {text}");
+ int k = 19;
+ new_i = InnerMethod2("test string", new_i, out k);
+ Console.WriteLine($"-- InnerMethod2 returned: {new_i}, and k: {k}");
+ }
+
+ static int InnerMethod2(string s, int i, out int k)
+ {
+ k = i + 10;
+ Console.WriteLine($"s: {s}, i: {i}, k: {k}");
+ return i - 2;
+ }
+
+ public class NestedInMath
+ {
+ public int InnerMethod(int i)
+ {
+ SimpleStructProperty = new SimpleStruct() { dt = new DateTime(2020, 1, 2, 3, 4, 5) };
+ int j = i + 10;
+ string foo_str = "foo";
+ Console.WriteLine($"i: {i} and j: {j}, foo_str: {foo_str} ");
+ j += 9;
+ Console.WriteLine($"i: {i} and j: {j}");
+ return j;
+ }
+
+ Math m = new Math();
+ public async System.Threading.Tasks.Task<bool> AsyncMethod0(string s, int i)
+ {
+ string local0 = "value0";
+ await System.Threading.Tasks.Task.Delay(1);
+ Console.WriteLine($"* time for the second await, local0: {local0}");
+ await AsyncMethodNoReturn();
+ return true;
+ }
+
+ public async System.Threading.Tasks.Task AsyncMethodNoReturn()
+ {
+ var ss = new SimpleStruct() { dt = new DateTime(2020, 1, 2, 3, 4, 5) };
+ var ss_arr = new SimpleStruct[] { };
+ //ss.gs.StringField = "field in GenericStruct";
+
+ //Console.WriteLine ($"Using the struct: {ss.dt}, {ss.gs.StringField}, ss_arr: {ss_arr.Length}");
+ string str = "AsyncMethodNoReturn's local";
+ //Console.WriteLine ($"* field m: {m}");
+ await System.Threading.Tasks.Task.Delay(1);
+ Console.WriteLine($"str: {str}");
+ }
+
+ public static async System.Threading.Tasks.Task<bool> AsyncTest(string s, int i)
+ {
+ var li = 10 + i;
+ var ls = s + "test";
+ return await new NestedInMath().AsyncMethod0(s, i);
+ }
+
+ public SimpleStruct SimpleStructProperty { get; set; }
+ }
+
+ public static void PrimitiveTypesTest()
+ {
+ char c0 = '€';
+ char c1 = 'A';
+ // TODO: other types!
+ // just trying to ensure vars don't get optimized out
+ if (c0 < 32 || c1 > 32)
+ Console.WriteLine($"{c0}, {c1}");
+ }
+
+ public static int DelegatesSignatureTest()
+ {
+ Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>> fn_func = (m, gs) => new GenericStruct<bool[]>();
+ Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>> fn_func_del = GenericStruct<int>.DelegateTargetForSignatureTest;
+ Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>> fn_func_null = null;
+ Func<bool> fn_func_only_ret = () => { Console.WriteLine ($"hello"); return true; };
+ var fn_func_arr = new Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>>[] {
+ (m, gs) => new GenericStruct<bool[]> () };
+
+ Math.DelegateForSignatureTest fn_del = GenericStruct<int>.DelegateTargetForSignatureTest;
+ Math.DelegateForSignatureTest fn_del_l = (m, gs) => new GenericStruct<bool[]> { StringField = "fn_del_l#lambda" };
+ var fn_del_arr = new Math.DelegateForSignatureTest[] { GenericStruct<int>.DelegateTargetForSignatureTest, (m, gs) => new GenericStruct<bool[]> { StringField = "fn_del_arr#1#lambda" } };
+ var m_obj = new Math();
+ Math.DelegateForSignatureTest fn_del_null = null;
+ var gs_gs = new GenericStruct<GenericStruct<int[]>>
+ {
+ List = new System.Collections.Generic.List<GenericStruct<int[]>>
+ {
+ new GenericStruct<int[]> { StringField = "gs#List#0#StringField" },
+ new GenericStruct<int[]> { StringField = "gs#List#1#StringField" }
+ }
+ };
+
+ Math.DelegateWithVoidReturn fn_void_del = Math.DelegateTargetWithVoidReturn;
+ var fn_void_del_arr = new Math.DelegateWithVoidReturn[] { Math.DelegateTargetWithVoidReturn };
+ Math.DelegateWithVoidReturn fn_void_del_null = null;
+
+ var rets = new GenericStruct<bool[]>[]
+ {
+ fn_func(m_obj, gs_gs),
+ fn_func_del(m_obj, gs_gs),
+ fn_del(m_obj, gs_gs),
+ fn_del_l(m_obj, gs_gs),
+ fn_del_arr[0](m_obj, gs_gs),
+ fn_func_arr[0](m_obj, gs_gs)
+ };
+
+ var gs = new GenericStruct<int[]>();
+ fn_void_del(gs);
+ fn_void_del_arr[0](gs);
+ fn_func_only_ret();
+ foreach (var ret in rets) Console.WriteLine($"ret: {ret}");
+ OuterMethod();
+ Console.WriteLine($"- {gs_gs.List[0].StringField}");
+ return 0;
+ }
+
+ public static int ActionTSignatureTest()
+ {
+ Action<GenericStruct<int[]>> fn_action = (_) => { };
+ Action<GenericStruct<int[]>> fn_action_del = Math.DelegateTargetWithVoidReturn;
+ Action fn_action_bare = () => { };
+ Action<GenericStruct<int[]>> fn_action_null = null;
+ var fn_action_arr = new Action<GenericStruct<int[]>>[]
+ {
+ (gs) => new GenericStruct<int[]>(),
+ Math.DelegateTargetWithVoidReturn,
+ null
+ };
+
+ var gs = new GenericStruct<int[]>();
+ fn_action(gs);
+ fn_action_del(gs);
+ fn_action_arr[0](gs);
+ fn_action_bare();
+ OuterMethod();
+ return 0;
+ }
+
+ public static int NestedDelegatesTest()
+ {
+ Func<Func<int, bool>, bool> fn_func = (_) => { return true; };
+ Func<Func<int, bool>, bool> fn_func_null = null;
+ var fn_func_arr = new Func<Func<int, bool>, bool>[] {
+ (gs) => { return true; } };
+
+ var fn_del_arr = new Func<Func<int, bool>, bool>[] { DelegateTargetForNestedFunc<Func<int, bool>> };
+ var m_obj = new Math();
+ Func<Func<int, bool>, bool> fn_del_null = null;
+ Func<int, bool> fs = (i) => i == 0;
+ fn_func(fs);
+ fn_del_arr[0](fs);
+ fn_func_arr[0](fs);
+ OuterMethod();
+ return 0;
+ }
+
+ public static void DelegatesAsMethodArgsTest()
+ {
+ var _dst_arr = new DelegateForSignatureTest[]
+ {
+ GenericStruct<int>.DelegateTargetForSignatureTest,
+ (m, gs) => new GenericStruct<bool[]>()
+ };
+ Func<char[], bool> _fn_func = (cs) => cs.Length == 0;
+ Action<GenericStruct<int>[]> _fn_action = (gss) => { };
+
+ new Math().MethodWithDelegateArgs(_dst_arr, _fn_func, _fn_action);
+ }
+
+ void MethodWithDelegateArgs(Math.DelegateForSignatureTest[] dst_arr, Func<char[], bool> fn_func,
+ Action<GenericStruct<int>[]> fn_action)
+ {
+ Console.WriteLine($"Placeholder for breakpoint");
+ OuterMethod();
+ }
+
+ public static async System.Threading.Tasks.Task MethodWithDelegatesAsyncTest()
+ {
+ await new Math().MethodWithDelegatesAsync();
+ }
+
+ async System.Threading.Tasks.Task MethodWithDelegatesAsync()
+ {
+ var _dst_arr = new DelegateForSignatureTest[]
+ {
+ GenericStruct<int>.DelegateTargetForSignatureTest,
+ (m, gs) => new GenericStruct<bool[]>()
+ };
+ Func<char[], bool> _fn_func = (cs) => cs.Length == 0;
+ Action<GenericStruct<int>[]> _fn_action = (gss) => { };
+
+ Console.WriteLine($"Placeholder for breakpoint");
+ await System.Threading.Tasks.Task.CompletedTask;
+ }
+
+ public delegate void DelegateWithVoidReturn(GenericStruct<int[]> gs);
+ public static void DelegateTargetWithVoidReturn(GenericStruct<int[]> gs) { }
+
+ delegate GenericStruct<bool[]> DelegateForSignatureTest(Math m, GenericStruct<GenericStruct<int[]>> gs);
+ static bool DelegateTargetForNestedFunc<T>(T arg) => true;
+
+ public struct SimpleStruct
+ {
+ public DateTime dt;
+ public GenericStruct<DateTime> gs;
+ }
+
+ public struct GenericStruct<T>
+ {
+ public System.Collections.Generic.List<T> List;
+ public string StringField;
+
+ public static GenericStruct<bool[]> DelegateTargetForSignatureTest(Math m, GenericStruct<GenericStruct<T[]>> gs) => new GenericStruct<bool[]>();
+ }
+
+}
+
+public class DebuggerTest
+{
+ public static void run_all()
+ {
+ locals();
+ }
+
+ public static int locals()
+ {
+ int l_int = 1;
+ char l_char = 'A';
+ long l_long = Int64.MaxValue;
+ ulong l_ulong = UInt64.MaxValue;
+ locals_inner();
+ return 0;
+ }
+
+ static void locals_inner() { }
+}
\ No newline at end of file
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildApp">
+ <PropertyGroup>
+ <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+ <TargetArchitecture>wasm</TargetArchitecture>
+ <TargetOS>Browser</TargetOS>
+ <MicrosoftNetCoreAppRuntimePackDir>$(ArtifactsBinDir)microsoft.netcore.app.runtime.browser-wasm\Release\runtimes\browser-wasm\</MicrosoftNetCoreAppRuntimePackDir>
+ <BuildDir>$(MSBuildThisFileDirectory)obj\$(Configuration)\wasm</BuildDir>
+ <AppDir>$(MSBuildThisFileDirectory)bin\$(Configuration)\publish</AppDir>
+ <RuntimeBuildConfig Condition="'$(RuntimeBuildConfig)' == ''">$(Configuration)</RuntimeBuildConfig>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <OutputType>Library</OutputType>
+ <NoWarn>219</NoWarn>
+ <DebugType>portable</DebugType>
+ </PropertyGroup>
+
+ <Target Name="RebuildWasmAppBuilder">
+ <ItemGroup>
+ <WasmAppBuildProject Include="$(RepoTasksDir)mobile.tasks\WasmAppBuilder\WasmAppBuilder.csproj" />
+ </ItemGroup>
+
+ <MSBuild Projects="@(WasmAppBuildProject)"
+ Properties="Configuration=$(Configuration);MSBuildRestoreSessionId=$([System.Guid]::NewGuid())"
+ Targets="Restore"/>
+
+ <MSBuild Projects="@(WasmAppBuildProject)"
+ Properties="Configuration=$(Configuration)"
+ Targets="Build;Publish"/>
+ </Target>
+
+ <UsingTask TaskName="WasmAppBuilder"
+ AssemblyFile="$(ArtifactsBinDir)WasmAppBuilder\$(Configuration)\$(NetCoreAppCurrent)\publish\WasmAppBuilder.dll"/>
+
+ <Target Name="BuildApp" DependsOnTargets="RebuildWasmAppBuilder;Build">
+ <ItemGroup>
+ <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)native"/>
+ <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)lib\$(NetCoreAppCurrent)"/>
+ </ItemGroup>
+ <WasmAppBuilder
+ AppDir="$(AppDir)"
+ MicrosoftNetCoreAppRuntimePackDir="$(MicrosoftNetCoreAppRuntimePackDir)"
+ MainAssembly="$(ArtifactsBinDir)debugger-test/wasm/Debug/debugger-test.dll"
+ MainJS="$(MonoProjectRoot)wasm\runtime-test.js"
+ DebugLevel="1"
+ AssemblySearchPaths="@(AssemblySearchPaths)"
+ ExtraAssemblies="$(ArtifactsBinDir)\System.Runtime.InteropServices.JavaScript\$(NetCoreAppCurrent)-Browser-$(RuntimeConfiguration)\System.Runtime.InteropServices.JavaScript.dll"/>
+ <Exec Command="chmod a+x $(AppDir)/run-v8.sh" />
+ </Target>
+
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public class Misc
+{ //Only append content to this class as the test suite depends on line info
+ public static int CreateObject(int foo, int bar)
+ {
+ var f = new Fancy()
+ {
+ Foo = foo,
+ Bar = bar,
+ };
+
+ Console.WriteLine($"{f.Foo} {f.Bar}");
+ return f.Foo + f.Bar;
+ }
+}
+
+public class Fancy
+{
+ public int Foo;
+ public int Bar { get; set; }
+ public static void Types()
+ {
+ double dPI = System.Math.PI;
+ float fPI = (float) System.Math.PI;
+
+ int iMax = int.MaxValue;
+ int iMin = int.MinValue;
+ uint uiMax = uint.MaxValue;
+ uint uiMin = uint.MinValue;
+
+ long l = uiMax * (long) 2;
+ long lMax = long.MaxValue; // cannot be represented as double
+ long lMin = long.MinValue; // cannot be represented as double
+
+ sbyte sbMax = sbyte.MaxValue;
+ sbyte sbMin = sbyte.MinValue;
+ byte bMax = byte.MaxValue;
+ byte bMin = byte.MinValue;
+
+ short sMax = short.MaxValue;
+ short sMin = short.MinValue;
+ ushort usMin = ushort.MinValue;
+ ushort usMax = ushort.MaxValue;
+
+ var d = usMin + usMax;
+ }
+}
\ No newline at end of file
--- /dev/null
+// 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.Tasks;
+namespace DebuggerTests
+{
+ public class ValueTypesTest
+ { //Only append content to this class as the test suite depends on line info
+
+ public static void MethodWithLocalStructs()
+ {
+ var ss_local = new SimpleStruct("set in MethodWithLocalStructs", 1, DateTimeKind.Utc);
+ var gs_local = new GenericStruct<ValueTypesTest> { StringField = "gs_local#GenericStruct<ValueTypesTest>#StringField" };
+
+ ValueTypesTest vt_local = new ValueTypesTest
+ {
+ StringField = "string#0",
+ SimpleStructField = new SimpleStruct("SimpleStructField#string#0", 5, DateTimeKind.Local),
+ SimpleStructProperty = new SimpleStruct("SimpleStructProperty#string#0", 2, DateTimeKind.Utc), DT = new DateTime(2020, 1, 2, 3, 4, 5), RGB = RGB.Blue
+ };
+ Console.WriteLine($"Using the struct: {ss_local.gs.StringField}, gs: {gs_local.StringField}, {vt_local.StringField}");
+ }
+
+ public static void TestStructsAsMethodArgs()
+ {
+ var ss_local = new SimpleStruct("ss_local#SimpleStruct#string#0", 5, DateTimeKind.Local);
+ var ss_ret = MethodWithStructArgs("TestStructsAsMethodArgs#label", ss_local, 3);
+ Console.WriteLine($"got back ss_local: {ss_local.gs.StringField}, ss_ret: {ss_ret.gs.StringField}");
+ }
+
+ static SimpleStruct MethodWithStructArgs(string label, SimpleStruct ss_arg, int x)
+ {
+ Console.WriteLine($"- ss_arg: {ss_arg.str_member}");
+ ss_arg.Kind = DateTimeKind.Utc;
+ ss_arg.str_member = $"ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member";
+ ss_arg.gs.StringField = $"ValueTypesTest#MethodWithStructArgs#updated#gs#StringField#{x}";
+ return ss_arg;
+ }
+
+ public static async Task<bool> MethodWithLocalStructsStaticAsync()
+ {
+ var ss_local = new SimpleStruct("set in MethodWithLocalStructsStaticAsync", 1, DateTimeKind.Utc);
+ var gs_local = new GenericStruct<int>
+ {
+ StringField = "gs_local#GenericStruct<ValueTypesTest>#StringField",
+ List = new System.Collections.Generic.List<int> { 5, 3 },
+ Options = Options.Option2
+
+ };
+
+ var result = await ss_local.AsyncMethodWithStructArgs(gs_local);
+ Console.WriteLine($"Using the struct: {ss_local.gs.StringField}, result: {result}");
+
+ return result;
+ }
+
+ public string StringField;
+ public SimpleStruct SimpleStructProperty { get; set; }
+ public SimpleStruct SimpleStructField;
+
+ public struct SimpleStruct
+ {
+ public string str_member;
+ public DateTime dt;
+ public GenericStruct<DateTime> gs;
+ public DateTimeKind Kind;
+
+ public SimpleStruct(string str, int f, DateTimeKind kind)
+ {
+ str_member = $"{str}#SimpleStruct#str_member";
+ dt = new DateTime(2020 + f, 1 + f, 2 + f, 3 + f, 5 + f, 6 + f);
+ gs = new GenericStruct<DateTime>
+ {
+ StringField = $"{str}#SimpleStruct#gs#StringField",
+ List = new System.Collections.Generic.List<DateTime> { new DateTime(2010 + f, 2 + f, 3 + f, 10 + f, 2 + f, 3 + f) },
+ Options = Options.Option1
+ };
+ Kind = kind;
+ }
+
+ public Task<bool> AsyncMethodWithStructArgs(GenericStruct<int> gs)
+ {
+ Console.WriteLine($"placeholder line for a breakpoint");
+ if (gs.List.Count > 0)
+ return Task.FromResult(true);
+
+ return Task.FromResult(false);
+ }
+ }
+
+ public struct GenericStruct<T>
+ {
+ public System.Collections.Generic.List<T> List;
+ public string StringField;
+
+ public Options Options { get; set; }
+ }
+
+ public DateTime DT { get; set; }
+ public RGB RGB;
+
+ public static void MethodWithLocalsForToStringTest(bool call_other)
+ {
+ var dt0 = new DateTime(2020, 1, 2, 3, 4, 5);
+ var dt1 = new DateTime(2010, 5, 4, 3, 2, 1);
+ var ts = dt0 - dt1;
+ var dto = new DateTimeOffset(dt0, new TimeSpan(4, 5, 0));
+ decimal dec = 123987123;
+ var guid = new Guid("3d36e07e-ac90-48c6-b7ec-a481e289d014");
+
+ var dts = new DateTime[]
+ {
+ new DateTime(1983, 6, 7, 5, 6, 10),
+ new DateTime(1999, 10, 15, 1, 2, 3)
+ };
+
+ var obj = new ClassForToStringTests
+ {
+ DT = new DateTime(2004, 10, 15, 1, 2, 3),
+ DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0)),
+ TS = ts,
+ Dec = 1239871,
+ Guid = guid
+ };
+
+ var sst = new StructForToStringTests
+ {
+ DT = new DateTime(2004, 10, 15, 1, 2, 3),
+ DTO = new DateTimeOffset(dt0, new TimeSpan(3, 15, 0)),
+ TS = ts,
+ Dec = 1239871,
+ Guid = guid
+ };
+ Console.WriteLine($"MethodWithLocalsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+ if (call_other)
+ MethodWithArgumentsForToStringTest(call_other, dt0, dt1, ts, dto, dec, guid, dts, obj, sst);
+ }
+
+ static void MethodWithArgumentsForToStringTest(
+ bool call_other, // not really used, just to help with using common code in the tests
+ DateTime dt0, DateTime dt1, TimeSpan ts, DateTimeOffset dto, decimal dec,
+ Guid guid, DateTime[] dts, ClassForToStringTests obj, StructForToStringTests sst)
+ {
+ Console.WriteLine($"MethodWithArgumentsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+ }
+
+ public static async Task MethodWithLocalsForToStringTestAsync(bool call_other)
+ {
+ var dt0 = new DateTime(2020, 1, 2, 3, 4, 5);
+ var dt1 = new DateTime(2010, 5, 4, 3, 2, 1);
+ var ts = dt0 - dt1;
+ var dto = new DateTimeOffset(dt0, new TimeSpan(4, 5, 0));
+ decimal dec = 123987123;
+ var guid = new Guid("3d36e07e-ac90-48c6-b7ec-a481e289d014");
+
+ var dts = new DateTime[]
+ {
+ new DateTime(1983, 6, 7, 5, 6, 10),
+ new DateTime(1999, 10, 15, 1, 2, 3)
+ };
+
+ var obj = new ClassForToStringTests
+ {
+ DT = new DateTime(2004, 10, 15, 1, 2, 3),
+ DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0)),
+ TS = ts,
+ Dec = 1239871,
+ Guid = guid
+ };
+
+ var sst = new StructForToStringTests
+ {
+ DT = new DateTime(2004, 10, 15, 1, 2, 3),
+ DTO = new DateTimeOffset(dt0, new TimeSpan(3, 15, 0)),
+ TS = ts,
+ Dec = 1239871,
+ Guid = guid
+ };
+ Console.WriteLine($"MethodWithLocalsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+ if (call_other)
+ await MethodWithArgumentsForToStringTestAsync(call_other, dt0, dt1, ts, dto, dec, guid, dts, obj, sst);
+ }
+
+ static async Task MethodWithArgumentsForToStringTestAsync(
+ bool call_other, // not really used, just to help with using common code in the tests
+ DateTime dt0, DateTime dt1, TimeSpan ts, DateTimeOffset dto, decimal dec,
+ Guid guid, DateTime[] dts, ClassForToStringTests obj, StructForToStringTests sst)
+ {
+ Console.WriteLine($"MethodWithArgumentsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+ }
+
+ public static void MethodUpdatingValueTypeMembers()
+ {
+ var obj = new ClassForToStringTests
+ {
+ DT = new DateTime(1, 2, 3, 4, 5, 6)
+ };
+ var vt = new StructForToStringTests
+ {
+ DT = new DateTime(4, 5, 6, 7, 8, 9)
+ };
+ Console.WriteLine($"#1");
+ obj.DT = new DateTime(9, 8, 7, 6, 5, 4);
+ vt.DT = new DateTime(5, 1, 3, 7, 9, 10);
+ Console.WriteLine($"#2");
+ }
+
+ public static async Task MethodUpdatingValueTypeLocalsAsync()
+ {
+ var dt = new DateTime(1, 2, 3, 4, 5, 6);
+ Console.WriteLine($"#1");
+ dt = new DateTime(9, 8, 7, 6, 5, 4);
+ Console.WriteLine($"#2");
+ }
+
+ public static void MethodUpdatingVTArrayMembers()
+ {
+ var ssta = new []
+ {
+ new StructForToStringTests { DT = new DateTime(1, 2, 3, 4, 5, 6) }
+ };
+ Console.WriteLine($"#1");
+ ssta[0].DT = new DateTime(9, 8, 7, 6, 5, 4);
+ Console.WriteLine($"#2");
+ }
+ }
+
+ class ClassForToStringTests
+ {
+ public DateTime DT;
+ public DateTimeOffset DTO;
+ public TimeSpan TS;
+ public decimal Dec;
+ public Guid Guid;
+ }
+
+ struct StructForToStringTests
+ {
+ public DateTime DT;
+ public DateTimeOffset DTO;
+ public TimeSpan TS;
+ public decimal Dec;
+ public Guid Guid;
+ }
+
+ public enum RGB
+ {
+ Red,
+ Green,
+ Blue
+ }
+
+ [Flags]
+ public enum Options
+ {
+ None = 0,
+ Option1 = 1,
+ Option2 = 2,
+ Option3 = 4,
+
+ All = Option1 | Option3
+ }
+}
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Simple
+{
+ public class Complex
+ {
+ public int A { get; set; }
+ public string B { get; set; }
+ object c;
+
+ public Complex(int a, string b)
+ {
+ A = a;
+ B = b;
+ this.c = this;
+ }
+
+ public int DoStuff()
+ {
+ return DoOtherStuff();
+ }
+
+ public int DoOtherStuff()
+ {
+ return DoEvenMoreStuff() - 1;
+ }
+
+ public int DoEvenMoreStuff()
+ {
+ return 1 + BreakOnThisMethod();
+ }
+
+ public int BreakOnThisMethod()
+ {
+ var x = A + 10;
+ c = $"{x}_{B}";
+
+ return x;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+function big_array_js_test (len) {
+ var big = new Array(len);
+ for (let i=0; i < len; i ++) {
+ big[i]=i + 1000;
+ }
+ console.log('break here');
+};
+
+function object_js_test () {
+ var obj = {
+ a_obj: { aa: 5, ab: 'foo' },
+ b_arr: [ 10, 12 ]
+ };
+
+ return obj;
+};
+
+function getters_js_test () {
+ var ptd = {
+ get Int () { return 5; },
+ get String () { return "foobar"; },
+ get DT () { return "dt"; },
+ get IntArray () { return [1,2,3]; },
+ get DTArray () { return ["dt0", "dt1"]; },
+ DTAutoProperty: "dt",
+ StringField: "string field value"
+ };
+ console.log (`break here`);
+ return ptd;
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+var Module = {
+ onRuntimeInitialized: function () {
+ config.loaded_cb = function () {
+ App.init ();
+ };
+ MONO.mono_load_runtime_and_bcl_args (config)
+ },
+};
},
},
+ mono_wasm_get_exception_object: function() {
+ var exception_obj = MONO.active_exception;
+ MONO.active_exception = null;
+ return exception_obj ;
+ },
+
mono_wasm_get_call_stack: function() {
if (!this.mono_wasm_current_bp_id)
this.mono_wasm_current_bp_id = Module.cwrap ("mono_wasm_current_bp_id", 'number');
continue;
}
- if (i + 1 < var_list.length)
- o.value = _fixup_value(var_list[i + 1].value);
+ if (i + 1 < var_list.length) {
+ _fixup_value(var_list[i + 1].value);
+ o = Object.assign (o, var_list [i + 1]);
+ }
out_list.push (o);
i += 2;
return final_var_list;
},
+ // Given `dotnet:object:foo:bar`,
+ // returns [ 'dotnet', 'object', 'foo:bar']
+ _split_object_id: function (id, delimiter = ':', count = 3) {
+ if (id === undefined || id == "")
+ return [];
+
+ if (delimiter === undefined) delimiter = ':';
+ if (count === undefined) count = 3;
+
+ var var_arr = id.split (delimiter);
+ var result = var_arr.splice (0, count - 1);
+
+ if (var_arr.length > 0)
+ result.push (var_arr.join (delimiter));
+ return result;
+ },
+
+ //
+ // @var_list: [ { index: <var_id>, name: <var_name> }, .. ]
mono_wasm_get_variables: function(scope, var_list) {
if (!this.mono_wasm_get_var_info)
this.mono_wasm_get_var_info = Module.cwrap ("mono_wasm_get_var_info", null, [ 'number', 'number', 'number']);
var numBytes = var_list.length * Int32Array.BYTES_PER_ELEMENT;
var ptr = Module._malloc(numBytes);
var heapBytes = new Int32Array(Module.HEAP32.buffer, ptr, numBytes);
- for (let i=0; i<var_list.length; i++)
- heapBytes[i] = var_list[i];
+ for (let i=0; i<var_list.length; i++) {
+ heapBytes[i] = var_list[i].index;
+ }
this._async_method_objectId = 0;
this.mono_wasm_get_var_info (scope, heapBytes.byteOffset, var_list.length);
Module._free(heapBytes.byteOffset);
var res = MONO._fixup_name_value_objects (this.var_info);
- //Async methods are special in the way that local variables can be lifted to generated class fields
- //value of "this" comes here either
for (let i in res) {
- var name = res [i].name;
- if (name != undefined && name.indexOf ('>') > 0)
- res [i].name = name.substring (1, name.indexOf ('>'));
- }
+ var res_name = res [i].name;
+
+ var value = res[i].value;
+ if (this._async_method_objectId != 0) {
+ //Async methods are special in the way that local variables can be lifted to generated class fields
+ //value of "this" comes here either
+ if (res_name !== undefined && res_name.indexOf ('>') > 0) {
+ // For async methods, we get the names too, so use that
+ // ALTHOUGH, the name wouldn't have `<>` for method args
+ res [i].name = res_name.substring (1, res_name.indexOf ('>'));
+ }
- if (this._async_method_objectId != 0) {
- for (let i in res) {
- if (res [i].value.isValueType != undefined && res [i].value.isValueType)
- res [i].value.objectId = `dotnet:valuetype:${this._async_method_objectId}:${res [i].fieldOffset}`;
+ if (value.isValueType)
+ value.objectId = `dotnet:valuetype:${this._async_method_objectId}:${res [i].fieldOffset}`;
+ } else if (res_name === undefined && var_list [i] !== undefined) {
+ // For non-async methods, we just have the var id, but we have the name
+ // from the caller
+ res [i].name = var_list [i].name;
}
}
return res;
},
+
mono_wasm_get_object_properties: function(objId, expandValueTypes) {
if (!this.mono_wasm_get_object_properties_info)
this.mono_wasm_get_object_properties_info = Module.cwrap ("mono_wasm_get_object_properties", null, [ 'number', 'bool' ]);
var res = MONO._filter_automatic_properties (MONO._fixup_name_value_objects (this.var_info));
for (var i = 0; i < res.length; i++) {
- if (res [i].value.isValueType != undefined && res [i].value.isValueType)
- res [i].value.objectId = `dotnet:valuetype:${objId}:${res [i].fieldOffset}`;
+ var res_val = res [i].value;
+ // we might not have a `.value`, like in case of getters which have a `.get` instead
+ if (res_val !== undefined && res_val.isValueType != undefined && res_val.isValueType)
+ res_val.objectId = `dotnet:valuetype:${objId}:${res [i].fieldOffset}`;
}
this.var_info = [];
var res = MONO._fixup_name_value_objects (this.var_info);
for (var i = 0; i < res.length; i++) {
- if (res [i].value.isValueType != undefined && res [i].value.isValueType)
+ var prop_value = res [i].value;
+ if (prop_value.isValueType) {
res [i].value.objectId = `dotnet:array:${objId}:${i}`;
+ } else if (prop_value.objectId !== undefined && prop_value.objectId.startsWith("dotnet:pointer")) {
+ prop_value.objectId = this._get_updated_ptr_id (prop_value.objectId, {
+ varName: `[${i}]`
+ });
+ }
}
this.var_info = [];
for (let i in var_list) {
var value = var_list [i].value;
- if (value == undefined || value.type != "object")
+ if (value === undefined)
continue;
- if (value.isValueType != true || value.expanded != true) // undefined would also give us false
+ if (value.objectId !== undefined && value.objectId.startsWith ("dotnet:pointer:")) {
+ var ptr_args = this._get_ptr_args (value.objectId);
+ if (ptr_args.varName === undefined) {
+ // It might have been already set in some cases, like arrays
+ // where the name would be `0`, but we want `[0]` for pointers,
+ // so the deref would look like `*[0]`
+ value.objectId = this._get_updated_ptr_id (value.objectId, {
+ varName: var_list [i].name
+ });
+ }
+ }
+
+ if (value.type != "object" || value.isValueType != true || value.expanded != true) // undefined would also give us false
continue;
+ // Generate objectId for expanded valuetypes
+
var objectId = value.objectId;
if (objectId == undefined)
objectId = `dotnet:valuetype:${this._next_value_type_id ()}`;
}
},
+ _get_cfo_res_details: function (objectId, args) {
+ if (!(objectId in this._call_function_res_cache))
+ throw new Error(`Could not find any object with id ${objectId}`);
+
+ var real_obj = this._call_function_res_cache [objectId];
+
+ var descriptors = Object.getOwnPropertyDescriptors (real_obj);
+ if (args.accessorPropertiesOnly) {
+ Object.keys (descriptors).forEach (k => {
+ if (descriptors [k].get === undefined)
+ Reflect.deleteProperty (descriptors, k);
+ });
+ }
+
+ var res_details = [];
+ Object.keys (descriptors).forEach (k => {
+ var new_obj;
+ var prop_desc = descriptors [k];
+ if (typeof prop_desc.value == "object") {
+ // convert `{value: { type='object', ... }}`
+ // to `{ name: 'foo', value: { type='object', ... }}
+ new_obj = Object.assign ({ name: k }, prop_desc);
+ } else if (prop_desc.value !== undefined) {
+ // This is needed for values that were not added by us,
+ // thus are like { value: 5 }
+ // instead of { value: { type = 'number', value: 5 }}
+ //
+ // This can happen, for eg., when `length` gets added for arrays
+ // or `__proto__`.
+ new_obj = {
+ name: k,
+ // merge/add `type` and `description` to `d.value`
+ value: Object.assign ({ type: (typeof prop_desc.value), description: '' + prop_desc.value },
+ prop_desc)
+ };
+ } else if (prop_desc.get !== undefined) {
+ // The real_obj has the actual getter. We are just returning a placeholder
+ // If the caller tries to run function on the cfo_res object,
+ // that accesses this property, then it would be run on `real_obj`,
+ // which *has* the original getter
+ new_obj = {
+ name: k,
+ get: {
+ className: "Function",
+ description: `get ${k} () {}`,
+ type: "function"
+ }
+ };
+ } else {
+ new_obj = { name: k, value: { type: "symbol", value: "<Unknown>", description: "<Unknown>"} };
+ }
+
+ res_details.push (new_obj);
+ });
+
+ return { __value_as_json_string__: JSON.stringify (res_details) };
+ },
+
+ _get_ptr_args: function (objectId) {
+ var parts = this._split_object_id (objectId);
+ if (parts.length != 3)
+ throw new Error (`Bug: Unexpected objectId format for a pointer, expected 3 parts: ${objectId}`);
+ return JSON.parse (parts [2]);
+ },
+
+ _get_updated_ptr_id: function (objectId, new_args) {
+ var old_args = {};
+ if (typeof (objectId) === 'string' && objectId.length)
+ old_args = this._get_ptr_args (objectId);
+
+ return `dotnet:pointer:${JSON.stringify ( Object.assign (old_args, new_args) )}`;
+ },
+
+ _get_deref_ptr_value: function (objectId) {
+ if (!this.mono_wasm_get_deref_ptr_value_info)
+ this.mono_wasm_get_deref_ptr_value_info = Module.cwrap("mono_wasm_get_deref_ptr_value", null, ['number', 'number']);
+
+ var ptr_args = this._get_ptr_args (objectId);
+ if (ptr_args.ptr_addr == 0 || ptr_args.klass_addr == 0)
+ throw new Error (`Both ptr_addr and klass_addr need to be non-zero, to dereference a pointer. objectId: ${objectId}`);
+
+ this.var_info = [];
+ var value_addr = new DataView (Module.HEAPU8.buffer).getUint32 (ptr_args.ptr_addr, /* littleEndian */ true);
+ this.mono_wasm_get_deref_ptr_value_info (value_addr, ptr_args.klass_addr);
+
+ var res = MONO._fixup_name_value_objects(this.var_info);
+ if (res.length > 0) {
+ if (ptr_args.varName === undefined)
+ throw new Error (`Bug: no varName found for the pointer. objectId: ${objectId}`);
+
+ res [0].name = `*${ptr_args.varName}`;
+ }
+
+ res = this._post_process_details (res);
+ this.var_info = [];
+ return res;
+ },
+
mono_wasm_get_details: function (objectId, args) {
var parts = objectId.split(":");
if (parts[0] != "dotnet")
var containerObjectId = parts[2];
return this._get_details_for_value_type(objectId, () => this.mono_wasm_get_object_properties(containerObjectId, true));
- case "cfo_res": {
- if (!(objectId in this._call_function_res_cache))
- throw new Error(`Could not find any object with id ${objectId}`);
-
- var real_obj = this._call_function_res_cache [objectId];
- if (args.accessorPropertiesOnly) {
- // var val_accessors = JSON.stringify ([
- // {
- // name: "__proto__",
- // get: { type: "function", className: "Function", description: "function get __proto__ () {}", objectId: "dotnet:cfo_res:9999" },
- // set: { type: "function", className: "Function", description: "function set __proto__ () {}", objectId: "dotnet:cfo_res:8888" },
- // isOwn: false
- // }], undefined, 4);
- return { __value_as_json_string__: "[]" };
- }
-
- // behaving as if (args.ownProperties == true)
- var descriptors = Object.getOwnPropertyDescriptors (real_obj);
- var own_properties = [];
- Object.keys (descriptors).forEach (k => {
- var new_obj;
- var prop_desc = descriptors [k];
- if (typeof prop_desc.value == "object") {
- // convert `{value: { type='object', ... }}`
- // to `{ name: 'foo', value: { type='object', ... }}
- new_obj = Object.assign ({ name: k}, prop_desc);
- } else {
- // This is needed for values that were not added by us,
- // thus are like { value: 5 }
- // instead of { value: { type = 'number', value: 5 }}
- //
- // This can happen, for eg., when `length` gets added for arrays
- // or `__proto__`.
- new_obj = {
- name: k,
- // merge/add `type` and `description` to `d.value`
- value: Object.assign ({ type: (typeof prop_desc.value), description: '' + prop_desc.value },
- prop_desc)
- };
- }
+ case "cfo_res":
+ return this._get_cfo_res_details (objectId, args);
- own_properties.push (new_obj);
- });
-
- return { __value_as_json_string__: JSON.stringify (own_properties) };
+ case "pointer": {
+ return this._get_deref_ptr_value (objectId);
}
default:
delete this._cache_call_function_res[objectId];
},
+ _invoke_getter_on_object: function (objectId, name) {
+ if (!this.mono_wasm_invoke_getter_on_object)
+ this.mono_wasm_invoke_getter_on_object = Module.cwrap ("mono_wasm_invoke_getter_on_object", 'void', [ 'number', 'string' ]);
+
+ if (objectId < 0) {
+ // invalid id
+ return [];
+ }
+
+ this.mono_wasm_invoke_getter_on_object (objectId, name);
+ var getter_res = MONO._post_process_details (MONO.var_info);
+
+ MONO.var_info = [];
+ return getter_res [0];
+ },
+
+ _create_proxy_from_object_id: function (objectId) {
+ var details = this.mono_wasm_get_details(objectId);
+
+ if (this._is_object_id_array (objectId))
+ return details.map (p => p.value);
+
+ var objIdParts = objectId.split (':');
+ var objIdNum = -1;
+ if (objectId.startsWith ("dotnet:object:"))
+ objIdNum = objIdParts [2];
+
+ var proxy = {};
+ Object.keys (details).forEach (p => {
+ var prop = details [p];
+ if (prop.get !== undefined) {
+ // TODO: `set`
+
+ // We don't add a `get` for non-object types right now,
+ // so, we shouldn't get here with objIdNum==-1
+ Object.defineProperty (proxy,
+ prop.name,
+ { get () { return MONO._invoke_getter_on_object (objIdNum, prop.name); } }
+ );
+ } else {
+ proxy [prop.name] = prop.value;
+ }
+ });
+
+ return proxy;
+ },
+
mono_wasm_call_function_on: function (request) {
+ if (request.arguments != undefined && !Array.isArray (request.arguments))
+ throw new Error (`"arguments" should be an array, but was ${request.arguments}`);
+
var objId = request.objectId;
var proxy;
if (objId in this._call_function_res_cache) {
proxy = this._call_function_res_cache [objId];
} else if (!objId.startsWith ('dotnet:cfo_res:')) {
- var details = this.mono_wasm_get_details(objId);
- var target_is_array = this._is_object_id_array (objId);
- proxy = target_is_array ? [] : {};
-
- Object.keys(details).forEach(p => {
- var prop = details[p];
- if (target_is_array) {
- proxy.push(prop.value);
- } else {
- if (prop.name != undefined)
- proxy [prop.name] = prop.value;
- else // when can this happen??
- proxy[''+p] = prop.value;
- }
- });
+ proxy = this._create_proxy_from_object_id (objId);
}
- var fn_args = request.arguments != undefined ? request.arguments.map(a => a.value) : [];
+ var fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : [];
var fn_eval_str = `var fn = ${request.functionDeclaration}; fn.call (proxy, ...[${fn_args}]);`;
var fn_res = eval (fn_eval_str);
- if (request.returnByValue)
+ if (fn_res == undefined) // should we just return undefined?
+ throw Error ('Function returned undefined result');
+
+ // primitive type
+ if (Object (fn_res) !== fn_res)
return fn_res;
- if (fn_res == undefined)
- throw Error ('Function returned undefined result');
+ // return .value, if it is a primitive type
+ if (fn_res.value !== undefined && Object (fn_res.value.value) !== fn_res.value.value)
+ return fn_res.value;
+
+ if (request.returnByValue)
+ return {type: "object", value: fn_res};
var fn_res_id = this._cache_call_function_res (fn_res);
if (Object.getPrototypeOf (fn_res) == Array.prototype) {
}
},
+ _clear_per_step_state: function () {
+ this._next_value_type_id_var = 0;
+ this._value_types_cache = {};
+ },
+
+ mono_wasm_debugger_resume: function () {
+ this._clear_per_step_state ();
+ },
+
mono_wasm_start_single_stepping: function (kind) {
console.log (">> mono_wasm_start_single_stepping " + kind);
if (!this.mono_wasm_setup_single_step)
this.mono_wasm_setup_single_step = Module.cwrap ("mono_wasm_setup_single_step", 'number', [ 'number']);
- this._next_value_type_id_var = 0;
- this._value_types_cache = {};
+ this._clear_per_step_state ();
return this.mono_wasm_setup_single_step (kind);
},
+ mono_wasm_set_pause_on_exceptions: function (state) {
+ if (!this.mono_wasm_pause_on_exceptions)
+ this.mono_wasm_pause_on_exceptions = Module.cwrap ("mono_wasm_pause_on_exceptions", 'number', [ 'number']);
+ var state_enum = 0;
+ switch (state) {
+ case 'uncaught':
+ state_enum = 1; //EXCEPTION_MODE_UNCAUGHT
+ break;
+ case 'all':
+ state_enum = 2; //EXCEPTION_MODE_ALL
+ break;
+ }
+ return this.mono_wasm_pause_on_exceptions (state_enum);
+ },
+
mono_wasm_runtime_ready: function () {
this.mono_wasm_runtime_is_ready = true;
// DO NOT REMOVE - magic debugger init function
console.debug ("mono_wasm_runtime_ready", "fe00e07a-5519-4dfe-b35a-f867dbaf2e28");
- this._next_value_type_id_var = 0;
- this._value_types_cache = {};
+ this._clear_per_step_state ();
// FIXME: where should this go?
this._next_call_function_res_id = 0;
},
// Set environment variable NAME to VALUE
- // Should be called before mono_load_runtime_and_bcl () in most cases
+ // Should be called before mono_load_runtime_and_bcl () in most cases
mono_wasm_setenv: function (name, value) {
if (!this.wasm_setenv)
this.wasm_setenv = Module.cwrap ('mono_wasm_setenv', null, ['string', 'string']);
// deprecated
mono_load_runtime_and_bcl: function (
- unused_vfs_prefix, deploy_prefix, enable_debugging, file_list, loaded_cb, fetch_file_cb
+ unused_vfs_prefix, deploy_prefix, debug_level, file_list, loaded_cb, fetch_file_cb
) {
var args = {
fetch_file_cb: fetch_file_cb,
loaded_cb: loaded_cb,
- enable_debugging: enable_debugging,
+ debug_level: debug_level,
assembly_root: deploy_prefix,
assets: []
};
// Initializes the runtime and loads assemblies, debug information, and other files.
// @args is a dictionary-style Object with the following properties:
// assembly_root: (required) the subfolder containing managed assemblies and pdbs
- // enable_debugging: (required)
+ // debug_level or enable_debugging: (required)
// assets: (required) a list of assets to load along with the runtime. each asset
// is a dictionary-style Object with the following properties:
// name: (required) the name of the asset, including extension.
if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE) {
try {
- load_runtime ("unused", args.enable_debugging);
+ load_runtime ("unused", args.debug_level);
} catch (ex) {
print ("MONO_WASM: load_runtime () failed: " + ex);
print ("MONO_WASM: Stacktrace: \n");
wasm_exit (1);
}
} else {
- load_runtime ("unused", args.enable_debugging);
+ load_runtime ("unused", args.debug_level);
}
MONO.mono_wasm_runtime_ready ();
},
_load_assets_and_runtime: function (args) {
+ if (args.enable_debugging)
+ args.debug_level = args.enable_debugging;
if (args.assembly_list)
throw new Error ("Invalid args (assembly_list was replaced by assets)");
if (args.runtime_assets)
});
},
- _mono_wasm_add_getter_var: function(className) {
+ _mono_wasm_add_getter_var: function(className, invokable) {
fixed_class_name = MONO._mono_csharp_fixup_class_name (className);
- var value = `${fixed_class_name} { get; }`;
- MONO.var_info.push({
- value: {
- type: "symbol",
- value: value,
- description: value
- }
- });
+ if (invokable != 0) {
+ var name;
+ if (MONO.var_info.length > 0)
+ name = MONO.var_info [MONO.var_info.length - 1].name;
+ name = (name === undefined) ? "" : name;
+
+ MONO.var_info.push({
+ get: {
+ className: "Function",
+ description: `get ${name} () {}`,
+ type: "function",
+ }
+ });
+ } else {
+ var value = `${fixed_class_name} { get; }`;
+ MONO.var_info.push({
+ value: {
+ type: "symbol",
+ description: value,
+ value: value,
+ }
+ });
+ }
},
_mono_wasm_add_array_var: function(className, objectId, length) {
break;
case "getter":
- MONO._mono_wasm_add_getter_var (str_value);
+ MONO._mono_wasm_add_getter_var (str_value, value);
break;
case "array":
break;
case "pointer": {
- MONO.var_info.push ({
- value: {
- type: "symbol",
- value: str_value,
- description: str_value
- }
- });
+ var fixed_value_str = MONO._mono_csharp_fixup_class_name (str_value);
+ if (value.klass_addr == 0 || value.ptr_addr == 0 || fixed_value_str.startsWith ('(void*')) {
+ // null or void*, which we can't deref
+ MONO.var_info.push({
+ value: {
+ type: "symbol",
+ value: fixed_value_str,
+ description: fixed_value_str
+ }
+ });
+ } else {
+ MONO.var_info.push({
+ value: {
+ type: "object",
+ className: fixed_value_str,
+ description: fixed_value_str,
+ objectId: this._get_updated_ptr_id ('', value)
+ }
+ });
+ }
}
break;
public string? MainJS { get; set; }
[Required]
public ITaskItem[]? AssemblySearchPaths { get; set; }
+ public int DebugLevel { get; set; }
public ITaskItem[]? ExtraAssemblies { get; set; }
public ITaskItem[]? FilesToIncludeInFileSystem { get; set; }
public ITaskItem[]? RemoteSources { get; set; }
{
[JsonPropertyName("assembly_root")]
public string AssemblyRoot { get; set; } = "managed";
- [JsonPropertyName("enable_debugging")]
- public int EnableDebugging { get; set; } = 0;
+ [JsonPropertyName("debug_level")]
+ public int DebugLevel { get; set; } = 0;
[JsonPropertyName("assets")]
public List<object> Assets { get; } = new List<object>();
[JsonPropertyName("remote_sources")]
// Create app
Directory.CreateDirectory(AppDir!);
Directory.CreateDirectory(Path.Join(AppDir, config.AssemblyRoot));
- foreach (var assembly in _assemblies!.Values)
+ foreach (var assembly in _assemblies!.Values) {
File.Copy(assembly.Location, Path.Join(AppDir, config.AssemblyRoot, Path.GetFileName(assembly.Location)), true);
+ if (DebugLevel > 0) {
+ var pdb = assembly.Location;
+ pdb = Path.ChangeExtension(pdb, ".pdb");
+ if (File.Exists(pdb))
+ File.Copy(pdb, Path.Join(AppDir, config.AssemblyRoot, Path.GetFileName(pdb)), true);
+ }
+ }
List<string> nativeAssets = new List<string>() { "dotnet.wasm", "dotnet.js", "dotnet.timezones.blat" };
File.Copy(Path.Join (MicrosoftNetCoreAppRuntimePackDir, "native", f), Path.Join(AppDir, f), true);
File.Copy(MainJS!, Path.Join(AppDir, "runtime.js"), true);
- foreach (var assembly in _assemblies.Values)
+ foreach (var assembly in _assemblies.Values) {
config.Assets.Add(new AssemblyEntry (Path.GetFileName(assembly.Location)));
+ if (DebugLevel > 0) {
+ var pdb = assembly.Location;
+ pdb = Path.ChangeExtension(pdb, ".pdb");
+ if (File.Exists(pdb))
+ config.Assets.Add(new AssemblyEntry (Path.GetFileName(pdb)));
+ }
+ }
+
+ config.DebugLevel = DebugLevel;
if (FilesToIncludeInFileSystem != null)
{