22 Commits

Author SHA1 Message Date
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
13 changed files with 1539 additions and 486 deletions

View File

@@ -1,5 +1,14 @@
# Change Log
## 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

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;
}
return adbPort;
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.3.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -66,6 +66,16 @@
"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
},
"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,6 +124,7 @@
"dependencies": {
"vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0",
"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()

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,11 @@ Debugger.prototype = {
adbclient: null,
stoppedlocation: null,
classes: {},
// classprepare notifier done
cpndone: false,
// classprepare filters
cpfilters: [],
preparedclasses: [],
stepids: {}, // hashmap<threadid,stepid>
suspendcount: 0, // refcount of suspend-all-threads
}
return this;
},
@@ -414,6 +416,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) {
@@ -425,37 +456,67 @@ Debugger.prototype = {
});
})
.then(function () {
this.session.suspendcount++;
this._trigger('suspended');
});
},
resume: function (extra) {
suspendthread: function (threadid, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
this._trigger('resuming');
this.session.stoppedlocation = null;
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.resume(),
cmd: this.JDWP.Commands.suspendthread(threadid),
});
})
.then((res,extra) => extra);
},
_resume:function(triggers, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
if (triggers) this._trigger('resuming');
const resume_cmd = (decoded,extra) => {
return this.session.adbclient.jdwp_command({
ths: this,
extra: extra,
cmd: this.JDWP.Commands.resume(),
});
}
// we must resume with the same number of suspends
var def = resume_cmd(null, extra);
for (var i=1; i < this.session.suspendcount; i++) {
def = def.then(resume_cmd);
}
this.session.stoppedlocation = null;
this.session.suspendcount = 0;
return def;
})
.then(function (decoded, extra) {
this._trigger('resumed');
if (triggers) this._trigger('resumed');
return extra;
});
},
resume: function (extra) {
return this._resume(true, extra);
},
_resumesilent: function () {
return this.ensureconnected()
.then(function () {
this.session.stoppedlocation = null;
return this._resume(false);
},
resumethread: function (threadid, extra) {
return this.ensureconnected(extra)
.then(function (extra) {
return this.session.adbclient.jdwp_command({
ths: this,
//extra: extra,
cmd: this.JDWP.Commands.resume(),
extra: extra,
cmd: this.JDWP.Commands.resumethread(threadid),
});
});
})
.then((res,extra) => extra);
},
step: function (steptype, threadid) {
@@ -498,11 +559,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 +571,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 +583,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]);
}
}
break;
// 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);
}
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 +788,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 +799,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 +884,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 +925,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,
@@ -1003,6 +1138,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++;
}
}
@@ -1249,6 +1386,8 @@ 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]);
}
};
@@ -1259,16 +1398,28 @@ Debugger.prototype = {
return cmd.promise();
},
_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) {
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 () {
// 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;
var loc = e.event.location;
@@ -1290,6 +1441,10 @@ Debugger.prototype = {
};
var cmd = this.session.adbclient.jdwp_command({
cmd: this.JDWP.Commands.SetSingleStep(steptype, threadid, onevent),
}).then(res => {
// 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 cmd.promise();
@@ -1310,13 +1465,27 @@ 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;
// 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) {
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 +1500,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 +1552,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 +1597,7 @@ Debugger.prototype = {
bplocs.push(bploc);
}
}
if (!bplocs.length) return;
// set all the breakpoints in one go...
return this._setupbreakpointsevent(bplocs);
})
@@ -1435,7 +1605,7 @@ Debugger.prototype = {
// when all the breakpoints for the newly-prepared type have been set...
this._resumesilent();
});
}
});
},
clearBreakOnExceptions: function(extra) {
@@ -1465,19 +1635,25 @@ Debugger.prototype = {
dbgr: this,
},
fn: function (e) {
this._findcmllocation(this.session.classes, e.event.throwlocation)
.then(tloc => {
this._findcmllocation(this.session.classes, e.event.catchlocation)
.then(cloc => {
var eventdata = {
event: e.event,
throwlocation: Object.assign({ threadid: e.event.threadid }, tloc),
catchlocation: Object.assign({ threadid: e.event.threadid }, cloc),
};
this.session.stoppedlocation = Object.assign({}, eventdata.throwlocation);
this._trigger('exception', eventdata);
})
})
// each exception hit contributes a global suspend
x.dbgr.session.suspendcount++;
// if this exception break occurred during a step request, we must manually clear the event
// or the (device-side) debugger will crash on next step
this._clearLastStepRequest(e.event.threadid, e).then(e => {
this._findcmllocation(this.session.classes, e.event.throwlocation)
.then(tloc => {
this._findcmllocation(this.session.classes, e.event.catchlocation)
.then(cloc => {
var eventdata = {
event: e.event,
throwlocation: Object.assign({ threadid: e.event.threadid }, tloc),
catchlocation: Object.assign({ threadid: e.event.threadid }, cloc),
};
this.session.stoppedlocation = Object.assign({}, eventdata.throwlocation);
this._trigger('exception', eventdata);
})
})
});
}.bind(this)
};

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);
},
@@ -627,6 +633,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 +678,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 +727,7 @@ function _JDWP() {
}
);
},
// nestedTypes is not implemented on android
nestedTypes:function(ci) {
return new Command('NestedTypes:'+ci.name, 2, 8,
function() {
@@ -709,7 +740,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;
}
@@ -1027,21 +1058,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);
function(m, i, res) {
m.encode(res,i);
},
onevent
);
@@ -1125,6 +1170,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 +1202,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(os.EOL) + '</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);

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

@@ -0,0 +1,186 @@
<!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:inline-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, 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 msg = currfilter ? `${currfilter.matchCounts.true}/${logcount}` : logcount
getId('lcount').textContent = msg;
}
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 = 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>