38 Commits

Author SHA1 Message Date
Dave Holoway
45e2dc2fe1 version 0.6.2 2018-12-16 21:10:16 +00:00
Dave Holoway
30ed5dea3b Fix logcat not launching (#50)
* use prepare instead of postinstall as vscode is a dev dependency  only

* add missing uuid dependency
update devDependencies
2018-12-16 21:08:01 +00:00
Dave Holoway
0eb44130a6 Downgrade vulnerable event-stream package (#48)
* regenerate package-lock

* update changelog
2018-12-03 14:05:10 +00:00
Dave Holoway
d1e7c86092 version 0.6 (#46) 2018-11-11 20:42:02 +00:00
Dave Holoway
690f9dc23a update debugger label (#45) 2018-11-11 20:34:43 +00:00
Dave Holoway
27ecd41b68 update default apkFile path (#43) 2018-11-11 20:22:40 +00:00
Dave Holoway
756a1cea29 update package dependencies (#41) 2018-11-11 20:11:21 +00:00
Dave Holoway
fc2ce97a23 breakpoints do not get enabled on startup (#40)
* add more trace around breakpoint config during startup

* use 'changed' instead of 'updated' when sending BreakpointEvents
2018-11-11 19:31:30 +00:00
Dave Holoway
de8abc62bc add trace support (#38)
* add basic support for sending console logs to OutputWindow

* add trace config setting

* standardise logs
2018-11-11 17:57:32 +00:00
Dave Holoway
8cc31476b3 fix breakpoints don't trigger when hit (#37)
* add errorcode to empty jdwp results

* use an empty line table if the command request fails
2018-11-11 15:20:28 +00:00
adelphes
494bb83cbf version 0.5.0 2018-05-06 20:32:57 +01:00
Dave Holoway
9fca5cbe8c Merge pull request #28 from adelphes/kotlin-support
add basic support for kotlin source files
2018-05-06 20:13:24 +01:00
adelphes
5f0a02b17f add basic support for kotlin source files 2018-05-06 20:02:31 +01:00
adelphes
da36e8e457 added extension package state info 2017-06-24 19:53:22 +01:00
adelphes
3dbfd8ef2a Improved array handling. Better multidimensional array support. 2017-06-24 16:18:22 +01:00
adelphes
4a31b83eb9 add support for evaluateName so Add To Watch works again 2017-06-24 13:16:36 +01:00
adelphes
261c06f1d6 Ensure the cached fields are populated before showing the Exception UI 2017-06-24 12:20:21 +01:00
adelphes
130d79f6c2 add initial support for method call expressions 2017-06-24 12:02:36 +01:00
adelphes
8baf894fc9 add basic support for Exception UI 2017-06-20 13:40:57 +01:00
adelphes
92bd003122 Refactor expression evaluation into its own file 2017-06-18 13:56:04 +01:00
adelphes
13f116b3b3 format integers into other bases 2017-06-18 13:40:13 +01:00
adelphes
140e48cbd1 Add newlines to output
Cancel certain requests if the thread has been resumed
2017-06-18 12:57:38 +01:00
adelphes
7e8f471df4 revert util cleanup until dependencies are sorted 2017-06-14 16:10:58 +01:00
adelphes
09905eb85a Fix output newlines 2017-06-14 15:57:17 +01:00
adelphes
e76773e8e4 code tidy - fix lint warnings for unused fns, params and locals 2017-05-12 14:45:59 +01:00
adelphes
c98c962172 version 0.4.1 2017-03-02 17:30:04 +00:00
adelphes
b3501d529a version 0.4.0 changelog 2017-03-02 17:21:56 +00:00
adelphes
6f9b2f7e78 version 0.4.0 2017-03-02 17:17:46 +00:00
adelphes
eab08cc501 Switch local demo gif link to github 2017-03-02 17:05:18 +00:00
adelphes
0b5be3020a Fixed exception details not being displayed
Make sure the exception scoperef is allocated before locals are retrieved.
2017-03-02 16:12:27 +00:00
adelphes
6451e38bca remove reference to $.noop 2017-02-12 13:43:39 +00:00
adelphes
101611841f Fix logcat lines issue and added count up display. 2017-02-05 23:28:02 +00:00
adelphes
cc02c1b089 added callStackDisplaySize config setting 2017-02-05 21:36:28 +00:00
adelphes
fdbd5df16b Improvements to multi-threaded debugging
Separate out thread-specific parts
Only pause event thread for step, bp and thread events
Continue now resumes the specified thread instead of all threads
Prioritise stepping thread to prevent context switching during step
Monitor thread starts/ends
2017-02-05 19:34:12 +00:00
adelphes
baa3fb6bfd version 0.3.1
bug fix release
2017-02-02 20:13:37 +00:00
adelphes
3cda2e5f1f Fix issue with exception breaks hanging
Temporarily resume the thread used to invoke toString() and re-suspend after.
2017-02-02 20:04:04 +00:00
adelphes
dfed765d21 Fix issue with crash on exception break 2017-02-02 19:19:20 +00:00
adelphes
b60ef65803 Fix issue with Android source not displaying
VSCode 1.9 prioritises srcref over path - we now set the ref to 0 if a source path is set.
2017-02-02 19:18:54 +00:00
19 changed files with 4842 additions and 1537 deletions

View File

@@ -1,5 +1,6 @@
.vscode/**
.vscode-test/**
images/**/*.gif
test/**
.gitignore
jsconfig.json

View File

@@ -1,5 +1,37 @@
# Change Log
### version 0.6.2
* Fix broken logcat command due to missing dependency
### version 0.6.1
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
### version 0.6.0
* Fix issue with breakpoints not enabling correctly
* Fix issue with JDWP failure on breakpoint hit
* Added support for diagnostic logs using trace configuration option
* Updated default apkFile path to match current releases of Android Studio
* Updated package dependencies
### version 0.5.0
* Debugger support for Kotlin source files
* Exception UI
* Fixed some console display issues
### 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
* Bug fixes
* Fix issue with exception breaks crashing debugger
* Fix issue with Android sources not displaying in VSCode 1.9
## version 0.3.0
* Support for Logcat filtering using regular expressions
* Improved expression parsing with support for arithmetic, bitwise, logical and relational operators
@@ -24,4 +56,4 @@ Initial release
* Simple watch expressions
* Breakpoints
* Large array chunking (performance)
* Stale build detection
* Stale build detection

View File

@@ -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).
![Launch Android App](images/demo.gif)
![Launch Android App](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/demo.gif)

View File

@@ -3,6 +3,7 @@
const vscode = require('vscode');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
const state = require('./src/state');
function getADBPort() {
var defaultPort = 5037;
@@ -38,6 +39,7 @@ function activate(context) {
var spliceparams = [context.subscriptions.length,0].concat(disposables);
Array.prototype.splice.apply(context.subscriptions,spliceparams);
}
exports.activate = activate;
// this method is called when your extension is deactivated

2701
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "0.3.0",
"version": "0.6.2",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -35,12 +35,15 @@
"breakpoints": [
{
"language": "java"
},
{
"language": "kotlin"
}
],
"debuggers": [
{
"type": "android",
"label": "Android Debug",
"label": "Android",
"program": "./src/debugMain.js",
"runtime": "node",
"configurationAttributes": {
@@ -59,7 +62,7 @@
"apkFile": {
"type": "string",
"description": "Fully qualified path to the built APK (Android Application Package)",
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk"
"default": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk"
},
"adbPort": {
"type": "integer",
@@ -71,6 +74,11 @@
"description": "Automatically launch 'adb start-server' if not already started. 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": {
"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",
@@ -85,6 +93,11 @@
"type": "string",
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
"default": ""
},
"trace": {
"type": "boolean",
"description": "Set to true to output debugging logs for diagnostics",
"default": "false"
}
}
}
@@ -92,10 +105,10 @@
"initialConfigurations": [
{
"type": "android",
"name": "Android Debug",
"name": "Android",
"request": "launch",
"appSrcRoot": "${workspaceRoot}/app/src/main",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
"adbPort": 5037
}
],
@@ -108,7 +121,7 @@
"request": "launch",
"name": "${2:Launch App}",
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
"adbPort": 5037
}
}
@@ -118,23 +131,24 @@
]
},
"scripts": {
"postinstall": "node ./node_modules/vscode/bin/install",
"prepare": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0",
"long":"^3.2.0",
"ws":"^1.1.1",
"vscode-debugprotocol": "^1.32.0",
"vscode-debugadapter": "^1.32.0",
"long": "^4.0.0",
"uuid": "^3.3.2",
"ws": "^1.1.1",
"xmldom": "^0.1.27",
"xpath": "^0.0.23"
"xpath": "^0.0.27"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^2.3.3",
"eslint": "^3.6.0",
"@types/node": "^6.0.40",
"@types/mocha": "^2.2.32"
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.5",
"eslint": "^5.9.0",
"mocha": "^5.2.0",
"typescript": "^3.1.6",
"vscode": "^1.1.26"
}
}
}

View File

@@ -403,9 +403,9 @@ ADBClient.prototype = {
this.logcatinfo = {
deferred: x.deferred,
buffer: '',
onlog: o.onlog||$.noop,
onlog: o.onlog||(()=>{}),
onlogdata: o.data,
onclose: o.onclose||$.noop,
onclose: o.onclose||(()=>{}),
fd: this.fd,
waitfn:_waitfornextlogcat,
}

File diff suppressed because it is too large Load Diff

View File

@@ -141,7 +141,8 @@ Debugger.prototype = {
cpfilters: [],
preparedclasses: [],
stepids: {}, // hashmap<threadid,stepid>
suspendcount: 0, // refcount of suspend-all-threads
threadsuspends: [], // hashmap<threadid, suspend-count>
invokes: {}, // hashmap<threadid, deferred>
}
return this;
},
@@ -456,18 +457,18 @@ Debugger.prototype = {
});
})
.then(function () {
this.session.suspendcount++;
this._trigger('suspended');
});
},
suspendthread: function (threadid, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
return this.ensureconnected({threadid,extra})
.then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) + 1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.suspendthread(threadid),
extra: x.extra,
cmd: this.JDWP.Commands.suspendthread(x.threadid),
});
})
.then((res,extra) => extra);
@@ -477,21 +478,12 @@ Debugger.prototype = {
return this.ensureconnected(extra)
.then(function (extra) {
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.suspendcount = 0;
return def;
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.resume(),
});
})
.then(function (decoded, extra) {
if (triggers) this._trigger('resumed');
@@ -508,31 +500,32 @@ Debugger.prototype = {
},
resumethread: function (threadid, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
return this.ensureconnected({threadid,extra})
.then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) - 1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.resumethread(threadid),
extra: x.extra,
cmd: this.JDWP.Commands.resumethread(x.threadid),
});
})
.then((res,extra) => extra);
},
step: function (steptype, threadid) {
var x = { steptype: steptype, threadid: threadid };
step: function (steptype, threadid, extra) {
var x = { steptype, threadid, extra };
return this.ensureconnected(x)
.then(function (x) {
this._trigger('stepping');
return this._setupstepevent(x.steptype, x.threadid);
return this._setupstepevent(x.steptype, x.threadid, x);
})
.then(function () {
return this._resumesilent();
.then(x => {
return this.resumethread(x.threadid, x.extra);
});
},
_splitsrcfpn: function (srcfpn) {
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.java$/);
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/);
return {
pkg: m[1].replace(/\/+/g, '.'),
type: m[2],
@@ -977,9 +970,20 @@ Debugger.prototype = {
},
invokeMethod: function (objectid, threadid, type_signature, method_name, method_sig, args, extra) {
var x = { objectid, threadid, type_signature, method_name, method_sig, args, extra };
x.return_type_signature = method_sig.match(/\)(.*)/)[1];
return this.gettypedebuginfo(x.return_type_signature)
var x = {
objectid, threadid, type_signature, method_name, method_sig, args, extra,
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 => {
x.return_type = dbgtypes[x.return_type_signature].type;
return this.gettypedebuginfo(x.type_signature);
@@ -1022,24 +1026,78 @@ Debugger.prototype = {
return o.def;
})
.then((typeinfo, method, x) => {
x.typeinfo = typeinfo;
x.method = method;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, typeinfo.info.typeid, method.methodid, x.args),
});
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, x.typeinfo.info.typeid, x.method.methodid, x.args),
})
})
.then((res, x) => {
// res = {return_value, exception}
if (/^0+$/.test(res.exception))
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
// 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) {
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,
@@ -1255,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();
@@ -1365,6 +1425,15 @@ Debugger.prototype = {
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
})
.then(function (linetable, methodinfo) {
// if the request failed, just return a blank table
if (linetable.errorcode) {
linetable = {
errorcode: linetable.errorcode,
start: '00000000000000000000000000000000',
end: '00000000000000000000000000000000',
lines:[],
}
}
// the linetable does not correlate code indexes with line numbers
// - location searching relies on the table being ordered by code indexes
linetable.lines.sort(function (a, b) {
@@ -1386,8 +1455,6 @@ Debugger.prototype = {
},
fn: function (e) {
var x = e.data;
// each class prepare contributes a global suspend
x.dbgr.session.suspendcount++;
x.onprepare.apply(x.dbgr, [e.event]);
}
};
@@ -1410,14 +1477,12 @@ Debugger.prototype = {
return clearStepCommand;
},
_setupstepevent: function (steptype, threadid) {
_setupstepevent: function (steptype, threadid, extra) {
var onevent = {
data: {
dbgr: this,
},
fn: function (e) {
// each step hit contributes a global suspend
e.data.dbgr.session.suspendcount++;
e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
.then(function (e) {
var x = e.data;
@@ -1441,10 +1506,12 @@ Debugger.prototype = {
};
var cmd = this.session.adbclient.jdwp_command({
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
if (this.session && res && res.id)
this.session.stepids[threadid] = res.id;
return extra;
});
return cmd.promise();
@@ -1471,8 +1538,6 @@ Debugger.prototype = {
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
};
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
// - set a new (unconditional) breakpoint in it's place
if (bp.conditions.hitcount) {
@@ -1635,8 +1700,6 @@ Debugger.prototype = {
dbgr: this,
},
fn: function (e) {
// each exception hit contributes a global suspend
x.dbgr.session.suspendcount++;
// 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
this._clearLastStepRequest(e.event.threadid, e).then(e => {
@@ -1704,6 +1767,30 @@ Debugger.prototype = {
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) {
return this.gettypedebuginfo(signature)
.then(function (classes) {

535
src/expressions.js Normal file
View File

@@ -0,0 +1,535 @@
'use strict'
const Long = require('long');
const $ = require('./jq-promise');
const { D } = require('./util');
const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals');
/*
Asynchronously evaluate an expression
*/
exports.evaluate = function(expression, thread, locals, vars, dbgr) {
D('evaluate: ' + expression);
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
if (thread && !thread.paused)
return reject_evaluation('not available');
// special case for evaluating exception messages
// - this is called if the user tries to evaluate ':msg' from the locals
if (expression === exmsg_var_name) {
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
if (msglocal) {
return resolve_evaluation(vars._local_to_variable(msglocal).value);
}
}
return reject_evaluation('not available');
}
const parse_array_or_fncall = function (e) {
var arg, res = { arr: [], call: null };
// pre-call array indexes
while (e.expr[0] === '[') {
e.expr = e.expr.slice(1).trim();
if ((arg = parse_expression(e)) === null) return null;
res.arr.push(arg);
if (e.expr[0] !== ']') return null;
e.expr = e.expr.slice(1).trim();
}
if (res.arr.length) return res;
// method call
if (e.expr[0] === '(') {
res.call = []; e.expr = e.expr.slice(1).trim();
if (e.expr[0] !== ')') {
for (; ;) {
if ((arg = parse_expression(e)) === null) return null;
res.call.push(arg);
if (e.expr[0] === ')') break;
if (e.expr[0] !== ',') return null;
e.expr = e.expr.slice(1).trim();
}
}
e.expr = e.expr.slice(1).trim();
// post-call array indexes
while (e.expr[0] === '[') {
e.expr = e.expr.slice(1).trim();
if ((arg = parse_expression(e)) === null) return null;
res.arr.push(arg);
if (e.expr[0] !== ']') return null;
e.expr = e.expr.slice(1).trim();
}
}
return res;
}
const parse_expression_term = function (e) {
if (e.expr[0] === '(') {
e.expr = e.expr.slice(1).trim();
var subexpr = { expr: e.expr };
var res = parse_expression(subexpr);
if (res) {
if (subexpr.expr[0] !== ')') return null;
e.expr = subexpr.expr.slice(1).trim();
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
// primitive typecast
var castexpr = parse_expression_term(e);
if (castexpr) castexpr.typecast = res.root_term;
res = castexpr;
}
}
return res;
}
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
if (unop) {
var op = unop[0].replace(/\s/g, '');
e.expr = e.expr.slice(unop[0].length).trim();
var res = parse_expression_term(e);
if (res) {
for (var i = op.length - 1; i >= 0; --i)
res = { operator: op[i], rhs: res };
}
return res;
}
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
if (!root_term) return null;
var res = {
root_term: root_term[0],
root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1],
array_or_fncall: null,
members: [],
typecast: ''
}
e.expr = e.expr.slice(res.root_term.length).trim();
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
// the root term is not allowed to be a method call
if (res.array_or_fncall.call) return null;
while (e.expr[0] === '.') {
// member expression
e.expr = e.expr.slice(1).trim();
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
if (!member_name) return null;
res.members.push(m = { member: member_name[0], array_or_fncall: null })
e.expr = e.expr.slice(m.member.length).trim();
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
}
return res;
}
const prec = {
'*': 1, '%': 1, '/': 1,
'+': 2, '-': 2,
'<<': 3, '>>': 3, '>>>': 3,
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
'==': 5, '!=': 5,
'&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11,
}
const parse_expression = function (e) {
var res = parse_expression_term(e);
if (!e.currprec) e.currprec = [12];
for (; ;) {
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
if (!binary_operator) break;
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
if (precdiff > 0) {
// bigger number -> lower precendence -> end of (sub)expression
break;
}
if (precdiff === 0 && binary_operator[0] !== '?') {
// equal precedence, ltr evaluation
break;
}
// higher or equal precendence
e.currprec.unshift(e.currprec[0] + precdiff);
e.expr = e.expr.slice(binary_operator[0].length).trim();
// current or higher precendence
if (binary_operator[0] === '?') {
res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null };
res.ternary_true = parse_expression(e);
if (e.expr[0] === ':') {
e.expr = e.expr.slice(1).trim();
res.ternary_false = parse_expression(e);
}
} else {
res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) };
}
e.currprec.shift();
}
return res;
}
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
const evaluate_number = (n) => {
n += '';
var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10;
if (m) {
switch (m[2]) {
case 'b': base = 2; n = m[1] + m[3]; break;
case 'x': base = 16; n = m[1] + m[3]; break;
default: base = 8; break;
}
}
if (base !== 16 && /[fFdD]$/.test(n)) {
numtype = /[fF]$/.test(n) ? 'float' : 'double';
n = n.slice(0, -1);
} else if (/[lL]$/.test(n)) {
numtype = 'long'
n = n.slice(0, -1);
} else {
numtype = /\./.test(n) ? 'double' : 'int';
}
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
else n = parseInt(n, base) | 0;
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true };
}
const evaluate_char = (char) => {
return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true };
}
const numberify = (local) => {
//if (local.type.signature==='C') return local.char.charCodeAt(0);
if (/^[FD]$/.test(local.type.signature))
return parseFloat(local.value);
if (local.type.signature === 'J')
return parseInt(local.value, 16);
return parseInt(local.value, 10);
}
const stringify = (local) => {
var s;
if (JTYPES.isString(local.type)) s = local.string;
else if (JTYPES.isChar(local.type)) s = local.char;
else if (JTYPES.isPrimitive(local.type)) s = '' + local.value;
else if (local.hasnullvalue) s = '(null)';
if (typeof s === 'string')
return $.Deferred().resolveWith(this, [s]);
return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
.then(s => s.string);
}
const evaluate_expression = (expr) => {
var q = $.Deferred(), local;
if (expr.operator) {
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`),
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
var lhs_local;
return !expr.lhs
? // unary operator
evaluate_expression(expr.rhs)
.then(rhs_local => {
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
rhs_local.value = !rhs_local.value;
return rhs_local;
}
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
switch (rhs_local.type.typename) {
case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break;
default: rhs_local = evaluate_number('' + ~rhs_local.value); break;
}
return rhs_local;
}
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
if (expr.operator === '+') return rhs_local;
switch (rhs_local.type.typename) {
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break;
default: rhs_local = evaluate_number('' + (-rhs_local.value)); break;
}
return rhs_local;
}
return invalid_operator('unary');
})
: // binary operator
evaluate_expression(expr.lhs)
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
.then(rhs_local => {
if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type))
|| (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) {
// one operand is a long, the other is an integer -> the result is a long
var a, b, lbase, rbase;
lbase = lhs_local.type.signature === 'J' ? 16 : 10;
rbase = rhs_local.type.signature === 'J' ? 16 : 10;
a = Long.fromString('' + lhs_local.value, false, lbase);
b = Long.fromString('' + rhs_local.value, false, rbase);
switch (expr.operator) {
case '+': a = a.add(b); break;
case '-': a = a.subtract(b); break;
case '*': a = a.multiply(b); break;
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
case '<<': a = a.shl(b); break;
case '>>': a = a.shr(b); break;
case '>>>': a = a.shru(b); break;
case '&': a = a.and(b); break;
case '|': a = a.or(b); break;
case '^': a = a.xor(b); break;
case '==': a = a.eq(b); break;
case '!=': a = !a.eq(b); break;
case '<': a = a.lt(b); break;
case '<=': a = a.lte(b); break;
case '>': a = a.gt(b); break;
case '>=': a = a.gte(b); break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true };
}
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
// both are (non-long) integer types
var a = numberify(lhs_local), b = numberify(rhs_local);
switch (expr.operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero();
case '%': if (b) { a %= b; break; } return divide_by_zero();
case '<<': a <<= b; break;
case '>>': a >>= b; break;
case '>>>': a >>>= b; break;
case '&': a &= b; break;
case '|': a |= b; break;
case '^': a ^= b; break;
case '==': a = a === b; break;
case '!=': a = a !== b; break;
case '<': a = a < b; break;
case '<=': a = a <= b; break;
case '>': a = a > b; break;
case '>=': a = a >= b; break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true };
}
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
var a = numberify(lhs_local), b = numberify(rhs_local);
switch (expr.operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': a /= b; break;
case '==': a = a === b; break;
case '!=': a = a !== b; break;
case '<': a = a < b; break;
case '<=': a = a <= b; break;
case '>': a = a > b; break;
case '>=': a = a >= b; break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
// one of them must be a float or double
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true };
}
else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') {
// boolean operands
var a = lhs_local.value, b = rhs_local.value;
switch (expr.operator) {
case '&': case '&&': a = a && b; break;
case '|': case '||': a = a || b; break;
case '^': a = !!(a ^ b); break;
case '==': a = a === b; break;
case '!=': a = a !== b; break;
default: return invalid_operator();
}
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(dbgr, lhs_local.string + rhs_str, { israw: true }));
}
return invalid_operator();
});
}
switch (expr.root_term_type) {
case 'boolean':
local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true };
break;
case 'null':
const nullvalue = '0000000000000000'; // null reference value
local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true };
break;
case 'ident':
local = locals && locals.find(l => l.name === expr.root_term);
break;
case 'hexint':
case 'octint':
case 'decint':
case 'decfloat':
local = evaluate_number(expr.root_term);
break;
case 'char':
case 'echar':
case 'uchar':
local = evaluate_char(decode_char(expr.root_term.slice(1, -1)))
break;
case 'string':
// we must get the runtime to create string instances
q = createJavaString(dbgr, expr.root_term);
local = { valid: true }; // make sure we don't fail the evaluation
break;
}
if (!local || !local.valid) return reject_evaluation('not available');
// we've got the root term variable - work out the rest
q = expr.array_or_fncall.arr.reduce((q, index_expr) => {
return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr));
}, q);
q = expr.members.reduce((q, m) => {
return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m));
}, q);
if (expr.typecast) {
q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast))
}
// if it's a string literal, we are already waiting for the runtime to create the string
// - otherwise, start the evalaution...
if (expr.root_term_type !== 'string')
q.resolveWith(this, [local]);
return q;
}
const evaluate_array_element = (index_expr, arr_local) => {
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
return evaluate_expression(index_expr)
.then(function (arr_local, idx_local) {
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 dbgr.getarrayvalues(arr_local, idx, 1)
}.bind(this, arr_local))
.then(els => els[0])
}
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');
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
var chain;
if (m.array_or_fncall.call) {
chain = evaluate_methodcall(m, obj_local);
}
// length is a 'fake' field of arrays, so special-case it
else if (JTYPES.isArray(obj_local.type) && m.member === 'length') {
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 = dbgr.getsuperinstance(obj_local);
}
// anything else must be a real field
else {
chain = dbgr.getFieldValue(obj_local, m.member, true)
}
return chain.then(local => {
if (m.array_or_fncall.arr.length) {
var q = $.Deferred();
m.array_or_fncall.arr.reduce((q, index_expr) => {
return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr));
}, q);
return q.resolveWith(this, [local]);
}
});
}
const evaluate_cast = (type, local) => {
if (type === local.type.typename) return local;
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
// boolean cannot be converted from anything else
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
if (local.type.typename === 'long') {
// long to something else
var value = Long.fromString(local.value, true, 16);
switch (true) {
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break;
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break;
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break;
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break;
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break;
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
default: return incompatible_cast();
}
} else {
switch (true) {
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
case (type === 'long'): local = evaluate_number(local.value + 'L'); break;
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break;
case (type === 'float'): break;
case (type === 'double'): break;
default: return incompatible_cast();
}
}
local.type = JTYPES[type];
return local;
}
var e = { expr: expression.trim() };
var parsed_expression = parse_expression(e);
// if there's anything left, it's an error
if (parsed_expression && !e.expr) {
// the expression is well-formed - start the (asynchronous) evaluation
return evaluate_expression(parsed_expression)
.then(local => {
var v = vars._local_to_variable(local);
return resolve_evaluation(v.value, v.variablesReference);
});
}
// the expression is not well-formed
return reject_evaluation('not available');
}

87
src/globals.js Normal file
View File

@@ -0,0 +1,87 @@
'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)]] },
}
function signatureToFullyQualifiedType(sig) {
var arr = sig.match(/^\[+/) || '';
if (arr) {
arr = '[]'.repeat(arr[0].length);
sig = sig.slice(0, arr.length/2);
}
var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/);
if (!m) return '';
if (m[3]) {
return m[3].replace(/[/$]/g,'.') + arr;
} else if (m[4]) {
return m[4].replace(/[/$]/g, '.') + arr;
}
return JTYPES.fromPrimSig(sig[0]) + arr;
}
// the special name given to exception message fields
const exmsg_var_name = ':msg';
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, signatureToFullyQualifiedType
});

View File

@@ -1,5 +1,5 @@
const $ = require('./jq-promise');
const { atob,btoa,D,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
const { btoa,D,E,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
/*
JDWP - The Java Debug Wire Protocol
*/
@@ -96,8 +96,8 @@ function _JDWP() {
return;
}
if (this.errorcode != 0) {
console.error("Command failed: error " + this.errorcode, this);
if (this.errorcode !== 0) {
E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this);
}
if (!this.errorcode && this.command && this.command.replydecodefn) {
@@ -109,7 +109,10 @@ function _JDWP() {
return;
}
this.decoded = {empty:true};
this.decoded = {
empty: true,
errorcode: this.errorcode,
};
}
this.decodereply = function(ths,s) {
@@ -371,6 +374,12 @@ function _JDWP() {
event.exception = this.decodeTaggedObjectID(o);
event.catchlocation = this.decodeLocation(o); // 0 = uncaught
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
event.reqid = this.decodeInt(o);
event.threadid = this.decodeORef(o);
@@ -531,12 +540,12 @@ function _JDWP() {
}
m = signature.match(/^(\[+)(.+)$/);
if (m) {
var elementtype = this.signaturetotype(m[2]);
var elementtype = this.signaturetotype(m[1].slice(0,-1) + m[2]);
return {
signature:signature,
arraydims:m[1].length,
elementtype: elementtype,
typename:elementtype.typename+m[1].replace(/\[/g,'[]'),
typename:elementtype.typename+'[]',
}
}
var primitivetypes = {
@@ -1048,7 +1057,7 @@ function _JDWP() {
}];
// kind(1=singlestep)
// 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) {
res.push(m1.modkind);
DataCoder.encodeRef(res, m1.threadid);
@@ -1084,7 +1093,7 @@ function _JDWP() {
}
// kind(2=breakpoint)
// 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) {
m.encode(res,i);
},
@@ -1099,6 +1108,26 @@ function _JDWP() {
// kind(2=breakpoint)
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) {
// a wrapper around SetEventRequest
var mods = [{
@@ -1133,7 +1162,7 @@ function _JDWP() {
});
// kind(4=exception)
// 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) {
res.push(m.modkind);
switch(m.modkind) {

View File

@@ -40,7 +40,7 @@ var Deferred = exports.Deferred = function(p, parent) {
var faildef = $.Deferred(null, this);
var p = this._promise.catch(function(a) {
if (a.stack) {
console.error(a.stack);
util.E(a.stack);
a = [a];
}
if (this.def._context === null && this.def._parent)

View File

@@ -1,6 +1,4 @@
'use strict'
// vscode stuff
const { EventEmitter, Uri } = require('vscode');
// node and external modules
const fs = require('fs');
const os = require('os');
@@ -38,7 +36,7 @@ class LogcatContent {
onlog: this.onLogcatContent.bind(this),
onclose: this.onLogcatDisconnect.bind(this),
});
}).then(x => {
}).then(() => {
this._state = 'connected';
this._initwait = null;
resolve(this.content);
@@ -55,20 +53,20 @@ class LogcatContent {
return this.htmlBootstrap({connected:true, status:'',oldlogs:''});
// if we're in the disconnected state, and this.content is called, it means the user has requested
// this logcat again - check if the device has reconnected
return this._initwait = new Promise((resolve, reject) => {
return this._initwait = new Promise((resolve/*, reject*/) => {
// clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again
this._prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
this._adbclient.logcat({
onlog: this.onLogcatContent.bind(this),
onclose: this.onLogcatDisconnect.bind(this),
}).then(x => {
}).then(() => {
// we successfully reconnected
this._state = 'connected';
this._prevlogs = null;
this._initwait = null;
resolve(this.content);
}).fail(e => {
}).fail((/*e*/) => {
// reconnection failed - put the logs back and return the cached info
this._logs = this._prevlogs._logs;
this._htmllogs = this._prevlogs._htmllogs;
@@ -117,7 +115,7 @@ class LogcatContent {
// no point in formatting the data if there are no connected clients
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
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));
}
// once we've updated all the clients, discard the info
@@ -161,7 +159,7 @@ class LogcatContent {
this.renotify();
}
}
onLogcatDisconnect(e) {
onLogcatDisconnect(/*e*/) {
if (this._state === 'disconnected') return;
this._state = 'disconnected';
this.sendDisconnectMsg();
@@ -215,7 +213,7 @@ LogcatContent.initWebSocketServer = function () {
this.wss = null;
LogcatContent._wssdone.resolveWith(LogcatContent, []);
});
this.wss.on('error', err => {
this.wss.on('error', (/*err*/) => {
if (!LogcatContent._wss) {
// listen failed -try the next port
this.retries++ , this.port++;
@@ -245,7 +243,7 @@ function openLogcatWindow(vscode) {
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
var adbargs = ['-P',''+adbport,'start-server'];
try {
var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
/*var stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
}
})
@@ -282,7 +280,7 @@ function openLogcatWindow(vscode) {
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
});
})
.fail(e => {
.fail((/*e*/) => {
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
});
}

View File

@@ -10,7 +10,7 @@
.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}
.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;}
.b {flex:0 0 auto;border-bottom: 1px solid rgba(128,128,128,.2); padding-bottom: .2em}
.vscode-high-contrast .b {border-color: #0cc}
@@ -56,7 +56,7 @@
const setStatus = (x) => { getId('status').textContent = ''+x; }
const start = () => {
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) => {
if (!rows) return window.getSelection().empty();
var range = document.createRange();
@@ -74,8 +74,17 @@
}
};
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;
setTimeout(updateLogCountDisplay, 1);
}
showFilterErr = (msg) => {
filter.style['border-color'] = 'red';
@@ -151,7 +160,7 @@
if (/^:logcat_cleared$/.test(rawlogs)) {
rows.innerHTML = '';
rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>');
logcount = 0;
logcount = prevlc = 0;
if (currfilter) currfilter.matchCounts = {true:0,false:0};
updateLogCountDisplay();
return;

11
src/state.js Normal file
View File

@@ -0,0 +1,11 @@
const vscode = require('vscode');
const fs = require('fs');
const path = require('path');
const os = require('os');
var adext = {};
try {
Object.assign(adext, JSON.parse(fs.readFileSync(path.join(path.dirname(__dirname),'package.json'),'utf8')));
} catch (ex) { }
exports.adext = adext;

129
src/threads.js Normal file
View 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;

View File

@@ -1,43 +1,48 @@
const crypto = require('crypto');
var nofn=function(){};
var D=exports.D=console.log.bind(console);
var E=exports.E=console.error.bind(console);
var W=exports.W=console.warn.bind(console);
var DD=nofn,cl=D,printf=D;
var nofn = function () { };
const messagePrintCallbacks = new Set();
var D = exports.D = (...args) => (console.log(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
var E = exports.E = (...args) => (console.error(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
var W = exports.W = (...args) => (console.warn(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
var DD = nofn, cl = D, printf = D;
var print_jdwp_data = nofn;// _print_jdwp_data;
var print_packet = nofn;//_print_packet;
Array.first = function(arr, fn, defaultvalue) {
exports.onMessagePrint = function(cb) {
messagePrintCallbacks.add(cb);
}
Array.first = function (arr, fn, defaultvalue) {
var idx = Array.indexOfFirst(arr, fn);
return idx < 0 ? defaultvalue : arr[idx];
}
Array.indexOfFirst = function(arr, fn) {
Array.indexOfFirst = function (arr, fn) {
if (!Array.isArray(arr)) return -1;
for (var i=0; i < arr.length; i++)
for (var i = 0; i < arr.length; i++)
if (fn(arr[i], i, arr))
return i;
return -1;
}
var isEmptyObject = exports.isEmptyObject = function(o) {
return typeof(o)==='object' && !Object.keys(o).length;
var isEmptyObject = exports.isEmptyObject = function (o) {
return typeof (o) === 'object' && !Object.keys(o).length;
}
var leftpad = exports.leftpad = function(char, len, s) {
var leftpad = exports.leftpad = function (char, len, s) {
while (s.length < len)
s = char + s;
return s;
}
var intToHex = exports.intToHex = function(i, minlen) {
var intToHex = exports.intToHex = function (i, minlen) {
var s = i.toString(16);
if (minlen) s = leftpad('0', minlen, s);
return s;
}
var intFromHex = exports.intFromHex = function(s, maxlen, defaultvalue) {
var intFromHex = exports.intFromHex = function (s, maxlen, defaultvalue) {
s = s.slice(0, maxlen);
if (!/^[0-9a-fA-F]+$/.test(s)) return defaultvalue;
return parseInt(s, 16);
@@ -45,53 +50,53 @@ var intFromHex = exports.intFromHex = function(s, maxlen, defaultvalue) {
var fdcache = [];
var index_of_file_fdn = function(n) {
var index_of_file_fdn = function (n) {
if (n <= 0) return -1;
for (var i=0; i < fdcache.length; i++) {
for (var i = 0; i < fdcache.length; i++) {
if (fdcache[i] && fdcache[i].n === n)
return i;
}
return -1;
}
var get_file_fd_from_fdn = function(n) {
var get_file_fd_from_fdn = function (n) {
var idx = index_of_file_fdn(n);
if (idx < 0) return null;
return fdcache[idx];
}
var remove_fd_from_cache = function(fd) {
var remove_fd_from_cache = function (fd) {
if (!fd) return;
var idx = index_of_file_fdn(fd.n);
if (idx>=0) fdcache.splice(idx, 1);
if (idx >= 0) fdcache.splice(idx, 1);
}
// add an offset so we don't conflict with tcp socketIds
var min_fd_num = 100000;
var _new_fd_count = 0;
var new_fd = this.new_fd = function(name, raw) {
var new_fd = this.new_fd = function (name, raw) {
var rwpipe = raw ? new Uint8Array(0) : [];
var fd = {
name: name,
n: min_fd_num + (++_new_fd_count),
raw: !!raw,
readpipe:rwpipe,
writepipe:rwpipe,
reader:null,
readerlen:0,
kickingreader:false,
total:{read:0,written:0},
readpipe: rwpipe,
writepipe: rwpipe,
reader: null,
readerlen: 0,
kickingreader: false,
total: { read: 0, written: 0 },
duplex: null,
closed:'',
read:function(cb) {
closed: '',
read: function (cb) {
if (this.raw)
throw 'Cannot read from raw fd';
if (this.reader && this.reader !== cb)
throw 'multiple readers?';
this.reader = cb;
this._kickreader();
this.reader = cb;
this._kickreader();
},
write:function(data) {
write: function (data) {
if (this.closed) {
D('Ignoring attempt to write to closed file: %o', this);
return;
@@ -100,23 +105,23 @@ var new_fd = this.new_fd = function(name, raw) {
D('Ignoring attempt to write object to raw file: %o', this);
return;
}
this.writepipe.push(data);
if (this.duplex) {
this.duplex._kickreader();
}
this.writepipe.push(data);
if (this.duplex) {
this.duplex._kickreader();
}
},
readbytes:function(len, cb) {
readbytes: function (len, cb) {
if (!this.raw)
throw 'Cannot readbytes from non-raw fd';
if (this.reader)
throw 'multiple readers?';
this.reader = cb;
this.reader = cb;
this.readerlen = len;
this._kickreader();
this._kickreader();
},
writebytes:function(buffer) {
writebytes: function (buffer) {
if (this.closed) {
D('Ignoring attempt to write to closed file: %o', this);
return;
@@ -135,35 +140,35 @@ var new_fd = this.new_fd = function(name, raw) {
newbuf.set(this.writepipe);
newbuf.set(buffer, this.writepipe.byteLength);
this.writepipe = newbuf;
if (this.duplex)
if (this.duplex)
this.duplex.readpipe = newbuf;
else
this.readpipe = newbuf;
D('new buffer size: %d (fd:%d)',this.writepipe.byteLength, this.n);
D('new buffer size: %d (fd:%d)', this.writepipe.byteLength, this.n);
this._kickreaders();
},
cancelread:function(flushfirst) {
cancelread: function (flushfirst) {
if (flushfirst)
this.flush();
this.reader = null;
this.readerlen = 0;
},
write_eof:function() {
write_eof: function () {
this.flush();
// eof is only relevant for read-until-close readers
if (this.raw && this.reader && this.readerlen === -1) {
this.reader({err:'eof'});
this.reader({ err: 'eof' });
}
},
flush:function() {
flush: function () {
this._doread();
},
close:function() {
if (this.closed)
close: function () {
if (this.closed)
return;
console.trace('Closing file %d: %o', this.n, this);
this.closed = 'closed';
@@ -175,24 +180,24 @@ var new_fd = this.new_fd = function(name, raw) {
remove_fd_from_cache(this);
},
_kickreaders:function() {
_kickreaders: function () {
if (this.duplex)
this.duplex._kickreader();
else
this._kickreader();
},
_kickreader:function() {
if (!this.reader) return;
if (this.kickingreader) return;
var t = this;
t.kickingreader = setTimeout(function() {
t.kickingreader = false;
t._doreadcheckclose();
}, 0);
_kickreader: function () {
if (!this.reader) return;
if (this.kickingreader) return;
var t = this;
t.kickingreader = setTimeout(function () {
t.kickingreader = false;
t._doreadcheckclose();
}, 0);
},
_doreadcheckclose:function() {
_doreadcheckclose: function () {
var cs = this.closed;
this._doread();
if (cs) {
@@ -203,13 +208,13 @@ var new_fd = this.new_fd = function(name, raw) {
this.readerlen = 0;
if (rucreader && rucreadercb) {
// terminate the read-until-close reader
D('terminating ruc reader. fd: %o',this);
rucreadercb({err:'File closed'});
D('terminating ruc reader. fd: %o', this);
rucreadercb({ err: 'File closed' });
}
}
},
_doread:function() {
_doread: function () {
if (this.raw) {
if (!this.reader) return;
if (this.readerlen > this.readpipe.byteLength) return;
@@ -218,7 +223,7 @@ var new_fd = this.new_fd = function(name, raw) {
this.reader = null, this.readerlen = 0;
var data;
if (len) {
var readlen = len>0?len:this.readpipe.byteLength;
var readlen = len > 0 ? len : this.readpipe.byteLength;
data = this.readpipe.subarray(0, readlen);
this.readpipe = this.readpipe.subarray(readlen);
if (this.duplex)
@@ -230,17 +235,17 @@ var new_fd = this.new_fd = function(name, raw) {
data = new Uint8Array(0);
}
data.asString = function() {
data.asString = function () {
return uint8ArrayToString(this);
};
data.intFromHex = function(len) {
len = len||this.byteLength;
var x = this.asString().slice(0,len);
data.intFromHex = function (len) {
len = len || this.byteLength;
var x = this.asString().slice(0, len);
if (!/^[0-9a-fA-F]+/.test(x)) return -1;
return parseInt(x, 16);
}
cb(null, data);
if (len < 0) {
// reset the reader
this.readbytes(len, cb);
@@ -259,103 +264,103 @@ var new_fd = this.new_fd = function(name, raw) {
return fd;
}
var intToCharString = function(n) {
var intToCharString = function (n) {
return String.fromCharCode(
(n>>0)&255,
(n>>8)&255,
(n>>16)&255,
(n>>24)&255
(n >> 0) & 255,
(n >> 8) & 255,
(n >> 16) & 255,
(n >> 24) & 255
);
}
var stringToUint8Array = function(s) {
var stringToUint8Array = function (s) {
var x = new Uint8Array(s.length);
for (var i=0; i < s.length; i++)
for (var i = 0; i < s.length; i++)
x[i] = s.charCodeAt(i);
return x;
}
var uint8ArrayToString = function(a) {
var uint8ArrayToString = function (a) {
var s = new Array(a.byteLength);
for (var i=0; i < a.byteLength; i++)
for (var i = 0; i < a.byteLength; i++)
s[i] = a[i];
return String.fromCharCode.apply(String, s);
}
// asynchronous array iterater
var iterate = function(arr, o) {
var isrange = typeof(arr)==='number';
if (isrange)
arr = { length: arr<0?0:arr };
var iterate = function (arr, o) {
var isrange = typeof (arr) === 'number';
if (isrange)
arr = { length: arr < 0 ? 0 : arr };
var x = {
value:arr,
isrange:isrange,
first:o.first||nofn,
each:o.each||(function() { this.next(); }),
last:o.last||nofn,
success:o.success||nofn,
error:o.error||nofn,
complete:o.complete||nofn,
_idx:0,
_donefirst:false,
_donelast:false,
abort:function(err) {
value: arr,
isrange: isrange,
first: o.first || nofn,
each: o.each || (function () { this.next(); }),
last: o.last || nofn,
success: o.success || nofn,
error: o.error || nofn,
complete: o.complete || nofn,
_idx: 0,
_donefirst: false,
_donelast: false,
abort: function (err) {
this.error(err);
this.complete();
return;
},
finish:function(res) {
finish: function (res) {
// finish early
if (typeof(res)!=='undefined') this.result = res;
this.success(res||this.result);
if (typeof (res) !== 'undefined') this.result = res;
this.success(res || this.result);
this.complete();
return;
},
iteratefirst:function() {
iteratefirst: function () {
if (!this.value.length) {
this.finish();
return;
}
this.first(this.value[this._idx],this._idx,this);
this.each(this.value[this._idx],this._idx,this);
this.first(this.value[this._idx], this._idx, this);
this.each(this.value[this._idx], this._idx, this);
},
iteratenext:function() {
iteratenext: function () {
if (++this._idx >= this.value.length) {
this.last(this.value[this._idx],this._idx,this);
this.last(this.value[this._idx], this._idx, this);
this.finish();
return;
}
this.each(this.value[this._idx],this._idx,this);
this.each(this.value[this._idx], this._idx, this);
},
next:function() {
next: function () {
var t = this;
setTimeout(function() {
t.iteratenext();
},0);
setTimeout(function () {
t.iteratenext();
}, 0);
},
nextorabort:function(err) {
if (err) this.abort(err);
else this.next();
nextorabort: function (err) {
if (err) this.abort(err);
else this.next();
},
};
setTimeout(function() { x.iteratefirst(); }, 0);
setTimeout(function () { x.iteratefirst(); }, 0);
return x;
};
var iterate_repeat = function(arr, count, o, j) {
var iterate_repeat = function (arr, count, o, j) {
iterate(arr, {
each: function(value, i, it) {
o.each(value, i, j||0, it);
each: function (value, i, it) {
o.each(value, i, j || 0, it);
},
success: function() {
success: function () {
if (!--count) {
o.success && o.success();
o.complete && o.complete();
return;
}
iterate_repeat(arr, count, o, (j||0)+1);
iterate_repeat(arr, count, o, (j || 0) + 1);
},
error:function(err) {
error: function (err) {
o.error && o.error();
o.complete && o.complete();
}
@@ -367,13 +372,13 @@ var iterate_repeat = function(arr, count, o, j) {
* @param {ArrayBuffer} buffer The array buffer to convert.
* @return {string} The textual representation of the array.
*/
var arrayBufferToString = exports.arrayBufferToString = function(buffer) {
var array = new Uint8Array(buffer);
var str = '';
for (var i = 0; i < array.length; ++i) {
str += String.fromCharCode(array[i]);
}
return str;
var arrayBufferToString = exports.arrayBufferToString = function (buffer) {
var array = new Uint8Array(buffer);
var str = '';
for (var i = 0; i < array.length; ++i) {
str += String.fromCharCode(array[i]);
}
return str;
};
/**
@@ -381,59 +386,59 @@ var arrayBufferToString = exports.arrayBufferToString = function(buffer) {
* @param {array} UTF-8 array
* @return {string} UTF-8 string
*/
var ary2utf8 = (function() {
var ary2utf8 = (function () {
var patterns = [
{pattern: '0xxxxxxx', bytes: 1},
{pattern: '110xxxxx', bytes: 2},
{pattern: '1110xxxx', bytes: 3},
{pattern: '11110xxx', bytes: 4},
{pattern: '111110xx', bytes: 5},
{pattern: '1111110x', bytes: 6}
];
patterns.forEach(function(item) {
item.header = item.pattern.replace(/[^10]/g, '');
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
item.pattern01 = parseInt(item.pattern01, 2);
item.mask_length = item.header.length;
item.data_length = 8 - item.header.length;
var mask = '';
for (var i = 0, len = item.mask_length; i < len; i++) {
mask += '1';
}
for (var i = 0, len = item.data_length; i < len; i++) {
mask += '0';
}
item.mask = mask;
item.mask = parseInt(item.mask, 2);
});
var patterns = [
{ pattern: '0xxxxxxx', bytes: 1 },
{ pattern: '110xxxxx', bytes: 2 },
{ pattern: '1110xxxx', bytes: 3 },
{ pattern: '11110xxx', bytes: 4 },
{ pattern: '111110xx', bytes: 5 },
{ pattern: '1111110x', bytes: 6 }
];
patterns.forEach(function (item) {
item.header = item.pattern.replace(/[^10]/g, '');
item.pattern01 = item.pattern.replace(/[^10]/g, '0');
item.pattern01 = parseInt(item.pattern01, 2);
item.mask_length = item.header.length;
item.data_length = 8 - item.header.length;
var mask = '';
for (var i = 0, len = item.mask_length; i < len; i++) {
mask += '1';
}
for (var i = 0, len = item.data_length; i < len; i++) {
mask += '0';
}
item.mask = mask;
item.mask = parseInt(item.mask, 2);
});
return function(ary) {
var codes = [];
var cur = 0;
while(cur < ary.length) {
var first = ary[cur];
var pattern = null;
for (var i = 0, len = patterns.length; i < len; i++) {
if ((first & patterns[i].mask) == patterns[i].pattern01) {
pattern = patterns[i];
break;
}
}
if (pattern == null) {
throw 'utf-8 decode error';
}
var rest = ary.slice(cur + 1, cur + pattern.bytes);
cur += pattern.bytes;
var code = '';
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
for (var i = 0, len = rest.length; i < len; i++) {
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
}
codes.push(parseInt(code, 2));
}
return String.fromCharCode.apply(null, codes);
};
return function (ary) {
var codes = [];
var cur = 0;
while (cur < ary.length) {
var first = ary[cur];
var pattern = null;
for (var i = 0, len = patterns.length; i < len; i++) {
if ((first & patterns[i].mask) == patterns[i].pattern01) {
pattern = patterns[i];
break;
}
}
if (pattern == null) {
throw 'utf-8 decode error';
}
var rest = ary.slice(cur + 1, cur + pattern.bytes);
cur += pattern.bytes;
var code = '';
code += ('00000000' + (first & (255 ^ pattern.mask)).toString(2)).slice(-pattern.data_length);
for (var i = 0, len = rest.length; i < len; i++) {
code += ('00000000' + (rest[i] & parseInt('111111', 2)).toString(2)).slice(-6);
}
codes.push(parseInt(code, 2));
}
return String.fromCharCode.apply(null, codes);
};
})();
@@ -442,52 +447,52 @@ var ary2utf8 = (function() {
* @param {string} UTF-8 string
* @return {array} UTF-8 array
*/
var utf82ary = (function() {
var utf82ary = (function () {
var patterns = [
{pattern: '0xxxxxxx', bytes: 1},
{pattern: '110xxxxx', bytes: 2},
{pattern: '1110xxxx', bytes: 3},
{pattern: '11110xxx', bytes: 4},
{pattern: '111110xx', bytes: 5},
{pattern: '1111110x', bytes: 6}
];
patterns.forEach(function(item) {
item.header = item.pattern.replace(/[^10]/g, '');
item.mask_length = item.header.length;
item.data_length = 8 - item.header.length;
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
});
var patterns = [
{ pattern: '0xxxxxxx', bytes: 1 },
{ pattern: '110xxxxx', bytes: 2 },
{ pattern: '1110xxxx', bytes: 3 },
{ pattern: '11110xxx', bytes: 4 },
{ pattern: '111110xx', bytes: 5 },
{ pattern: '1111110x', bytes: 6 }
];
patterns.forEach(function (item) {
item.header = item.pattern.replace(/[^10]/g, '');
item.mask_length = item.header.length;
item.data_length = 8 - item.header.length;
item.max_bit_length = (item.bytes - 1) * 6 + item.data_length;
});
var code2utf8array = function(code) {
var pattern = null;
var code01 = code.toString(2);
for (var i = 0, len = patterns.length; i < len; i++) {
if (code01.length <= patterns[i].max_bit_length) {
pattern = patterns[i];
break;
}
}
if (pattern == null) {
throw 'utf-8 encode error';
}
var ary = [];
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
code01 = code01.slice(0, -6);
}
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
return ary;
};
var code2utf8array = function (code) {
var pattern = null;
var code01 = code.toString(2);
for (var i = 0, len = patterns.length; i < len; i++) {
if (code01.length <= patterns[i].max_bit_length) {
pattern = patterns[i];
break;
}
}
if (pattern == null) {
throw 'utf-8 encode error';
}
var ary = [];
for (var i = 0, len = pattern.bytes - 1; i < len; i++) {
ary.unshift(parseInt('10' + ('000000' + code01.slice(-6)).slice(-6), 2));
code01 = code01.slice(0, -6);
}
ary.unshift(parseInt(pattern.header + ('00000000' + code01).slice(-pattern.data_length), 2));
return ary;
};
return function(str) {
var codes = [];
for (var i = 0, len = str.length; i < len; i++) {
var code = str.charCodeAt(i);
Array.prototype.push.apply(codes, code2utf8array(code));
}
return codes;
};
return function (str) {
var codes = [];
for (var i = 0, len = str.length; i < len; i++) {
var code = str.charCodeAt(i);
Array.prototype.push.apply(codes, code2utf8array(code));
}
return codes;
};
})();
@@ -496,89 +501,88 @@ var utf82ary = (function() {
* @param {string} string The string to convert.
* @return {ArrayBuffer} An array buffer whose bytes correspond to the string.
*/
var stringToArrayBuffer = exports.stringToArrayBuffer = function(string) {
var buffer = new ArrayBuffer(string.length);
var bufferView = new Uint8Array(buffer);
for (var i = 0; i < string.length; i++) {
bufferView[i] = string.charCodeAt(i);
}
return buffer;
var stringToArrayBuffer = exports.stringToArrayBuffer = function (string) {
var buffer = new ArrayBuffer(string.length);
var bufferView = new Uint8Array(buffer);
for (var i = 0; i < string.length; i++) {
bufferView[i] = string.charCodeAt(i);
}
return buffer;
};
var str2ab = exports.str2ab = stringToArrayBuffer;
var ab2str = exports.ab2str = arrayBufferToString;
var str2u8arr = exports.str2u8arr = function(s) {
var str2u8arr = exports.str2u8arr = function (s) {
return new Uint8Array(str2ab(s));
}
exports.getutf8bytes = function(str) {
var utf8 = [];
for (var i=0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
| (str.charCodeAt(i) & 0x3ff));
utf8.push(0xf0 | (charcode >>18),
0x80 | ((charcode>>12) & 0x3f),
0x80 | ((charcode>>6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
return utf8;
}
exports.fromutf8bytes = function(array) {
var out, i, len, c;
var char2, char3;
out = "";
len = array.length;
i = 0;
while(i < len) {
c = array[i++];
switch(c >> 4)
{
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
exports.getutf8bytes = function (str) {
var utf8 = [];
for (var i = 0; i < str.length; i++) {
var charcode = str.charCodeAt(i);
if (charcode < 0x80) utf8.push(charcode);
else if (charcode < 0x800) {
utf8.push(0xc0 | (charcode >> 6),
0x80 | (charcode & 0x3f));
}
}
return out;
else if (charcode < 0xd800 || charcode >= 0xe000) {
utf8.push(0xe0 | (charcode >> 12),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
// surrogate pair
else {
i++;
// UTF-16 encodes 0x10000-0x10FFFF by
// subtracting 0x10000 and splitting the
// 20 bits of 0x0-0xFFFFF into two halves
charcode = 0x10000 + (((charcode & 0x3ff) << 10)
| (str.charCodeAt(i) & 0x3ff));
utf8.push(0xf0 | (charcode >> 18),
0x80 | ((charcode >> 12) & 0x3f),
0x80 | ((charcode >> 6) & 0x3f),
0x80 | (charcode & 0x3f));
}
}
return utf8;
}
exports.arraybuffer_concat = function() {
var bufs=[], total=0;
for (var i=0; i < arguments.length; i++) {
exports.fromutf8bytes = function (array) {
var out, i, len, c;
var char2, char3;
out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
}
exports.arraybuffer_concat = function () {
var bufs = [], total = 0;
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (!a || !a.byteLength) continue;
bufs.push(a);
@@ -589,16 +593,16 @@ exports.arraybuffer_concat = function() {
case 1: return new Uint8Array(bufs[0]);
}
var res = new Uint8Array(total);
for (var i=0, j=0; i < bufs.length; i++) {
for (var i = 0, j = 0; i < bufs.length; i++) {
res.set(bufs[i], j);
j += bufs[i].byteLength;
}
return res;
}
exports.remove_from_list = function(arr, item, searchfn) {
if (!searchfn) searchfn = function(a,b) { return a===b; };
for (var i=0; i < arr.length; i++) {
exports.remove_from_list = function (arr, item, searchfn) {
if (!searchfn) searchfn = function (a, b) { return a === b; };
for (var i = 0; i < arr.length; i++) {
var found = searchfn(arr[i], item);
if (found) {
return {
@@ -610,22 +614,22 @@ exports.remove_from_list = function(arr, item, searchfn) {
D('Object %o not removed from list %o', item, arr);
}
exports.dumparr = function(arr, offset, count) {
offset=offset||0;
count = count||(count===0?0:arr.length);
if (count > arr.length-offset)
count = arr.length-offset;
exports.dumparr = function (arr, offset, count) {
offset = offset || 0;
count = count || (count === 0 ? 0 : arr.length);
if (count > arr.length - offset)
count = arr.length - offset;
var s = '';
while (count--) {
s += ' '+('00'+arr[offset++].toString(16)).slice(-2);
s += ' ' + ('00' + arr[offset++].toString(16)).slice(-2);
}
return s.slice(1);
}
exports.btoa = function(arr) {
return new Buffer(arr,'binary').toString('base64');
exports.btoa = function (arr) {
return new Buffer(arr, 'binary').toString('base64');
}
exports.atob = function(base64) {
return new Buffer(base64, 'base64').toString('binary');
exports.atob = function (base64) {
return new Buffer(base64, 'base64').toString('binary');
}

410
src/variables.js Normal file
View File

@@ -0,0 +1,410 @@
'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,
});
// create the fully qualified names to use for evaluation
fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`);
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, {varinfo})
.then((elements, x) => {
elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`);
x.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, evaluateName = v.fqname || v.name, formats = {}, 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 first 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 = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
formats.oct = formats.bin = '';
// 24 bit chunks...
for (var s=v64hex,uint; s; s = s.slice(0,-6)) {
uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
formats.oct = uint.toString(8) + formats.oct;
formats.bin = uint.toString(2) + formats.bin;
}
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
break;
case /^[BIS]$/.test(v.type.signature):
objvalue = formats.dec = v.value.toString();
var uint = (v.value >>> 0);
formats.hex = '0x' + uint.toString(16);
formats.oct = '0c' + uint.toString(8);
formats.bin = '0b' + uint.toString(2);
break;
default:
// other primitives: 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,
evaluateName,
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;