Closed andrewcrawley closed 8 years ago
We need a thrownOrUserUnhandled
value. Otherwise LGTM.
What does the "Debug adapter when one of the listed exceptions is thrown" comment mean? Should it be "Debug adapter behavior"?
So, the proposal is to place the code based exceptions (like win32 exceptions) inside the name? Are we planning on the debug adapter just knowing the numerical format? (In vs, it is hex for javascript, native prefixed by 0x).
Except for those two comments, LGTM
@jacdavis it would be in the id
field. I would propose having VS use EXCEPTION_CODE_DISPLAY_IN_HEX
bit for the exception category in the VS exception metrics to decide if it should format the code in hex (with a '0x' prefix) or decimal.
@gregg-miskelly - My intent was that the ExceptionBreakMode enum would be used as flags, so you could do "thrown | userUnhandled". Is this not what you meant?
@jacdavis - Typo. : \ You're correct as to what it should say.
@andrewcrawley: if you can do that with the json schema - sounds great to me. I would have thought that wouldn't work there, but happy to be wrong if that works.
We'll verify this proposal against our scenarios and send a PR.
This protocol addition is most likely something that VS Code will provide UI for. So I need some more time before I can provide feedback.
@andrewcrawley here is my feedback and a modified proposal that tries to be more general and would work with a (not yet existing) VS Code exception configuration UI.
The debug protocol cannot prescribe a file based persistency mechanism or a file format. So if a client and/or a debug adapter want to use a file based approach, they could introduce a private protocol for this. E.g. the debug adapter could return the location of the file in the exceptionDetails
request (see below) if it detects a UI client that supports this (and the DA would return an empty exceptions array in this case).
The original proposal is very specific about how exceptions are grouped in categories, resulting in a two level hierarchy. I could easily imagine that other debuggers only need a flat list without categories or a multi level hierarchy if the class hierarchy of exception classes is reflected.
For this reason I propose a recursive type ExceptionDetails
:
/** An ExceptionDetails is shown in the UI as an option for configuring an exception or a group of exceptions. */
export interface ExceptionDetails {
/** The internal ID of the exception. This value is for configuring the exception in the setExceptionBreakpoints request. */
id: string;
/** The name is shown in the UI either as the name of an exception or the name of a group of exceptions. */
name: string;
/** Id for the default behavior of the exception. See type 'ExceptionBehavior'.*/
defaultBehaviorId: string;
/** Optional set of sub exceptions. */
exceptions?: ExceptionDetails[];
}
Since the value set of the original 'ExceptionBreakMode' is language/runtime specific, we need a way to communicate the individual values to the UI. I suggest that we introduce a capability exceptionBehaviors
which returns an array of 'ExceptionBehavior':
export interface Capabilities {
// ...
/** Available exception break behaviors for the setExceptionBreakpoints request. */
exceptionBehaviors?: ExceptionBehavior[];
// ...
}
export interface ExceptionBehavior {
/** The internal ID of the behavior. */
id: string;
/** The name of the behavior. This will be shown in the UI. */
name: string;
}
The 'official' way to get the list of all configurable exceptions is via the exceptionDetails
request. A capability supportsExceptionDetailsRequest
announces that the DA implements the request.
export interface Capabilities {
// ...
/** The debug adapter supports the exceptionDetailsRequest. */
supportsExceptionDetailsRequest?: boolean;
// ...
}
/** Details of all configurable exceptions can be retrieved with the ExceptionDetailsRequest. */
export interface ExceptionDetailsRequest extends Request {
// command: 'exceptionDetails';
arguments: ExceptionDetailsArguments;
}
/** Arguments for 'exceptionDetails' request. */
export interface ExceptionDetailsArguments {
}
/** Response to 'exceptionDetails' request. */
export interface ExceptionDetailsResponse extends Response {
body: {
/** All exception details. */
exceptions: ExceptionDetails[];
};
}
For configuration of the exception behavior I suggest that we use the existing setExceptionBreakpoints
request and add an optional attribute for the new approach. 'Categories' or the exception hierarchy is not surfaced in this API because the assumption is that this information can always be inferred from the exceptionId
.
/** Arguments for the 'setExceptionBreakpoints' request. */
export interface SetExceptionBreakpointsArguments {
// ... old 'filters' attribute
/** Exception Ids and their mode. */
exceptions?: ExceptionConfiguration[];
}
/** Represents the configuration of a single exception. */
export interface ExceptionConfiguration {
/** Id of the exception. */
exceptionId: string;
/** Id of the exception behavior when exception is thrown. */
behaviorId: string;
}
I think this looks reasonable, for the most part. name
and defaultBehaviorId
on ExceptionDetails
should probably be nullable, because in many cases the ID will be usable as a name (e.g. "System.ArgumentException"), and most exceptions are ignored by default. VS only supports a single level of categorization, but we can flatten the list we get if necessary.
I'm concerned with the chattiness of the SetExceptionBreakponts
mechanism, which is why my original proposal had a separate mechanism for allowing exceptions / categories to be enabled and disabled without having to re-send the entire list. SetExceptionBreakpoints
would allow enabling all exceptions in a category with a short request, but if I then disable a single exception in that category, I'd have to send the full list. For C#, that single message would be almost 40KB!
Retrieving the list of exceptions via the protocol is problematic for a couple of reasons. Off the top of my head:
As you say, we can come up with a private contact for a file-based mechanism, but in practice, any extension that wants to support exceptions in VS is going to have to support that private contract, so it might as well be standardized. I think the best option would be for an extension's package.json
to point to a JSON file defining the exceptions, just like it points to the debug adapter itself. This avoids the "first run" issue and gives us something we can consume on install to populate our reg keys.
@weinand I am not sure you are correctly understanding what is meant by 'categories'. Categories are things like 'JavaScript exceptions', 'C++ exceptions', '.NET Framework exceptions', etc. Therefore, all debug adapters that support exceptions at all, can support categories.
We used to have a UI that supported a full tree. We found that this made the UI harder to use, so when we rewrote it for VS 2015 we eliminated the flexible hierarchy in favor of the simple one level tree with categories at the top.
@gregg-miskelly I think I understand the concept "Categories":
I'm just trying to find an API that supports debuggers/runtimes that do not have a concept of categories or even need a deeper hierarchy.
A quick 'survey' showed this:
Xamarin:
Eclipse:
xCode:
So a two level hierarchy does not seem to be a widespread concept (but a many-level hierarchy isn't either). My proposal is a pay-as-you-go approach that supports 1, 2, and n levels (including the category/exception separation of VS).
But before we can finalise the shape of the ExceptionDetails
, I'll try to respond to @andrewcrawley's concerns about how the list of exceptions is retrieved.
Right, I understand that not all debugger UIs might have a category concept, but it isn't hard for a debug adapter to support this concept (if (category != myExceptions) return). I think it is rather fundamental to having any sort of exception configuration UI because otherwise all debug adapters will need to understand exception settings that every other debug adapter added). I definitely don't see a problem with supporting nested exception settings, but it does make the UI more complicated. So I will leave that one up to you.
After discussing this in the weekly planning call, we came to the conclusion that this protocol is too specific to be added to the VS Code debug protocol at the moment.
Reopening after another round of discussions (e.g. #11552).
My summary of the discussions we had so far:
@andrewcrawley here is a new proposal:
A simple setExceptionBreakpoints
API that obeys the 5 bullet items from above could be based on category/exception paths and a subset of glob patterns (see https://www.npmjs.com/package/minimatch).
/** Arguments for the 'setExceptionBreakpoints' request. */
export interface SetExceptionBreakpointsArguments {
/** Categories or exceptions for which state will be modified */
filters?: FilterState[];
}
export interface FilterState {
pattern?: string;
mode: ExceptionBreakMode;
}
Here are five glob patterns packed into a single SetExceptionBreakpointsArguments
:
"filters": [
{ // every exception
pattern: "**",
mode: ExceptionBreakMode.thrown
},
{ // every exception in 'cat1'
pattern: "cat1/**",
mode: ExceptionBreakMode.thrown
},
{ // every exception in 'cat1' or 'cat2'
pattern: "{cat1,cat2}/**",
mode: ExceptionBreakMode.thrown
},
{ // exception 'ex1' in 'cat1'
pattern: "cat1/ex1/**",
mode: ExceptionBreakMode.thrown
},
{ // every exception but 'ex1' or 'ex3' in 'cat1'
pattern: "cat1/!(ex1|ex3)/**",
mode: ExceptionBreakMode.thrown
}
]
(combining these particular patterns doesn't make sense because the first already includes all exceptions; I'm just using them here to show 5 useful patterns).
Nice characteristic of this approach:
"cat1/**/*Error*"
matches all exceptions in category 'cat1' that contain the word 'Error'.If we want to avoid patterns and their parsing in the debug adapter we could model the filters by using these types:
export interface FilterState {
path?: FilterPathSegment[];
mode: ExceptionBreakMode;
}
export interface FilterPathSegment {
negate?: boolean; // use complement set of or'ed names
names: string[]; // or'ed names
}
With this the 5 filters from above would look like this:
"filters": [
{ // every exception
mode: ExceptionBreakMode.thrown
},
{ // every exception in 'cat1'
path: [
{ names: [ "cat1" ] }
],
mode: ExceptionBreakMode.thrown
},
{ // every exception in 'cat1' or 'cat2'
path: [
{ names: [ "cat1", "cat2" ] }
],
mode: ExceptionBreakMode.thrown
},
{ // exception 'ex1' in 'cat1'
path: [
{ names: [ "cat1" ] },
{ names: [ "ex1" ] }
],
mode: ExceptionBreakMode.thrown
},
{ // every exception but 'ex1' or 'ex3' in 'cat1'
path: [
{ names: [ "cat1" ] },
{ negate: true, names: [ "ex1", "ex2" ] }
],
mode: ExceptionBreakMode.thrown
}
]
I'm leaning more towards the second proposal because it simplifies the implementation in debug adapters.
FYI @wardengnaw @rajkumar42 @wesrupert
From a protocol standpoint, I think proposal 2 is more consistent, as it conveys the same information through the json structure. Similarly to how we don't pass breakpoints as { location: "path/to/file.type:23::verified" }
and require the debug adapter to perform additional parsing, but as { source: { path: "path/to/file.type" }, line: 23, verified: true }
.
@wesrupert yes, I started with proposal 1 because it was easier to grasp for someone who is familiar with glob patterns. But proposal 2 is less readable but more in line with the rest of the protocol.
@andrewcrawley @wesrupert @wardengnaw @rajkumar42 here is the complete proposal (see PR https://github.com/Microsoft/vscode-debugadapter-node/pull/88):
/** Information about the capabilities of a debug adapter. */
export interface Capabilities {
//...
/** The debug adapter supports 'exceptionOptions' on the setExceptionBreakpoints request. */
supportsExceptionOptions?: boolean;
//...
}
/** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'.
The request configures the debuggers response to thrown exceptions.
If an execption is configured to break, a StoppedEvent is fired (event type 'exception').
*/
export interface SetExceptionBreakpointsRequest extends Request {
// command: 'setExceptionBreakpoints';
arguments: SetExceptionBreakpointsArguments;
}
/** Arguments for 'setExceptionBreakpoints' request. */
export interface SetExceptionBreakpointsArguments {
/** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */
filters: string[];
/** Configuration options for selected exceptions. */
exceptionOptions?: ExceptionOptions[];
}
/** An ExceptionOptions assigns configuration options to a set of exceptions. */
export interface ExceptionOptions {
/** A path that selects a single or multiple exceptions in a tree.
If 'path' is missing, the whole tree is selected.
By convention the first segment of the path is a category that is used to group exceptions in the UI. */
path?: ExceptionPathSegment[];
/** Condition when a thrown exception should result in a break. */
breakMode: ExceptionBreakMode;
}
/** This enumeration defines all possible conditions when a thrown exception should result in a break.
never: never breaks,
always: always breaks,
unhandled: breaks when excpetion unhandled,
userUnhandled: breaks if the exception is not handled by user code.
*/
export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled';
/** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions.
If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing
or it matches anything except the names provided if 'negate' is true. */
export interface ExceptionPathSegment {
/** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */
negate?: boolean;
/** Depending on the value of 'negate' the names that should match or not match. */
names: string[];
}
@andrewcrawley @gregg-miskelly @jacdavis @wesrupert @wardengnaw @rajkumar42 Some examples:
Break on every exception:
"exceptionOptions": [
{
breakMode: "always"
}
]
Break on every exception in the '.NET Exceptions' category:
"exceptionOptions": [
{
path: [
{ names: [ ".NET Exceptions" ] }
],
breakMode: "always"
}
]
Break on every exception in the '.NET Exceptions' or 'Win32 Exceptions' categories:
"exceptionOptions": [
{
path: [
{ names: [ ".NET Exceptions", "Win32 Exceptions" ] }
],
breakMode: "always"
}
]
Break on 'System.InvalidOperationException' in category '.NET Exceptions':
"exceptionOptions": [
{
path: [
{ names: [ ".NET Exceptions" ] }
{ names: [ "System.InvalidOperationException" ] }
],
breakMode: "always"
}
]
Break on every exception in category '.NET Exceptions' but not "System.InvalidOperationException":
"exceptionOptions": [
{
path: [
{ names: [ ".NET Exceptions" ] }
{ negate: true, names: [ "System.InvalidOperationException" ] }
],
breakMode: "always"
}
]
Break on all non user handled '.NET Exceptions' with the exception of "System.InvalidOperationException" for which to always break on:
"exceptionOptions": [
{
path: [
{ names: [ ".NET Exceptions" ] }
],
breakMode: "userUnhandled"
},
{
path: [
{ names: [ ".NET Exceptions" ] }
{ names: [ "System.InvalidOperationException" ] }
],
breakMode: "always"
}
]
LGTM. BTW: andrewcrawley is on vacation this week.
I'm planning to merge https://github.com/Microsoft/vscode-debugadapter-node/pull/88 tomorrow.
We'd like to be able to support the VS exception experience, where a list of exceptions is presented to the user, who can set various properties to control which exceptions will cause the debugger to break.
Since the number of exceptions supported in this way is large (the default list in VS contains ~500 exceptions for C# and ~230 for Java) and the list does not change from session to session, we propose that the list of exceptions would be maintained in a separate file rather than provided by the debug adapter in its
Capabilities
. The schema of this file would look like:Proposed protocol changes:
@jacdavis @gregg-miskelly @richardstanton @tzwlai