From 8baf894fc9246a575879859fd80718431a9dbbbb Mon Sep 17 00:00:00 2001 From: adelphes Date: Tue, 20 Jun 2017 13:40:57 +0100 Subject: [PATCH] add basic support for Exception UI --- package.json | 4 +-- src/debugMain.js | 75 +++++++++++++++++++++++++++++++++++++++++++----- src/globals.js | 18 +++++++++++- 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6f57485..6bed16f 100644 --- a/package.json +++ b/package.json @@ -127,8 +127,8 @@ "test": "node ./node_modules/vscode/bin/test" }, "dependencies": { - "vscode-debugprotocol": "^1.15.0", - "vscode-debugadapter": "^1.15.0", + "vscode-debugprotocol": "^1.20.0", + "vscode-debugadapter": "^1.20.0", "long": "^3.2.0", "ws": "^1.1.1", "xmldom": "^0.1.27", diff --git a/src/debugMain.js b/src/debugMain.js index 15ba82a..ca9fda2 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -22,7 +22,7 @@ const { D, isEmptyObject } = require('./util'); const { AndroidVariables } = require('./variables'); const { evaluate } = require('./expressions'); const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037); -const { ensure_path_end_slash,is_subpath_of,variableRefToThreadId } = require('./globals'); +const { exmsg_var_name, signatureToFullyQualifiedType, ensure_path_end_slash,is_subpath_of,variableRefToThreadId } = require('./globals'); class AndroidDebugSession extends DebugSession { @@ -100,6 +100,9 @@ class AndroidDebugSession extends DebugSession { // we support hit-count conditional breakpoints response.body.supportsHitConditionalBreakpoints = true; + // we support the new ExceptionInfoRequest + response.body.supportsExceptionInfoRequest = true; + this.sendResponse(response); } @@ -781,7 +784,10 @@ class AndroidDebugSession extends DebugSession { : null; const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId); pkginfo && (highest_known_source=i); - stack.push(new StackFrame(stack_frame_id, name, src, linenum, 0)); + // we don't support column number when reporting source locations (because JDWP only supports line-granularity) + // but in order to get the Exception UI to show, we must have a non-zero column + const colnum = (!i && x.thread.paused.last_exception && x.thread.paused.reasons[0]==='exception') ? 1 : 0; + stack.push(new StackFrame(stack_frame_id, name, src, linenum, colnum)); } // trim the stack to exclude calls above the known sources if (this.callStackDisplaySize > 0) { @@ -815,13 +821,22 @@ class AndroidDebugSession extends DebugSession { if (last_exception && !last_exception.objvar) { // retrieve the exception object thread.allocateExceptionScopeReference(args.frameId); - this.dbgr.getExceptionLocal(last_exception.exception, {response,scopes,last_exception}) + this.dbgr.getExceptionLocal(last_exception.exception, {thread,response,scopes,last_exception}) .then((ex_local,x) => { + x.last_exception.objvar = ex_local; + return $.when(x, x.thread.getVariables(x.last_exception.scopeRef)); + }) + .then((x, vars) => { var {response,scopes,last_exception} = x; - last_exception.objvar = ex_local; // put the exception first - otherwise it can get lost if there's a lot of locals - scopes.unshift(new Scope("Exception: "+ex_local.type.typename, last_exception.scopeRef, false)); - this.sendResponse(response); + scopes.unshift(new Scope("Exception: " + last_exception.objvar.type.typename, last_exception.scopeRef, false)); + this.sendResponse(response); + // notify the exceptionInfo who may be waiting on us + if (last_exception.waitForExObject) { + var def = last_exception.waitForExObject; + last_exception.waitForExObject = null; + def.resolveWith(this, []); + } }) .fail((/*e*/) => { this.sendResponse(response); }); return; @@ -891,7 +906,7 @@ class AndroidDebugSession extends DebugSession { return; } } - var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid); + var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid, thread.paused.last_exception && "Exception thrown"); thread.paused.stoppedEvent = event; this.sendEvent(event); } @@ -1104,6 +1119,52 @@ class AndroidDebugSession extends DebugSession { }) } + exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) { + var thread = this.getThread(args.threadId); + if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response); + if (!thread.paused) return this.cancelRequestThreadNotSuspended('Exception info', args.threadId, response); + if (!thread.paused.last_exception) return this.failRequest('No exception available', response); + + if (!thread.paused.last_exception.objvar) { + // we must wait for the exception object to be retreived as a local (along with the message field) + if (!thread.paused.last_exception.waitForExObject) { + thread.paused.last_exception.waitForExObject = $.Deferred().then(() => { + // redo the request + this.exceptionInfoRequest(response, args); + }); + } + return; + } + var exobj = thread.paused.last_exception.objvar; + var exmsg = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name); + exmsg = (exmsg && exmsg.string) || ''; + + response.body = { + /** ID of the exception that was thrown. */ + exceptionId: exobj.type.typename, + /** Descriptive text for the exception provided by the debug adapter. */ + description: exmsg, + /** Mode that caused the exception notification to be raised. */ + //'never' | 'always' | 'unhandled' | 'userUnhandled'; + breakMode: 'always', + /** Detailed information about the exception. */ + details: { + /** Message contained in the exception. */ + message: exmsg, + /** Short type name of the exception object. */ + typeName: exobj.type.typename, + /** Fully-qualified type name of the exception object. */ + fullTypeName: signatureToFullyQualifiedType(exobj.type.signature), + /** Optional expression that can be evaluated in the current scope to obtain the exception object. */ + //evaluateName: "evaluateName", + /** Stack trace at the time the exception was thrown. */ + //stackTrace: "stackTrace", + /** Details of the exception contained by this exception, if any. */ + //innerException: [], + } + } + this.sendResponse(response); + } } diff --git a/src/globals.js b/src/globals.js index 16a9404..1ff927e 100644 --- a/src/globals.js +++ b/src/globals.js @@ -27,6 +27,22 @@ const JTYPES = { fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] }, } +function signatureToFullyQualifiedType(sig) { + var arr = sig.match(/^\[+/) || ''; + if (arr) { + arr = '[]'.repeat(arr[0].length); + sig = sig.slice(0, arr.length/2); + } + var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/); + if (!m) return ''; + if (m[3]) { + return m[3].replace(/[/$]/g,'.') + arr; + } else if (m[4]) { + return m[4].replace(/[/$]/g, '.') + arr; + } + return JTYPES.fromPrimSig(sig[0]) + arr; +} + // the special name given to exception message fields const exmsg_var_name = ':msg'; @@ -67,5 +83,5 @@ function variableRefToThreadId(variablesReference) { Object.assign(exports, { - JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString + JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType });