mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
added support for setting variable values
This commit is contained in:
248
src/debugMain.js
248
src/debugMain.js
@@ -59,6 +59,11 @@ const NumberBaseConverter = {
|
||||
return negdigits.reverse().map(d => d.toString(base)).join('');
|
||||
},
|
||||
convertBase(str, fromBase, toBase) {
|
||||
if (fromBase === 10 && /[eE]/.test(str)) {
|
||||
// convert exponents to a string of zeros
|
||||
var s = str.split(/[eE]/);
|
||||
str = s[0] + '0'.repeat(parseInt(s[1],10)); // works for 0/+ve exponent,-ve throws
|
||||
}
|
||||
var digits = str.split('').map(d => parseInt(d,fromBase)).reverse();
|
||||
var outArray = [], power = [1];
|
||||
for (var i = 0; i < digits.length; i++) {
|
||||
@@ -69,6 +74,26 @@ const NumberBaseConverter = {
|
||||
}
|
||||
return outArray.reverse().map(d => d.toString(toBase)).join('');
|
||||
},
|
||||
decToHex(decstr, minlen) {
|
||||
var res, isneg = decstr[0] === '-';
|
||||
if (isneg) decstr = decstr.slice(1)
|
||||
decstr = decstr.match(/^0*(.+)$/)[1]; // strip leading zeros
|
||||
if (decstr.length < 16 && !/[eE]/.test(decstr)) { // 16 = Math.pow(2,52).toString().length
|
||||
// less than 52 bits - just use parseInt
|
||||
res = parseInt(decstr, 10).toString(16);
|
||||
} else {
|
||||
res = NumberBaseConverter.convertBase(decstr, 10, 16);
|
||||
}
|
||||
if (isneg) {
|
||||
res = NumberBaseConverter.twosComplement(res, 16);
|
||||
if (/^[0-7]/.test(res)) res = 'f'+res; //msb must be set for -ve numbers
|
||||
} else if (/^[^0-7]/.test(res))
|
||||
res = '0' + res; // msb must not be set for +ve numbers
|
||||
if (minlen && res.length < minlen) {
|
||||
res = (isneg?'f':'0').repeat(minlen - res.length) + res;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
hexToDec(hexstr, signed) {
|
||||
var res, isneg = /^[^0-7]/.test(hexstr);
|
||||
if (hexstr.match(/^0*(.+)$/)[1].length*4 < 52) {
|
||||
@@ -103,12 +128,28 @@ const JTYPES = {
|
||||
isReference(t) { return /^[L[]/.test(t.signature) },
|
||||
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
|
||||
isInteger(t) { return /^[BIJS]$/.test(t.signature) },
|
||||
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
||||
}
|
||||
|
||||
function ensure_path_end_slash(p) {
|
||||
return p + (/[\\/]$/.test(p) ? '' : path.sep);
|
||||
}
|
||||
|
||||
function decode_char(c) {
|
||||
switch(true) {
|
||||
case /^\\[^u]$/.test(c):
|
||||
// backslash escape
|
||||
var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]];
|
||||
return x || c[1];
|
||||
case /^\\u[0-9a-fA-F]{4}$/.test(c):
|
||||
// unicode escape
|
||||
return String.fromCharCode(parseInt(c.slice(2),16));
|
||||
case c.length===1 :
|
||||
return c;
|
||||
}
|
||||
throw new Error('Invalid character value');
|
||||
}
|
||||
|
||||
class AndroidDebugSession extends DebugSession {
|
||||
|
||||
/**
|
||||
@@ -174,6 +215,9 @@ class AndroidDebugSession extends DebugSession {
|
||||
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
|
||||
];
|
||||
|
||||
// we support modifying variable values
|
||||
response.body.supportsSetVariable = true;
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
@@ -676,8 +720,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
/**
|
||||
* Converts locals (or other vars) in debugger format into Variable objects used by VSCode
|
||||
*/
|
||||
_locals_to_variables(vars) {
|
||||
return vars.map(v => {
|
||||
_local_to_variable(v) {
|
||||
var varref = 0, objvalue, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
|
||||
switch(true) {
|
||||
case v.hasnullvalue && JTYPES.isReference(v.type):
|
||||
@@ -717,7 +760,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
objvalue = v.type.typename;
|
||||
break;
|
||||
case v.type.signature === 'C':
|
||||
const cmap = {'\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'}, cc = v.value.charCodeAt(0);
|
||||
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'}, cc = v.value.charCodeAt(0);
|
||||
if (cmap[v.value]) {
|
||||
objvalue = `'\\${cmap[v.value]}'`;
|
||||
} else if (cc < 32) {
|
||||
@@ -745,15 +788,13 @@ class AndroidDebugSession extends DebugSession {
|
||||
value: objvalue,
|
||||
variablesReference: varref,
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) {
|
||||
|
||||
const return_mapped_vars = (vars, response) => {
|
||||
response.body = {
|
||||
variables: this._locals_to_variables(vars.filter(v => v.valid))
|
||||
variables: vars.filter(v => v.valid).map(v => this._local_to_variable(v))
|
||||
};
|
||||
this.sendResponse(response);
|
||||
}
|
||||
@@ -957,6 +998,175 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendEvent(new StoppedEvent("exception", parseInt(e.throwlocation.threadid,16)));
|
||||
}
|
||||
|
||||
setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
|
||||
const failSetVariableRequest = (response, reason) => {
|
||||
response.success = false;
|
||||
response.message = reason;
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
var v = this._variableHandles[args.variablesReference];
|
||||
if (!v || !v.cached) {
|
||||
failSetVariableRequest(response, `Variable '${args.name}' not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
var destvar = v.cached.find(v => v.name===args.name);
|
||||
|
||||
// be nice and remove any superfluous whitespace
|
||||
var value = args.value.trim();
|
||||
|
||||
if (!args || !args.value) {
|
||||
// just ignore blank requests
|
||||
var vsvar = this._local_to_variable(destvar);
|
||||
response.body = {
|
||||
value: vsvar.value,
|
||||
type: vsvar.type,
|
||||
variablesReference: vsvar.variablesReference,
|
||||
};
|
||||
this.sendResponse(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// non-string reference types can only set to null
|
||||
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
|
||||
if (value !== 'null') {
|
||||
failSetVariableRequest(response, 'Object references can only be set to null');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// convert the new value into a debugger-compatible object
|
||||
var m, num, data, datadef;
|
||||
switch(true) {
|
||||
case value === 'null':
|
||||
data = {valuetype:'oref',value:null}; // null object reference
|
||||
break;
|
||||
case /^(true|false)$/.test(value):
|
||||
data = {valuetype:'boolean',value:value!=='false'}; // boolean literal
|
||||
break;
|
||||
case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)):
|
||||
// hex integer- convert to decimal and fall through
|
||||
if (m[1].length < 52/4)
|
||||
value = parseInt(value, 16).toString(10);
|
||||
else
|
||||
value = NumberBaseConverter.hexToDec(value);
|
||||
m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/);
|
||||
// fall-through
|
||||
case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)):
|
||||
// decimal integer
|
||||
num = parseFloat(value, 10); // parseInt() can't handle exponents
|
||||
switch(true) {
|
||||
case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break;
|
||||
case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break;
|
||||
case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break;
|
||||
case /inf/i.test(num): failSetVariableRequest(response,`Value '${args.value}' exceeds the maximum number range.`); return;
|
||||
case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break;
|
||||
default:
|
||||
// long (or larger) - need to use the arbitrary precision class
|
||||
data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)};
|
||||
switch(true){
|
||||
case data.value.length > 16:
|
||||
case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]):
|
||||
// number exceeds signed 63 bit - make it a float
|
||||
data = {valuetype:'float',value:num};
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)):
|
||||
// Java special float constants
|
||||
data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]};
|
||||
break;
|
||||
case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity
|
||||
data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity};
|
||||
break;
|
||||
case !!(m=value.match(/^nan$/i)): // allow js nan
|
||||
data = {valuetype:'float',value:NaN};
|
||||
break;
|
||||
case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)):
|
||||
case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)):
|
||||
// decimal float
|
||||
num = parseFloat(value);
|
||||
data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num};
|
||||
break;
|
||||
case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)):
|
||||
// character literal
|
||||
var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) :
|
||||
m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]]
|
||||
: m[3]
|
||||
data = {valuetype:'char',value:cvalue};
|
||||
break;
|
||||
case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)):
|
||||
// string literal - we need to get the runtime to create a new string first
|
||||
datadef = this.createJavaString(value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
||||
break;
|
||||
default:
|
||||
// invalid literal
|
||||
failSetVariableRequest(response, `'${args.value}' is not a valid literal value.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!datadef) {
|
||||
// as a nicety, if the destination is a string, stringify any primitive value
|
||||
if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) {
|
||||
datadef = this.createJavaString(data.value.toString(), {israw:true})
|
||||
.then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
||||
} else if (destvar.type.signature.length===1) {
|
||||
// if the destination is a primitive, we need to range-check it here
|
||||
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
|
||||
// weirdness if we allow primitives to be set with out-of-range values
|
||||
var validmap = {
|
||||
B:'byte,char', // char may not fit - we special-case this later
|
||||
S:'byte,short,char',
|
||||
I:'byte,short,int,char',
|
||||
J:'byte,short,int,long,char',
|
||||
F:'byte,short,int,long,char,float',
|
||||
D:'byte,short,int,long,char,double,float',
|
||||
C:'byte,short,char',Z:'boolean',
|
||||
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
|
||||
};
|
||||
var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0;
|
||||
if (destvar.type.signature === 'B' && data.valuetype === 'char')
|
||||
is_in_range = validmap.isCharInRangeForByte(data.value);
|
||||
if (!is_in_range) {
|
||||
failSetVariableRequest(response, `Value '${args.value}' is not compatible with variable type: ${destvar.type.typename}`);
|
||||
return;
|
||||
}
|
||||
// check complete - make sure the type matches the destination and use a resolved deferred with the value
|
||||
if (destvar.type.signature!=='C' && data.valuetype === 'char')
|
||||
data.value = data.value.charCodeAt(0); // convert char to it's int value
|
||||
if (destvar.type.signature==='J' && typeof data.value === 'number')
|
||||
data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs
|
||||
data.valuetype = destvar.type.typename;
|
||||
|
||||
datadef = $.Deferred().resolveWith(this,[data]);
|
||||
}
|
||||
}
|
||||
|
||||
datadef.then(data => {
|
||||
// setxxxvalue sets the new value and then returns a new local for the variable
|
||||
switch(destvar.vtype) {
|
||||
case 'field': return this.dbgr.setfieldvalue(destvar, data);
|
||||
case 'local': return this.dbgr.setlocalvalue(destvar, data);
|
||||
default: throw new Error('Unsupported variable type');
|
||||
}
|
||||
})
|
||||
.then(newlocalvar => {
|
||||
Object.assign(destvar, newlocalvar);
|
||||
var vsvar = this._local_to_variable(destvar);
|
||||
response.body = {
|
||||
value: vsvar.value,
|
||||
type: vsvar.type,
|
||||
variablesReference: vsvar.variablesReference,
|
||||
};
|
||||
this.sendResponse(response);
|
||||
})
|
||||
.fail(e => {
|
||||
failSetVariableRequest(response, 'Variable update failed.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by VSCode to perform watch, console and hover evaluations
|
||||
*/
|
||||
@@ -1002,6 +1212,12 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.doEvaluateRequest.apply(this, this._evals_queue[0]);
|
||||
}
|
||||
|
||||
createJavaString(s, opts) {
|
||||
const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char);
|
||||
// return a deferred, which resolves to a local variable named 'literal'
|
||||
return this.dbgr.createstring(raw);
|
||||
}
|
||||
|
||||
doEvaluateRequest(response, args) {
|
||||
|
||||
// just in case the user starts the app running again, before we've evaluated everything in the queue
|
||||
@@ -1045,7 +1261,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
return res;
|
||||
}
|
||||
var 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+)?)|('[^\\']')|('\\[frntv0]')|('\\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;
|
||||
var res = {
|
||||
root_term: root_term[0],
|
||||
@@ -1068,15 +1284,6 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const descape_char = (c) => {
|
||||
if (c.length===2) {
|
||||
// backslash escape
|
||||
var x = {'f':'\f','r':'\r','n':'\n','t':'\t',v:'\v'}[c[1]];
|
||||
return x || (c[1]==='0'?String.fromCharCode(0):c[1]);
|
||||
}
|
||||
// unicode escape
|
||||
return String.fromCharCode(parseInt(c.slice(2,6),16));
|
||||
}
|
||||
var reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
||||
var evaluate_number = (n) => {
|
||||
const numtype = /\./.test(n) ? JTYPES.double : JTYPES.int;
|
||||
@@ -1102,16 +1309,13 @@ class AndroidDebugSession extends DebugSession {
|
||||
local = evaluate_number(expr.root_term);
|
||||
break;
|
||||
case 'char':
|
||||
local = expr.root_term[1]; // fall-through
|
||||
case 'echar':
|
||||
case 'uchar':
|
||||
!local && (local = descape_char(expr.root_term.slice(1,-1))); // fall-through
|
||||
local = { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.char,value:local,valid:true };
|
||||
local = { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.char,value:decode_char(expr.root_term.slice(1,-1)),valid:true };
|
||||
break;
|
||||
case 'string':
|
||||
const raw = expr.root_term.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,descape_char);
|
||||
// we must get the runtime to create string instances
|
||||
q = this.dbgr.createstring(raw);
|
||||
q = this.createJavaString(expr.root_term);
|
||||
local = {valid:true}; // make sure we don't fail the evaluation
|
||||
break;
|
||||
}
|
||||
@@ -1173,7 +1377,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// the expression is well-formed - start the (asynchronous) evaluation
|
||||
evaluate_expression(parsed_expression)
|
||||
.then(function(response,local) {
|
||||
var v = this._locals_to_variables([local])[0];
|
||||
var v = this._local_to_variable(local);
|
||||
this.sendResponseAndDoNext(response, v.value, v.variablesReference);
|
||||
}.bind(this,response))
|
||||
.fail(function(response,reason) {
|
||||
|
||||
@@ -396,7 +396,8 @@ function _JDWP() {
|
||||
res.push((i)&255);
|
||||
},
|
||||
encodeChar: function(res, c) {
|
||||
this.encodeShort(res, c.charCodeAt(0));
|
||||
// c can either be a 1 char string or an integer
|
||||
this.encodeShort(res, typeof c === 'string' ? c.charCodeAt(0) : c);
|
||||
},
|
||||
encodeString : function(res, s) {
|
||||
var utf8bytes = getutf8bytes(s);
|
||||
@@ -405,6 +406,7 @@ function _JDWP() {
|
||||
res.push(utf8bytes[i]);
|
||||
},
|
||||
encodeRef: function(res, ref) {
|
||||
if (ref === null) ref = this.nullRefValue();
|
||||
for(var i=0; i < ref.length; i+=2) {
|
||||
res.push(parseInt(ref.substring(i,i+2), 16));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user