Skip to content

Commit 0bc5cd5

Browse files
radicalthaystg
andauthored
[release/5.0-rc2] Backport wasm debugger improvements, and fixes (#42057)
This fixes variable inspection and expression evaluation in the WASM debugger. It markedly improves developer experience in the debugger around watch expressions, locals, and object properties. It fixes the issues listed below, improves error handling and adds extensive tests. Issues fixed: #41818 #41744 #41846 mono/mono#20310 #41406 #40245 #41447 #41276 #41990 --- * [wasm][debugger] Support deep member accesses for EvaluteOnCallFrame (#40836) * [wasm][debugger] Support deep member accesses for EvaluteOnCallFrame Eg. `obj.Property.X`, or `obj.Y + obj.Z.p` Each of the member access expressions (like `a.b.c`) must be of only primitive types. Though if the expression is a single member access (and nothing else, like `"a.b.c"`), then that can be a non-primitive type. This works by sending the expression to the browser, where it gets resolved by `library_mono.js`. And that takes an easy route for doing this, essentially just fetching the list of locals/properties, and using that. There are better ways to do this, that will be explored later. * [wasm][debugger][tests] Remove some debug spew * [wasm][debugger] fix formatting with dotnet-format (cherry picked from commit 907f7da) * [wasm][debugger] Fix expression evaluation when it is a reference (#41135) * [wasm][debugger] Fix expression evaluation when it is a reference .. preceded by spaces. Eg: `" foo.dateTime", or `" foo.count"` * Explanation of the fix: - these particular expressions end up in the code path where we get a SimpleMemberAccessExpression, and only one member access (like `a.b.c`) was found. - that code path had `return memberAccessValues[0]?["value"]?.Value<JObject>();` - which is incorrect, and we need to return `memberAccessValues[0]`, which is the value itself. (cherry picked from commit ec59f65) * [wasm][debugger] Correctly handle empty, and whitespace-only strings (#41424) There are two cases being fixed here: 1. str=='', or str=' ' - We check `str_value == 0`, and for the above cases JS returns true, due to type coercion. - So, we show the result as a null string. 2. str==null - debugger.c adds the value for this with `mono_wasm_add_typed_value ("string", NULL, 0)` - the second argument is converted to a string with `Module.UTF8ToString(..)`, but when it's `0`/NULL, we get an empty string. And that becomes a null string, because of (1). Fixing this by using `===` operator to avoid type coercion. Fixes #41276 (cherry picked from commit 0795094) * [wasm] Disable an extraneous debug message (#41468) Recent debugger test runs were showing lot of `CWL: Failed to lookup sequence point` messages. These are being shown for cases like: `CWL: list_frames: Failed to lookup sequence point. method: runtime_invoke_direct_void, native_offset: 56` This is a warning, and doesn't need to be emitted by default. (cherry picked from commit c4841c5) * [wasm][debugger] Breakpoint stopping after it's removed (#41479) * Remove unnecessary WriteLine. Fix #41447. * Creating unit test about remove breakpoint. * Implementing @radical suggestions! (cherry picked from commit 513ade6) * [wasm][debugger] Add support for surfacing inherited members (#41480) * [wasm][debugger][tests] Update to use `TDateTime` - this ensures that we check the datetime, and some property getters on it, wherever we have a datetime. * [wasm][debugger][tests] Add labels to more checks * [wasm][debugger] Add support for surfacing inherited members - surface inherited fields, and properties - we try to support `Runtime.getProperties`'s two arguments: - `ownProperties`, and `accessorsOnly` - `ownProperties`: for JS, this means return only the object's own members (not inherited ones) - `accessorsOnly`: for JS, this means return all the getters Actual implementation: - In practice, VSCode, and Chrome debugger seem to only send `{ ownProperties: true, accessorsOnly: false }`, and `{ ownProperties: false, accessorsOnly: true }`. The combination of which means - that we don't return any inherited fields! - But we want to show inherited fields too, so to get that behavior we essentially *ignore* `ownProperties`. IOW, - `ownProperties`: we return all fields, and properties - `accessorsOnly`: we return only the getters, including the inherited ones - Another thing to note is the case for auto-properties - these have a backing field - and we usually return the backing field's value, instead of returning a getter - To continue with that, auto-properties are *not* returned for `accessorsOnly` - The code in `mini-wasm-debugger.c` does handle these two arguments, but that is currently disabled by not passing the args to debugger.c at all - Instead, we get the *full* list of members, and try to filter it in `library_mono.js` - which includes handling property overrides, or shadowing by new properties/fields in derived classes * [wasm][debugger][tests] Fix simple warnings * [wasm][debugger][tests] Fix warnings introduced in this PR * [wasm][debugger][tests] Fix indentation * [wasm][debugger] Correctly handle local structs in async methods - When we have a struct local in an async instance method, it doesn't get expanded, since we have a containerId (the async object), and we can expand/access it later. - When the IDE asks us to expand it with `{accessorPropertiesOnly: true}`: - we get the expanded json, but `_filter_automatic_properties` tries to return just the accessors, but that doesn't handle the expanded members of nested structs! - That is done in `extract_and_cache_value_types`, which is run *after* `_filter_automatic_properties`, but by that time we have already lost the expanded members! - So, `_get_vt_properties` fails with `Unknown valuetype id`, because it doesn't have anything to return at that point. - This is being solved by ignoring the getProperties args in case of expanding valuetypes. - that means that we can correctly extract, and cache the whole object. - And after that, we can return accessors/others, based on the args. * [wasm][debugger] Fix warnings in debugger-test-app, and turn on warnAsError * For some cases, debugger seems to give the actual method name instead of MoveNext for async methods (cherry picked from commit b25b2bc) * [wasm][debugger] Add support for Nullable<T> (#41559) * [wasm][debugger] Add support for Nullable<T> Return the value, or null. Fixes mono/mono#20310 * Address review feedback - merge functions * [wasm][debugger] run dotnet-format on the debugger test app * [wasm][debugger] simplify function sig, based on usage - addresses review feedback from @lewing * [wasm][debugger] Simplify the function further, based on @lewing's .. excellent suggestion! (cherry picked from commit 66f4b4b) * [wasm][debugger] Show actual data for boxed values (#41562) * [wasm][debugger] Add support for Nullable<T> Return the value, or null. Fixes mono/mono#20310 * Address review feedback - merge functions * [wasm][debugger] run dotnet-format on the debugger test app * [wasm][debugger] simplify function sig, based on usage - addresses review feedback from @lewing * [wasm][debugger] Simplify the function further, based on @lewing's .. excellent suggestion! * [wasm][debugger] Show actual data for boxed values Eg. `object o = "foobar"` This will show the string `"foobar"`, instead of an object, in the debugger. (cherry picked from commit 4fd87bc) * [wasm][debugger] Small improvements to fail gracefully (#41713) * [wasm][debugger] Instead of failing completely, skip the problematic .. property. Some times we might not get a `value`, or `name`, or it might instead have a `get`. Handle those cases correctly when combining the name/value/get objects. This showed up in case of a `MulticastDelegate`, where we didn't have a `value`, and ended up incorrectly combining the name/value objects, thus returning incorrect locals. * [wasm][debugger] Handle MulticastDelegate, and events - Essentially we just surface these as a symbol showing the type name * [wasm][debugger] Fail gracefully when vt could not be expanded * [wasm][debugger] Handle invalid scope ids scope<0, or scope too high - This does get filtered at the proxy level, but app side should be able to handle it too * [wasm][debugger] Handle invalid/missing/failed value descriptions - Eg. missing because of invalid param/local id, or value description failed because of some as yet unsupported type * [wasm][debugger] Fix frame indexing - `collect_frames`, `list_frames` - both iterate over stack frames. They skip some frames. This debug proxy assigns a simple index to each of the received(filtered) frames. - so, if we had `[ frame0, (skipped frame1), frame2 ]`, the proxy will have `[ frame0(index:0), frame2(index:1) ]` - `describe_variables_on_frame` also iterates over stack frames, and tries to find a given frame by an index. And this index is what the proxy had assigned. - because some frames were skipped, this function also needs to skip the *same* ones, so that it can find the intended frame. - Instead of trying to keep these in sync, here the indexing is changed to be the real index found as we iterate over the frames. - And the proxy just uses that. - So, we will have `[ frame0(index:0), frame2(index:2) ]` This showed up in a trace in aspnetcore, which was called via reflection. And that frame didn't get added to the list because it was not `MONO_WRAPPER_NONE`, which caused the indexing to get out of sync. Fixes: #41818 * fix warning: remove unused var * rebase on master, fix errors * Make frame indices returned from debugger.c, 0-based - Earlier this 1-based, and it was being adjusted in `MonoProxy`. - Based on @lewing's suggestion, changing this to be 0-based in debugger.c, itself, thus removing the need to "fixup" in `MonoProxy`. * dotnet-format fixes (cherry picked from commit 2e4e75b) * Fix wasm sample after that was broken after this PR: #40478 (#41277) (cherry picked from commit f384168) * [wasm][debugger] Avoid infinite loop when we have a boxed `new object` (#42059) * [wasm][debugger] Avoid infinite loop when we have a boxed `new object` Eg. `object o = new object(); object o1 = o;` - Avoid infinitely looping for `o1` * [wasm][debugger] Handle valuetypes boxed in classes that are not .. type `object`. Prompted by @lambdageek's comment - #42059 (comment) (cherry picked from commit b58eba3) Co-authored-by: Thays Grazia <thaystg@gmail.com>
1 parent 8f5961c commit 0bc5cd5

24 files changed

+3050
-641
lines changed

src/mono/mono/mini/mini-wasm-debugger.c

Lines changed: 193 additions & 81 deletions
Large diffs are not rendered by default.

src/mono/netcore/sample/wasm/browser/WasmSample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
</ItemGroup>
3737
<WasmAppBuilder
3838
AppDir="$(AppDir)"
39-
ExtraAssemblies="$(MicrosoftNetCoreAppRuntimePackDir)lib\$(NetCoreAppCurrent)\System.Runtime.InteropServices.JavaScript.dll"
39+
ExtraAssemblies="$(MicrosoftNetCoreAppRuntimePackDir)lib\$(NetCoreAppCurrent)\System.Private.Runtime.InteropServices.JavaScript.dll"
4040
MicrosoftNetCoreAppRuntimePackDir="$(MicrosoftNetCoreAppRuntimePackDir)"
4141
MainAssembly="bin\WasmSample.dll"
4242
MainJS="runtime.js"

src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ public static MonoCommands GetScopeVariables(int scopeId, params VarInfo[] vars)
193193
return new MonoCommands($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject(var_ids)})");
194194
}
195195

