mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
first pass at hit-count breakpoints
This commit is contained in:
116
src/debugMain.js
116
src/debugMain.js
@@ -222,6 +222,9 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
// we support modifying variable values
|
// we support modifying variable values
|
||||||
response.body.supportsSetVariable = true;
|
response.body.supportsSetVariable = true;
|
||||||
|
|
||||||
|
// we support hit-count conditional breakpoints
|
||||||
|
response.body.supportsHitConditionalBreakpoints = true;
|
||||||
|
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,9 +585,15 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
*/
|
*/
|
||||||
setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) {
|
setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) {
|
||||||
var srcfpn = args.source && args.source.path;
|
var srcfpn = args.source && args.source.path;
|
||||||
var clientLines = args.lines;
|
|
||||||
D('setBreakPointsRequest: ' + srcfpn);
|
D('setBreakPointsRequest: ' + srcfpn);
|
||||||
|
|
||||||
|
const unverified_breakpoint = (src_bp,reason) => {
|
||||||
|
var bp = new Breakpoint(false,src_bp.line);
|
||||||
|
bp.id = ++this._breakpointId;
|
||||||
|
bp.message = reason;
|
||||||
|
return bp;
|
||||||
|
}
|
||||||
|
|
||||||
// the file must lie inside one of the source packages we found (and it must be have a .java extension)
|
// the file must lie inside one of the source packages we found (and it must be have a .java extension)
|
||||||
var srcfolder = path.dirname(srcfpn);
|
var srcfolder = path.dirname(srcfpn);
|
||||||
var pkginfo;
|
var pkginfo;
|
||||||
@@ -596,12 +605,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
// source file is not a java file or is outside of the known source packages
|
// source file is not a java file or is outside of the known source packages
|
||||||
// just send back a list of unverified breakpoints
|
// just send back a list of unverified breakpoints
|
||||||
response.body = {
|
response.body = {
|
||||||
breakpoints: args.lines.map(l => {
|
breakpoints: args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is outside of the project source tree'))
|
||||||
var bp = new Breakpoint(false,l);
|
|
||||||
bp.id = ++this._breakpointId;
|
|
||||||
bp.message = 'The breakpoint location is outside of the project source tree';
|
|
||||||
return bp;
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
this.sendResponse(response);
|
this.sendResponse(response);
|
||||||
return;
|
return;
|
||||||
@@ -612,16 +616,26 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
var relative_fpn = srcfpn.slice(pkginfo.srcroot.length);
|
var relative_fpn = srcfpn.slice(pkginfo.srcroot.length);
|
||||||
|
|
||||||
// delete any existing breakpoints not in the list
|
// delete any existing breakpoints not in the list
|
||||||
|
var src_line_nums = args.breakpoints.map(bp => bp.line);
|
||||||
this.dbgr.clearbreakpoints(javabp => {
|
this.dbgr.clearbreakpoints(javabp => {
|
||||||
var remove = javabp.srcfpn===relative_fpn && !clientLines.includes(javabp.linenum);
|
var remove = javabp.srcfpn===relative_fpn && !src_line_nums.includes(javabp.linenum);
|
||||||
if (remove) javabp.vsbp = null;
|
if (remove) javabp.vsbp = null;
|
||||||
return remove;
|
return remove;
|
||||||
});
|
});
|
||||||
|
|
||||||
// return the list of new and existing breakpoints
|
// return the list of new and existing breakpoints
|
||||||
var breakpoints = clientLines.map((line,idx) => {
|
var breakpoints = args.breakpoints.map((src_bp,idx) => {
|
||||||
var dbgline = this.convertClientLineToDebugger(line);
|
var dbgline = this.convertClientLineToDebugger(src_bp.line);
|
||||||
var javabp = this.dbgr.setbreakpoint(relative_fpn, dbgline);
|
var options = {};
|
||||||
|
if (src_bp.hitCondition) {
|
||||||
|
// the hit condition is an expression that requires evaluation
|
||||||
|
// until we get more comprehensive evaluation support, just allow integer literals
|
||||||
|
var m = src_bp.hitCondition.match(/^\s*(?:0x([0-9a-f]+)|0b([01]+)|0*(\d+([e]\+?\d+)?))\s*$/i);
|
||||||
|
var hitcount = m && (m[3] ? parseFloat(m[3]) : m[2] ? parseInt(m[2],2) : parseInt(m[1],16));
|
||||||
|
if (!m || hitcount < 0 || hitcount > 0x7fffffff) return unverified_breakpoint(src_bp, 'The breakpoint is configured with an invalid hit count value');
|
||||||
|
options.hitcount = hitcount;
|
||||||
|
}
|
||||||
|
var javabp = this.dbgr.setbreakpoint(relative_fpn, dbgline, options);
|
||||||
if (!javabp.vsbp) {
|
if (!javabp.vsbp) {
|
||||||
// state is one of: set,notloaded,enabled,removed
|
// state is one of: set,notloaded,enabled,removed
|
||||||
var verified = !!javabp.state.match(/set|enabled/);
|
var verified = !!javabp.state.match(/set|enabled/);
|
||||||
@@ -1291,13 +1305,6 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
this.doNextEvaluateRequest();
|
this.doNextEvaluateRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResponseAndDoNext(response, value, varref) {
|
|
||||||
response.body = { result:value, variablesReference:varref|0 };
|
|
||||||
this.sendResponse(response);
|
|
||||||
this._evals_queue.shift();
|
|
||||||
this.doNextEvaluateRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
doNextEvaluateRequest() {
|
doNextEvaluateRequest() {
|
||||||
if (!this._evals_queue.length) return;
|
if (!this._evals_queue.length) return;
|
||||||
var args = this._evals_queue[0][1];
|
var args = this._evals_queue[0][1];
|
||||||
@@ -1317,19 +1324,48 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
|
|
||||||
doEvaluateRequest(response, args) {
|
doEvaluateRequest(response, args) {
|
||||||
|
|
||||||
|
const sendEvaluateResponseAndDoNext = (value, varref) => {
|
||||||
|
response.body = { result:value, variablesReference:varref|0 };
|
||||||
|
this.sendResponse(response);
|
||||||
|
this._evals_queue.shift();
|
||||||
|
this.doNextEvaluateRequest();
|
||||||
|
}
|
||||||
|
|
||||||
// just in case the user starts the app running again, before we've evaluated everything in the queue
|
// just in case the user starts the app running again, before we've evaluated everything in the queue
|
||||||
if (this._running) {
|
if (this._running) {
|
||||||
return this.sendResponseAndDoNext(response, '(running)');
|
sendEvaluateResponseAndDoNext('(running)');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var v = this._variableHandles[args.frameId];
|
||||||
|
var locals = v && v.frame && v.cached;
|
||||||
|
|
||||||
|
this.evaluate(args.expression, locals)
|
||||||
|
.then((value,variablesReference) => {
|
||||||
|
sendEvaluateResponseAndDoNext(value, variablesReference);
|
||||||
|
})
|
||||||
|
.fail(err => {
|
||||||
|
sendEvaluateResponseAndDoNext(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Asynchronously evaluate an expression
|
||||||
|
*/
|
||||||
|
evaluate(expression, locals) {
|
||||||
|
D('evaluate: ' + expression);
|
||||||
|
|
||||||
|
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
||||||
|
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
|
||||||
|
|
||||||
// special case for evaluating exception messages
|
// special case for evaluating exception messages
|
||||||
// - this is called if the user uses "Copy value" from the locals
|
// - this is called if the user uses "Copy value" from the locals
|
||||||
if (args.expression===this._exmsg_var_name && this._last_exception && this._last_exception.cached) {
|
if (expression===this._exmsg_var_name && this._last_exception && this._last_exception.cached) {
|
||||||
var msglocal = this._last_exception.cached.find(v => v.name===this._exmsg_var_name);
|
var msglocal = this._last_exception.cached.find(v => v.name===this._exmsg_var_name);
|
||||||
if (msglocal) return this.sendResponseAndDoNext(response, msglocal.string);
|
if (msglocal) return resolve_evaluation(msglocal.string);
|
||||||
}
|
}
|
||||||
|
|
||||||
var parse_array_or_fncall = function(e) {
|
const parse_array_or_fncall = function(e) {
|
||||||
var arg, res = {arr:[], call:null};
|
var arg, res = {arr:[], call:null};
|
||||||
// pre-call array indexes
|
// pre-call array indexes
|
||||||
while (e.expr[0] === '[') {
|
while (e.expr[0] === '[') {
|
||||||
@@ -1364,7 +1400,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
var parse_expression = function(e) {
|
const parse_expression = function(e) {
|
||||||
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|(\d+(?:\.\d+)?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|(\d+(?:\.\d+)?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||||
if (!root_term) return null;
|
if (!root_term) return null;
|
||||||
var res = {
|
var res = {
|
||||||
@@ -1388,13 +1424,12 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
var reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
const evaluate_number = (n) => {
|
||||||
var evaluate_number = (n) => {
|
|
||||||
const numtype = /\./.test(n) ? JTYPES.double : JTYPES.int;
|
const numtype = /\./.test(n) ? JTYPES.double : JTYPES.int;
|
||||||
const iszero = /^0+(\.0*)?$/.test(n);
|
const iszero = /^0+(\.0*)?$/.test(n);
|
||||||
return { vtype:'literal',name:'',hasnullvalue:iszero,type:numtype,value:n,valid:true };
|
return { vtype:'literal',name:'',hasnullvalue:iszero,type:numtype,value:n,valid:true };
|
||||||
}
|
}
|
||||||
var evaluate_expression = (expr) => {
|
const evaluate_expression = (expr) => {
|
||||||
var q = $.Deferred(), local;
|
var q = $.Deferred(), local;
|
||||||
switch(expr.root_term_type) {
|
switch(expr.root_term_type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
@@ -1405,9 +1440,8 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
local = { vtype:'literal',name:'',hasnullvalue:true,type:JTYPES.null,value:nullvalue,valid:true };
|
local = { vtype:'literal',name:'',hasnullvalue:true,type:JTYPES.null,value:nullvalue,valid:true };
|
||||||
break;
|
break;
|
||||||
case 'ident':
|
case 'ident':
|
||||||
var v = this._variableHandles[args.frameId];
|
if (!locals) reject_evaluation(`Cannot find variable: ${expr.root_term}`);
|
||||||
if (v && v.frame && v.cached)
|
local = locals.find(l => l.name === expr.root_term);
|
||||||
local = v.cached.find(l => l.name === expr.root_term);
|
|
||||||
break;
|
break;
|
||||||
case 'number':
|
case 'number':
|
||||||
local = evaluate_number(expr.root_term);
|
local = evaluate_number(expr.root_term);
|
||||||
@@ -1437,7 +1471,7 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
q.resolveWith(this,[local]);
|
q.resolveWith(this,[local]);
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
var evaluate_array_element = (index_expr, arr_local) => {
|
const evaluate_array_element = (index_expr, arr_local) => {
|
||||||
if (arr_local.type.signature[0] !== '[') return reject_evaluation('TypeError: value is not an array');
|
if (arr_local.type.signature[0] !== '[') return reject_evaluation('TypeError: value is not an array');
|
||||||
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||||
return evaluate_expression(index_expr)
|
return evaluate_expression(index_expr)
|
||||||
@@ -1449,10 +1483,10 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
}.bind(this,arr_local))
|
}.bind(this,arr_local))
|
||||||
.then(els => els[0])
|
.then(els => els[0])
|
||||||
}
|
}
|
||||||
var evaluate_methodcall = (m, obj_local) => {
|
const evaluate_methodcall = (m, obj_local) => {
|
||||||
return reject_evaluation('Error: method calls are not supported');
|
return reject_evaluation('Error: method calls are not supported');
|
||||||
}
|
}
|
||||||
var evaluate_member = (m, obj_local) => {
|
const evaluate_member = (m, obj_local) => {
|
||||||
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
||||||
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||||
if (m.array_or_fncall.call) return evaluate_methodcall(m, obj_local);
|
if (m.array_or_fncall.call) return evaluate_methodcall(m, obj_local);
|
||||||
@@ -1473,25 +1507,21 @@ class AndroidDebugSession extends DebugSession {
|
|||||||
return field;
|
return field;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
D('evaluate: ' + args.expression);
|
|
||||||
var e = { expr:args.expression };
|
var e = { expr:expression.trim() };
|
||||||
var parsed_expression = parse_expression(e);
|
var parsed_expression = parse_expression(e);
|
||||||
// if there's anything left, it's an error
|
// if there's anything left, it's an error
|
||||||
if (parsed_expression && !e.expr) {
|
if (parsed_expression && !e.expr) {
|
||||||
// the expression is well-formed - start the (asynchronous) evaluation
|
// the expression is well-formed - start the (asynchronous) evaluation
|
||||||
evaluate_expression(parsed_expression)
|
return evaluate_expression(parsed_expression)
|
||||||
.then(function(response,local) {
|
.then(local => {
|
||||||
var v = this._local_to_variable(local);
|
var v = this._local_to_variable(local);
|
||||||
this.sendResponseAndDoNext(response, v.value, v.variablesReference);
|
return resolve_evaluation(v.value, v.variablesReference);
|
||||||
}.bind(this,response))
|
});
|
||||||
.fail(function(response,reason) {
|
|
||||||
this.sendResponseAndDoNext(response, reason.message);
|
|
||||||
}.bind(this,response))
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the expression is not well-formed
|
// the expression is not well-formed
|
||||||
this.sendResponseAndDoNext(response, 'not available');
|
return reject_evaluation('not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ Debugger.prototype = {
|
|||||||
return this.breakpoints.all.slice();
|
return this.breakpoints.all.slice();
|
||||||
},
|
},
|
||||||
|
|
||||||
setbreakpoint: function (srcfpn, line) {
|
setbreakpoint: function (srcfpn, line, conditions) {
|
||||||
var cls = this._splitsrcfpn(srcfpn);
|
var cls = this._splitsrcfpn(srcfpn);
|
||||||
var bid = cls.qtype + ':' + line;
|
var bid = cls.qtype + ':' + line;
|
||||||
var newbp = this.breakpoints.bysrcloc[bid];
|
var newbp = this.breakpoints.bysrcloc[bid];
|
||||||
@@ -510,8 +510,11 @@ Debugger.prototype = {
|
|||||||
pkg: cls.pkg,
|
pkg: cls.pkg,
|
||||||
type: cls.type,
|
type: cls.type,
|
||||||
linenum: line,
|
linenum: line,
|
||||||
|
conditions: Object.assign({},conditions),
|
||||||
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
|
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
|
||||||
state: 'set'// set,notloaded,enabled,removed
|
state: 'set', // set,notloaded,enabled,removed
|
||||||
|
hitcount: 0, // number of times this bp was hit during execution
|
||||||
|
stopcount: 0. // number of times this bp caused a break into the debugger
|
||||||
};
|
};
|
||||||
this.breakpoints.all.push(newbp);
|
this.breakpoints.all.push(newbp);
|
||||||
this.breakpoints.bysrcloc[bid] = newbp;
|
this.breakpoints.bysrcloc[bid] = newbp;
|
||||||
@@ -1334,13 +1337,25 @@ Debugger.prototype = {
|
|||||||
linenum: bp.linenum,
|
linenum: bp.linenum,
|
||||||
threadid: e.event.threadid
|
threadid: e.event.threadid
|
||||||
};
|
};
|
||||||
|
|
||||||
var eventdata = {
|
var eventdata = {
|
||||||
event: e.event,
|
event: e.event,
|
||||||
stoppedlocation: stoppedloc,
|
stoppedlocation: stoppedloc,
|
||||||
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
|
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
|
||||||
};
|
};
|
||||||
x.dbgr.session.stoppedlocation = stoppedloc;
|
x.dbgr.session.stoppedlocation = stoppedloc;
|
||||||
|
// if this was a conditional breakpoint, it will have been automatically cleared
|
||||||
|
// - set a new (unconditional) breakpoint in it's place
|
||||||
|
if (bp.conditions.hitcount) {
|
||||||
|
bp.hitcount += bp.conditions.hitcount;
|
||||||
|
delete bp.conditions.hitcount;
|
||||||
|
var bploc = x.dbgr.breakpoints.enabled[cmlkey].bploc;
|
||||||
|
x.dbgr.session.adbclient.jdwp_command({
|
||||||
|
cmd: x.dbgr.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, null, onevent),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
bp.hitcount++;
|
||||||
|
}
|
||||||
|
bp.stopcount++;
|
||||||
x.dbgr._trigger('bphit', eventdata);
|
x.dbgr._trigger('bphit', eventdata);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1355,11 +1370,12 @@ Debugger.prototype = {
|
|||||||
cmlkeys.push(cmlkey);
|
cmlkeys.push(cmlkey);
|
||||||
this.breakpoints.enabled[cmlkey] = {
|
this.breakpoints.enabled[cmlkey] = {
|
||||||
bp: bploc.bp,
|
bp: bploc.bp,
|
||||||
|
bploc: {c:bploc.c,m:bploc.m,l:bploc.l},
|
||||||
requestid: null,
|
requestid: null,
|
||||||
};
|
};
|
||||||
bparr.push(bploc.bp);
|
bparr.push(bploc.bp);
|
||||||
var cmd = this.session.adbclient.jdwp_command({
|
var cmd = this.session.adbclient.jdwp_command({
|
||||||
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, onevent),
|
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, bploc.bp.conditions.hitcount, onevent),
|
||||||
});
|
});
|
||||||
setbpcmds.push(cmd);
|
setbpcmds.push(cmd);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/jdwp.js
30
src/jdwp.js
@@ -1051,21 +1051,35 @@ function _JDWP() {
|
|||||||
onevent
|
onevent
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
SetBreakpoint:function(ci, mi, idx, onevent) {
|
SetBreakpoint:function(ci, mi, idx, hitcount, onevent) {
|
||||||
// a wrapper around SetEventRequest
|
// a wrapper around SetEventRequest
|
||||||
var mods = [{
|
var mods = [{
|
||||||
modkind:7, // location
|
modkind:7, // location
|
||||||
loc:{ type:ci.info.reftype.value, cid:ci.info.typeid, mid:mi.methodid, idx:idx }
|
loc:{ type:ci.info.reftype.value, cid:ci.info.typeid, mid:mi.methodid, idx:idx },
|
||||||
|
encode(res) {
|
||||||
|
res.push(this.modkind);
|
||||||
|
res.push(this.loc.type);
|
||||||
|
DataCoder.encodeRef(res, this.loc.cid);
|
||||||
|
DataCoder.encodeRef(res, this.loc.mid);
|
||||||
|
DataCoder.encodeLong(res, this.loc.idx);
|
||||||
|
}
|
||||||
}];
|
}];
|
||||||
|
if (hitcount > 0) {
|
||||||
|
// remember when setting a hitcount, the event is automatically cancelled after being fired
|
||||||
|
mods.unshift({
|
||||||
|
modkind:1,
|
||||||
|
count: hitcount,
|
||||||
|
encode(res) {
|
||||||
|
res.push(this.modkind);
|
||||||
|
DataCoder.encodeInt(res, this.count);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
// kind(2=breakpoint)
|
// kind(2=breakpoint)
|
||||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
return this.SetEventRequest("breakpoint",2,2,mods,
|
return this.SetEventRequest("breakpoint",2,2,mods,
|
||||||
function(m1, i, res) {
|
function(m, i, res) {
|
||||||
res.push(m1.modkind);
|
m.encode(res,i);
|
||||||
res.push(m1.loc.type);
|
|
||||||
DataCoder.encodeRef(res, m1.loc.cid);
|
|
||||||
DataCoder.encodeRef(res, m1.loc.mid);
|
|
||||||
DataCoder.encodeLong(res, m1.loc.idx);
|
|
||||||
},
|
},
|
||||||
onevent
|
onevent
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user