diff --git a/src/debugMain.js b/src/debugMain.js index ca9fda2..4b48ce6 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -9,7 +9,6 @@ const crypto = require('crypto'); const dom = require('xmldom').DOMParser; const fs = require('fs'); const os = require('os'); -const Long = require('long'); const path = require('path'); const xpath = require('xpath'); @@ -1103,7 +1102,7 @@ class AndroidDebugSession extends DebugSession { // wait for any locals in the given context to be retrieved getvars.then((thread, locals, vars) => { - return evaluate(args.expression, thread, locals, vars); + return evaluate(args.expression, thread, locals, vars, this.dbgr); }) .then((value,variablesReference) => { response.body = { result:value, variablesReference:variablesReference|0 }; @@ -1138,7 +1137,7 @@ class AndroidDebugSession extends DebugSession { 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, diff --git a/src/debugger.js b/src/debugger.js index 7fba208..e477ad5 100644 --- a/src/debugger.js +++ b/src/debugger.js @@ -1054,6 +1054,50 @@ Debugger.prototype = { return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra); }, + findNamedMethods(type_signature, name, method_signature) { + var x = { type_signature, name, method_signature } + const ismatch = function(x, y) { + if (!x || (x === y)) return true; + return (x instanceof RegExp) && x.test(y); + } + return this.gettypedebuginfo(x.type_signature) + .then(dbgtype => this._ensuremethods(dbgtype[x.type_signature])) + .then(typeinfo => ({ + // resolving the methods only resolves the non-inherited methods + // if we can't find a matching method, we need to search the super types + dbgr: this, + def: $.Deferred(), + matches:[], + find_methods(typeinfo) { + for (var mid in typeinfo.methods) { + var m = typeinfo.methods[mid]; + // does the name match + if (!ismatch(x.name, m.name)) continue; + // does the signature match + if (!ismatch(x.method_signature, m.genericsig || m.sig)) continue; + // add it to the results + this.matches.push(m); + } + // search the supertype + if (typeinfo.type.signature === 'Ljava/lang/Object;') { + this.def.resolveWith(this.dbgr, [this.matches]); + return this; + } + this.dbgr._ensuresuper(typeinfo) + .then(typeinfo => { + return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature) + }) + .then((dbgtype, sig) => { + return this.dbgr._ensuremethods(dbgtype[sig]) + }) + .then(typeinfo => { + this.find_methods(typeinfo) + }); + return this; + } + }).find_methods(typeinfo).def) + }, + getstringchars: function (stringref, extra) { return this.session.adbclient.jdwp_command({ ths: this, @@ -1269,8 +1313,10 @@ Debugger.prototype = { return $.Deferred().resolveWith(this, [typeinfo]); } if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') { - typeinfo.super = null; - return $.Deferred().resolveWith(this, [typeinfo]); + if (typeinfo.info.reftype.string !== 'array') { + typeinfo.super = null; + return $.Deferred().resolveWith(this, [typeinfo]); + } } typeinfo.super = $.Deferred(); diff --git a/src/expressions.js b/src/expressions.js index cf07fad..dc23b48 100644 --- a/src/expressions.js +++ b/src/expressions.js @@ -1,4 +1,5 @@ 'use strict' +const Long = require('long'); const $ = require('./jq-promise'); const { D } = require('./util'); const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals'); @@ -6,7 +7,7 @@ const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./glo /* Asynchronously evaluate an expression */ -exports.evaluate = function(expression, thread, locals, vars) { +exports.evaluate = function(expression, thread, locals, vars, dbgr) { D('evaluate: ' + expression); const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]); @@ -202,7 +203,7 @@ exports.evaluate = function(expression, thread, locals, vars) { else if (local.hasnullvalue) s = '(null)'; if (typeof s === 'string') return $.Deferred().resolveWith(this, [s]); - return this.dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature) + return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature) .then(s => s.string); } const evaluate_expression = (expr) => { @@ -334,7 +335,7 @@ exports.evaluate = function(expression, thread, locals, vars) { return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true }; } else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) { - return stringify(rhs_local).then(rhs_str => createJavaString(this.dbgr, lhs_local.string + rhs_str, { israw: true })); + return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true })); } return invalid_operator(); }); @@ -363,7 +364,7 @@ exports.evaluate = function(expression, thread, locals, vars) { break; case 'string': // we must get the runtime to create string instances - q = createJavaString(this.dbgr, expr.root_term); + q = createJavaString(dbgr, expr.root_term); local = { valid: true }; // make sure we don't fail the evaluation break; } @@ -392,12 +393,67 @@ exports.evaluate = function(expression, thread, locals, vars) { if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value'); var idx = numberify(idx_local); if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`); - return this.dbgr.getarrayvalues(arr_local, idx, 1) + return dbgr.getarrayvalues(arr_local, idx, 1) }.bind(this, arr_local)) .then(els => els[0]) } - const evaluate_methodcall = (/*m, obj_local*/) => { - return reject_evaluation('Error: method calls are not supported'); + const evaluate_methodcall = (m, obj_local) => { + // until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls + if (m.array_or_fncall.call.length) + return reject_evaluation('Error: method calls with parameter values are not supported'); + + // find any methods matching the member name with any parameters in the signature + return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/) + .then(methods => { + if (!methods[0]) + return reject_evaluation(`Error: method '${m.member}()' not found`); + // evaluate any parameters (and wait for the results) + return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression)); + }) + .then((x,...paramValues) => { + // filter the method based upon the types of parameters - note that null types and integer literals can match multiple types + paramValues = paramValues = paramValues.map(p => p[0]); + var matchers = paramValues.map(p => { + switch(true) { + case p.type.signature === 'I': + // match bytes/shorts/ints/longs/floats/doubles within range + if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/ + if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/ + return /^[IJFD]$/; + case p.type.signature === 'F': + return /^[FD]$/; + case p.type.signature === 'Lnull;': + return /^[LT\[]/; // any reference type + default: + // anything else must be an exact signature match (for now - in reality we should allow subclassed type) + return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`); + } + }); + var methods = x.methods.filter(m => { + // extract a list of parameter types + var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g; + for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) { + ptypes.push(x[0]); + } + // the last paramter type is the return value + ptypes.pop(); + // check if they match + if (ptypes.length !== paramValues.length) + return; + return matchers.filter(m => { + return !m.test(ptypes.shift()) + }).length === 0; + }); + if (!methods[0]) + return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`); + // convert the parameters to exact debugger-compatible values + paramValues = paramValues.map(p => { + if (p.type.signature.length === 1) + return { type: p.type.typename, value: p.value}; + return { type: 'oref', value: p.value }; + }) + return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {}); + }); } const evaluate_member = (m, obj_local) => { if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type'); @@ -408,15 +464,15 @@ exports.evaluate = function(expression, thread, locals, vars) { } // length is a 'fake' field of arrays, so special-case it else if (JTYPES.isArray(obj_local.type) && m.member === 'length') { - chain = evaluate_number(obj_local.arraylen); + chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen)); } // we also special-case :super (for object instances) else if (JTYPES.isObject(obj_local.type) && m.member === ':super') { - chain = this.dbgr.getsuperinstance(obj_local); + chain = dbgr.getsuperinstance(obj_local); } // anything else must be a real field else { - chain = this.dbgr.getFieldValue(obj_local, m.member, true) + chain = dbgr.getFieldValue(obj_local, m.member, true) } return chain.then(local => {