196+
public static MonoCommands EvaluateMemberAccess(int scopeId, string expr, params VarInfo[] vars)
197+
{
198+
var var_ids = vars.Select(v => new { index = v.Index, name = v.Name }).ToArray();
199+
return new MonoCommands($"MONO.mono_wasm_eval_member_access({scopeId}, {JsonConvert.SerializeObject(var_ids)}, '', '{expr}')");
200+
}
201+
196202
public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");
197203

198204
public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
@@ -285,7 +291,7 @@ internal class ExecutionContext
285291
internal DebugStore store;
286292
public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore>();
287293

288-
public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken>();
294+
Dictionary<int, PerScopeCache> perScopeCaches { get; } = new Dictionary<int, PerScopeCache>();
289295

290296
public DebugStore Store
291297
{
@@ -298,11 +304,26 @@ public DebugStore Store
298304
}
299305
}
300306

307+
public PerScopeCache GetCacheForScope(int scope_id)
308+
{
309+
if (perScopeCaches.TryGetValue(scope_id, out var cache))
310+
return cache;
311+
312+
cache = new PerScopeCache();
313+
perScopeCaches[scope_id] = cache;
314+
return cache;
315+
}
316+
301317
public void ClearState()
302318
{
303319
CallStack = null;
304-
LocalsCache.Clear();
320+
perScopeCaches.Clear();
305321
}
322+
}
306323

