35 Commits

Author SHA1 Message Date
adelphes
c98c962172 version 0.4.1 2017-03-02 17:30:04 +00:00
adelphes
b3501d529a version 0.4.0 changelog 2017-03-02 17:21:56 +00:00
adelphes
6f9b2f7e78 version 0.4.0 2017-03-02 17:17:46 +00:00
adelphes
eab08cc501 Switch local demo gif link to github 2017-03-02 17:05:18 +00:00
adelphes
0b5be3020a Fixed exception details not being displayed
Make sure the exception scoperef is allocated before locals are retrieved.
2017-03-02 16:12:27 +00:00
adelphes
6451e38bca remove reference to $.noop 2017-02-12 13:43:39 +00:00
adelphes
101611841f Fix logcat lines issue and added count up display. 2017-02-05 23:28:02 +00:00
adelphes
cc02c1b089 added callStackDisplaySize config setting 2017-02-05 21:36:28 +00:00
adelphes
fdbd5df16b Improvements to multi-threaded debugging
Separate out thread-specific parts
Only pause event thread for step, bp and thread events
Continue now resumes the specified thread instead of all threads
Prioritise stepping thread to prevent context switching during step
Monitor thread starts/ends
2017-02-05 19:34:12 +00:00
adelphes
baa3fb6bfd version 0.3.1
bug fix release
2017-02-02 20:13:37 +00:00
adelphes
3cda2e5f1f Fix issue with exception breaks hanging
Temporarily resume the thread used to invoke toString() and re-suspend after.
2017-02-02 20:04:04 +00:00
adelphes
dfed765d21 Fix issue with crash on exception break 2017-02-02 19:19:20 +00:00
adelphes
b60ef65803 Fix issue with Android source not displaying
VSCode 1.9 prioritises srcref over path - we now set the ref to 0 if a source path is set.
2017-02-02 19:18:54 +00:00
adelphes
214a48b1e1 version 0.3 2017-02-01 18:51:19 +00:00
adelphes
d3ff22395e Fix breakpoint failures on windows
Make sure relative_fpn uses forward slashes as path delimiters
2017-02-01 18:41:25 +00:00
adelphes
ac0b29fb15 Auto-start ADB for logcat views 2017-02-01 12:44:36 +00:00
adelphes
a47de40088 More configuration settings
Added autoStartADB and logcatPort  launch config settings
Moved AndroidContentProvider into it's own file.
2017-02-01 12:22:25 +00:00
adelphes
e23fc698a2 auto-start ADB if it's not running 2017-02-01 11:04:40 +00:00
adelphes
d324990e28 Improved multi-threaded debug support
Include thread names in display
Use a thread id mapping for vscode to fix problems with Android reusing thread ids
Stepping only resumes the paused thread, Continue resumes all.
2017-02-01 10:18:04 +00:00
adelphes
7b670b36ad Improve breakpoint searching
Search for inner types when identifying breakpoint locations
2017-02-01 10:13:02 +00:00
adelphes
d1ab9a8339 Android debugger class enhancements
Support for thread info - name and status
Reference count global suspends to allow correct resuming when multiple events are triggered.
Added methods for suspend/resume individual threads
2017-02-01 10:10:22 +00:00
adelphes
ab68b83900 jq promise fixes
return promise in always()
allow when() to accept a single array of arguments
2017-02-01 09:59:46 +00:00
adelphes
a677d636f3 Fix #17
Clear last step request on exception break
2017-01-31 15:00:45 +00:00
adelphes
71fcf1b760 Code tidy
Prevent multiple StoppedEvents being sent when debugger reports multiple events (bp,step,exception) at the same location.
2017-01-31 14:18:19 +00:00
adelphes
2727dd1b0c Refactored setBreakPointsRequest
Breakpoints are now updated using an async queue
Allow source locations inside ANDROID_HOME
Fixes #14
2017-01-31 13:40:54 +00:00
adelphes
585b8cca29 Improve logcat support
Move html into a separate resource file.
Implement regex filtering and logcat clear.
Fixes #7
2017-01-30 18:51:03 +00:00
adelphes
b27d972613 Improve evaluation support
Allow arithmetic, bitwise, logical and relational operators and primitive casts.
2017-01-29 20:25:21 +00:00
adelphes
ec38695bcc include type info when reporting missing fields
Store character primitives as a uint16 integer with a separate char field.
2017-01-29 19:06:32 +00:00
adelphes
88c20d6470 implement a more reliable evaluation queue 2017-01-28 19:12:35 +00:00
adelphes
6f3f5eee74 Improve expression parsing
Allow ':super' to be used as a member.
Search super types when locating field members.
Fixes #16
2017-01-28 15:54:26 +00:00
adelphes
6463558034 Fix issues with expressions not completing
Moved NumberBaseConverter into its own file
2017-01-28 14:12:11 +00:00
adelphes
1b7fb3d60a first pass at hit-count breakpoints 2017-01-27 13:59:36 +00:00
adelphes
2f93ecc16f We only need platform-tools for adb 2017-01-27 10:19:50 +00:00
adelphes
3e1a8dce37 bump version 2017-01-26 17:59:01 +00:00
adelphes
6bb8047db6 Stop the Android runtime from barfing into logcat
Split field retrieval into statics and instances
2017-01-26 17:58:37 +00:00
17 changed files with 2348 additions and 983 deletions

View File

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

View File

@@ -1,5 +1,28 @@
# Change Log
### version 0.4.1
* One day I will learn to update the changelog **before** I hit publish
* Updated changelog
### version 0.4.0
* Debugger performance improvements
* Fixed exception details not being displayed in locals
* Fixed some logcat display issues
### version 0.3.1
* Bug fixes
* Fix issue with exception breaks crashing debugger
* Fix issue with Android sources not displaying in VSCode 1.9
## version 0.3.0
* Support for Logcat filtering using regular expressions
* Improved expression parsing with support for arithmetic, bitwise, logical and relational operators
* Multi-threaded debugging support (experimental)
* Hit count breakpoints
* Android source breakpoints
* Automatic adb server start
* Bug fixes
## version 0.2.0
* Support for Logcat viewing [ Command Palette -> Android: View Logcat ]
* Support for modifying local variables, object fields and array elements (literal values only)

View File

@@ -12,7 +12,7 @@ This is a preview version of the Android for VS Code Extension. The extension al
## Requirements
You must have [Android SDK Tools](https://developer.android.com/studio/releases/sdk-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
You must have [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
> You are not required to have Android Studio installed - if you have Android Studio installed, make sure there are no active instances of it when using this extension or you may run into problems with ADB.
## Limitations
@@ -58,4 +58,4 @@ The following settings are used to configure the debugger:
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
![Launch Android App](images/demo.gif)
![Launch Android App](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/demo.gif)

View File

@@ -1,20 +1,15 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode');
const { AndroidContentProvider, openLogcatWindow } = require('./src/logcat');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
function getADBPort() {
var adbPort = 5037;
// there's surely got to be a better way than this...
var configs = vscode.workspace.getConfiguration('launch.configurations');
for (var i=0,config; config=configs.get(''+i); i++) {
if (config.type!=='android') continue;
if (config.request!=='launch') continue;
if (typeof config.adbPort === 'number' && config.adbPort === (config.adbPort|0))
adbPort = config.adbPort;
break;
}
var defaultPort = 5037;
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
return adbPort;
return defaultPort;
}
// this method is called when your extension is activated

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "0.2.0",
"version": "0.4.1",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -66,6 +66,21 @@
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
"default": 5037
},
"autoStartADB": {
"type": "boolean",
"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",
"default": 7038
},
"staleBuild": {
"type": "string",
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
@@ -114,7 +129,8 @@
"dependencies": {
"vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0",
"ws":"^1.1.1",
"long": "^3.2.0",
"ws": "^1.1.1",
"xmldom": "^0.1.27",
"xpath": "^0.0.23"
},

View File

@@ -87,6 +87,29 @@ ADBClient.prototype = {
});
},
test_adb_connection : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [null, x.o.extra]);
})
.fail(function(err) {
// if we fail, still resolve the deferred, passing the error
x.deferred.resolveWith(x.o.ths||this, [err, x.o.extra]);
});
return x.deferred;
},
list_devices : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
@@ -380,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,
}

78
src/contentprovider.js Normal file
View File

@@ -0,0 +1,78 @@
'use strict'
// vscode stuff
const { workspace, EventEmitter, Uri } = require('vscode');
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
constructor() {
this._docs = {}; // hashmap<url, LogcatContent>
this._onDidChange = new EventEmitter();
}
dispose() {
this._onDidChange.dispose();
}
/**
* An event to signal a resource has changed.
*/
get onDidChange() {
return this._onDidChange.event;
}
/**
* Provide textual content for a given uri.
*
* The editor will use the returned string-content to create a readonly
* [document](TextDocument). Resources allocated should be released when
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
*
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
* @param token A cancellation token.
* @return A string or a thenable that resolves to such.
*/
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
var doc = this._docs[uri];
if (doc) return this._docs[uri].content;
switch (uri.authority) {
// android-dev-ext://logcat/read?<deviceid>
case 'logcat': return this.provideLogcatDocumentContent(uri);
}
throw new Error('Document Uri not recognised');
}
provideLogcatDocumentContent(uri) {
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
const { LogcatContent } = require('./logcat');
var doc = this._docs[uri] = new LogcatContent(this, uri);
return doc.content;
}
}
// the statics
AndroidContentProvider.SCHEME = 'android-dev-ext';
AndroidContentProvider.register = (ctx, workspace) => {
var provider = new AndroidContentProvider();
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
ctx.subscriptions.push(registration);
ctx.subscriptions.push(provider);
}
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
return uri.with({
query: deviceId
});
}
AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => {
// there's surely got to be a better way than this...
var configs = workspace.getConfiguration('launch.configurations');
for (var i=0,config; config=configs.get(''+i); i++) {
if (config.type!=='android') continue;
if (config.request!=='launch') continue;
if (config[name]) return config[name];
break;
}
return defvalue;
}
exports.AndroidContentProvider = AndroidContentProvider;

