mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 09:59:25 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494bb83cbf | ||
|
|
9fca5cbe8c | ||
|
|
5f0a02b17f | ||
|
|
da36e8e457 | ||
|
|
3dbfd8ef2a | ||
|
|
4a31b83eb9 | ||
|
|
261c06f1d6 | ||
|
|
130d79f6c2 | ||
|
|
8baf894fc9 | ||
|
|
92bd003122 | ||
|
|
13f116b3b3 | ||
|
|
140e48cbd1 | ||
|
|
7e8f471df4 | ||
|
|
09905eb85a | ||
|
|
e76773e8e4 | ||
|
|
c98c962172 | ||
|
|
b3501d529a | ||
|
|
6f9b2f7e78 | ||
|
|
eab08cc501 | ||
|
|
0b5be3020a | ||
|
|
6451e38bca | ||
|
|
101611841f | ||
|
|
cc02c1b089 | ||
|
|
fdbd5df16b |
@@ -1,5 +1,6 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
images/**/*.gif
|
||||
test/**
|
||||
.gitignore
|
||||
jsconfig.json
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,19 @@
|
||||
# Change Log
|
||||
|
||||
### 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
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
18
package.json
18
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "android-dev-ext",
|
||||
"displayName": "Android",
|
||||
"description": "Android debugging support for VS Code",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"publisher": "adelphes",
|
||||
"preview": true,
|
||||
"license": "MIT",
|
||||
@@ -35,6 +35,9 @@
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "java"
|
||||
},
|
||||
{
|
||||
"language": "kotlin"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
@@ -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",
|
||||
@@ -122,10 +130,10 @@
|
||||
"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.20.0",
|
||||
"vscode-debugadapter": "^1.20.0",
|
||||
"long": "^3.2.0",
|
||||
"ws": "^1.1.1",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "^0.0.23"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
1467
src/debugMain.js
1467
src/debugMain.js
File diff suppressed because it is too large
Load Diff
159
src/debugger.js
159
src/debugger.js
@@ -141,8 +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;
|
||||
},
|
||||
@@ -457,7 +457,6 @@ Debugger.prototype = {
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
this.session.suspendcount++;
|
||||
this._trigger('suspended');
|
||||
});
|
||||
},
|
||||
@@ -479,21 +478,12 @@ Debugger.prototype = {
|
||||
return this.ensureconnected(extra)
|
||||
.then(function (extra) {
|
||||
if (triggers) this._trigger('resuming');
|
||||
const resume_cmd = (decoded,extra) => {
|
||||
this.session.stoppedlocation = null;
|
||||
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;
|
||||
})
|
||||
.then(function (decoded, extra) {
|
||||
if (triggers) this._trigger('resumed');
|
||||
@@ -522,20 +512,20 @@ Debugger.prototype = {
|
||||
.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],
|
||||
@@ -980,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);
|
||||
@@ -1027,17 +1028,6 @@ Debugger.prototype = {
|
||||
.then((typeinfo, method, x) => {
|
||||
x.typeinfo = typeinfo;
|
||||
x.method = method;
|
||||
// in order to invoke the method, we must undo any manual suspends of the specified thread
|
||||
// (and then resuspend after)
|
||||
var def = $.Deferred().resolveWith(this,[null,x]);
|
||||
for (var i=0; i < this.session.threadsuspends[x.threadid]|0; i++) {
|
||||
def = def.then((res,x) => this.session.adbclient.jdwp_command({
|
||||
ths: this, extra:x, cmd: this.JDWP.Commands.resumethread(x.threadid),
|
||||
}));
|
||||
}
|
||||
return def;
|
||||
})
|
||||
.then((res,x) => {
|
||||
return this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
extra: x,
|
||||
@@ -1045,28 +1035,69 @@ Debugger.prototype = {
|
||||
})
|
||||
})
|
||||
.then((res, x) => {
|
||||
// save the result and re-suspend the thread
|
||||
x.res = res;
|
||||
var def = $.Deferred().resolveWith(this,[null,x]);
|
||||
for (var i=0; i < this.session.threadsuspends[x.threadid]|0; i++) {
|
||||
def = def.then((res,x) => this.session.adbclient.jdwp_command({
|
||||
ths: this, extra:x, cmd: this.JDWP.Commands.suspendthread(x.threadid),
|
||||
}));
|
||||
}
|
||||
return def.then((res,x) => $.Deferred().resolveWith(this, [x.res, x]));
|
||||
})
|
||||
.then((res, x) => {
|
||||
// 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,
|
||||
@@ -1282,9 +1313,11 @@ Debugger.prototype = {
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
}
|
||||
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||
if (typeinfo.info.reftype.string !== 'array') {
|
||||
typeinfo.super = null;
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
}
|
||||
}
|
||||
|
||||
typeinfo.super = $.Deferred();
|
||||
this.session.adbclient.jdwp_command({
|
||||
@@ -1413,8 +1446,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]);
|
||||
}
|
||||
};
|
||||
@@ -1437,14 +1468,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;
|
||||
@@ -1468,10 +1497,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();
|
||||
@@ -1498,8 +1529,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) {
|
||||
@@ -1662,8 +1691,6 @@ Debugger.prototype = {
|
||||
dbgr: this,
|
||||
},
|
||||
fn: function (e) {
|
||||
// each exception hit contributes a global suspend
|
||||
e.data.dbgr.session.suspendcount++;
|
||||
// if this exception break occurred during a step request, we must manually clear the event
|
||||
// or the (device-side) debugger will crash on next step
|
||||
this._clearLastStepRequest(e.event.threadid, e).then(e => {
|
||||
@@ -1731,6 +1758,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
535
src/expressions.js
Normal 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
87
src/globals.js
Normal 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
|
||||
});
|
||||
36
src/jdwp.js
36
src/jdwp.js
@@ -371,6 +371,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 +537,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 +1054,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 +1090,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 +1105,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 +1159,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) {
|
||||
|
||||
@@ -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?');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
11
src/state.js
Normal 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
129
src/threads.js
Normal file
@@ -0,0 +1,129 @@
|
||||
'use strict'
|
||||
|
||||
const { AndroidVariables } = require('./variables');
|
||||
const $ = require('./jq-promise');
|
||||
|
||||
/*
|
||||
Class used to manage a single thread reported by JDWP
|
||||
*/
|
||||
class AndroidThread {
|
||||
constructor(session, threadid, vscode_threadid) {
|
||||
// the AndroidDebugSession instance
|
||||
this.session = session;
|
||||
// the Android debugger instance
|
||||
this.dbgr = session.dbgr;
|
||||
// the java thread id (hex string)
|
||||
this.threadid = threadid;
|
||||
// the vscode thread id (number)
|
||||
this.vscode_threadid = vscode_threadid;
|
||||
// the (Java) name of the thread
|
||||
this.name = null;
|
||||
// the thread break info
|
||||
this.paused = null;
|
||||
// the timeout during a step which, if it expires, we allow other threads to break
|
||||
this.stepTimeout = null;
|
||||
}
|
||||
|
||||
threadNotSuspendedError() {
|
||||
return new Error(`Thread ${this.vscode_threadid} not suspended`);
|
||||
}
|
||||
|
||||
addStackFrameVariable(frame, level) {
|
||||
if (!this.paused) throw this.threadNotSuspendedError();
|
||||
var frameId = (this.vscode_threadid * 1e9) + (level * 1e6);
|
||||
var stack_frame_var = {
|
||||
frame, frameId,
|
||||
locals: null,
|
||||
}
|
||||
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
|
||||
}
|
||||
|
||||
allocateExceptionScopeReference(frameId) {
|
||||
if (!this.paused) return;
|
||||
if (!this.paused.last_exception) return;
|
||||
this.paused.last_exception.frameId = frameId;
|
||||
this.paused.last_exception.scopeRef = frameId + 1;
|
||||
}
|
||||
|
||||
getVariables(variablesReference) {
|
||||
if (!this.paused)
|
||||
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
||||
|
||||
// is this reference a stack frame
|
||||
var stack_frame_var = this.paused.stack_frame_vars[variablesReference];
|
||||
if (stack_frame_var) {
|
||||
// frame locals request
|
||||
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref));
|
||||
}
|
||||
|
||||
// is this refrence an exception scope
|
||||
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
|
||||
var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId];
|
||||
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef));
|
||||
}
|
||||
|
||||
// work out which stack frame this reference is for
|
||||
var frameId = Math.trunc(variablesReference/1e6) * 1e6;
|
||||
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
||||
|
||||
return stack_frame_var.locals.getVariables(variablesReference);
|
||||
}
|
||||
|
||||
_ensureLocals(varinfo) {
|
||||
if (!this.paused)
|
||||
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
|
||||
|
||||
// evaluate can call this using frameId as the argument
|
||||
if (typeof varinfo === 'number')
|
||||
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
|
||||
|
||||
// if we're currently processing it (or we've finished), just return the promise
|
||||
if (this.paused.locals_done[varinfo.frameId])
|
||||
return this.paused.locals_done[varinfo.frameId];
|
||||
|
||||
// create a new promise
|
||||
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
|
||||
|
||||
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
|
||||
.then((locals,x) => {
|
||||
// make sure we are still paused...
|
||||
if (!this.paused)
|
||||
throw this.threadNotSuspendedError();
|
||||
|
||||
// sort the locals by name, except for 'this' which always goes first
|
||||
locals.sort((a,b) => {
|
||||
if (a.name === b.name) return 0;
|
||||
if (a.name === 'this') return -1;
|
||||
if (b.name === 'this') return +1;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
|
||||
// create a new local variable with the results and resolve the promise
|
||||
var varinfo = x.varinfo;
|
||||
varinfo.cached = locals;
|
||||
x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others
|
||||
x.varinfo.locals.setVariable(varinfo.frameId, varinfo);
|
||||
|
||||
var last_exception = this.paused.last_exception;
|
||||
if (last_exception) {
|
||||
x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception);
|
||||
}
|
||||
|
||||
x.def.resolveWith(this, [varinfo.frameId]);
|
||||
})
|
||||
.fail(e => {
|
||||
x.def.rejectWith(this, [e]);
|
||||
})
|
||||
return def;
|
||||
}
|
||||
|
||||
setVariableValue(args) {
|
||||
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
|
||||
var stack_frame_var = this.paused.stack_frame_vars[frameId];
|
||||
return this._ensureLocals(stack_frame_var).then(varref => {
|
||||
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.AndroidThread = AndroidThread;
|
||||
283
src/util.js
283
src/util.js
@@ -1,43 +1,43 @@
|
||||
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 () { };
|
||||
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 print_jdwp_data = nofn;// _print_jdwp_data;
|
||||
var print_packet = nofn;//_print_packet;
|
||||
|
||||
Array.first = function(arr, fn, defaultvalue) {
|
||||
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,45 +45,45 @@ 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)
|
||||
@@ -91,7 +91,7 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
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;
|
||||
@@ -106,7 +106,7 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
}
|
||||
},
|
||||
|
||||
readbytes:function(len, cb) {
|
||||
readbytes: function (len, cb) {
|
||||
if (!this.raw)
|
||||
throw 'Cannot readbytes from non-raw fd';
|
||||
if (this.reader)
|
||||
@@ -116,7 +116,7 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
this._kickreader();
|
||||
},
|
||||
|
||||
writebytes:function(buffer) {
|
||||
writebytes: function (buffer) {
|
||||
if (this.closed) {
|
||||
D('Ignoring attempt to write to closed file: %o', this);
|
||||
return;
|
||||
@@ -139,30 +139,30 @@ var new_fd = this.new_fd = function(name, raw) {
|
||||
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() {
|
||||
close: function () {
|
||||
if (this.closed)
|
||||
return;
|
||||
console.trace('Closing file %d: %o', this.n, this);
|
||||
@@ -175,24 +175,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() {
|
||||
_kickreader: function () {
|
||||
if (!this.reader) return;
|
||||
if (this.kickingreader) return;
|
||||
var t = this;
|
||||
t.kickingreader = setTimeout(function() {
|
||||
t.kickingreader = setTimeout(function () {
|
||||
t.kickingreader = false;
|
||||
t._doreadcheckclose();
|
||||
}, 0);
|
||||
},
|
||||
|
||||
_doreadcheckclose:function() {
|
||||
_doreadcheckclose: function () {
|
||||
var cs = this.closed;
|
||||
this._doread();
|
||||
if (cs) {
|
||||
@@ -203,13 +203,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 +218,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,12 +230,12 @@ 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);
|
||||
}
|
||||
@@ -259,103 +259,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';
|
||||
var iterate = function (arr, o) {
|
||||
var isrange = typeof (arr) === 'number';
|
||||
if (isrange)
|
||||
arr = { length: arr<0?0:arr };
|
||||
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() {
|
||||
setTimeout(function () {
|
||||
t.iteratenext();
|
||||
},0);
|
||||
}, 0);
|
||||
},
|
||||
nextorabort:function(err) {
|
||||
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,7 +367,7 @@ 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 arrayBufferToString = exports.arrayBufferToString = function (buffer) {
|
||||
var array = new Uint8Array(buffer);
|
||||
var str = '';
|
||||
for (var i = 0; i < array.length; ++i) {
|
||||
@@ -381,17 +381,17 @@ 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}
|
||||
{ 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) {
|
||||
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);
|
||||
@@ -408,10 +408,10 @@ var ary2utf8 = (function() {
|
||||
item.mask = parseInt(item.mask, 2);
|
||||
});
|
||||
|
||||
return function(ary) {
|
||||
return function (ary) {
|
||||
var codes = [];
|
||||
var cur = 0;
|
||||
while(cur < ary.length) {
|
||||
while (cur < ary.length) {
|
||||
var first = ary[cur];
|
||||
var pattern = null;
|
||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||
@@ -442,24 +442,24 @@ 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}
|
||||
{ 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) {
|
||||
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 code2utf8array = function (code) {
|
||||
var pattern = null;
|
||||
var code01 = code.toString(2);
|
||||
for (var i = 0, len = patterns.length; i < len; i++) {
|
||||
@@ -480,7 +480,7 @@ var utf82ary = (function() {
|
||||
return ary;
|
||||
};
|
||||
|
||||
return function(str) {
|
||||
return function (str) {
|
||||
var codes = [];
|
||||
for (var i = 0, len = str.length; i < len; i++) {
|
||||
var code = str.charCodeAt(i);
|
||||
@@ -496,7 +496,7 @@ 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 stringToArrayBuffer = exports.stringToArrayBuffer = function (string) {
|
||||
var buffer = new ArrayBuffer(string.length);
|
||||
var bufferView = new Uint8Array(buffer);
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
@@ -507,13 +507,13 @@ var stringToArrayBuffer = exports.stringToArrayBuffer = function(string) {
|
||||
|
||||
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) {
|
||||
exports.getutf8bytes = function (str) {
|
||||
var utf8 = [];
|
||||
for (var i=0; i < str.length; i++) {
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var charcode = str.charCodeAt(i);
|
||||
if (charcode < 0x80) utf8.push(charcode);
|
||||
else if (charcode < 0x800) {
|
||||
@@ -522,7 +522,7 @@ exports.getutf8bytes = function(str) {
|
||||
}
|
||||
else if (charcode < 0xd800 || charcode >= 0xe000) {
|
||||
utf8.push(0xe0 | (charcode >> 12),
|
||||
0x80 | ((charcode>>6) & 0x3f),
|
||||
0x80 | ((charcode >> 6) & 0x3f),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
// surrogate pair
|
||||
@@ -531,28 +531,27 @@ exports.getutf8bytes = function(str) {
|
||||
// UTF-16 encodes 0x10000-0x10FFFF by
|
||||
// subtracting 0x10000 and splitting the
|
||||
// 20 bits of 0x0-0xFFFFF into two halves
|
||||
charcode = 0x10000 + (((charcode & 0x3ff)<<10)
|
||||
charcode = 0x10000 + (((charcode & 0x3ff) << 10)
|
||||
| (str.charCodeAt(i) & 0x3ff));
|
||||
utf8.push(0xf0 | (charcode >>18),
|
||||
0x80 | ((charcode>>12) & 0x3f),
|
||||
0x80 | ((charcode>>6) & 0x3f),
|
||||
utf8.push(0xf0 | (charcode >> 18),
|
||||
0x80 | ((charcode >> 12) & 0x3f),
|
||||
0x80 | ((charcode >> 6) & 0x3f),
|
||||
0x80 | (charcode & 0x3f));
|
||||
}
|
||||
}
|
||||
return utf8;
|
||||
}
|
||||
|
||||
exports.fromutf8bytes = function(array) {
|
||||
exports.fromutf8bytes = function (array) {
|
||||
var out, i, len, c;
|
||||
var char2, char3;
|
||||
|
||||
out = "";
|
||||
len = array.length;
|
||||
i = 0;
|
||||
while(i < len) {
|
||||
while (i < len) {
|
||||
c = array[i++];
|
||||
switch(c >> 4)
|
||||
{
|
||||
switch (c >> 4) {
|
||||
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
||||
// 0xxxxxxx
|
||||
out += String.fromCharCode(c);
|
||||
@@ -576,9 +575,9 @@ exports.fromutf8bytes = function(array) {
|
||||
return out;
|
||||
}
|
||||
|
||||
exports.arraybuffer_concat = function() {
|
||||
var bufs=[], total=0;
|
||||
for (var i=0; i < arguments.length; i++) {
|
||||
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 +588,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 +609,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) {
|
||||
exports.atob = function (base64) {
|
||||
return new Buffer(base64, 'base64').toString('binary');
|
||||
}
|
||||
|
||||
410
src/variables.js
Normal file
410
src/variables.js
Normal 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;
|
||||
Reference in New Issue
Block a user