324+
internal class PerScopeCache
325+
{
326+
public Dictionary<string, JObject> Locals { get; } = new Dictionary<string, JObject>();
327+
public Dictionary<string, JObject> MemberReferences { get; } = new Dictionary<string, JObject>();
307328
}
308329
}

src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs

Lines changed: 220 additions & 81 deletions
Large diffs are not rendered by default.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using Microsoft.Extensions.Logging;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
11+
12+
namespace Microsoft.WebAssembly.Diagnostics
13+
{
14+
internal class MemberReferenceResolver
15+
{
16+
private MessageId messageId;
17+
private int scopeId;
18+
private MonoProxy proxy;
19+
private ExecutionContext ctx;
20+
private PerScopeCache scopeCache;
21+
private VarInfo[] varIds;
22+
private ILogger logger;
23+
private bool locals_fetched = false;
24+
25+
public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, MessageId msg_id, int scope_id, ILogger logger)
26+
{
27+
messageId = msg_id;
28+
scopeId = scope_id;
29+
this.proxy = proxy;
30+
this.ctx = ctx;
31+
this.logger = logger;
32+
scopeCache = ctx.GetCacheForScope(scope_id);
33+
}
34+
35+
// Checks Locals, followed by `this`
36+
public async Task<JObject> Resolve(string var_name, CancellationToken token)
37+
{
38+
if (scopeCache.Locals.Count == 0 && !locals_fetched)
39+
{
40+
var scope_res = await proxy.GetScopeProperties(messageId, scopeId, token);
41+
if (scope_res.IsErr)
42+
throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}");
43+
locals_fetched = true;
44+
}
45+
46+
if (scopeCache.Locals.TryGetValue(var_name, out var obj))
47+
{
48+
return obj["value"]?.Value<JObject>();
49+
}
50+
51+
if (scopeCache.MemberReferences.TryGetValue(var_name, out var ret))
52+
return ret;
53+
54+
if (varIds == null)
55+
{
56+
var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId);
57+
varIds = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
58+
}
59+
60+
var res = await proxy.SendMonoCommand(messageId, MonoCommands.EvaluateMemberAccess(scopeId, var_name, varIds), token);
61+
if (res.IsOk)
62+
{
63+
ret = res.Value?["result"]?["value"]?["value"]?.Value<JObject>();
64+
scopeCache.MemberReferences[var_name] = ret;
65+
}
66+
else
67+
{
68+
logger.LogDebug(res.Error.ToString());
69+
}
70+
71+
return ret;
72+
}
73+
74+
}
75+
}