File diff suppressed because it is too large Load Diff

View File

@@ -137,9 +137,12 @@ Debugger.prototype = {
adbclient: null,
stoppedlocation: null,
classes: {},
// classprepare notifier done
cpndone: false,
// classprepare filters
cpfilters: [],
preparedclasses: [],
stepids: {}, // hashmap<threadid,stepid>
threadsuspends: [], // hashmap<threadid, suspend-count>
invokes: {}, // hashmap<threadid, deferred>
}
return this;
},
@@ -414,6 +417,35 @@ Debugger.prototype = {
});
},
threadinfos: function(thread_ids, extra) {
if (!Array.isArray(thread_ids))
thread_ids = [thread_ids];
var o = {
dbgr: this, thread_ids, extra, threadinfos:[], idx:0,
next() {
var thread_id = this.thread_ids[this.idx];
if (typeof(thread_id) === 'undefined')
return $.Deferred().resolveWith(this.dbgr, [this.threadinfos, this.extra]);
var info = {
threadid: thread_id,
name:'',
status:null,
};
return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadname(info.threadid) })
.then((name,info) => {
info.name = name;
return this.dbgr.session.adbclient.jdwp_command({ ths:this.dbgr, extra:info, cmd:this.dbgr.JDWP.Commands.threadstatus(info.threadid) })
})
.then((status, info) => {
info.status = status;
this.threadinfos.push(info);
})
.always(() => (this.idx++,this.next()))
}
};
return this.ensureconnected(o).then(o => o.next());
},
suspend: function (extra) {
return this.ensureconnected(extra)
.then(function (extra) {
@@ -429,10 +461,23 @@ Debugger.prototype = {
});
},
resume: function (extra) {
suspendthread: function (threadid, extra) {
return this.ensureconnected({threadid,extra})
.then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) + 1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x.extra,
cmd: this.JDWP.Commands.suspendthread(x.threadid),
});
})
.then((res,extra) => extra);
},
_resume:function(triggers, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
this._trigger('resuming');
if (triggers) this._trigger('resuming');
this.session.stoppedlocation = null;
return this.session.adbclient.jdwp_command({
ths: this,
@@ -441,32 +486,41 @@ Debugger.prototype = {
});
})
.then(function (decoded, extra) {
this._trigger('resumed');
if (triggers) this._trigger('resumed');
return extra;
});
},
_resumesilent: function () {
return this.ensureconnected()
.then(function () {
this.session.stoppedlocation = null;
return this.session.adbclient.jdwp_command({
ths: this,
//extra: extra,
cmd: this.JDWP.Commands.resume(),
});
});
resume: function (extra) {
return this._resume(true, extra);
},
step: function (steptype, threadid) {
var x = { steptype: steptype, threadid: threadid };
_resumesilent: function () {
return this._resume(false);
},
resumethread: function (threadid, extra) {
return this.ensureconnected({threadid,extra})
.then(function (x) {
this.session.threadsuspends[x.threadid] = (this.session.threadsuspends[x.threadid]|0) - 1;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x.extra,
cmd: this.JDWP.Commands.resumethread(x.threadid),
});
})
.then((res,extra) => extra);
},
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);
});
},
@@ -498,11 +552,11 @@ Debugger.prototype = {
return this.breakpoints.all.slice();
},
setbreakpoint: function (srcfpn, line) {
setbreakpoint: function (srcfpn, line, conditions) {
var cls = this._splitsrcfpn(srcfpn);
var bid = cls.qtype + ':' + line;
var newbp = this.breakpoints.bysrcloc[bid];
if (newbp) return newbp;
if (newbp) return $.Deferred().resolveWith(this, [newbp]);
newbp = {
id: bid,
srcfpn: srcfpn,
@@ -510,8 +564,11 @@ Debugger.prototype = {
pkg: cls.pkg,
type: cls.type,
linenum: line,
conditions: Object.assign({},conditions),
sigpattern: new RegExp('^L' + cls.qtype + '([$][$a-zA-Z0-9_]+)?;$'),
state: 'set'// set,notloaded,enabled,removed
state: 'set', // set,notloaded,enabled,removed
hitcount: 0, // number of times this bp was hit during execution
stopcount: 0. // number of times this bp caused a break into the debugger
};
this.breakpoints.all.push(newbp);
this.breakpoints.bysrcloc[bid] = newbp;
@@ -519,25 +576,40 @@ Debugger.prototype = {
// what happens next depends upon what state we are in
switch (this.status()) {
case 'connected':
//this._changebpstate([newbp], 'set');
//this._changebpstate([newbp], 'notloaded');
newbp.state = 'notloaded';
if (this.session.cpndone) {
var bploc = this._findbplocation(this.session.classes, newbp);
if (bploc) {
this._setupbreakpointsevent([bploc]);
// try and load the class - if the runtime hasn't loaded it yet, this will just return an empty classes object
return this._loadclzinfo('L'+newbp.qtype+';')
.then(classes => {
var bploc = this._findbplocation(classes, newbp);
if (!bploc) {
// the required location may be inside a nested class (anonymous or named)
// Since Android doesn't support the NestedTypes JDWP call (ffs), all we can do here
// is look for existing (cached) loaded types matching inner type signatures
for (var sig in this.session.classes) {
if (newbp.sigpattern.test(sig))
classes[sig] = this.session.classes[sig];
}
// try again
bploc = this._findbplocation(classes, newbp);
}
break;
if (!bploc) {
// we couldn't identify a matching location - either the class is not yet loaded or the
// location doesn't correspond to any code. In case it's the former, make sure we are notified
// when classes in this package are loaded
return this._ensureClassPrepareForPackage(newbp.pkg);
}
// we found a matching location - set the breakpoint event
return this._setupbreakpointsevent([bploc]);
})
.then(() => newbp)
case 'connecting':
case 'disconnected':
default:
//this._changebpstate([newbp], 'set');
newbp.state = 'set';
break;
}
return newbp;
return $.Deferred().resolveWith(this, [newbp]);
},
clearbreakpoint: function (srcfpn, line) {
@@ -709,6 +781,8 @@ Debugger.prototype = {
},
getsupertype: function (local, extra) {
if (local.type.signature==='Ljava/lang/Object;')
return $.Deferred().rejectWith(this,[new Error('java.lang.Object has no super type')]);
return this.gettypedebuginfo(local.type.signature, { local: local, extra: extra })
.then(function (dbgtype, x) {
return this._ensuresuper(dbgtype[x.local.type.signature])
@@ -718,6 +792,15 @@ Debugger.prototype = {
});
},
getsuperinstance: function (local, extra) {
return this.getsupertype(local, {local,extra})
.then(function (supertypeinfo, x) {
var castobj = Object.assign({}, x.local);
castobj.type = supertypeinfo;
return $.Deferred().resolveWith(this, [castobj, x.extra]);
});
},
createstring: function (string, extra) {
return this.ensureconnected({ string: string, extra: extra })
.then(function (x) {
@@ -794,14 +877,38 @@ Debugger.prototype = {
})
.then(function (typeinfo, x) {
x.typeinfo = typeinfo;
// the Android runtime now pointlessly barfs into logcat if an instance value is used
// to retrieve a static field. So, we now split into two calls...
x.splitfields = typeinfo.fields.reduce((z,f) => {
if (f.modbits & 8) z.static.push(f); else z.instance.push(f);
return z;
}, {instance:[],static:[]});
// if there are no instance fields, just resolve with an empty array
if (!x.splitfields.instance.length)
return $.Deferred().resolveWith(this,[[], x]);
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, typeinfo.fields),
cmd: this.JDWP.Commands.GetFieldValues(x.objvar.value, x.splitfields.instance),
});
})
.then(function (fieldvalues, x) {
return this._mapvalues('field', x.typeinfo.fields, fieldvalues, { objvar: x.objvar }, x);
.then(function (instance_fieldvalues, x) {
x.instance_fieldvalues = instance_fieldvalues;
// and now the statics (with a type reference)
if (!x.splitfields.static.length)
return $.Deferred().resolveWith(this,[[], x]);
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.GetStaticFieldValues(x.splitfields.static[0].typeid, x.splitfields.static),
});
})
.then(function (static_fieldvalues, x) {
x.static_fieldvalues = static_fieldvalues;
// make sure the fields and values match up...
var fields = x.splitfields.instance.concat(x.splitfields.static);
var values = x.instance_fieldvalues.concat(x.static_fieldvalues);
return this._mapvalues('field', fields, values, { objvar: x.objvar }, x);
})
.then(function (res, x) {
for (var i = 0; i < res.length; i++) {
@@ -811,6 +918,27 @@ Debugger.prototype = {
});
},
getFieldValue: function(objvar, fieldname, includeInherited, extra) {
const findfield = x => {
return this.getfieldvalues(x.objvar, x)
.then((fields, x) => {
var field = fields.find(f => f.name === x.fieldname);
if (field) return $.Deferred().resolveWith(this,[field,x.extra]);
if (!x.includeInherited || x.objvar.type.signature==='Ljava/lang/Object;') {
var fqtname = [x.reqtype.package,x.reqtype.typename].join('.');
return $.Deferred().rejectWith(this,[new Error(`No such field '${x.fieldname}' in type ${fqtname}`), x.extra]);
}
// search supertype
return this.getsuperinstance(x.objvar, x)
.then((superobjvar,x) => {
x.objvar = superobjvar;
return x.findfield(x);
});
});
}
return findfield({findfield, objvar, fieldname, includeInherited, extra, reqtype:objvar.type});
},
getExceptionLocal: function (ex_ref_value, extra) {
var x = {
ex_ref_value: ex_ref_value,
@@ -842,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);
@@ -887,18 +1026,28 @@ Debugger.prototype = {
return o.def;
})
.then((typeinfo, method, x) => {
x.typeinfo = typeinfo;
x.method = method;
return this.session.adbclient.jdwp_command({
ths: this,
extra: x,
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, typeinfo.info.typeid, method.methodid, x.args),
});
cmd: this.JDWP.Commands.InvokeMethod(x.objectid, x.threadid, x.typeinfo.info.typeid, x.method.methodid, x.args),
})
})
.then((res, x) => {
// res = {return_value, exception}
if (/^0+$/.test(res.exception))
return this._mapvalues('return', [{ name:'{return}', type:x.return_type }], [res.return_value], {}, x);
// todo - handle reutrn exceptions
})
.then((res, x) => $.Deferred().resolveWith(this, [res[0], x.extra])); // res = {return_value, exception}
.then((res, x) => {
x.def.resolveWith(this, [res[0], x.extra]);
})
.always(function(invokes) {
invokes.shift();
if (invokes.length)
this._doInvokeMethod(invokes[0]);
}.bind(this,this.session.invokes[x.threadid]));
},
invokeToString(objectid, threadid, type_signature, extra) {
@@ -1003,6 +1152,8 @@ Debugger.prototype = {
arrayfields.push(info);
else if (keys[i].type.signature === 'Ljava/lang/String;')
stringfields.push(info);
else if (keys[i].type.signature === 'C')
info.char = info.valid ? String.fromCodePoint(info.value) : '';
i++;
}
}
@@ -1259,16 +1410,26 @@ Debugger.prototype = {
return cmd.promise();
},
_setupstepevent: function (steptype, threadid) {
_clearLastStepRequest: function (threadid, extra) {
if (!this.session || !this.session.stepids[threadid])
return $.Deferred().resolveWith(this,[extra]);
var clearStepCommand = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.ClearStep(this.session.stepids[threadid]),
extra: extra,
}).then((decoded, extra) => extra);
this.session.stepids[threadid] = 0;
return clearStepCommand;
},
_setupstepevent: function (steptype, threadid, extra) {
var onevent = {
data: {
dbgr: this,
},
fn: function (e) {
e.data.dbgr.session.adbclient.jdwp_command({
cmd: e.data.dbgr.JDWP.Commands.ClearStep(e.event.reqid),
})
.then(function () {
e.data.dbgr._clearLastStepRequest(e.event.threadid, e)
.then(function (e) {
var x = e.data;
var loc = e.event.location;
@@ -1290,6 +1451,12 @@ Debugger.prototype = {
};
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
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();
@@ -1310,13 +1477,25 @@ Debugger.prototype = {
linenum: bp.linenum,
threadid: e.event.threadid
};
var eventdata = {
event: e.event,
stoppedlocation: stoppedloc,
bp: x.dbgr.breakpoints.enabled[cmlkey].bp,
};
x.dbgr.session.stoppedlocation = stoppedloc;
// 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) {
bp.hitcount += bp.conditions.hitcount;
delete bp.conditions.hitcount;
var bploc = x.dbgr.breakpoints.enabled[cmlkey].bploc;
x.dbgr.session.adbclient.jdwp_command({
cmd: x.dbgr.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, null, onevent),
});
} else {
bp.hitcount++;
}
bp.stopcount++;
x.dbgr._trigger('bphit', eventdata);
}
};
@@ -1331,11 +1510,12 @@ Debugger.prototype = {
cmlkeys.push(cmlkey);
this.breakpoints.enabled[cmlkey] = {
bp: bploc.bp,
bploc: {c:bploc.c,m:bploc.m,l:bploc.l},
requestid: null,
};
bparr.push(bploc.bp);
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, onevent),
cmd: this.JDWP.Commands.SetBreakpoint(bploc.c, bploc.m, bploc.l, bploc.bp.conditions.hitcount, onevent),
});
setbpcmds.push(cmd);
}
@@ -1382,29 +1562,28 @@ Debugger.prototype = {
_initbreakpoints: function () {
var deferreds = [{ dbgr: this }];
var donetypes = {};
// reset any current associations
this.breakpoints.enabled = {};
// set all the breakpoints to the notloaded state
this._changebpstate(this.breakpoints.all, 'notloaded');
// setup class prepare notifications for all the current packages
// setup class prepare notifications for all the packages associated with breakpoints
// when each class is prepared, we initialise any breakpoints for it
for (var pkg in this.session.build.packages) {
try {
var def = this._setupclassprepareevent(pkg + '.*', _onclassprepared);
deferreds.push(def);
} catch (e) {
D('Ignoring additional class prepared notification for: ' + preppedclass.type.signature);
}
}
var cpdefs = this.breakpoints.all.map(bp => this._ensureClassPrepareForPackage(bp.pkg));
deferreds = deferreds.concat(cpdefs);
return $.when.apply($, deferreds).then(function (x) {
x.dbgr.session.cpndone = true;
return $.Deferred().resolveWith(x.dbgr);
});
},
function _onclassprepared(preppedclass) {
_ensureClassPrepareForPackage: function(pkg) {
var filter = pkg + '.*';
if (this.session.cpfilters.includes(filter))
return $.Deferred().resolveWith(this,[]); // already setup
this.session.cpfilters.push(filter);
return this._setupclassprepareevent(filter, preppedclass => {
// if the class prepare events have overlapping packages (mypackage.*, mypackage.another.*), we will get
// multiple notifications (which duplicates breakpoints, etc)
if (this.session.preparedclasses.includes(preppedclass.type.signature)) {
@@ -1428,6 +1607,7 @@ Debugger.prototype = {
bplocs.push(bploc);
}
}
if (!bplocs.length) return;
// set all the breakpoints in one go...
return this._setupbreakpointsevent(bplocs);
})
@@ -1435,7 +1615,7 @@ Debugger.prototype = {
// when all the breakpoints for the newly-prepared type have been set...
this._resumesilent();
});
}
});
},
clearBreakOnExceptions: function(extra) {
@@ -1465,6 +1645,9 @@ Debugger.prototype = {
dbgr: this,
},
fn: function (e) {
// 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 => {
this._findcmllocation(this.session.classes, e.event.throwlocation)
.then(tloc => {
this._findcmllocation(this.session.classes, e.event.catchlocation)
@@ -1478,6 +1661,7 @@ Debugger.prototype = {
this._trigger('exception', eventdata);
})
})
});
}.bind(this)
};
@@ -1528,6 +1712,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) {

71
src/globals.js Normal file
View File

@@ -0,0 +1,71 @@
'use strict'
const path = require('path');
// some commonly used Java types in debugger-compatible format
const JTYPES = {
byte: {typename:'byte',signature:'B'},
short: {typename:'short',signature:'S'},
int: {typename:'int',signature:'I'},
long: {typename:'long',signature:'J'},
float: {typename:'float',signature:'F'},
double: {typename:'double',signature:'D'},
char: {typename:'char',signature:'C'},
boolean: {typename:'boolean',signature:'Z'},
null: {typename:'null',signature:'Lnull;'}, // null has no type really, but we need something for literals
String: {typename:'String',signature:'Ljava/lang/String;'},
Object: {typename:'Object',signature:'Ljava/lang/Object;'},
isArray(t) { return t.signature[0]==='[' },
isObject(t) { return t.signature[0]==='L' },
isReference(t) { return /^[L[]/.test(t.signature) },
isPrimitive(t) { return !JTYPES.isReference(t.signature) },
isInteger(t) { return /^[BCIJS]$/.test(t.signature) },
isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) },
isString(t) { return t.signature === this.String.signature },
isChar(t) { return t.signature === this.char.signature },
isBoolean(t) { return t.signature === this.boolean.signature },
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
}
// the special name given to exception message fields
const exmsg_var_name = ':msg';
function createJavaString(dbgr, s, opts) {
const raw = (opts && opts.israw) ? s : s.slice(1,-1).replace(/\\u[0-9a-fA-F]{4}|\\./,decode_char);
// return a deferred, which resolves to a local variable named 'literal'
return dbgr.createstring(raw);
}
function decode_char(c) {
switch(true) {
case /^\\[^u]$/.test(c):
// backslash escape
var x = {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v','0':String.fromCharCode(0)}[c[1]];
return x || c[1];
case /^\\u[0-9a-fA-F]{4}$/.test(c):
// unicode escape
return String.fromCharCode(parseInt(c.slice(2),16));
case c.length===1 :
return c;
}
throw new Error('Invalid character value');
}
function ensure_path_end_slash(p) {
return p + (/[\\/]$/.test(p) ? '' : path.sep);
}
function is_subpath_of(fpn, subpath) {
if (!subpath || !fpn) return false;
subpath = ensure_path_end_slash(''+subpath);
return fpn.slice(0,subpath.length) === subpath;
}
function variableRefToThreadId(variablesReference) {
return (variablesReference / 1e9)|0;
}
Object.assign(exports, {
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString
});

View File

@@ -169,7 +169,7 @@ function _JDWP() {
return i<32768?i:i-65536;
},
decodeChar: function(o) {
return String.fromCharCode((o.data[o.idx++]<<8)+o.data[o.idx++]);
return (o.data[o.idx++]<<8)+o.data[o.idx++]; // uint16
},
decodeBoolean: function(o) {
return o.data[o.idx++] != 0;
@@ -250,6 +250,12 @@ function _JDWP() {
decodeStatus : function(o) {
return this.mapflags(this.decodeInt(o), ['verified','prepared','initialized','error']);
},
decodeThreadStatus : function(o) {
return ['zombie','running','sleeping','monitor','wait'][this.decodeInt(o)] || '';
},
decodeSuspendStatus : function(o) {
return this.decodeInt(o) ? 'suspended': '';
},
decodeTaggedObjectID : function(o) {
return this.decodeValue(o);
},
@@ -365,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);
@@ -627,6 +639,28 @@ function _JDWP() {
}
);
},
GetStaticFieldValues:function(typeid, fields) {
return new Command('GetStaticFieldValues:'+typeid, 2, 6,
function() {
var res=[];
DataCoder.encodeRef(res, typeid);
DataCoder.encodeInt(res, fields.length);
for (var i in fields) {
DataCoder.encodeRef(res, fields[i].fieldid);
}
return res;
},
function(o) {
var res = [];
var arrlen = DataCoder.decodeInt(o);
while (--arrlen>=0) {
var v = DataCoder.decodeValue(o);
res.push(v);
}
return res;
}
);
},
sourcefile:function(ci) {
return new Command('SourceFile:'+ci.name, 2, 7,
function() {
@@ -650,7 +684,9 @@ function _JDWP() {
var arrlen = DataCoder.decodeInt(o);
var res = [];
while (--arrlen>=0) {
res.push(DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{type:'signature'},{genericsig:'string'},{modbits:'int'}]));
var field = DataCoder.decodeList(o, [{fieldid:'fref'},{name:'string'},{type:'signature'},{genericsig:'string'},{modbits:'int'}]);
field.typeid = ci.info.typeid;
res.push(field);
}
return res;
}
@@ -697,6 +733,7 @@ function _JDWP() {
}
);
},
// nestedTypes is not implemented on android
nestedTypes:function(ci) {
return new Command('NestedTypes:'+ci.name, 2, 8,
function() {
@@ -709,7 +746,7 @@ function _JDWP() {
var arrlen = DataCoder.decodeInt(o);
while (--arrlen>=0) {
var v = DataCoder.decodeList(o, [{reftype:'reftype'},{typeid:'tref'}]);
res.vars.push(v);
res.push(v);
}
return res;
}
@@ -1017,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);
@@ -1027,21 +1064,35 @@ function _JDWP() {
onevent
);
},
SetBreakpoint:function(ci, mi, idx, onevent) {
SetBreakpoint:function(ci, mi, idx, hitcount, onevent) {
// a wrapper around SetEventRequest
var mods = [{
modkind:7, // location
loc:{ type:ci.info.reftype.value, cid:ci.info.typeid, mid:mi.methodid, idx:idx }
loc:{ type:ci.info.reftype.value, cid:ci.info.typeid, mid:mi.methodid, idx:idx },
encode(res) {
res.push(this.modkind);
res.push(this.loc.type);
DataCoder.encodeRef(res, this.loc.cid);
DataCoder.encodeRef(res, this.loc.mid);
DataCoder.encodeLong(res, this.loc.idx);
}
}];
if (hitcount > 0) {
// remember when setting a hitcount, the event is automatically cancelled after being fired
mods.unshift({
modkind:1,
count: hitcount,
encode(res) {
res.push(this.modkind);
DataCoder.encodeInt(res, this.count);
}
})
}
// kind(2=breakpoint)
// suspendpolicy(0=none,1=event-thread,2=all)
return this.SetEventRequest("breakpoint",2,2,mods,
function(m1, i, res) {
res.push(m1.modkind);
res.push(m1.loc.type);
DataCoder.encodeRef(res, m1.loc.cid);
DataCoder.encodeRef(res, m1.loc.mid);
DataCoder.encodeLong(res, m1.loc.idx);
return this.SetEventRequest("breakpoint",2,1,mods,
function(m, i, res) {
m.encode(res,i);
},
onevent
);
@@ -1054,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 = [{
@@ -1088,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) {
@@ -1125,6 +1196,26 @@ function _JDWP() {
resume:function() {
return new Command('resume',1, 9, null, null);
},
suspendthread:function(threadid) {
return new Command('suspendthread:'+threadid,11, 2,
function() {
var res = [];
DataCoder.encodeRef(res, this);
return res;
}.bind(threadid),
null
);
},
resumethread:function(threadid) {
return new Command('resumethread:'+threadid,11, 3,
function() {
var res = [];
DataCoder.encodeRef(res, this);
return res;
}.bind(threadid),
null
);
},
allthreads:function() {
return new Command('allthreads',1, 4,
null,
@@ -1137,7 +1228,34 @@ function _JDWP() {
return res;
}
);
},
threadname:function(threadid) {
return new Command('threadname',11,1,
function() {
var res=[];
DataCoder.encodeRef(res, this);
return res;
}.bind(threadid),
function(o) {
return DataCoder.decodeString(o);
}
);
},
threadstatus:function(threadid) {
return new Command('threadstatus',11,4,
function() {
var res=[];
DataCoder.encodeRef(res, this);
return res;
}.bind(threadid),
function(o) {
return {
thread: DataCoder.decodeThreadStatus(o),
suspend: DataCoder.decodeSuspendStatus(o),
}
}
);
},
};
}

View File

@@ -32,7 +32,7 @@ var Deferred = exports.Deferred = function(p, parent) {
var thendef = this.then(fn);
this.fail(function() {
// we cannot bind thendef to the function because we need the caller's this to resolve the thendef
thendef.resolveWith(this, Array.prototype.map.call(arguments,x=>x));
return thendef.resolveWith(this, Array.prototype.map.call(arguments,x=>x))._promise;
});
return thendef;
},
@@ -50,6 +50,8 @@ var Deferred = exports.Deferred = function(p, parent) {
var res = this.fn.apply(this.def._context,a);
if (res === undefined)
return a;
if (res && res._isdeferred)
return res._promise;
return res;
}.bind({def:faildef,fn:fn}));
faildef._promise = faildef._original = p;
@@ -106,6 +108,9 @@ var Deferred = exports.Deferred = function(p, parent) {
// $.when() is jQuery's version of Promise.all()
// - this version just scans the array of arguments waiting on any Deferreds in turn before finally resolving the return Deferred
var when = exports.when = function() {
if (arguments.length === 1 && Array.isArray(arguments[0])) {
return when.apply(this,...arguments).then(() => [...arguments]);
}
var x = {
def: $.Deferred(),
args: Array.prototype.map.call(arguments,x=>x),

View File

@@ -2,11 +2,13 @@
// vscode stuff
const { EventEmitter, Uri } = require('vscode');
// node and external modules
const fs = require('fs');
const os = require('os');
const path = require('path');
const WebSocketServer = require('ws').Server;
// our stuff
const { ADBClient } = require('./adbclient');
const { AndroidContentProvider } = require('./contentprovider');
const $ = require('./jq-promise');
const { D } = require('./util');
@@ -26,6 +28,7 @@ class LogcatContent {
this._notifying = 0;
this._refreshRate = 200; // ms
this._state = '';
this._htmltemplate = '';
this._adbclient = new ADBClient(uri.query);
this._initwait = new Promise((resolve, reject) => {
this._state = 'connecting';
@@ -44,11 +47,12 @@ class LogcatContent {
reject(e);
})
});
LogcatContent.byLogcatID[this._logcatid] = this;
}
get content() {
if (this._initwait) return this._initwait;
if (this._state !== 'disconnected')
return this.htmlBootstrap(true, '');
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) => {
@@ -71,94 +75,65 @@ class LogcatContent {
this._oldhtmllogs = this._prevlogs._oldhtmllogs;
this._prevlogs = null;
this._initwait = null;
var cached_content = this.htmlBootstrap(false, 'Device disconnected');
var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)});
resolve(cached_content);
})
});
}
sendDisconnectMsg() {
sendClientMessage(msg) {
var clients = LogcatContent._wss.clients.filter(client => client._logcatid === this._logcatid);
clients.forEach(client => client.send(':disconnect'));
clients.forEach(client => client.send(msg+'\n')); // include a newline to try and persuade a buffer write
}
sendDisconnectMsg() {
this.sendClientMessage(':disconnect');
}
onClientConnect(client) {
if (this._oldhtmllogs.length) {
var lines = '<div class="logblock">' + this._oldhtmllogs.join(os.EOL) + '</div>';
client.send(lines);
}
// if the window is tabbed away and then returned to, vscode assumes the content
// has not changed from the original bootstrap. So it proceeds to load the html page (with no data),
// causing a connection to the WSServer as if the connection is still valid (which it was, originally).
// If it's not, tell the client (again) that the device has disconnected
if (this._state === 'disconnected')
this.sendDisconnectMsg();
}
onClientMessage(client, message) {
if (message === 'cmd:clear_logcat') {
if (this._state !== 'connected') return;
new ADBClient(this._adbclient.deviceid).shell_cmd({command:'logcat -c'})
.then(() => {
// clear everything and tell the clients
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
this.sendClientMessage(':logcat_cleared');
})
.fail(e => {
D('Clear logcat command failed: ' + e.message);
})
}
}
updateLogs() {
// 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 style="display:inline-block">' + this._htmllogs.join('') + '</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
this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 5000);
this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 10000);
this._htmllogs = [], this._logs = [];
}
htmlBootstrap(connected, statusmsg) {
return `<!DOCTYPE html>
<html><head>
<style type="text/css">
.V {color:#999}
.D {color:#519B4F}
.I {color:#CCC0D3}
.W {color:#BD955C}
.E {color:#f88}
.F {color:#f66}
.hide {display:none}
.unhide {display:inline-block}
</style></head>
<body style="color:#fff;font-size:.9em">
<div id="status" style="color:#888">${statusmsg}</div>
<div id="rows">${this._oldhtmllogs.join(os.EOL)}</div>
<script>
function start() {
var rows = document.getElementById('rows');
var last_known_scroll_position=0, selectall=0;
var selecttext = (rows) => {
if (!rows) return window.getSelection().empty();
var range = document.createRange();
range.selectNode(rows);
window.getSelection().addRange(range);
}
window.addEventListener('scroll', function(e) {
if ((last_known_scroll_position = window.scrollY)===0) {
var hidden = document.getElementsByClassName('hide');
for (var i=hidden.length-1; i>=0; i--)
hidden[i].className='unhide';
}
});
window.addEventListener('keypress', function(e) {
if (e.ctrlKey && /[aA]/.test(e.key) && !selectall) {
selectall = 1;
selecttext(rows);
}
});
window.addEventListener('keyup', function(e) {
selectall = 0;
/^escape$/i.test(e.key) && selecttext(null);
});
var setStatus = (x) => { document.getElementById('status').textContent = x; }
var connect = () => {
try {
setStatus('Connecting...');
var x = new WebSocket('ws://127.0.0.1:${LogcatContent._wssport}/${this._logcatid}');
x.onopen = e => { setStatus('') };x.onclose = e => { };x.onerror = e => { setStatus('Connection error') }
x.onmessage = e => {
var logs = e.data;
if (/^:disconnect$/.test(logs)) {
x.close(),setStatus('Device disconnected');
return;
}
if (last_known_scroll_position > 0)
logs = '<div class="hide">'+logs+'</div>';
rows && rows.insertAdjacentHTML('afterbegin',logs);
};
}
catch(e) { setStatus('Connection exception') }
}
${connected ? '' : '//'} connect();
}
setTimeout(start, 100);
</script>
</body>
</html>`;
htmlBootstrap(vars) {
if (!this._htmltemplate)
this._htmltemplate = fs.readFileSync(path.join(__dirname,'res/logcat.html'), 'utf8');
vars = Object.assign({
logcatid: this._logcatid,
wssport: LogcatContent._wssport,
}, vars);
// simple value replacement using !{name} as the placeholder
var html = this._htmltemplate.replace(/!\{(.*?)\}/g, (match,expr) => ''+(vars[expr.trim()]||''));
return html;
}
renotify() {
if (++this._notifying > 1) return;
@@ -172,16 +147,17 @@ class LogcatContent {
}
onLogcatContent(e) {
if (e.logs.length) {
var mrfirst = e.logs.slice().reverse();
this._logs = mrfirst.concat(this._logs);
mrfirst.forEach(log => {
var mrlast = e.logs.slice();
this._logs = this._logs.concat(mrlast);
mrlast.forEach(log => {
if (!(log = log.trim())) return;
// replace html-interpreted chars
var m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/);
var style = (m && m[1]) || '';
log = log.replace(/[&"'<>]/g, c => ({ '&': '&amp;', '"': '&quot;', "'": '&#39;', '<': '&lt;', '>': '&gt;' }[c]));
this._htmllogs.unshift(`<div class="${style}">${log}</div>`);
})
this._htmllogs.unshift(`<div class="log ${style}">${log}</div>`);
});
this.renotify();
}
}
@@ -192,31 +168,49 @@ class LogcatContent {
}
}
// hashmap of all LogcatContent instances, keyed on device id
LogcatContent.byLogcatID = {};
LogcatContent.initWebSocketServer = function () {
if (LogcatContent._wssdone) {
// already inited
return LogcatContent._wssdone;
}
// retrieve the logcat websocket port
var default_wssport = 7038;
var wssport = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport);
if (typeof wssport !== 'number' || wssport <= 0 || wssport >= 65536 || wssport !== (wssport|0))
wssport = default_wssport;
LogcatContent._wssdone = $.Deferred();
({
wss: null,
port: 31100,
startport: wssport,
port: wssport,
retries: 0,
tryCreateWSS() {
this.wss = new WebSocketServer({ host: '127.0.0.1', port: this.port }, () => {
// success - save the info and resolve the deferred
LogcatContent._wssport = this.port;
LogcatContent._wssstartport = this.startport;
LogcatContent._wss = this.wss;
this.wss.on('connection', client => {
// the client uses the url path to signify which logcat data it wants
client._logcatid = client.upgradeReq.url.match(/^\/?(.*)$/)[1];
// we're not really interested in anything the client sends
/*client.on('message', message => {
console.log('ws received: %s', message);
});
client.on('close', e => {
console.log('ws close');
var lc = LogcatContent.byLogcatID[client._logcatid];
if (lc) lc.onClientConnect(client);
else client.close();
client.on('message', function(message) {
var lc = LogcatContent.byLogcatID[this._logcatid];
if (lc) lc.onClientMessage(this, message);
}.bind(client));
/*client.on('close', e => {
console.log('client close');
});*/
// try and make sure we don't delay writes
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
});
this.wss = null;
LogcatContent._wssdone.resolveWith(LogcatContent, []);
@@ -233,68 +227,30 @@ LogcatContent.initWebSocketServer = function () {
return LogcatContent._wssdone;
}
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
constructor() {
this._logs = {}; // hashmap<url, LogcatContent>
this._onDidChange = new EventEmitter();
}
dispose() {
this._onDidChange.dispose();
}
/**
* An event to signal a resource has changed.
*/
get onDidChange() {
return this._onDidChange.event;
}
/**
* Provide textual content for a given uri.
*
* The editor will use the returned string-content to create a readonly
* [document](TextDocument). Resources allocated should be released when
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
*
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
* @param token A cancellation token.
* @return A string or a thenable that resolves to such.
*/
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
var doc = this._logs[uri];
if (doc) return this._logs[uri].content;
switch (uri.authority) {
// android-dev-ext://logcat/read?<deviceid>
case 'logcat': return this.provideLogcatDocumentContent(uri);
}
throw new Error('Document Uri not recognised');
}
provideLogcatDocumentContent(uri) {
var doc = this._logs[uri] = new LogcatContent(this, uri);
return doc.content;
}
}
// the statics
AndroidContentProvider.SCHEME = 'android-dev-ext';
AndroidContentProvider.register = (ctx, workspace) => {
var provider = new AndroidContentProvider();
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
ctx.subscriptions.push(registration);
ctx.subscriptions.push(provider);
}
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
return uri.with({
query: deviceId
});
function getADBPort() {
var defaultPort = 5037;
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
return adbPort;
return defaultPort;
}
function openLogcatWindow(vscode) {
new ADBClient().list_devices().then(devices => {
new ADBClient().test_adb_connection()
.then(err => {
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
var adbport = getADBPort();
var autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
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'});
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
}
})
.then(() => new ADBClient().list_devices())
.then(devices => {
switch(devices.length) {
case 0:
vscode.window.showInformationMessage('Logcat cannot be displayed. No Android devices are currently connected');
@@ -331,5 +287,5 @@ function openLogcatWindow(vscode) {
});
}
exports.AndroidContentProvider = AndroidContentProvider;
exports.LogcatContent = LogcatContent;
exports.openLogcatWindow = openLogcatWindow;

89
src/nbc.js Normal file
View File

@@ -0,0 +1,89 @@
// arbitrary precision helper class for 64 bit numbers
const NumberBaseConverter = {
// Adds two arrays for the given base (10 or 16), returning the result.
add(x, y, base) {
var z = [], n = Math.max(x.length, y.length), carry = 0, i = 0;
while (i < n || carry) {
var xi = i < x.length ? x[i] : 0;
var yi = i < y.length ? y[i] : 0;
var zi = carry + xi + yi;
z.push(zi % base);
carry = Math.floor(zi / base);
i++;
}
return z;
},
// Returns a*x, where x is an array of decimal digits and a is an ordinary
// JavaScript number. base is the number base of the array x.
multiplyByNumber(num, x, base) {
if (num < 0) return null;
if (num == 0) return [];
var result = [], power = x;
for(;;) {
if (num & 1) {
result = this.add(result, power, base);
}
num = num >> 1;
if (num === 0) return result;
power = this.add(power, power, base);
}
},
twosComplement(str, base) {
const invdigits = str.split('').map(c => base - 1 - parseInt(c,base)).reverse();
const negdigits = this.add(invdigits, [1], base).slice(0,str.length);
return negdigits.reverse().map(d => d.toString(base)).join('');
},
convertBase(str, fromBase, toBase) {
if (fromBase === 10 && /[eE]/.test(str)) {
// convert exponents to a string of zeros
var s = str.split(/[eE]/);
str = s[0] + '0'.repeat(parseInt(s[1],10)); // works for 0/+ve exponent,-ve throws
}
var digits = str.split('').map(d => parseInt(d,fromBase)).reverse();
var outArray = [], power = [1];
for (var i = 0; i < digits.length; i++) {
if (digits[i]) {
outArray = this.add(outArray, this.multiplyByNumber(digits[i], power, toBase), toBase);
}
power = this.multiplyByNumber(fromBase, power, toBase);
}
return outArray.reverse().map(d => d.toString(toBase)).join('');
},
decToHex(decstr, minlen) {
var res, isneg = decstr[0] === '-';
if (isneg) decstr = decstr.slice(1)
decstr = decstr.match(/^0*(.+)$/)[1]; // strip leading zeros
if (decstr.length < 16 && !/[eE]/.test(decstr)) { // 16 = Math.pow(2,52).toString(10).length
// less than 52 bits - just use parseInt
res = parseInt(decstr, 10).toString(16);
} else {
res = NumberBaseConverter.convertBase(decstr, 10, 16);
}
if (isneg) {
res = NumberBaseConverter.twosComplement(res, 16);
if (/^[0-7]/.test(res)) res = 'f'+res; //msb must be set for -ve numbers
} else if (/^[^0-7]/.test(res))
res = '0' + res; // msb must not be set for +ve numbers
if (minlen && res.length < minlen) {
res = (isneg?'f':'0').repeat(minlen - res.length) + res;
}
return res;
},
hexToDec(hexstr, signed) {
var res, isneg = /^[^0-7]/.test(hexstr);
if (hexstr.match(/^0*(.+)$/)[1].length*4 < 52) {
// less than 52 bits - just use parseInt
res = parseInt(hexstr, 16);
if (signed && isneg) res = -res;
return res.toString(10);
}
if (isneg) {
hexstr = NumberBaseConverter.twosComplement(hexstr, 16);
}
res = (isneg ? '-' : '') + NumberBaseConverter.convertBase(hexstr, 16, 10);
return res;
},
};
Object.assign(exports, NumberBaseConverter);

195
src/res/logcat.html Normal file
View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<style type="text/css">
.V {color:#777} .vscode-light .V {color:#999} .vscode-high-contrast .V {color:#fff}
.D {color:#8b8} .vscode-light .D {color:#292} .vscode-high-contrast .D {color:#0a0}
.I {color:#99a} .vscode-light .I {color:#557} .vscode-high-contrast .I {color:#aaf}
.W {color:#C84} .vscode-light .W {color:#F80} .vscode-high-contrast .W {color:#f80}
.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: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}
.c {flex: 1 1 auto;overflow:auto;padding-top: .2em}
.g {margin:.6em .2em 0 .2em;background: none;border: 1px solid #444; color:#888;padding:.3em .8em;font-size: .9em}
.g:hover:enabled {color:#ccc;cursor: pointer} .g:focus:enabled,.g:focus:hover:enabled {color:#eee;cursor: pointer}
.vscode-light .g:enabled {color: #666;}
.vscode-light .g:hover:enabled,.vscode-light .g:focus:enabled,.vscode-light .g:focus:hover:enabled {color: #333;background: #eee;}
.vscode-high-contrast .g:enabled {color: #fff;border-color: #0cc;background:none;}
.vscode-high-contrast .g:hover:enabled,.vscode-high-contrast .g:focus:enabled {border-color: darkorange;}
.h{display: flex;align-items: center;flex-wrap: wrap;}
.log { white-space:nowrap }
.filter { display:none }
body {font-size:.9em}
#q {margin:.6em .2em 0 .2em;padding:.3em;width:20em;outline:none;border:solid 1px #444;color: #eee;background:rgba(128,128,128,.05);}
#q:hover { border-color: #464; }
#q:focus,#q:focus:hover { border-color: #4a4; }
.vscode-light #q {color: #333;border-color: #444;background:none;}
.vscode-light #q:hover { border-color: #464; }
.vscode-light #q:focus,.vscode-light #q:focus:hover { border-color: #4a4; }
.vscode-high-contrast #q {color: #fff;border-color: #0cc;background:none;}
.vscode-high-contrast #q:hover {border-color: darkorange;}
.vscode-high-contrast #q:focus,.vscode-high-contrast #q:focus:hover {border-color: darkorange;}
#lcount {font-family:monospace;font-size:1em;margin-top:.4em}
.vscode-dark #lcount {color:#484;} .vscode-light #lcount {color:#484;} .vscode-high-contrast #lcount {color:#0d0;}
.vscode-dark #status {color:#eee;} .vscode-light #status {color:#333;} .vscode-high-contrast #status {color:#fff;}
</style>
</head>
<body>
<div class="a">
<div class="b">
<div class="h"><input id="q" placeholder="Filter regex"/><button id="clearlcbtn" class="g" disabled="true">Clear logcat</button></div>
<div id="lcount"></div>
</div>
<div id="rc" class="c">
<div id="status">!{status}</div>
<div id="rows">!{oldlogs}</div>
</div>
</div>
<script>
(function() {
const getId = document.getElementById.bind(document);
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, prevlc=0, currfilter,ws;
var selecttext = (rows) => {
if (!rows) return window.getSelection().empty();
var range = document.createRange();
range.selectNode(rows);
window.getSelection().addRange(range);
};
getId('clearlcbtn').onclick = (e) => {
ws && ws.send('cmd:clear_logcat');
}
getId('rc').onscroll = (e) => {
if ((last_known_scroll_position = e.target.scrollTop)===0) {
var hidden = document.getElementsByClassName('hide');
for (var i=hidden.length-1; i>=0; i--)
hidden[i].className = hidden[i].className.replace(/\bhide\b/g,'').trim();
}
};
updateLogCountDisplay = () => {
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';
}
updateFilter = (new_filter_source) => {
if (currfilter && currfilter.source === new_filter_source) return; // nothing's changed
var newfilter = null;
if (new_filter_source) {
try {
newfilter = new RegExp(new_filter_source, 'i');
newfilter.matchCounts = {true:0,false:0};
}
catch(err) { return showFilterErr('Invalid regular expression') }
}
// reset the filtered elements
var logs = document.getElementsByClassName('log');
for (var i=logs.length-1,m; i>=0; i--) {
m = newfilter ? newfilter.test(logs[i].textContent) : 1;
logs[i].className = logs[i].className.replace(/\bfilter\b|$/g,m?'':' filter').trim();
newfilter && newfilter.matchCounts[!!m]++;
}
currfilter = newfilter;
updateLogCountDisplay();
}
var filter_pause = 0;
filter.oninput = (e) => {
if (filter_pause++) return;
filter.style['border-color'] = '';
({
wait() {
setTimeout(() => {
if (filter_pause === 1)
return filter_pause=0,updateFilter(filter.value);
filter_pause = 1;
this.wait();
},250);
}
}).wait();
};
filter.onkeyup = (e) => {
// when enter/escape is pressed - lose focus
/^(escape|enter)$/i.test(e.key) && filter.blur();
}
window.addEventListener('keypress', function(e) {
if (e.ctrlKey && /[aA]/.test(e.key) && !selectall) {
selectall = 1;
selecttext(rows);
}
});
window.addEventListener('keyup', function(e) {
selectall = 0;
/^escape$/i.test(e.key) && selecttext(null);
});
var connect = () => {
try {
setStatus('Connecting...');
var x = new WebSocket('ws://127.0.0.1:!{wssport}/!{logcatid}');
x.onopen = e => {
setStatus('');getId('clearlcbtn').disabled = false;ws = x;
};
x.onclose = e => {
ws = null;
getId('clearlcbtn').disabled = true;
};
x.onerror = e => { setStatus('Connection error') }
x.onmessage = e => {
if (!rows) return;
var rawlogs = e.data.trim();
if (/^:disconnect$/.test(rawlogs)) {
x.close(),setStatus('Device disconnected');
return;
}
if (/^:logcat_cleared$/.test(rawlogs)) {
rows.innerHTML = '';
rows.insertAdjacentHTML('afterbegin','<div>---- log cleared ----</div>');
logcount = prevlc = 0;
if (currfilter) currfilter.matchCounts = {true:0,false:0};
updateLogCountDisplay();
return;
}
if (last_known_scroll_position > 0)
rawlogs = '<div class="hide">'+rawlogs+'</div>';
rows.insertAdjacentHTML('afterbegin',rawlogs);
var logs = rows.firstElementChild.getElementsByClassName('log');
logcount += logs.length;
// apply the filter to the newly insert elements
if (currfilter) {
for (var i=logs.length-1,m; i>=0; i--) {
m = currfilter.test(logs[i].textContent);
if (!m) logs[i].className += ' filter';
currfilter.matchCounts[!!m]++;
}
}
updateLogCountDisplay();
};
}
catch(e) { setStatus('Connection exception') }
}
!{connected} && connect();
}
window.addEventListener("load", function(event) {
try { start(); } catch(e) {setStatus('start exception: '+e.message);}
});
})();
</script>
</body>
</html>

129
src/threads.js Normal file
View File

@@ -0,0 +1,129 @@
'use strict'
const { AndroidVariables } = require('./variables');
const $ = require('./jq-promise');
/*
Class used to manage a single thread reported by JDWP
*/
class AndroidThread {
constructor(session, threadid, vscode_threadid) {
// the AndroidDebugSession instance
this.session = session;
// the Android debugger instance
this.dbgr = session.dbgr;
// the java thread id (hex string)
this.threadid = threadid;
// the vscode thread id (number)
this.vscode_threadid = vscode_threadid;
// the (Java) name of the thread
this.name = null;
// the thread break info
this.paused = null;
// the timeout during a step which, if it expires, we allow other threads to break
this.stepTimeout = null;
}
threadNotSuspendedError() {
return new Error(`Thread ${this.vscode_threadid} not suspended`);
}
addStackFrameVariable(frame, level) {
if (!this.paused) throw this.threadNotSuspendedError();
var frameId = (this.vscode_threadid * 1e9) + (level * 1e6);
var stack_frame_var = {
frame, frameId,
locals: null,
}
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
}
allocateExceptionScopeReference(frameId) {
if (!this.paused) return;
if (!this.paused.last_exception) return;
this.paused.last_exception.frameId = frameId;
this.paused.last_exception.scopeRef = frameId + 1;
}
getVariables(variablesReference) {
if (!this.paused)
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
// is this reference a stack frame
var stack_frame_var = this.paused.stack_frame_vars[variablesReference];
if (stack_frame_var) {
// frame locals request
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref));
}
// is this refrence an exception scope
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId];
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef));
}
// work out which stack frame this reference is for
var frameId = Math.trunc(variablesReference/1e6) * 1e6;
var stack_frame_var = this.paused.stack_frame_vars[frameId];
return stack_frame_var.locals.getVariables(variablesReference);
}
_ensureLocals(varinfo) {
if (!this.paused)
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
// evaluate can call this using frameId as the argument
if (typeof varinfo === 'number')
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
// if we're currently processing it (or we've finished), just return the promise
if (this.paused.locals_done[varinfo.frameId])
return this.paused.locals_done[varinfo.frameId];
// create a new promise
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
.then((locals,x) => {
// make sure we are still paused...
if (!this.paused)
throw this.threadNotSuspendedError();
// sort the locals by name, except for 'this' which always goes first
locals.sort((a,b) => {
if (a.name === b.name) return 0;
if (a.name === 'this') return -1;
if (b.name === 'this') return +1;
return a.name.localeCompare(b.name);
})
// create a new local variable with the results and resolve the promise
var varinfo = x.varinfo;
varinfo.cached = locals;
x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others
x.varinfo.locals.setVariable(varinfo.frameId, varinfo);
var last_exception = this.paused.last_exception;
if (last_exception) {
x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception);
}
x.def.resolveWith(this, [varinfo.frameId]);
})
.fail(e => {
x.def.rejectWith(this, [e]);
})
return def;
}
setVariableValue(args) {
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
var stack_frame_var = this.paused.stack_frame_vars[frameId];
return this._ensureLocals(stack_frame_var).then(varref => {
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
});
}
}
exports.AndroidThread = AndroidThread;

389
src/variables.js Normal file
View File

@@ -0,0 +1,389 @@
'use strict'
const { JTYPES, exmsg_var_name, createJavaString } = require('./globals');
const NumberBaseConverter = require('./nbc');
const $ = require('./jq-promise');
/*
Class used to manage stack frame locals and other evaluated expressions
*/
class AndroidVariables {
constructor(session, baseId) {
this.session = session;
this.dbgr = session.dbgr;
// the incremental reference id generator for stack frames, locals, etc
this.nextId = baseId;
// hashmap of variables and frames
this.variableHandles = {};
// hashmap<objectid, variablesReference>
this.objIdCache = {};
// allow primitives to be expanded to show more info
this._expandable_prims = false;
}
addVariable(varinfo) {
var variablesReference = ++this.nextId;
this.variableHandles[variablesReference] = varinfo;
return variablesReference;
}
clear() {
this.variableHandles = {};
}
setVariable(variablesReference, varinfo) {
this.variableHandles[variablesReference] = varinfo;
}
_getObjectIdReference(type, objvalue) {
// we need the type signature because we must have different id's for
// an instance and it's supertype instance (which obviously have the same objvalue)
var key = type.signature + objvalue;
return this.objIdCache[key] || (this.objIdCache[key] = ++this.nextId);
}
getVariables(variablesReference) {
var varinfo = this.variableHandles[variablesReference];
if (!varinfo) {
return $.Deferred().resolve([]);
}
else if (varinfo.cached) {
return $.Deferred().resolve(this._local_to_variable(varinfo.cached));
}
else if (varinfo.objvar) {
// object fields request
return this.dbgr.getsupertype(varinfo.objvar, {varinfo})
.then((supertype, x) => {
x.supertype = supertype;
return this.dbgr.getfieldvalues(x.varinfo.objvar, x);
})
.then((fields, x) => {
// add an extra msg field for exceptions
if (!x.varinfo.exception) return;
x.fields = fields;
return this.dbgr.invokeToString(x.varinfo.objvar.value, x.varinfo.threadid, varinfo.objvar.type.signature, x)
.then((call,x) => {
call.name = exmsg_var_name;
x.fields.unshift(call);
return $.Deferred().resolveWith(this, [x.fields, x]);
});
})
.then((fields, x) => {
// ignore supertypes of Object
x.supertype && x.supertype.signature!=='Ljava/lang/Object;' && fields.unshift({
vtype:'super',
name:':super',
hasnullvalue:false,
type: x.supertype,
value: x.varinfo.objvar.value,
valid:true,
});
x.varinfo.cached = fields;
return this._local_to_variable(fields);
});
}
else if (varinfo.arrvar) {
// array elements request
var range = varinfo.range, count = range[1] - range[0];
// should always have a +ve count, but just in case...
if (count <= 0) return $.Deferred().resolve([]);
// add some hysteresis
if (count > 110) {
// create subranges in the sub-power of 10
var subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100), variables = [];
for (var i=range[0],varref,v; i < range[1]; i+= subrangelen) {
varref = ++this.nextId;
v = this.variableHandles[varref] = { varref:varref, arrvar:varinfo.arrvar, range:[i, Math.min(i+subrangelen, range[1])] };
variables.push({name:`[${v.range[0]}..${v.range[1]-1}]`,type:'',value:'',variablesReference:varref});
}
return $.Deferred().resolve(variables);
}
// get the elements for the specified range
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count)
.then((elements) => {
varinfo.cached = elements;
return this._local_to_variable(elements);
});
}
else if (varinfo.bigstring) {
return this.dbgr.getstringchars(varinfo.bigstring.value)
.then((s) => {
return this._local_to_variable([{name:'<value>',hasnullvalue:false,string:s,type:JTYPES.String,valid:true}]);
});
}
else if (varinfo.primitive) {
// convert the primitive value into alternate formats
var variables = [], bits = {J:64,I:32,S:16,B:8}[varinfo.signature];
const pad = (u,base,len) => ('0000000000000000000000000000000'+u.toString(base)).slice(-len);
switch(varinfo.signature) {
case 'Ljava/lang/String;':
variables.push({name:'<length>',type:'',value:varinfo.value.toString(),variablesReference:0});
break;
case 'C':
variables.push({name:'<charCode>',type:'',value:varinfo.value.charCodeAt(0).toString(),variablesReference:0});
break;
case 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work
var v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
const s4 = { hi:parseInt(v64hex.slice(0,8),16), lo:parseInt(v64hex.slice(-8),16) };
variables.push(
{name:'<binary>',type:'',value:pad(s4.hi,2,32)+pad(s4.lo,2,32),variablesReference:0}
,{name:'<decimal>',type:'',value:NumberBaseConverter.hexToDec(v64hex,false),variablesReference:0}
,{name:'<hex>',type:'',value:pad(s4.hi,16,8)+pad(s4.lo,16,8),variablesReference:0}
);
break;
default:// integer/short/byte value
const u = varinfo.value >>> 0;
variables.push(
{name:'<binary>',type:'',value:pad(u,2,bits),variablesReference:0}
,{name:'<decimal>',type:'',value:u.toString(10),variablesReference:0}
,{name:'<hex>',type:'',value:pad(u,16,bits/4),variablesReference:0}
);
break;
}
return $.Deferred().resolve(variables);
}
else if (varinfo.frame) {
// frame locals request - this should be handled by AndroidDebugThread instance
return $.Deferred().resolve([]);
} else {
// something else?
return $.Deferred().resolve([]);
}
}
/**
* Converts locals (or other vars) in debugger format into Variable objects used by VSCode
*/
_local_to_variable(v) {
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._local_to_variable(v));
var varref = 0, objvalue, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
switch(true) {
case v.hasnullvalue && JTYPES.isReference(v.type):
// null object or array type
objvalue = 'null';
break;
case v.type.signature === JTYPES.Object.signature:
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
objvalue = v.type.typename;
break;
case v.type.signature === JTYPES.String.signature:
objvalue = JSON.stringify(v.string);
if (v.biglen) {
// since this is a big string - make it viewable on expand
varref = ++this.nextId;
this.variableHandles[varref] = {varref:varref, bigstring:v};
objvalue = `String (length:${v.biglen})`;
}
else if (this._expandable_prims) {
// as a courtesy, allow strings to be expanded to see their length
varref = ++this.nextId;
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.string.length};
}
break;
case JTYPES.isArray(v.type):
// non-null array type - if it's not zero-length add another variable reference so the user can expand
if (v.arraylen) {
varref = this._getObjectIdReference(v.type, v.value);
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
}
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound
break;
case JTYPES.isObject(v.type):
// non-null object instance - add another variable reference so the user can expand
varref = this._getObjectIdReference(v.type, v.value);
this.variableHandles[varref] = {varref:varref, objvar:v};
objvalue = v.type.typename;
break;
case v.type.signature === 'C':
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\'};
if (cmap[v.char]) {
objvalue = `'\\${cmap[v.char]}'`;
} else if (v.value < 32) {
objvalue = v.value ? `'\\u${('000'+v.value.toString(16)).slice(-4)}'` : "'\\0'";
} else objvalue = `'${v.char}'`;
break;
case v.type.signature === 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
objvalue = NumberBaseConverter.hexToDec(v64hex, true);
break;
default:
// other primitives: int, boolean, etc
objvalue = v.value.toString();
break;
}
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
varref = ++this.nextId;
this.variableHandles[varref] = {varref:varref, signature:v.type.signature, primitive:true, value:v.value};
}
return {
name: v.name,
type: typename,
value: objvalue,
variablesReference: varref,
}
}
setVariableValue(args) {
const failSetVariableRequest = reason => $.Deferred().reject(new Error(reason));
var v = this.variableHandles[args.variablesReference];
if (!v || !v.cached) {
return failSetVariableRequest(`Variable '${args.name}' not found`);
}
var destvar = v.cached.find(v => v.name===args.name);
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
return failSetVariableRequest(`The value is read-only and cannot be updated.`);
}
// be nice and remove any superfluous whitespace
var value = args.value.trim();
if (!value) {
// just ignore blank requests
var vsvar = this._local_to_variable(destvar);
return $.Deferred().resolve(vsvar);
}
// non-string reference types can only set to null
if (/^L/.test(destvar.type.signature) && destvar.type.signature !== JTYPES.String.signature) {
if (value !== 'null') {
return failSetVariableRequest('Object references can only be set to null');
}
}
// convert the new value into a debugger-compatible object
var m, num, data, datadef;
switch(true) {
case value === 'null':
data = {valuetype:'oref',value:null}; // null object reference
break;
case /^(true|false)$/.test(value):
data = {valuetype:'boolean',value:value!=='false'}; // boolean literal
break;
case !!(m=value.match(/^[+-]?0x([0-9a-f]+)$/i)):
// hex integer- convert to decimal and fall through
if (m[1].length < 52/4)
value = parseInt(value, 16).toString(10);
else
value = NumberBaseConverter.hexToDec(value);
m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/);
// fall-through
case !!(m=value.match(/^[+-]?[0-9]+([eE][+]?[0-9]+)?$/)):
// decimal integer
num = parseFloat(value, 10); // parseInt() can't handle exponents
switch(true) {
case (num >= -128 && num <= 127): data = {valuetype:'byte',value:num}; break;
case (num >= -32768 && num <= 32767): data = {valuetype:'short',value:num}; break;
case (num >= -2147483648 && num <= 2147483647): data = {valuetype:'int',value:num}; break;
case /inf/i.test(num): return failSetVariableRequest(`Value '${value}' exceeds the maximum number range.`);
case /^[FD]$/.test(destvar.type.signature): data = {valuetype:'float',value:num}; break;
default:
// long (or larger) - need to use the arbitrary precision class
data = {valuetype:'long',value:NumberBaseConverter.decToHex(value, 16)};
switch(true){
case data.value.length > 16:
case num > 0 && data.value.length===16 && /[^0-7]/.test(data.value[0]):
// number exceeds signed 63 bit - make it a float
data = {valuetype:'float',value:num};
break;
}
}
break;
case !!(m=value.match(/^(Float|Double)\s*\.\s*(POSITIVE_INFINITY|NEGATIVE_INFINITY|NaN)$/)):
// Java special float constants
data = {valuetype:m[1].toLowerCase(),value:{POSITIVE_INFINITY:Infinity,NEGATIVE_INFINITY:-Infinity,NaN:NaN}[m[2]]};
break;
case !!(m=value.match(/^([+-])?infinity$/i)):// allow js infinity
data = {valuetype:'float',value:m[1]!=='-'?Infinity:-Infinity};
break;
case !!(m=value.match(/^nan$/i)): // allow js nan
data = {valuetype:'float',value:NaN};
break;
case !!(m=value.match(/^[+-]?[0-9]+[eE][-][0-9]+([dDfF])?$/)):
case !!(m=value.match(/^[+-]?[0-9]*\.[0-9]+(?:[eE][+-]?[0-9]+)?([dDfF])?$/)):
// decimal float
num = parseFloat(value);
data = {valuetype:/^[dD]$/.test(m[1]) ? 'double': 'float',value:num};
break;
case !!(m=value.match(/^'(?:\\u([0-9a-fA-F]{4})|\\([bfrntv0'])|(.))'$/)):
// character literal
var cvalue = m[1] ? String.fromCharCode(parseInt(m[1],16)) :
m[2] ? {b:'\b',f:'\f',r:'\r',n:'\n',t:'\t',v:'\v',0:'\0',"'":"'"}[m[2]]
: m[3]
data = {valuetype:'char',value:cvalue};
break;
case !!(m=value.match(/^"[^"\\\n]*(\\.[^"\\\n]*)*"$/)):
// string literal - we need to get the runtime to create a new string first
datadef = createJavaString(this.dbgr, value).then(stringlit => ({valuetype:'oref', value:stringlit.value}));
break;
default:
// invalid literal
return failSetVariableRequest(`'${value}' is not a valid Java literal.`);
}
if (!datadef) {
// as a nicety, if the destination is a string, stringify any primitive value
if (data.valuetype !== 'oref' && destvar.type.signature === JTYPES.String.signature) {
datadef = createJavaString(this.dbgr, data.value.toString(), {israw:true})
.then(stringlit => ({valuetype:'oref', value:stringlit.value}));
} else if (destvar.type.signature.length===1) {
// if the destination is a primitive, we need to range-check it here
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
// weirdness if we allow primitives to be set with out-of-range values
var validmap = {
B:'byte,char', // char may not fit - we special-case this later
S:'byte,short,char',
I:'byte,short,int,char',
J:'byte,short,int,long,char',
F:'byte,short,int,long,char,float',
D:'byte,short,int,long,char,double,float',
C:'byte,short,char',Z:'boolean',
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
};
var is_in_range = (validmap[destvar.type.signature]||'').indexOf(data.valuetype) >= 0;
if (destvar.type.signature === 'B' && data.valuetype === 'char')
is_in_range = validmap.isCharInRangeForByte(data.value);
if (!is_in_range) {
return failSetVariableRequest(`'${value}' is not compatible with variable type: ${destvar.type.typename}`);
}
// check complete - make sure the type matches the destination and use a resolved deferred with the value
if (destvar.type.signature!=='C' && data.valuetype === 'char')
data.value = data.value.charCodeAt(0); // convert char to it's int value
if (destvar.type.signature==='J' && typeof data.value === 'number')
data.value = NumberBaseConverter.decToHex(''+data.value,16); // convert ints to hex-string longs
data.valuetype = destvar.type.typename;
datadef = $.Deferred().resolveWith(this,[data]);
}
}
return datadef.then(data => {
// setxxxvalue sets the new value and then returns a new local for the variable
switch(destvar.vtype) {
case 'field': return this.dbgr.setfieldvalue(destvar, data);
case 'local': return this.dbgr.setlocalvalue(destvar, data);
case 'arrelem':
var idx = parseInt(args.name, 10), count=1;
if (idx < 0 || idx >= destvar.data.arrobj.arraylen) throw new Error('Array index out of bounds');
return this.dbgr.setarrayvalues(destvar.data.arrobj, idx, count, data);
default: throw new Error('Unsupported variable type');
}
})
.then(newlocalvar => {
if (destvar.vtype === 'arrelem') newlocalvar = newlocalvar[0];
Object.assign(destvar, newlocalvar);
var vsvar = this._local_to_variable(destvar);
return vsvar;
})
.fail(e => {
return failSetVariableRequest(`Variable update failed. ${e.message||''}`);
});
}
}
exports.AndroidVariables = AndroidVariables;