mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c98c962172 | ||
|
|
b3501d529a | ||
|
|
6f9b2f7e78 | ||
|
|
eab08cc501 | ||
|
|
0b5be3020a | ||
|
|
6451e38bca | ||
|
|
101611841f | ||
|
|
cc02c1b089 | ||
|
|
fdbd5df16b |
@@ -1,5 +1,6 @@
|
|||||||
.vscode/**
|
.vscode/**
|
||||||
.vscode-test/**
|
.vscode-test/**
|
||||||
|
images/**/*.gif
|
||||||
test/**
|
test/**
|
||||||
.gitignore
|
.gitignore
|
||||||
jsconfig.json
|
jsconfig.json
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
### version 0.4.1
|
||||||
|
* One day I will learn to update the changelog **before** I hit publish
|
||||||
|
* Updated changelog
|
||||||
|
|
||||||
|
### version 0.4.0
|
||||||
|
* Debugger performance improvements
|
||||||
|
* Fixed exception details not being displayed in locals
|
||||||
|
* Fixed some logcat display issues
|
||||||
|
|
||||||
### version 0.3.1
|
### version 0.3.1
|
||||||
* Bug fixes
|
* Bug fixes
|
||||||
* Fix issue with exception breaks crashing debugger
|
* Fix issue with exception breaks crashing debugger
|
||||||
|
|||||||
@@ -58,4 +58,4 @@ The following settings are used to configure the debugger:
|
|||||||
|
|
||||||
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
11
package.json
11
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "android-dev-ext",
|
"name": "android-dev-ext",
|
||||||
"displayName": "Android",
|
"displayName": "Android",
|
||||||
"description": "Android debugging support for VS Code",
|
"description": "Android debugging support for VS Code",
|
||||||
"version": "0.3.1",
|
"version": "0.4.1",
|
||||||
"publisher": "adelphes",
|
"publisher": "adelphes",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -71,6 +71,11 @@
|
|||||||
"description": "Automatically launch 'adb start-server' if not already started. Default: true",
|
"description": "Automatically launch 'adb start-server' if not already started. Default: true",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
|
"callStackDisplaySize": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of entries to display in call stack views (for locations outside of the project source). 0 shows the entire call stack. Default: 1",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
"logcatPort": {
|
"logcatPort": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
|
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
|
||||||
@@ -124,8 +129,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-debugprotocol": "^1.15.0",
|
"vscode-debugprotocol": "^1.15.0",
|
||||||
"vscode-debugadapter": "^1.15.0",
|
"vscode-debugadapter": "^1.15.0",
|
||||||
"long":"^3.2.0",
|
"long": "^3.2.0",
|
||||||
"ws":"^1.1.1",
|
"ws": "^1.1.1",
|
||||||
"xmldom": "^0.1.27",
|
"xmldom": "^0.1.27",
|
||||||
"xpath": "^0.0.23"
|
"xpath": "^0.0.23"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -403,9 +403,9 @@ ADBClient.prototype = {
|
|||||||
this.logcatinfo = {
|
this.logcatinfo = {
|
||||||
deferred: x.deferred,
|
deferred: x.deferred,
|
||||||
buffer: '',
|
buffer: '',
|
||||||
onlog: o.onlog||$.noop,
|
onlog: o.onlog||(()=>{}),
|
||||||
onlogdata: o.data,
|
onlogdata: o.data,
|
||||||
onclose: o.onclose||$.noop,
|
onclose: o.onclose||(()=>{}),
|
||||||
fd: this.fd,
|
fd: this.fd,
|
||||||
waitfn:_waitfornextlogcat,
|
waitfn:_waitfornextlogcat,
|
||||||
}
|
}
|
||||||
|
|||||||
942
src/debugMain.js
942
src/debugMain.js
File diff suppressed because it is too large
Load Diff
119
src/debugger.js
119
src/debugger.js
@@ -141,8 +141,8 @@ Debugger.prototype = {
|
|||||||
cpfilters: [],
|
cpfilters: [],
|
||||||
preparedclasses: [],
|
preparedclasses: [],
|
||||||
stepids: {}, // hashmap<threadid,stepid>
|
stepids: {}, // hashmap<threadid,stepid>
|
||||||
suspendcount: 0, // refcount of suspend-all-threads
|
|
||||||
threadsuspends: [], // hashmap<threadid, suspend-count>
|
threadsuspends: [], // hashmap<threadid, suspend-count>
|
||||||
|
invokes: {}, // hashmap<threadid, deferred>
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -457,7 +457,6 @@ Debugger.prototype = {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
this.session.suspendcount++;
|
|
||||||
this._trigger('suspended');
|
this._trigger('suspended');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -479,21 +478,12 @@ Debugger.prototype = {
|
|||||||
return this.ensureconnected(extra)
|
return this.ensureconnected(extra)
|
||||||
.then(function (extra) {
|
.then(function (extra) {
|
||||||
if (triggers) this._trigger('resuming');
|
if (triggers) this._trigger('resuming');
|
||||||
const resume_cmd = (decoded,extra) => {
|
|
||||||
return this.session.adbclient.jdwp_command({
|
|
||||||
ths: this,
|
|
||||||
extra: extra,
|
|
||||||
cmd: this.JDWP.Commands.resume(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// we must resume with the same number of suspends
|
|
||||||
var def = resume_cmd(null, extra);
|
|
||||||
for (var i=1; i < this.session.suspendcount; i++) {
|
|
||||||
def = def.then(resume_cmd);
|
|
||||||
}
|
|
||||||
this.session.stoppedlocation = null;
|
this.session.stoppedlocation = null;
|
||||||
this.session.suspendcount = 0;
|
return this.session.adbclient.jdwp_command({
|
||||||
return def;
|
ths: this,
|
||||||
|
extra: extra,
|
||||||
|
cmd: this.JDWP.Commands.resume(),
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(function (decoded, extra) {
|
.then(function (decoded, extra) {
|
||||||
if (triggers) this._trigger('resumed');
|
if (triggers) this._trigger('resumed');
|
||||||
@@ -522,15 +512,15 @@ Debugger.prototype = {
|
|||||||
.then((res,extra) => extra);
|
.then((res,extra) => extra);
|
||||||
},
|
},
|
||||||
|
|
||||||
step: function (steptype, threadid) {
|
step: function (steptype, threadid, extra) {
|
||||||
var x = { steptype: steptype, threadid: threadid };
|
var x = { steptype, threadid, extra };
|
||||||
return this.ensureconnected(x)
|
return this.ensureconnected(x)
|
||||||
.then(function (x) {
|
.then(function (x) {
|
||||||
this._trigger('stepping');
|
this._trigger('stepping');
|
||||||
return this._setupstepevent(x.steptype, x.threadid);
|
return this._setupstepevent(x.steptype, x.threadid, x);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(x => {
|
||||||
return this._resumesilent();
|
return this.resumethread(x.threadid, x.extra);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -980,9 +970,20 @@ Debugger.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
invokeMethod: function (objectid, threadid, type_signature, method_name, method_sig, args, extra) {
|
invokeMethod: function (objectid, threadid, type_signature, method_name, method_sig, args, extra) {
|
||||||
var x = { objectid, threadid, type_signature, method_name, method_sig, args, extra };
|
var x = {
|
||||||
x.return_type_signature = method_sig.match(/\)(.*)/)[1];
|
objectid, threadid, type_signature, method_name, method_sig, args, extra,
|
||||||
return this.gettypedebuginfo(x.return_type_signature)
|
return_type_signature: method_sig.match(/\)(.*)/)[1],
|
||||||
|
def: $.Deferred()
|
||||||
|
};
|
||||||
|
// we must wait until any previous invokes on the same thread have completed
|
||||||
|
var invokes = this.session.invokes[threadid] = (this.session.invokes[threadid] || []);
|
||||||
|
if (invokes.push(x) === 1)
|
||||||
|
this._doInvokeMethod(x);
|
||||||
|
return x.def;
|
||||||
|
},
|
||||||
|
|
||||||
|
_doInvokeMethod: function (x) {
|
||||||
|
this.gettypedebuginfo(x.return_type_signature)
|
||||||
.then(dbgtypes => {
|
.then(dbgtypes => {
|
||||||
x.return_type = dbgtypes[x.return_type_signature].type;
|
x.return_type = dbgtypes[x.return_type_signature].type;
|
||||||
return this.gettypedebuginfo(x.type_signature);
|
return this.gettypedebuginfo(x.type_signature);
|
||||||
@@ -1027,17 +1028,6 @@ Debugger.prototype = {
|
|||||||
.then((typeinfo, method, x) => {
|
.then((typeinfo, method, x) => {
|
||||||
x.typeinfo = typeinfo;
|
x.typeinfo = typeinfo;
|
||||||
x.method = method;
|
x.method = method;
|
||||||
// in order to invoke the method, we must undo any manual suspends of the specified thread
|
|
||||||
// (and then resuspend after)
|
|
||||||
var def = $.Deferred().resolveWith(this,[null,x]);
|
|
||||||
for (var i=0; i < this.session.threadsuspends[x.threadid]|0; i++) {
|
|
||||||
def = def.then((res,x) => this.session.adbclient.jdwp_command({
|
|
||||||
ths: this, extra:x, cmd: this.JDWP.Commands.resumethread(x.threadid),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return def;
|
|
||||||
})
|
|
||||||
.then((res,x) => {
|
|
||||||
return this.session.adbclient.jdwp_command({
|
return this.session.adbclient.jdwp_command({
|
||||||
ths: this,
|
ths: this,
|
||||||
extra: x,
|
extra: x,
|
||||||
@@ -1045,22 +1035,19 @@ Debugger.prototype = {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then((res, x) => {
|
.then((res, x) => {
|
||||||
// save the result and re-suspend the thread
|
// res = {return_value, exception}
|
||||||
x.res = res;
|
|
||||||
var def = $.Deferred().resolveWith(this,[null,x]);
|
|
||||||
for (var i=0; i < this.session.threadsuspends[x.threadid]|0; i++) {
|
|
||||||
def = def.then((res,x) => this.session.adbclient.jdwp_command({
|
|
||||||
ths: this, extra:x, cmd: this.JDWP.Commands.suspendthread(x.threadid),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return def.then((res,x) => $.Deferred().resolveWith(this, [x.res, x]));
|
|
||||||
})
|
|
||||||
.then((res, x) => {
|
|
||||||
if (/^0+$/.test(res.exception))
|
if (/^0+$/.test(res.exception))
|
||||||
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
|
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
|
||||||
// todo - handle reutrn exceptions
|
// todo - handle reutrn exceptions
|
||||||
})
|
})
|
||||||
.then((res, x) => $.Deferred().resolveWith(this, [res[0], x.extra])); // res = {return_value, exception}
|
.then((res, x) => {
|
||||||
|
x.def.resolveWith(this, [res[0], x.extra]);
|
||||||
|
})
|
||||||
|
.always(function(invokes) {
|
||||||
|
invokes.shift();
|
||||||
|
if (invokes.length)
|
||||||
|
this._doInvokeMethod(invokes[0]);
|
||||||
|
}.bind(this,this.session.invokes[x.threadid]));
|
||||||
},
|
},
|
||||||
|
|
||||||
invokeToString(objectid, threadid, type_signature, extra) {
|
invokeToString(objectid, threadid, type_signature, extra) {
|
||||||
@@ -1413,8 +1400,6 @@ Debugger.prototype = {
|
|||||||
},
|
},
|
||||||
fn: function (e) {
|
fn: function (e) {
|
||||||
var x = e.data;
|
var x = e.data;
|
||||||
// each class prepare contributes a global suspend
|
|
||||||
x.dbgr.session.suspendcount++;
|
|
||||||
x.onprepare.apply(x.dbgr, [e.event]);
|
x.onprepare.apply(x.dbgr, [e.event]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1437,14 +1422,12 @@ Debugger.prototype = {
|
|||||||
return clearStepCommand;
|
return clearStepCommand;
|
||||||
},
|
},
|
||||||
|
|
||||||
_setupstepevent: function (steptype, threadid) {
|
_setupstepevent: function (steptype, threadid, extra) {
|
||||||
var onevent = {
|
var onevent = {
|
||||||
data: {
|
data: {
|
||||||
dbgr: this,
|
dbgr: this,
|
||||||
},
|
},
|
||||||
fn: function (e) {
|
fn: function (e) {
|
||||||
// each step hit contributes a global suspend
|
|
||||||
e.data.dbgr.session.suspendcount++;
|
|
||||||
e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
|
e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
|
||||||
.then(function (e) {
|
.then(function (e) {
|
||||||
var x = e.data;
|
var x = e.data;
|
||||||
@@ -1468,10 +1451,12 @@ Debugger.prototype = {
|
|||||||
};
|
};
|
||||||
var cmd = this.session.adbclient.jdwp_command({
|
var cmd = this.session.adbclient.jdwp_command({
|
||||||
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
|
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
|
||||||
}).then(res => {
|
extra: extra,
|
||||||
|
}).then((res,extra) => {
|
||||||
// save the step id so we can manually clear it if an exception break occurs
|
// save the step id so we can manually clear it if an exception break occurs
|
||||||
if (this.session && res && res.id)
|
if (this.session && res && res.id)
|
||||||
this.session.stepids[threadid] = res.id;
|
this.session.stepids[threadid] = res.id;
|
||||||
|
return extra;
|
||||||
});
|
});
|
||||||
|
|
||||||
return cmd.promise();
|
return cmd.promise();
|
||||||
@@ -1498,8 +1483,6 @@ Debugger.prototype = {
|
|||||||
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
|
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
|
||||||
};
|
};
|
||||||
x.dbgr.session.stoppedlocation = stoppedloc;
|
x.dbgr.session.stoppedlocation = stoppedloc;
|
||||||
// each breakpoint hit contributes a global suspend
|
|
||||||
x.dbgr.session.suspendcount++;
|
|
||||||
// if this was a conditional breakpoint, it will have been automatically cleared
|
// if this was a conditional breakpoint, it will have been automatically cleared
|
||||||
// - set a new (unconditional) breakpoint in it's place
|
// - set a new (unconditional) breakpoint in it's place
|
||||||
if (bp.conditions.hitcount) {
|
if (bp.conditions.hitcount) {
|
||||||
@@ -1662,8 +1645,6 @@ Debugger.prototype = {
|
|||||||
dbgr: this,
|
dbgr: this,
|
||||||
},
|
},
|
||||||
fn: function (e) {
|
fn: function (e) {
|
||||||
// each exception hit contributes a global suspend
|
|
||||||
e.data.dbgr.session.suspendcount++;
|
|
||||||
// if this exception break occurred during a step request, we must manually clear the event
|
// if this exception break occurred during a step request, we must manually clear the event
|
||||||
// or the (device-side) debugger will crash on next step
|
// or the (device-side) debugger will crash on next step
|
||||||
this._clearLastStepRequest(e.event.threadid, e).then(e => {
|
this._clearLastStepRequest(e.event.threadid, e).then(e => {
|
||||||
@@ -1731,6 +1712,30 @@ Debugger.prototype = {
|
|||||||
return o.def;
|
return o.def;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setThreadNotify: function(extra) {
|
||||||
|
var onevent = {
|
||||||
|
data: {
|
||||||
|
dbgr: this,
|
||||||
|
},
|
||||||
|
fn: function (e) {
|
||||||
|
// the thread notifiers don't give any location information
|
||||||
|
//this.session.stoppedlocation = ...
|
||||||
|
this._trigger('threadchange', {state:e.event.state, threadid:e.event.threadid});
|
||||||
|
}.bind(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.ensureconnected(extra)
|
||||||
|
.then((extra) => this.session.adbclient.jdwp_command({
|
||||||
|
cmd: this.JDWP.Commands.ThreadStartNotify(onevent),
|
||||||
|
extra:extra,
|
||||||
|
}))
|
||||||
|
.then((res,extra) => this.session.adbclient.jdwp_command({
|
||||||
|
cmd: this.JDWP.Commands.ThreadEndNotify(onevent),
|
||||||
|
extra:extra,
|
||||||
|
}))
|
||||||
|
.then((res, extra) => extra);
|
||||||
|
},
|
||||||
|
|
||||||
_loadclzinfo: function (signature) {
|
_loadclzinfo: function (signature) {
|
||||||
return this.gettypedebuginfo(signature)
|
return this.gettypedebuginfo(signature)
|
||||||
.then(function (classes) {
|
.then(function (classes) {
|
||||||
|
|||||||
71
src/globals.js
Normal file
71
src/globals.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// some commonly used Java types in debugger-compatible format
|
||||||
|
const JTYPES = {
|
||||||
|
byte: {typename:'byte',signature:'B'},
|
||||||
|
short: {typename:'short',signature:'S'},
|
||||||
|
int: {typename:'int',signature:'I'},
|
||||||
|
long: {typename:'long',signature:'J'},
|
||||||
|
float: {typename:'float',signature:'F'},
|
||||||
|
double: {typename:'double',signature:'D'},
|
||||||
|
char: {typename:'char',signature:'C'},
|
||||||
|
boolean: {typename:'boolean',signature:'Z'},
|
||||||
|
null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals
|
||||||
|
String: {typename:'String',signature:'Ljava/lang/String;'},
|
||||||
|
Object: {typename:'Object',signature:'Ljava/lang/Object;'},
|
||||||
|
isArray(t) { return t.signature[0]==='[' },
|
||||||
|
isObject(t) { return t.signature[0]==='L' },
|
||||||
|
isReference(t) { return /^[L[]/.test(t.signature) },
|
||||||
|
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
|
||||||
|
isInteger(t) { return /^[BCIJS]$/.test(t.signature) },
|
||||||
|
isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) },
|
||||||
|
isString(t) { return t.signature === this.String.signature },
|
||||||
|
isChar(t) { return t.signature === this.char.signature },
|
||||||
|
isBoolean(t) { return t.signature === this.boolean.signature },
|
||||||
|
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
||||||
|
}
|
||||||
|
|
||||||
|
// the special name given to exception message fields
|
||||||
|
const exmsg_var_name = ':msg';
|
||||||
|
|
||||||
|
function createJavaString(dbgr, 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 dbgr.createstring(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensure_path_end_slash(p) {
|
||||||
|
return p + (/[\\/]$/.test(p) ? '' : path.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_subpath_of(fpn, subpath) {
|
||||||
|
if (!subpath || !fpn) return false;
|
||||||
|
subpath = ensure_path_end_slash(''+subpath);
|
||||||
|
return fpn.slice(0,subpath.length) === subpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function variableRefToThreadId(variablesReference) {
|
||||||
|
return (variablesReference / 1e9)|0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Object.assign(exports, {
|
||||||
|
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString
|
||||||
|
});
|
||||||
32
src/jdwp.js
32
src/jdwp.js
@@ -371,6 +371,12 @@ function _JDWP() {
|
|||||||
event.exception = this.decodeTaggedObjectID(o);
|
event.exception = this.decodeTaggedObjectID(o);
|
||||||
event.catchlocation = this.decodeLocation(o); // 0 = uncaught
|
event.catchlocation = this.decodeLocation(o); // 0 = uncaught
|
||||||
break;
|
break;
|
||||||
|
case 6: // thread start
|
||||||
|
case 7: // thread end
|
||||||
|
event.reqid = this.decodeInt(o);
|
||||||
|
event.threadid = this.decodeORef(o);
|
||||||
|
event.state = event.kind.value === 6 ? 'start' : 'end';
|
||||||
|
break;
|
||||||
case 8: // classprepare
|
case 8: // classprepare
|
||||||
event.reqid = this.decodeInt(o);
|
event.reqid = this.decodeInt(o);
|
||||||
event.threadid = this.decodeORef(o);
|
event.threadid = this.decodeORef(o);
|
||||||
@@ -1048,7 +1054,7 @@ function _JDWP() {
|
|||||||
}];
|
}];
|
||||||
// kind(1=singlestep)
|
// kind(1=singlestep)
|
||||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
return this.SetEventRequest("step",1,2,mods,
|
return this.SetEventRequest("step",1,1,mods,
|
||||||
function(m1, i, res) {
|
function(m1, i, res) {
|
||||||
res.push(m1.modkind);
|
res.push(m1.modkind);
|
||||||
DataCoder.encodeRef(res, m1.threadid);
|
DataCoder.encodeRef(res, m1.threadid);
|
||||||
@@ -1084,7 +1090,7 @@ function _JDWP() {
|
|||||||
}
|
}
|
||||||
// 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,1,mods,
|
||||||
function(m, i, res) {
|
function(m, i, res) {
|
||||||
m.encode(res,i);
|
m.encode(res,i);
|
||||||
},
|
},
|
||||||
@@ -1099,6 +1105,26 @@ function _JDWP() {
|
|||||||
// kind(2=breakpoint)
|
// kind(2=breakpoint)
|
||||||
return this.ClearEvent("breakpoint",2,requestid);
|
return this.ClearEvent("breakpoint",2,requestid);
|
||||||
},
|
},
|
||||||
|
ThreadStartNotify:function(onevent) {
|
||||||
|
// a wrapper around SetEventRequest
|
||||||
|
var mods = [];
|
||||||
|
// kind(6=threadstart)
|
||||||
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
|
return this.SetEventRequest("threadstart",6,1,mods,
|
||||||
|
function() {},
|
||||||
|
onevent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ThreadEndNotify:function(onevent) {
|
||||||
|
// a wrapper around SetEventRequest
|
||||||
|
var mods = [];
|
||||||
|
// kind(7=threadend)
|
||||||
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
|
return this.SetEventRequest("threadend",7,1,mods,
|
||||||
|
function() {},
|
||||||
|
onevent
|
||||||
|
);
|
||||||
|
},
|
||||||
OnClassPrepare:function(pattern, onevent) {
|
OnClassPrepare:function(pattern, onevent) {
|
||||||
// a wrapper around SetEventRequest
|
// a wrapper around SetEventRequest
|
||||||
var mods = [{
|
var mods = [{
|
||||||
@@ -1133,7 +1159,7 @@ function _JDWP() {
|
|||||||
});
|
});
|
||||||
// kind(4=exception)
|
// kind(4=exception)
|
||||||
// suspendpolicy(0=none,1=event-thread,2=all)
|
// suspendpolicy(0=none,1=event-thread,2=all)
|
||||||
return this.SetEventRequest("exception",4,2,mods,
|
return this.SetEventRequest("exception",4,1,mods,
|
||||||
function(m, i, res) {
|
function(m, i, res) {
|
||||||
res.push(m.modkind);
|
res.push(m.modkind);
|
||||||
switch(m.modkind) {
|
switch(m.modkind) {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class LogcatContent {
|
|||||||
// no point in formatting the data if there are no connected clients
|
// no point in formatting the data if there are no connected clients
|
||||||
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
|
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
|
||||||
if (clients.length) {
|
if (clients.length) {
|
||||||
var lines = '<div class="logblock">' + this._htmllogs.join(os.EOL) + '</div>';
|
var lines = '<div class="logblock">' + this._htmllogs.join('') + '</div>';
|
||||||
clients.forEach(client => client.send(lines));
|
clients.forEach(client => client.send(lines));
|
||||||
}
|
}
|
||||||
// once we've updated all the clients, discard the info
|
// once we've updated all the clients, discard the info
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
.E {color:#f88} .vscode-light .E {color:#f55} .vscode-high-contrast .E {color:#f00}
|
.E {color:#f88} .vscode-light .E {color:#f55} .vscode-high-contrast .E {color:#f00}
|
||||||
.F {color:#f66} .vscode-light .F {color:#f00} .vscode-high-contrast .F {color:#f00}
|
.F {color:#f66} .vscode-light .F {color:#f00} .vscode-high-contrast .F {color:#f00}
|
||||||
.hide {display:none}
|
.hide {display:none}
|
||||||
.logblock {display:inline-block}
|
.logblock {display:block}
|
||||||
.a {display:flex;flex-direction:column;position:absolute;top:0;bottom:0;left:0;right:0;}
|
.a {display:flex;flex-direction:column;position:absolute;top:0;bottom:0;left:0;right:0;}
|
||||||
.b {flex:0 0 auto;border-bottom: 1px solid rgba(128,128,128,.2); padding-bottom: .2em}
|
.b {flex:0 0 auto;border-bottom: 1px solid rgba(128,128,128,.2); padding-bottom: .2em}
|
||||||
.vscode-high-contrast .b {border-color: #0cc}
|
.vscode-high-contrast .b {border-color: #0cc}
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
const setStatus = (x) => { getId('status').textContent = ''+x; }
|
const setStatus = (x) => { getId('status').textContent = ''+x; }
|
||||||
const start = () => {
|
const start = () => {
|
||||||
var rows = getId('rows'), filter = getId('q');
|
var rows = getId('rows'), filter = getId('q');
|
||||||
var last_known_scroll_position=0, selectall=0, logcount=0, currfilter,ws;
|
var last_known_scroll_position=0, selectall=0, logcount=0, prevlc=0, currfilter,ws;
|
||||||
var selecttext = (rows) => {
|
var selecttext = (rows) => {
|
||||||
if (!rows) return window.getSelection().empty();
|
if (!rows) return window.getSelection().empty();
|
||||||
var range = document.createRange();
|
var range = document.createRange();
|
||||||
@@ -74,8 +74,17 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
updateLogCountDisplay = () => {
|
updateLogCountDisplay = () => {
|
||||||
var msg = currfilter ? `${currfilter.matchCounts.true}/${logcount}` : logcount
|
var diff = logcount - prevlc;
|
||||||
|
if (diff <= 0 || diff > 100) {
|
||||||
|
prevlc = logcount;
|
||||||
|
var msg = currfilter ? `${currfilter.matchCounts.true}/${logcount}` : logcount;
|
||||||
|
getId('lcount').textContent = msg;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevlc++;
|
||||||
|
var msg = currfilter ? `${currfilter.matchCounts.true}/${prevlc}` : prevlc;
|
||||||
getId('lcount').textContent = msg;
|
getId('lcount').textContent = msg;
|
||||||
|
setTimeout(updateLogCountDisplay, 1);
|
||||||
}
|
}
|
||||||
showFilterErr = (msg) => {
|
showFilterErr = (msg) => {
|
||||||
filter.style['border-color'] = 'red';
|
filter.style['border-color'] = 'red';
|
||||||
@@ -151,7 +160,7 @@
|
|||||||
if (/^:logcat_cleared$/.test(rawlogs)) {
|
if (/^:logcat_cleared$/.test(rawlogs)) {
|
||||||
rows.innerHTML = '';
|
rows.innerHTML = '';
|
||||||
rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>');
|
rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>');
|
||||||
logcount = 0;
|
logcount = prevlc = 0;
|
||||||
if (currfilter) currfilter.matchCounts = {true:0,false:0};
|
if (currfilter) currfilter.matchCounts = {true:0,false:0};
|
||||||
updateLogCountDisplay();
|
updateLogCountDisplay();
|
||||||
return;
|
return;
|
||||||
|
|||||||
129
src/threads.js
Normal file
129
src/threads.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { AndroidVariables } = require('./variables');
|
||||||
|
const $ = require('./jq-promise');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class used to manage a single thread reported by JDWP
|
||||||
|
*/
|
||||||
|
class AndroidThread {
|
||||||
|
constructor(session, threadid, vscode_threadid) {
|
||||||
|
// the AndroidDebugSession instance
|
||||||
|
this.session = session;
|
||||||
|
// the Android debugger instance
|
||||||
|
this.dbgr = session.dbgr;
|
||||||
|
// the java thread id (hex string)
|
||||||
|
this.threadid = threadid;
|
||||||
|
// the vscode thread id (number)
|
||||||
|
this.vscode_threadid = vscode_threadid;
|
||||||
|
// the (Java) name of the thread
|
||||||
|
this.name = null;
|
||||||
|
// the thread break info
|
||||||
|
this.paused = null;
|
||||||
|
// the timeout during a step which, if it expires, we allow other threads to break
|
||||||
|
this.stepTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
threadNotSuspendedError() {
|
||||||
|
return new Error(`Thread ${this.vscode_threadid} not suspended`);
|
||||||
|
}
|
||||||
|
|
||||||
|
addStackFrameVariable(frame, level) {
|
||||||
|
if (!this.paused) throw this.threadNotSuspendedError();
|
||||||
|
var frameId = (this.vscode_threadid * 1e9) + (level * 1e6);
|
||||||
|
var stack_frame_var = {
|
||||||
|
frame, frameId,
|
||||||
|
locals: null,
|
||||||
|
}
|
||||||
|
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocateExceptionScopeReference(frameId) {
|
||||||
|
if (!this.paused) return;
|
||||||
|
if (!this.paused.last_exception) return;
|
||||||
|
this.paused.last_exception.frameId = frameId;
|
||||||
|
this.paused.last_exception.scopeRef = frameId + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariables(variablesReference) {
|
||||||
|
if (!this.paused)
|
||||||
|
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
||||||
|
|
||||||
|
// is this reference a stack frame
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[variablesReference];
|
||||||
|
if (stack_frame_var) {
|
||||||
|
// frame locals request
|
||||||
|
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref));
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this refrence an exception scope
|
||||||
|
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId];
|
||||||
|
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef));
|
||||||
|
}
|
||||||
|
|
||||||
|
// work out which stack frame this reference is for
|
||||||
|
var frameId = Math.trunc(variablesReference/1e6) * 1e6;
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
||||||
|
|
||||||
|
return stack_frame_var.locals.getVariables(variablesReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureLocals(varinfo) {
|
||||||
|
if (!this.paused)
|
||||||
|
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
||||||
|
|
||||||
|
// evaluate can call this using frameId as the argument
|
||||||
|
if (typeof varinfo === 'number')
|
||||||
|
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
|
||||||
|
|
||||||
|
// if we're currently processing it (or we've finished), just return the promise
|
||||||
|
if (this.paused.locals_done[varinfo.frameId])
|
||||||
|
return this.paused.locals_done[varinfo.frameId];
|
||||||
|
|
||||||
|
// create a new promise
|
||||||
|
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
|
||||||
|
|
||||||
|
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
|
||||||
|
.then((locals,x) => {
|
||||||
|
// make sure we are still paused...
|
||||||
|
if (!this.paused)
|
||||||
|
throw this.threadNotSuspendedError();
|
||||||
|
|
||||||
|
// sort the locals by name, except for 'this' which always goes first
|
||||||
|
locals.sort((a,b) => {
|
||||||
|
if (a.name === b.name) return 0;
|
||||||
|
if (a.name === 'this') return -1;
|
||||||
|
if (b.name === 'this') return +1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})
|
||||||
|
|
||||||
|
// create a new local variable with the results and resolve the promise
|
||||||
|
var varinfo = x.varinfo;
|
||||||
|
varinfo.cached = locals;
|
||||||
|
x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others
|
||||||
|
x.varinfo.locals.setVariable(varinfo.frameId, varinfo);
|
||||||
|
|
||||||
|
var last_exception = this.paused.last_exception;
|
||||||
|
if (last_exception) {
|
||||||
|
x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
x.def.resolveWith(this, [varinfo.frameId]);
|
||||||
|
})
|
||||||
|
.fail(e => {
|
||||||
|
x.def.rejectWith(this, [e]);
|
||||||
|
})
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariableValue(args) {
|
||||||
|
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
|
||||||
|
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
||||||
|
return this._ensureLocals(stack_frame_var).then(varref => {
|
||||||
|
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AndroidThread = AndroidThread;
|
||||||
389
src/variables.js
Normal file
389
src/variables.js
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { JTYPES, exmsg_var_name, createJavaString } = require('./globals');
|
||||||
|
const NumberBaseConverter = require('./nbc');
|
||||||
|
const $ = require('./jq-promise');
|
||||||
|
|
||||||
|
/*
|
||||||
|
Class used to manage stack frame locals and other evaluated expressions
|
||||||
|
*/
|
||||||
|
class AndroidVariables {
|
||||||
|
|
||||||
|
constructor(session, baseId) {
|
||||||
|
this.session = session;
|
||||||
|
this.dbgr = session.dbgr;
|
||||||
|
// the incremental reference id generator for stack frames, locals, etc
|
||||||
|
this.nextId = baseId;
|
||||||
|
// hashmap of variables and frames
|
||||||
|
this.variableHandles = {};
|
||||||
|
// hashmap<objectid, variablesReference>
|
||||||
|
this.objIdCache = {};
|
||||||
|
// allow primitives to be expanded to show more info
|
||||||
|
this._expandable_prims = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addVariable(varinfo) {
|
||||||
|
var variablesReference = ++this.nextId;
|
||||||
|
this.variableHandles[variablesReference] = varinfo;
|
||||||
|
return variablesReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.variableHandles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariable(variablesReference, varinfo) {
|
||||||
|
this.variableHandles[variablesReference] = varinfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getObjectIdReference(type, objvalue) {
|
||||||
|
// we need the type signature because we must have different id's for
|
||||||
|
// an instance and it's supertype instance (which obviously have the same objvalue)
|
||||||
|
var key = type.signature + objvalue;
|
||||||
|
return this.objIdCache[key] || (this.objIdCache[key] = ++this.nextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariables(variablesReference) {
|
||||||
|
var varinfo = this.variableHandles[variablesReference];
|
||||||
|
if (!varinfo) {
|
||||||
|
return $.Deferred().resolve([]);
|
||||||
|
}
|
||||||
|
else if (varinfo.cached) {
|
||||||
|
return $.Deferred().resolve(this._local_to_variable(varinfo.cached));
|
||||||
|
}
|
||||||
|
else if (varinfo.objvar) {
|
||||||
|
// object fields request
|
||||||
|
return this.dbgr.getsupertype(varinfo.objvar, {varinfo})
|
||||||
|
.then((supertype, x) => {
|
||||||
|
x.supertype = supertype;
|
||||||
|
return this.dbgr.getfieldvalues(x.varinfo.objvar, x);
|
||||||
|
})
|
||||||
|
.then((fields, x) => {
|
||||||
|
// add an extra msg field for exceptions
|
||||||
|
if (!x.varinfo.exception) return;
|
||||||
|
x.fields = fields;
|
||||||
|
return this.dbgr.invokeToString(x.varinfo.objvar.value, x.varinfo.threadid, varinfo.objvar.type.signature, x)
|
||||||
|
.then((call,x) => {
|
||||||
|
call.name = exmsg_var_name;
|
||||||
|
x.fields.unshift(call);
|
||||||
|
return $.Deferred().resolveWith(this, [x.fields, x]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((fields, x) => {
|
||||||
|
// ignore supertypes of Object
|
||||||
|
x.supertype && x.supertype.signature!=='Ljava/lang/Object;' && fields.unshift({
|
||||||
|
vtype:'super',
|
||||||
|
name:':super',
|
||||||
|
hasnullvalue:false,
|
||||||
|
type: x.supertype,
|
||||||
|
value: x.varinfo.objvar.value,
|
||||||
|
valid:true,
|
||||||
|
});
|
||||||
|
x.varinfo.cached = fields;
|
||||||
|
return this._local_to_variable(fields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (varinfo.arrvar) {
|
||||||
|
// array elements request
|
||||||
|
var range = varinfo.range, count = range[1] - range[0];
|
||||||
|
// should always have a +ve count, but just in case...
|
||||||
|
if (count <= 0) return $.Deferred().resolve([]);
|
||||||
|
// add some hysteresis
|
||||||
|
if (count > 110) {
|
||||||
|
// create subranges in the sub-power of 10
|
||||||
|
var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = [];
|
||||||
|
for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) {
|
||||||
|
varref = ++this.nextId;
|
||||||
|
v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] };
|
||||||
|
variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref});
|
||||||
|
}
|
||||||
|
return $.Deferred().resolve(variables);
|
||||||
|
}
|
||||||
|
// get the elements for the specified range
|
||||||
|
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count)
|
||||||
|
.then((elements) => {
|
||||||
|
varinfo.cached = elements;
|
||||||
|
return this._local_to_variable(elements);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (varinfo.bigstring) {
|
||||||
|
return this.dbgr.getstringchars(varinfo.bigstring.value)
|
||||||
|
.then((s) => {
|
||||||
|
return this._local_to_variable([{name:'<value>',hasnullvalue:false,string:s,type:JTYPES.String,valid:true}]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (varinfo.primitive) {
|
||||||
|
// convert the primitive value into alternate formats
|
||||||
|
var variables = [], bits = {J:64,I:32,S:16,B:8}[varinfo.signature];
|
||||||
|
const pad = (u,base,len) => ('0000000000000000000000000000000'+u.toString(base)).slice(-len);
|
||||||
|
switch(varinfo.signature) {
|
||||||
|
case 'Ljava/lang/String;':
|
||||||
|
variables.push({name:'<length>',type:'',value:varinfo.value.toString(),variablesReference:0});
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
variables.push({name:'<charCode>',type:'',value:varinfo.value.charCodeAt(0).toString(),variablesReference:0});
|
||||||
|
break;
|
||||||
|
case 'J':
|
||||||
|
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||||
|
var v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
|
||||||
|
const s4 = { hi:parseInt(v64hex.slice(0,8),16), lo:parseInt(v64hex.slice(-8),16) };
|
||||||
|
variables.push(
|
||||||
|
{name:'<binary>',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0}
|
||||||
|
,{name:'<decimal>',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0}
|
||||||
|
,{name:'<hex>',type:'',value:pad(s4.hi,16,8)+pad(s4.lo,16,8),variablesReference:0}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:// integer/short/byte value
|
||||||
|
const u = varinfo.value >>> 0;
|
||||||
|
variables.push(
|
||||||
|
{name:'<binary>',type:'',value:pad(u,2,bits),variablesReference:0}
|
||||||
|
,{name:'<decimal>',type:'',value:u.toString(10),variablesReference:0}
|
||||||
|
,{name:'<hex>',type:'',value:pad(u,16,bits/4),variablesReference:0}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $.Deferred().resolve(variables);
|
||||||
|
}
|
||||||
|
else if (varinfo.frame) {
|
||||||
|
// frame locals request - this should be handled by AndroidDebugThread instance
|
||||||
|
return $.Deferred().resolve([]);
|
||||||
|
} else {
|
||||||
|
// something else?
|
||||||
|
return $.Deferred().resolve([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts locals (or other vars) in debugger format into Variable objects used by VSCode
|
||||||
|
*/
|
||||||
|
_local_to_variable(v) {
|
||||||
|
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._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):
|
||||||
|
// null object or array type
|
||||||
|
objvalue = 'null';
|
||||||
|
break;
|
||||||
|
case v.type.signature === JTYPES.Object.signature:
|
||||||
|
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
|
||||||
|
objvalue = v.type.typename;
|
||||||
|
break;
|
||||||
|
case v.type.signature === JTYPES.String.signature:
|
||||||
|
objvalue = JSON.stringify(v.string);
|
||||||
|
if (v.biglen) {
|
||||||
|
// since this is a big string - make it viewable on expand
|
||||||
|
varref = ++this.nextId;
|
||||||
|
this.variableHandles[varref] = {varref:varref, bigstring:v};
|
||||||
|
objvalue = `String (length:${v.biglen})`;
|
||||||
|
}
|
||||||
|
else if (this._expandable_prims) {
|
||||||
|
// as a courtesy, allow strings to be expanded to see their length
|
||||||
|
varref = ++this.nextId;
|
||||||
|
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case JTYPES.isArray(v.type):
|
||||||
|
// non-null array type - if it's not zero-length add another variable reference so the user can expand
|
||||||
|
if (v.arraylen) {
|
||||||
|
varref = this._getObjectIdReference(v.type, v.value);
|
||||||
|
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
|
||||||
|
}
|
||||||
|
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound
|
||||||
|
break;
|
||||||
|
case JTYPES.isObject(v.type):
|
||||||
|
// non-null object instance - add another variable reference so the user can expand
|
||||||
|
varref = this._getObjectIdReference(v.type, v.value);
|
||||||
|
this.variableHandles[varref] = {varref:varref, objvar:v};
|
||||||
|
objvalue = v.type.typename;
|
||||||
|
break;
|
||||||
|
case v.type.signature === 'C':
|
||||||
|
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'};
|
||||||
|
if (cmap[v.char]) {
|
||||||
|
objvalue = `'\\${cmap[v.char]}'`;
|
||||||
|
} else if (v.value < 32) {
|
||||||
|
objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'";
|
||||||
|
} else objvalue = `'${v.char}'`;
|
||||||
|
break;
|
||||||
|
case v.type.signature === 'J':
|
||||||
|
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||||
|
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
||||||
|
objvalue = NumberBaseConverter.hexToDec(v64hex, true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// other primitives: int, boolean, etc
|
||||||
|
objvalue = v.value.toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
|
||||||
|
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
|
||||||
|
varref = ++this.nextId;
|
||||||
|
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: v.name,
|
||||||
|
type: typename,
|
||||||
|
value: objvalue,
|
||||||
|
variablesReference: varref,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariableValue(args) {
|
||||||
|
const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason));
|
||||||
|
|
||||||
|
var v = this.variableHandles[args.variablesReference];
|
||||||
|
if (!v || !v.cached) {
|
||||||
|
return failSetVariableRequest(`Variable '${args.name}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
var destvar = v.cached.find(v => v.name===args.name);
|
||||||
|
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
|
||||||
|
return failSetVariableRequest(`The value is read-only and cannot be updated.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// be nice and remove any superfluous whitespace
|
||||||
|
var value = args.value.trim();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
// just ignore blank requests
|
||||||
|
var vsvar = this._local_to_variable(destvar);
|
||||||
|
return $.Deferred().resolve(vsvar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-string reference types can only set to null
|
||||||
|
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
|
||||||
|
if (value !== 'null') {
|
||||||
|
return failSetVariableRequest('Object references can only be set to null');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`);
|
||||||
|
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 = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// invalid literal
|
||||||
|
return failSetVariableRequest(`'${value}' is not a valid Java literal.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = createJavaString(this.dbgr, 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) {
|
||||||
|
return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`);
|
||||||
|
}
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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);
|
||||||
|
case 'arrelem':
|
||||||
|
var idx = parseInt(args.name, 10), count=1;
|
||||||
|
if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds');
|
||||||
|
return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data);
|
||||||
|
default: throw new Error('Unsupported variable type');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(newlocalvar => {
|
||||||
|
if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0];
|
||||||
|
Object.assign(destvar, newlocalvar);
|
||||||
|
var vsvar = this._local_to_variable(destvar);
|
||||||
|
return vsvar;
|
||||||
|
})
|
||||||
|
.fail(e => {
|
||||||
|
return failSetVariableRequest(`Variable update failed. ${e.message||''}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.AndroidVariables = AndroidVariables;
|
||||||
Reference in New Issue
Block a user