src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs

Lines changed: 32 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ protected override async Task<bool> AcceptCommand(MessageId id, string method, J
170170

171171
case "Debugger.enable":
172172
{
173-
System.Console.WriteLine("recebi o Debugger.enable");
174173
var resp = await SendCommand(id, method, args, token);
175174

176175
context.DebuggerId = resp.Value["debuggerId"]?.ToString();
@@ -426,7 +425,9 @@ protected override async Task<bool> AcceptCommand(MessageId id, string method, J
426425
async Task<Result> RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
427426
{
428427
if (objectId.Scheme == "scope")
428+
{
429429
return await GetScopeProperties(id, int.Parse(objectId.Value), token);
430+
}
430431

431432
var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token);
432433
if (res.IsErr)
@@ -456,7 +457,6 @@ async Task<Result> RuntimeGetProperties(MessageId id, DotnetObjectId objectId, J
456457
return res;
457458
}
458459

459-
//static int frame_id=0;
460460
async Task<bool> OnPause(SessionId sessionId, JObject args, CancellationToken token)
461461
{
462462
//FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime
@@ -517,12 +517,11 @@ async Task<bool> OnPause(SessionId sessionId, JObject args, CancellationToken to
517517
}
518518

519519
var frames = new List<Frame>();
520-
int frame_id = 0;
521520
var the_mono_frames = res.Value?["result"]?["value"]?["frames"]?.Values<JObject>();
522521

523522
foreach (var mono_frame in the_mono_frames)
524523
{
525-
++frame_id;
524+
int frame_id = mono_frame["frame_id"].Value<int>();
526525
var il_pos = mono_frame["il_pos"].Value<int>();
527526
var method_token = mono_frame["method_token"].Value<uint>();
528527
var assembly_name = mono_frame["assembly_name"].Value<string>();
@@ -559,12 +558,12 @@ async Task<bool> OnPause(SessionId sessionId, JObject args, CancellationToken to
559558

560559
Log("info", $"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
561560
Log("info", $"\tmethod {method_name} location: {location}");
562-
frames.Add(new Frame(method, location, frame_id - 1));
561+
frames.Add(new Frame(method, location, frame_id));
563562

564563
callFrames.Add(new
565564
{
566565
functionName = method_name,
567-
callFrameId = $"dotnet:scope:{frame_id - 1}",
566+
callFrameId = $"dotnet:scope:{frame_id}",
568567
functionLocation = method.StartLocation.AsLocation(),
569568

570569
location = location.AsLocation(),
@@ -581,7 +580,7 @@ async Task<bool> OnPause(SessionId sessionId, JObject args, CancellationToken to
581580
@type = "object",
582581
className = "Object",
583582
description = "Object",
584-
objectId = $"dotnet:scope:{frame_id-1}",
583+
objectId = $"dotnet:scope:{frame_id}",
585584
},
586585
name = method_name,
587586
startLocation = method.StartLocation.AsLocation(),
@@ -673,64 +672,6 @@ async Task<bool> Step(MessageId msg_id, StepKind kind, CancellationToken token)
673672
return true;
674673
}
675674

676-
internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
677-
{
678-
if (ctx.LocalsCache.TryGetValue(expression, out obj))
679-
{
680-
if (only_search_on_this && obj["fromThis"] == null)
681-
return false;
682-
return true;
683-
}
684-
return false;
685-
}
686-
687-
internal async Task<JToken> TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
688-
{
689-
JToken thisValue = null;
690-
var context = GetContext(msg_id);
691-
if (context.CallStack == null)
692-
return null;
693-
694-
if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
695-
return obj;
696-
697-
var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id);
698-
var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
699-
//get_this
700-
var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token);
701-
702-
var scope_values = res.Value?["result"]?["value"]?.Values<JObject>()?.ToArray();
703-
thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value<string>() == "this");
704-
705-
if (!only_search_on_this)
706-
{
707-
if (thisValue != null && expression == "this")
708-
return thisValue;
709-
710-
var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value<string>() == expression);
711-
if (value != null)
712-
return value;
713-
}
714-
715-
//search in scope
716-
if (thisValue != null)
717-
{
718-
if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId))
719-
return null;
720-
721-
res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token);
722-
scope_values = res.Value?["result"]?["value"]?.Values<JObject>().ToArray();
723-
var foundValue = scope_values.FirstOrDefault(v => v["name"].Value<string>() == expression);
724-
if (foundValue != null)
725-
{
726-
foundValue["fromThis"] = true;
727-
context.LocalsCache[foundValue["name"].Value<string>()] = foundValue;
728-
return foundValue;
729-
}
730-
}
731-
return null;
732-
}
733-
734675
async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
735676
{
736677
try
@@ -739,35 +680,40 @@ async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string ex
739680
if (context.CallStack == null)
740681
return false;
741682

742-
var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token);
683+
var resolver = new MemberReferenceResolver(this, context, msg_id, scope_id, logger);
743684

744-
if (varValue != null)
685+
JObject retValue = await resolver.Resolve(expression, token);
686+
if (retValue == null)
687+
{
688+
retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token);
689+
}
690+
691+
if (retValue != null)
745692
{
746693
SendResponse(msg_id, Result.OkFromObject(new
747694
{
748-
result = varValue["value"]
695+
result = retValue
749696
}), token);
750-
return true;
751697
}
752-
753-
string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token);
754-
SendResponse(msg_id, Result.OkFromObject(new
698+
else
755699
{
756-
result = new
757-
{
758-
value = retValue
759-
}
760-
}), token);
761-
return true;
700+
SendResponse(msg_id, Result.Err($"Unable to evaluate '{expression}'"), token);
701+
}
702+
}
703+
catch (ReturnAsErrorException ree)
704+
{
705+
SendResponse(msg_id, ree.Error, token);
762706
}
763707
catch (Exception e)
764708
{
765-
logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
709+
logger.LogDebug($"Error in EvaluateOnCallFrame for expression '{expression}' with '{e}.");
710+
SendResponse(msg_id, Result.Exception(e), token);
766711
}
767-
return false;
712+
713+
return true;
768714
}
769715

770-
async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
716+
internal async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
771717
{
772718
try
773719
{
@@ -788,8 +734,11 @@ async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, Cancellati
788734
if (values == null || values.Length == 0)
789735
return Result.OkFromObject(new { result = Array.Empty<object>() });
790736

737+
var frameCache = ctx.GetCacheForScope(scope_id);
791738
foreach (var value in values)
792-
ctx.LocalsCache[value["name"]?.Value<string>()] = value;
739+
{
740+
frameCache.Locals[value["name"]?.Value<string>()] = value;
741+
}
793742

794743
return Result.OkFromObject(new { result = values });
795744
}
@@ -902,7 +851,7 @@ async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken to
902851
bp.State = BreakpointState.Disabled;
903852
}
904853
}
905-
breakpointRequest.Locations.Clear();
854+
context.BreakpointRequests.Remove(bpid);
906855
}
907856

908857
async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token)

0 commit comments

Comments
 (0)