25 Commits

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

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

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

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

* add trace config setting

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

* use an empty line table if the command request fails
2018-11-11 15:20:28 +00:00
adelphes
494bb83cbf version 0.5.0 2018-05-06 20:32:57 +01:00
Dave Holoway
9fca5cbe8c Merge pull request #28 from adelphes/kotlin-support
add basic support for kotlin source files
2018-05-06 20:13:24 +01:00
adelphes
5f0a02b17f add basic support for kotlin source files 2018-05-06 20:02:31 +01:00
adelphes
da36e8e457 added extension package state info 2017-06-24 19:53:22 +01:00
adelphes
3dbfd8ef2a Improved array handling. Better multidimensional array support. 2017-06-24 16:18:22 +01:00
adelphes
4a31b83eb9 add support for evaluateName so Add To Watch works again 2017-06-24 13:16:36 +01:00
adelphes
261c06f1d6 Ensure the cached fields are populated before showing the Exception UI 2017-06-24 12:20:21 +01:00
adelphes
130d79f6c2 add initial support for method call expressions 2017-06-24 12:02:36 +01:00
adelphes
8baf894fc9 add basic support for Exception UI 2017-06-20 13:40:57 +01:00
adelphes
92bd003122 Refactor expression evaluation into its own file 2017-06-18 13:56:04 +01:00
adelphes
13f116b3b3 format integers into other bases 2017-06-18 13:40:13 +01:00
adelphes
140e48cbd1 Add newlines to output
Cancel certain requests if the thread has been resumed
2017-06-18 12:57:38 +01:00
adelphes
7e8f471df4 revert util cleanup until dependencies are sorted 2017-06-14 16:10:58 +01:00
adelphes
09905eb85a Fix output newlines 2017-06-14 15:57:17 +01:00
adelphes
e76773e8e4 code tidy - fix lint warnings for unused fns, params and locals 2017-05-12 14:45:59 +01:00
14 changed files with 3856 additions and 863 deletions

View File

@@ -1,5 +1,23 @@
# Change Log # Change Log
### version 0.6.2
* Fix broken logcat command due to missing dependency
### version 0.6.1
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
### version 0.6.0
* Fix issue with breakpoints not enabling correctly
* Fix issue with JDWP failure on breakpoint hit
* Added support for diagnostic logs using trace configuration option
* Updated default apkFile path to match current releases of Android Studio
* Updated package dependencies
### version 0.5.0
* Debugger support for Kotlin source files
* Exception UI
* Fixed some console display issues
### version 0.4.1 ### version 0.4.1
* One day I will learn to update the changelog **before** I hit publish * One day I will learn to update the changelog **before** I hit publish
* Updated changelog * Updated changelog

View File

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

2701
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,14 +1,14 @@
'use strict' 'use strict'
const { const {
DebugSession, DebugSession,
ContinuedEvent, InitializedEvent, ExitedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent, Event, InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent,
Thread, StackFrame, Scope, Source, Handles, Breakpoint } = require('vscode-debugadapter'); Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
// node and external modules // node and external modules
const crypto = require('crypto'); const crypto = require('crypto');
const dom = require('xmldom').DOMParser; const dom = require('xmldom').DOMParser;
const fs = require('fs'); const fs = require('fs');
const Long = require('long'); const os = require('os');
const path = require('path'); const path = require('path');
const xpath = require('xpath'); const xpath = require('xpath');
@@ -16,12 +16,12 @@ const xpath = require('xpath');
const { ADBClient } = require('./adbclient'); const { ADBClient } = require('./adbclient');
const { Debugger } = require('./debugger'); const { Debugger } = require('./debugger');
const $ = require('./jq-promise'); const $ = require('./jq-promise');
const NumberBaseConverter = require('./nbc');
const { AndroidThread } = require('./threads'); const { AndroidThread } = require('./threads');
const { D, isEmptyObject } = require('./util'); const { D, onMessagePrint, isEmptyObject } = require('./util');
const { AndroidVariables } = require('./variables'); const { AndroidVariables } = require('./variables');
const { evaluate } = require('./expressions');
const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037); const ws_proxy = require('./wsproxy').proxy.Server(6037, 5037);
const { JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString } = require('./globals'); const { exmsg_var_name, signatureToFullyQualifiedType, ensure_path_end_slash,is_subpath_of,variableRefToThreadId } = require('./globals');
class AndroidDebugSession extends DebugSession { class AndroidDebugSession extends DebugSession {
@@ -73,6 +73,9 @@ class AndroidDebugSession extends DebugSession {
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests // flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
this._isDisconnecting = false; this._isDisconnecting = false;
// trace flag for printing diagnostic messages to the client Output Window
this.trace = false;
// this debugger uses one-based lines and columns // this debugger uses one-based lines and columns
this.setDebuggerLinesStartAt1(true); this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true); this.setDebuggerColumnsStartAt1(true);
@@ -82,7 +85,7 @@ class AndroidDebugSession extends DebugSession {
* The 'initialize' request is the first request called by the frontend * The 'initialize' request is the first request called by the frontend
* to interrogate the features the debug adapter provides. * to interrogate the features the debug adapter provides.
*/ */
initializeRequest(response/*: DebugProtocol.InitializeResponse*/, args/*: DebugProtocol.InitializeRequestArguments*/) { initializeRequest(response/*: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments*/) {
// This debug adapter implements the configurationDoneRequest. // This debug adapter implements the configurationDoneRequest.
response.body.supportsConfigurationDoneRequest = true; response.body.supportsConfigurationDoneRequest = true;
@@ -99,22 +102,39 @@ class AndroidDebugSession extends DebugSession {
// we support hit-count conditional breakpoints // we support hit-count conditional breakpoints
response.body.supportsHitConditionalBreakpoints = true; response.body.supportsHitConditionalBreakpoints = true;
// we support the new ExceptionInfoRequest
response.body.supportsExceptionInfoRequest = true;
this.sendResponse(response); this.sendResponse(response);
} }
LOG(msg) { LOG(msg) {
D(msg); if (!this.trace) {
this.sendEvent(new OutputEvent(msg)); D(msg);
}
// VSCode no longer auto-newlines output
this.sendEvent(new OutputEvent(msg + os.EOL));
} }
WARN(msg) { WARN(msg) {
D(msg = 'Warning: '+msg); D(msg = 'Warning: '+msg);
this.sendEvent(new OutputEvent(msg)); // the message will already be sent if trace is enabled
if (!this.trace) {
this.sendEvent(new OutputEvent(msg + os.EOL));
}
} }
failRequest(msg, response) { failRequest(msg, response) {
// yeah, it can happen sometimes... // yeah, it can happen sometimes...
this.WARN(msg); msg && this.WARN(msg);
if (response) {
response.success = false;
this.sendResponse(response);
}
}
cancelRequest(msg, response) {
D(msg); // just log it in debug - don't output it to the client
if (response) { if (response) {
response.success = false; response.success = false;
this.sendResponse(response); this.sendResponse(response);
@@ -129,6 +149,11 @@ class AndroidDebugSession extends DebugSession {
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response); this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
} }
cancelRequestThreadNotSuspended(requestName, threadId, response) {
// now that vscode can resume threads before the locals,callstack,etc are retrieved, we only need to cancel the request
this.cancelRequest(`${requestName} cancelled. Thread ${threadId} is not suspended`, response);
}
getThread(id) { getThread(id) {
var t; var t;
switch(typeof id) { switch(typeof id) {
@@ -206,6 +231,10 @@ class AndroidDebugSession extends DebugSession {
} }
launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) { launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
if (args && args.trace) {
this.trace = args.trace;
onMessagePrint(this.LOG.bind(this));
}
try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {} try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {}
// app_src_root must end in a path-separator for correct validation of sub-paths // app_src_root must end in a path-separator for correct validation of sub-paths
@@ -343,6 +372,9 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response); this.sendResponse(response);
return this.dbgr.resume(); return this.dbgr.resume();
}) })
.then(() => {
this.LOG('Application started');
})
.fail(e => { .fail(e => {
// exceptions use message, adbclient uses msg // exceptions use message, adbclient uses msg
this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available')); this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available'));
@@ -462,7 +494,7 @@ class AndroidDebugSession extends DebugSession {
package: pkgname, package: pkgname,
package_path: fpn, package_path: fpn,
srcroot: path.join(app_root,src_folder), srcroot: path.join(app_root,src_folder),
public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.java$/.test(sf)).map(sf => sf.match(/^(.*)\.java$/)[1]) public_classes: subfiles.filter(sf => /^[a-zA-Z_$][a-zA-Z0-9_$]*\.(?:java|kt)$/.test(sf)).map(sf => sf.match(/^(.*)\.(?:java|kt)$/)[1])
} }
} }
// add the subfiles to the list to process // add the subfiles to the list to process
@@ -503,7 +535,8 @@ class AndroidDebugSession extends DebugSession {
}) })
} }
configurationDoneRequest(response, args) { configurationDoneRequest(response/*, args*/) {
D('configurationDoneRequest');
this.waitForConfigurationDone.resolve(); this.waitForConfigurationDone.resolve();
this.sendResponse(response); this.sendResponse(response);
} }
@@ -518,7 +551,7 @@ class AndroidDebugSession extends DebugSession {
} }
} }
disconnectRequest(response, args) { disconnectRequest(response/*, args*/) {
D('disconnectRequest'); D('disconnectRequest');
this._isDisconnecting = true; this._isDisconnecting = true;
// if we're connected, ask ADB to terminate the app // if we're connected, ask ADB to terminate the app
@@ -534,13 +567,14 @@ class AndroidDebugSession extends DebugSession {
} }
onBreakpointStateChange(e) { onBreakpointStateChange(e) {
D('onBreakpointStateChange');
e.breakpoints.forEach(javabp => { e.breakpoints.forEach(javabp => {
// if there's no associated vsbp we're deleting it, so just ignore the update // if there's no associated vsbp we're deleting it, so just ignore the update
if (!javabp.vsbp) return; if (!javabp.vsbp) return;
var verified = !!javabp.state.match(/set|enabled/); var verified = !!javabp.state.match(/set|enabled/);
javabp.vsbp.verified = verified; javabp.vsbp.verified = verified;
javabp.vsbp.message = null; javabp.vsbp.message = null;
this.sendEvent(new BreakpointEvent('updated', javabp.vsbp)); this.sendEvent(new BreakpointEvent('changed', javabp.vsbp));
}); });
} }
@@ -565,6 +599,14 @@ class AndroidDebugSession extends DebugSession {
return bp; return bp;
} }
const sendBPResponse = (response, breakpoints) => {
D('setBreakPointsRequest response ' + JSON.stringify(breakpoints.map(bp => bp.verified)));
response.body = {
breakpoints,
};
this.sendResponse(response);
}
// the file must lie inside one of the source packages we found (and it must be have a .java extension) // the file must lie inside one of the source packages we found (and it must be have a .java extension)
var srcfolder = path.dirname(srcfpn); var srcfolder = path.dirname(srcfpn);
var pkginfo; var pkginfo;
@@ -577,13 +619,10 @@ class AndroidDebugSession extends DebugSession {
// create a fake pkginfo to use to construct the bp // create a fake pkginfo to use to construct the bp
pkginfo = { srcroot:this._android_sources_path } pkginfo = { srcroot:this._android_sources_path }
} }
if (!pkginfo || !/\.java$/i.test(srcfpn)) { if (!pkginfo || !/\.(java|kt)$/i.test(srcfpn)) {
// source file is not a java file or is outside of the known source packages // source file is not a java file or is outside of the known source packages
// just send back a list of unverified breakpoints // just send back a list of unverified breakpoints
response.body = { sendBPResponse(response, args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid')));
breakpoints: args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid'))
};
this.sendResponse(response);
return; return;
} }
@@ -633,7 +672,7 @@ class AndroidDebugSession extends DebugSession {
javabp.vsbp.order = idx; javabp.vsbp.order = idx;
javabp_arr.push(javabp); javabp_arr.push(javabp);
}). }).
then(javabp => _setup_breakpoints(o, ++idx, javabp_arr)); then((/*javabp*/) => _setup_breakpoints(o, ++idx, javabp_arr));
}; };
if (!this._set_breakpoints_queue) { if (!this._set_breakpoints_queue) {
@@ -650,10 +689,7 @@ class AndroidDebugSession extends DebugSession {
this._setup_breakpoints(this._queue[0]).then(javabp_arr => { this._setup_breakpoints(this._queue[0]).then(javabp_arr => {
// send back the VS Breakpoint instances // send back the VS Breakpoint instances
var response = this._queue[0].response; var response = this._queue[0].response;
response.body = { sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp));
breakpoints: javabp_arr.map(javabp => javabp.vsbp)
};
this._dbgr.sendResponse(response);
// .. and do the next one // .. and do the next one
this._queue.shift(); this._queue.shift();
this._next(); this._next();
@@ -681,7 +717,7 @@ class AndroidDebugSession extends DebugSession {
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) { threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
if (this._threads.array.length) { if (this._threads.array.length) {
console.log('threadsRequest: ' + this._threads.array.length); D('threadsRequest: ' + this._threads.array.length);
response.body = { response.body = {
threads: this._threads.array.filter(x=>x).map(t => { threads: this._threads.array.filter(x=>x).map(t => {
var javaid = parseInt(t.threadid, 16); var javaid = parseInt(t.threadid, 16);
@@ -702,7 +738,7 @@ class AndroidDebugSession extends DebugSession {
}; };
this.sendResponse(response); this.sendResponse(response);
}) })
.fail(e => { .fail(() => {
response.success = false; response.success = false;
this.sendResponse(response); this.sendResponse(response);
}); });
@@ -716,7 +752,7 @@ class AndroidDebugSession extends DebugSession {
// debugger threadid's are a padded 64bit hex string // debugger threadid's are a padded 64bit hex string
var thread = this.getThread(args.threadId); var thread = this.getThread(args.threadId);
if (!thread) return this.failRequestNoThread('Stack trace', args.threadId, response); if (!thread) return this.failRequestNoThread('Stack trace', args.threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Stack trace', args.threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Stack trace', args.threadId, response);
// retrieve the (stack) frames from the debugger // retrieve the (stack) frames from the debugger
this.dbgr.getframes(thread.threadid, {response, args, thread}) this.dbgr.getframes(thread.threadid, {response, args, thread})
@@ -732,8 +768,7 @@ class AndroidDebugSession extends DebugSession {
const endFrame = Math.min(startFrame + maxLevels, frames.length); const endFrame = Math.min(startFrame + maxLevels, frames.length);
var stack = [], totalFrames = frames.length, highest_known_source=0; var stack = [], totalFrames = frames.length, highest_known_source=0;
const android_src_path = this._android_sources_path || '{Android SDK}'; const android_src_path = this._android_sources_path || '{Android SDK}';
const device_api_level = this.dbgr.session.apilevel || '25'; for (var i = startFrame; (i < endFrame) && x.thread.paused; i++) {
for (var i= startFrame; i < endFrame; i++) {
// the stack_frame_id must be unique across all threads // the stack_frame_id must be unique across all threads
const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId; const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId;
const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`; const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`;
@@ -767,7 +802,10 @@ class AndroidDebugSession extends DebugSession {
: null; : null;
const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId); const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId);
pkginfo && (highest_known_source=i); pkginfo && (highest_known_source=i);
stack.push(new StackFrame(stack_frame_id, name, src, linenum, 0)); // we don't support column number when reporting source locations (because JDWP only supports line-granularity)
// but in order to get the Exception UI to show, we must have a non-zero column
const colnum = (!i && x.thread.paused.last_exception && x.thread.paused.reasons[0]==='exception') ? 1 : 0;
stack.push(new StackFrame(stack_frame_id, name, src, linenum, colnum));
} }
// trim the stack to exclude calls above the known sources // trim the stack to exclude calls above the known sources
if (this.callStackDisplaySize > 0) { if (this.callStackDisplaySize > 0) {
@@ -781,7 +819,7 @@ class AndroidDebugSession extends DebugSession {
}; };
this.sendResponse(response); this.sendResponse(response);
}) })
.fail((e,x) => { .fail(() => {
this.failRequest('No call stack is available', response); this.failRequest('No call stack is available', response);
}); });
} }
@@ -790,7 +828,7 @@ class AndroidDebugSession extends DebugSession {
var threadId = variableRefToThreadId(args.frameId); var threadId = variableRefToThreadId(args.frameId);
var thread = this.getThread(threadId); var thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Scopes',threadId, response); if (!thread) return this.failRequestNoThread('Scopes',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Scopes',threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Scopes', threadId, response);
var scopes = [new Scope("Local", args.frameId, false)]; var scopes = [new Scope("Local", args.frameId, false)];
response.body = { response.body = {
@@ -801,21 +839,30 @@ class AndroidDebugSession extends DebugSession {
if (last_exception && !last_exception.objvar) { if (last_exception && !last_exception.objvar) {
// retrieve the exception object // retrieve the exception object
thread.allocateExceptionScopeReference(args.frameId); thread.allocateExceptionScopeReference(args.frameId);
this.dbgr.getExceptionLocal(last_exception.exception, {response,scopes,last_exception}) this.dbgr.getExceptionLocal(last_exception.exception, {thread,response,scopes,last_exception})
.then((ex_local,x) => { .then((ex_local,x) => {
var {response,scopes,last_exception} = x; x.last_exception.objvar = ex_local;
last_exception.objvar = ex_local; return $.when(x, x.thread.getVariables(x.last_exception.scopeRef));
// put the exception first - otherwise it can get lost if there's a lot of locals
scopes.unshift(new Scope("Exception: "+ex_local.type.typename, last_exception.scopeRef, false));
this.sendResponse(response);
}) })
.fail(e => { this.sendResponse(response); }); .then((x, vars) => {
var {response,scopes,last_exception} = x;
// put the exception first - otherwise it can get lost if there's a lot of locals
scopes.unshift(new Scope("Exception: " + last_exception.objvar.type.typename, last_exception.scopeRef, false));
this.sendResponse(response);
// notify the exceptionInfo who may be waiting on us
if (last_exception.waitForExObject) {
var def = last_exception.waitForExObject;
last_exception.waitForExObject = null;
def.resolveWith(this, []);
}
})
.fail((/*e*/) => { this.sendResponse(response); });
return; return;
} }
this.sendResponse(response); this.sendResponse(response);
} }
sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) { sourceRequest(response/*: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments*/) {
var content = var content =
`/* `/*
The source for this class is unavailable. The source for this class is unavailable.
@@ -844,7 +891,7 @@ class AndroidDebugSession extends DebugSession {
var threadId = variableRefToThreadId(args.variablesReference); var threadId = variableRefToThreadId(args.variablesReference);
var thread = this.getThread(threadId); var thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Variables',threadId, response); if (!thread) return this.failRequestNoThread('Variables',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Variables',threadId, response); if (!thread.paused) return this.cancelRequestThreadNotSuspended('Variables',threadId, response);
thread.getVariables(args.variablesReference) thread.getVariables(args.variablesReference)
.then(vars => { .then(vars => {
@@ -877,7 +924,7 @@ class AndroidDebugSession extends DebugSession {
return; return;
} }
} }
var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid); var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid, thread.paused.last_exception && "Exception thrown");
thread.paused.stoppedEvent = event; thread.paused.stoppedEvent = event;
this.sendEvent(event); this.sendEvent(event);
} }
@@ -926,7 +973,7 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response); this.sendResponse(response);
// we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting // we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting
t.stepTimeout = setTimeout(t => { t.stepTimeout = setTimeout(t => {
console.log('Step timeout on thread:'+t.threadid); D('Step timeout on thread:'+t.threadid);
t.stepTimeout = null; t.stepTimeout = null;
this.checkPendingThreadBreaks(); this.checkPendingThreadBreaks();
}, 2000, t); }, 2000, t);
@@ -1070,11 +1117,11 @@ class AndroidDebugSession extends DebugSession {
if (!this._evals_queue.length) { if (!this._evals_queue.length) {
return; return;
} }
var {response, args, getvars, thread} = this._evals_queue[0]; var {response, args, getvars} = this._evals_queue[0];
// wait for any locals in the given context to be retrieved // wait for any locals in the given context to be retrieved
getvars.then((thread, locals, vars) => { getvars.then((thread, locals, vars) => {
return this.evaluate(args.expression, thread, locals, vars); return evaluate(args.expression, thread, locals, vars, this.dbgr);
}) })
.then((value,variablesReference) => { .then((value,variablesReference) => {
response.body = { result:value, variablesReference:variablesReference|0 }; response.body = { result:value, variablesReference:variablesReference|0 };
@@ -1090,479 +1137,52 @@ class AndroidDebugSession extends DebugSession {
}) })
} }
/* exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) {
Asynchronously evaluate an expression var thread = this.getThread(args.threadId);
*/ if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
evaluate(expression, thread, locals, vars) { if (!thread.paused) return this.cancelRequestThreadNotSuspended('Exception info', args.threadId, response);
D('evaluate: ' + expression); if (!thread.paused.last_exception) return this.failRequest('No exception available', response);
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]); if (!thread.paused.last_exception.objvar || !thread.paused.last_exception.cached) {
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]); // we must wait for the exception object to be retreived as a local (along with the message field)
if (!thread.paused.last_exception.waitForExObject) {
if (thread && !thread.paused) thread.paused.last_exception.waitForExObject = $.Deferred().then(() => {
return reject_evaluation('not available'); // redo the request
this.exceptionInfoRequest(response, args);
// special case for evaluating exception messages
// - this is called if the user tries to evaluate ':msg' from the locals
if (expression===exmsg_var_name) {
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
if (msglocal) {
return resolve_evaluation(vars._local_to_variable(msglocal).value);
}
}
return reject_evaluation('not available');
}
const parse_array_or_fncall = function(e) {
var arg, res = {arr:[], call:null};
// pre-call array indexes
while (e.expr[0] === '[') {
e.expr = e.expr.slice(1).trim();
if ((arg = parse_expression(e)) === null) return null;
res.arr.push(arg);
if (e.expr[0] !== ']') return null;
e.expr = e.expr.slice(1).trim();
}
if (res.arr.length) return res;
// method call
if (e.expr[0] === '(') {
res.call = []; e.expr = e.expr.slice(1).trim();
if (e.expr[0] !== ')') {
for (;;) {
if ((arg = parse_expression(e)) === null) return null;
res.call.push(arg);
if (e.expr[0] === ')') break;
if (e.expr[0] !== ',') return null;
e.expr = e.expr.slice(1).trim();
}
}
e.expr = e.expr.slice(1).trim();
// post-call array indexes
while (e.expr[0] === '[') {
e.expr = e.expr.slice(1).trim();
if ((arg = parse_expression(e)) === null) return null;
res.arr.push(arg);
if (e.expr[0] !== ']') return null;
e.expr = e.expr.slice(1).trim();
}
}
return res;
}
const parse_expression_term = function(e) {
if (e.expr[0] === '(') {
e.expr = e.expr.slice(1).trim();
var subexpr = {expr:e.expr};
var res = parse_expression(subexpr);
if (res) {
if (subexpr.expr[0] !== ')') return null;
e.expr = subexpr.expr.slice(1).trim();
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
// primitive typecast
var castexpr = parse_expression_term(e);
if (castexpr) castexpr.typecast = res.root_term;
res = castexpr;
}
}
return res;
}
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
if (unop) {
var op = unop[0].replace(/\s/g,'');
e.expr = e.expr.slice(unop[0].length).trim();
var res = parse_expression_term(e);
if (res) {
for (var i=op.length-1; i >= 0; --i)
res = { operator:op[i], rhs:res };
}
return res;
}
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
if (!root_term) return null;
var res = {
root_term: root_term[0],
root_term_type: ['boolean','boolean','null','ident','hexint','octint','decfloat','decint','char','echar','uchar','string'][[1,2,3,4,5,6,7,8,9,10,11,12].find(x => root_term[x])-1],
array_or_fncall: null,
members:[],
typecast:''
}
e.expr = e.expr.slice(res.root_term.length).trim();
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
// the root term is not allowed to be a method call
if (res.array_or_fncall.call) return null;
while (e.expr[0] === '.') {
// member expression
e.expr = e.expr.slice(1).trim();
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
if (!member_name) return null;
res.members.push(m = {member:member_name[0], array_or_fncall:null})
e.expr = e.expr.slice(m.member.length).trim();
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
}
return res;
}
const prec = {
'*':1,'%':1,'/':1,
'+':2,'-':2,
'<<':3,'>>':3,'>>>':3,
'<':4,'>':4,'<=':4,'>=':4,'instanceof':4,
'==':5,'!=':5,
'&':6,'^':7,'|':8,'&&':9,'||':10,'?':11,
}
const parse_expression = function(e) {
var res = parse_expression_term(e);
if (!e.currprec) e.currprec = [12];
for (;;) {
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/ );
if (!binary_operator) break;
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
if (precdiff > 0) {
// bigger number -> lower precendence -> end of (sub)expression
break;
}
if (precdiff === 0 && binary_operator[0]!=='?') {
// equal precedence, ltr evaluation
break;
}
// higher or equal precendence
e.currprec.unshift(e.currprec[0]+precdiff);
e.expr = e.expr.slice(binary_operator[0].length).trim();
// current or higher precendence
if (binary_operator[0]==='?') {
res = { condition:res, operator:binary_operator[0], ternary_true:null, ternary_false:null };
res.ternary_true = parse_expression(o);
symbol(e,':');
res.ternary_false = parse_expression(o);
} else {
res = { lhs:res, operator:binary_operator[0], rhs:parse_expression(e) };
}
e.currprec.shift();
}
return res;
}
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
const evaluate_number = (n) => {
n += '';
var numtype,m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base=10;
if (m) {
switch(m[2]) {
case 'b': base = 2; n = m[1]+m[3]; break;
case 'x': base = 16; n = m[1]+m[3]; break;
default: base = 8; break;
}
}
if (base !== 16 && /[fFdD]$/.test(n)) {
numtype = /[fF]$/.test(n) ? 'float' : 'double';
n = n.slice(0,-1);
} else if (/[lL]$/.test(n)) {
numtype = 'long'
n = n.slice(0,-1);
} else {
numtype = /\./.test(n) ? 'double' : 'int';
}
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
else n = parseInt(n, base)|0;
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
return { vtype:'literal',name:'',hasnullvalue:iszero,type:JTYPES[numtype],value:n,valid:true };
}
const evaluate_char = (char) => {
return { vtype:'literal',name:'',char:char,hasnullvalue:false,type:JTYPES.char,value:char.charCodeAt(0),valid:true };
}
const numberify = (local) => {
//if (local.type.signature==='C') return local.char.charCodeAt(0);
if (/^[FD]$/.test(local.type.signature))
return parseFloat(local.value);
if (local.type.signature==='J')
return parseInt(local.value,16);
return parseInt(local.value,10);
}
const stringify = (local) => {
var s;
if (JTYPES.isString(local.type)) s = local.string;
else if (JTYPES.isChar(local.type)) s= local.char;
else if (JTYPES.isPrimitive(local.type)) s = ''+local.value;
else if (local.hasnullvalue) s= '(null)';
if (typeof s === 'string')
return $.Deferred().resolveWith(this,[s]);
return this.dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
.then(s => s.string);
}
const evaluate_expression = (expr) => {
var q = $.Deferred(), local;
if (expr.operator) {
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary?'type':'types'} for operator '${expr.operator}'`),
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
var lhs_local;
return !expr.lhs
? // unary operator
evaluate_expression(expr.rhs)
.then(rhs_local => {
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
rhs_local.value = !rhs_local.value;
return rhs_local;
}
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
switch (rhs_local.type.typename) {
case 'long': rhs_local.value = rhs_local.value.replace(/./g,c => (15 - parseInt(c,16)).toString(16)); break;
default: rhs_local = evaluate_number(''+~rhs_local.value); break;
}
return rhs_local;
}
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
if (expr.operator === '+') return rhs_local;
switch (rhs_local.type.typename) {
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value,false,16).neg()); break;
default: rhs_local = evaluate_number(''+(-rhs_local.value)); break;
}
return rhs_local;
}
return invalid_operator('unary');
})
: // binary operator
evaluate_expression(expr.lhs)
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
.then(rhs_local => {
if ((lhs_local.type.signature==='J' && JTYPES.isInteger(rhs_local.type))
|| (rhs_local.type.signature==='J' && JTYPES.isInteger(lhs_local.type))) {
// one operand is a long, the other is an integer -> the result is a long
var a,b, lbase, rbase;
lbase = lhs_local.type.signature==='J' ? 16 : 10;
rbase = rhs_local.type.signature==='J' ? 16 : 10;
a = Long.fromString(''+lhs_local.value, false, lbase);
b = Long.fromString(''+rhs_local.value, false, rbase);
switch(expr.operator) {
case '+': a = a.add(b); break;
case '-': a = a.subtract(b); break;
case '*': a = a.multiply(b); break;
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
case '<<': a = a.shl(b); break;
case '>>': a = a.shr(b); break;
case '>>>': a = a.shru(b); break;
case '&': a = a.and(b); break;
case '|': a = a.or(b); break;
case '^': a = a.xor(b); break;
case '==': a = a.eq(b); break;
case '!=': a = !a.eq(b); break;
case '<': a = a.lt(b); break;
case '<=': a = a.lte(b); break;
case '>': a = a.gt(b); break;
case '>=': a = a.gte(b); break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.long,value:hex_long(a),valid:true };
}
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
// both are (non-long) integer types
var a = numberify(lhs_local), b= numberify(rhs_local);
switch(expr.operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': if (b) { a = Math.trunc(a/b); break } return divide_by_zero();
case '%': if (b) { a %= b; break; } return divide_by_zero();
case '<<': a<<=b; break;
case '>>': a>>=b; break;
case '>>>': a>>>=b; break;
case '&': a&=b; break;
case '|': a|=b; break;
case '^': a^=b; break;
case '==': a = a===b; break;
case '!=': a = a!==b; break;
case '<': a = a<b; break;
case '<=': a = a<=b; break;
case '>': a = a>b; break;
case '>=': a = a>=b; break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.int,value:''+a,valid:true };
}
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
var a = numberify(lhs_local), b= numberify(rhs_local);
switch(expr.operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': a /= b; break;
case '==': a = a===b; break;
case '!=': a = a!==b; break;
case '<': a = a<b; break;
case '<=': a = a<=b; break;
case '>': a = a>b; break;
case '>=': a = a>=b; break;
default: return invalid_operator();
}
if (typeof a === 'boolean')
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
// one of them must be a float or double
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES[result_type],value:''+a,valid:true };
}
else if (lhs_local.type.signature==='Z' && rhs_local.type.signature==='Z') {
// boolean operands
var a = lhs_local.value, b = rhs_local.value;
switch(expr.operator) {
case '&': case '&&': a = a && b; break;
case '|': case '||': a = a || b; break;
case '^': a = !!(a ^ b); break;
case '==': a = a===b; break;
case '!=': a = a!==b; break;
default: return invalid_operator();
}
return { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:a,valid:true };
}
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
return stringify(rhs_local).then(rhs_str => createJavaString(this.dbgr, lhs_local.string + rhs_str,{israw:true}));
}
return invalid_operator();
});
}
switch(expr.root_term_type) {
case 'boolean':
local = { vtype:'literal',name:'',hasnullvalue:false,type:JTYPES.boolean,value:expr.root_term!=='false',valid:true };
break;
case 'null':
const nullvalue = '0000000000000000'; // null reference value
local = { vtype:'literal',name:'',hasnullvalue:true,type:JTYPES.null,value:nullvalue,valid:true };
break;
case 'ident':
local = locals && locals.find(l => l.name === expr.root_term);
break;
case 'hexint':
case 'octint':
case 'decint':
case 'decfloat':
local = evaluate_number(expr.root_term);
break;
case 'char':
case 'echar':
case 'uchar':
local = evaluate_char(decode_char(expr.root_term.slice(1,-1)))
break;
case 'string':
// we must get the runtime to create string instances
q = createJavaString(this.dbgr, expr.root_term);
local = {valid:true}; // make sure we don't fail the evaluation
break;
}
if (!local || !local.valid) return reject_evaluation('not available');
// we've got the root term variable - work out the rest
q = expr.array_or_fncall.arr.reduce((q,index_expr) => {
return q.then(function(index_expr,local) { return evaluate_array_element.call(this,index_expr,local) }.bind(this,index_expr));
}, q);
q = expr.members.reduce((q,m) => {
return q.then(function(m,local) { return evaluate_member.call(this,m,local) }.bind(this,m));
}, q);
if (expr.typecast) {
q = q.then(function(type,local) { return evaluate_cast.call(this,type,local) }.bind(this,expr.typecast) )
}
// if it's a string literal, we are already waiting for the runtime to create the string
// - otherwise, start the evalaution...
if (expr.root_term_type !== 'string')
q.resolveWith(this,[local]);
return q;
}
const evaluate_array_element = (index_expr, arr_local) => {
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
return evaluate_expression(index_expr)
.then(function(arr_local, idx_local) {
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
var idx = numberify(idx_local);
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
return this.dbgr.getarrayvalues(arr_local, idx, 1)
}.bind(this,arr_local))
.then(els => els[0])
}
const evaluate_methodcall = (m, obj_local) => {
return reject_evaluation('Error: method calls are not supported');
}
const evaluate_member = (m, obj_local) => {
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
var chain;
if (m.array_or_fncall.call){
chain = evaluate_methodcall(m, obj_local);
}
// length is a 'fake' field of arrays, so special-case it
else if (JTYPES.isArray(obj_local.type) && m.member==='length') {
chain = evaluate_number(obj_local.arraylen);
}
// we also special-case :super (for object instances)
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
chain = this.dbgr.getsuperinstance(obj_local);
}
// anything else must be a real field
else {
chain = this.dbgr.getFieldValue(obj_local, m.member, true)
}
return chain.then(local => {
if (m.array_or_fncall.arr.length) {
var q = $.Deferred();
m.array_or_fncall.arr.reduce((q,index_expr) => {
return q.then(function(index_expr,local) { return evaluate_array_element(index_expr,local) }.bind(this,index_expr));
}, q);
return q.resolveWith(this, [local]);
}
});
}
const evaluate_cast = (type,local) => {
if (type === local.type.typename) return local;
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
// boolean cannot be converted from anything else
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
if (local.type.typename === 'long') {
// long to something else
var value = Long.fromString(local.value, true, 16);
switch(true) {
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2),16) << 24) >> 24); break;
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4),16) << 16) >> 16); break;
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8),16) | 0)); break;
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4),16))); break;
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber()+'F'); break;
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
default: return incompatible_cast();
}
} else {
switch(true) {
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
case (type === 'long'): local = evaluate_number(local.value+'L');break;
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value|0)); break;
case (type === 'float'): break;
case (type === 'double'): break;
default: return incompatible_cast();
}
}
local.type = JTYPES[type];
return local;
}
var e = { expr:expression.trim() };
var parsed_expression = parse_expression(e);
// if there's anything left, it's an error
if (parsed_expression && !e.expr) {
// the expression is well-formed - start the (asynchronous) evaluation
return evaluate_expression(parsed_expression)
.then(local => {
var v = vars._local_to_variable(local);
return resolve_evaluation(v.value, v.variablesReference);
}); });
}
return;
} }
var exobj = thread.paused.last_exception.objvar;
var exmsg = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
exmsg = (exmsg && exmsg.string) || '';
// the expression is not well-formed response.body = {
return reject_evaluation('not available'); /** ID of the exception that was thrown. */
} exceptionId: exobj.type.typename,
/** Descriptive text for the exception provided by the debug adapter. */
description: exmsg,
/** Mode that caused the exception notification to be raised. */
//'never' | 'always' | 'unhandled' | 'userUnhandled';
breakMode: 'always',
/** Detailed information about the exception. */
details: {
/** Message contained in the exception. */
message: exmsg,
/** Short type name of the exception object. */
typeName: exobj.type.typename,
/** Fully-qualified type name of the exception object. */
fullTypeName: signatureToFullyQualifiedType(exobj.type.signature),
/** Optional expression that can be evaluated in the current scope to obtain the exception object. */
//evaluateName: "evaluateName",
/** Stack trace at the time the exception was thrown. */
//stackTrace: "stackTrace",
/** Details of the exception contained by this exception, if any. */
//innerException: [],
}
}
this.sendResponse(response);
}
} }

View File

@@ -525,7 +525,7 @@ Debugger.prototype = {
}, },
_splitsrcfpn: function (srcfpn) { _splitsrcfpn: function (srcfpn) {
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.java$/); var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/);
return { return {
pkg: m[1].replace(/\/+/g, '.'), pkg: m[1].replace(/\/+/g, '.'),
type: m[2], type: m[2],
@@ -1054,6 +1054,50 @@ Debugger.prototype = {
return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra); return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra);
}, },
findNamedMethods(type_signature, name, method_signature) {
var x = { type_signature, name, method_signature }
const ismatch = function(x, y) {
if (!x || (x === y)) return true;
return (x instanceof RegExp) && x.test(y);
}
return this.gettypedebuginfo(x.type_signature)
.then(dbgtype => this._ensuremethods(dbgtype[x.type_signature]))
.then(typeinfo => ({
// resolving the methods only resolves the non-inherited methods
// if we can't find a matching method, we need to search the super types
dbgr: this,
def: $.Deferred(),
matches:[],
find_methods(typeinfo) {
for (var mid in typeinfo.methods) {
var m = typeinfo.methods[mid];
// does the name match
if (!ismatch(x.name, m.name)) continue;
// does the signature match
if (!ismatch(x.method_signature, m.genericsig || m.sig)) continue;
// add it to the results
this.matches.push(m);
}
// search the supertype
if (typeinfo.type.signature === 'Ljava/lang/Object;') {
this.def.resolveWith(this.dbgr, [this.matches]);
return this;
}
this.dbgr._ensuresuper(typeinfo)
.then(typeinfo => {
return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature)
})
.then((dbgtype, sig) => {
return this.dbgr._ensuremethods(dbgtype[sig])
})
.then(typeinfo => {
this.find_methods(typeinfo)
});
return this;
}
}).find_methods(typeinfo).def)
},
getstringchars: function (stringref, extra) { getstringchars: function (stringref, extra) {
return this.session.adbclient.jdwp_command({ return this.session.adbclient.jdwp_command({
ths: this, ths: this,
@@ -1269,8 +1313,10 @@ Debugger.prototype = {
return $.Deferred().resolveWith(this, [typeinfo]); return $.Deferred().resolveWith(this, [typeinfo]);
} }
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') { if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
typeinfo.super = null; if (typeinfo.info.reftype.string !== 'array') {
return $.Deferred().resolveWith(this, [typeinfo]); typeinfo.super = null;
return $.Deferred().resolveWith(this, [typeinfo]);
}
} }
typeinfo.super = $.Deferred(); typeinfo.super = $.Deferred();
@@ -1379,6 +1425,15 @@ Debugger.prototype = {
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo), cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
}) })
.then(function (linetable, methodinfo) { .then(function (linetable, methodinfo) {
// if the request failed, just return a blank table
if (linetable.errorcode) {
linetable = {
errorcode: linetable.errorcode,
start: '00000000000000000000000000000000',
end: '00000000000000000000000000000000',
lines:[],
}
}
// the linetable does not correlate code indexes with line numbers // the linetable does not correlate code indexes with line numbers
// - location searching relies on the table being ordered by code indexes // - location searching relies on the table being ordered by code indexes
linetable.lines.sort(function (a, b) { linetable.lines.sort(function (a, b) {

535
src/expressions.js Normal file
View File

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

View File

@@ -27,6 +27,22 @@ const JTYPES = {
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] }, fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
} }
function signatureToFullyQualifiedType(sig) {
var arr = sig.match(/^\[+/) || '';
if (arr) {
arr = '[]'.repeat(arr[0].length);
sig = sig.slice(0, arr.length/2);
}
var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/);
if (!m) return '';
if (m[3]) {
return m[3].replace(/[/$]/g,'.') + arr;
} else if (m[4]) {
return m[4].replace(/[/$]/g, '.') + arr;
}
return JTYPES.fromPrimSig(sig[0]) + arr;
}
// the special name given to exception message fields // the special name given to exception message fields
const exmsg_var_name = ':msg'; const exmsg_var_name = ':msg';
@@ -67,5 +83,5 @@ function variableRefToThreadId(variablesReference) {
Object.assign(exports, { Object.assign(exports, {
JTYPES,exmsg_var_name,ensure_path_end_slash,is_subpath_of,decode_char,variableRefToThreadId,createJavaString JTYPES, exmsg_var_name, ensure_path_end_slash, is_subpath_of, decode_char, variableRefToThreadId, createJavaString, signatureToFullyQualifiedType
}); });

View File

@@ -1,5 +1,5 @@
const $ = require('./jq-promise'); const $ = require('./jq-promise');
const { atob,btoa,D,getutf8bytes,fromutf8bytes,intToHex } = require('./util'); const { btoa,D,E,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
/* /*
JDWP - The Java Debug Wire Protocol JDWP - The Java Debug Wire Protocol
*/ */
@@ -96,8 +96,8 @@ function _JDWP() {
return; return;
} }
if (this.errorcode != 0) { if (this.errorcode !== 0) {
console.error("Command failed: error " + this.errorcode, this); E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this);
} }
if (!this.errorcode && this.command && this.command.replydecodefn) { if (!this.errorcode && this.command && this.command.replydecodefn) {
@@ -109,7 +109,10 @@ function _JDWP() {
return; return;
} }
this.decoded = {empty:true}; this.decoded = {
empty: true,
errorcode: this.errorcode,
};
} }
this.decodereply = function(ths,s) { this.decodereply = function(ths,s) {
@@ -537,12 +540,12 @@ function _JDWP() {
} }
m = signature.match(/^(\[+)(.+)$/); m = signature.match(/^(\[+)(.+)$/);
if (m) { if (m) {
var elementtype = this.signaturetotype(m[2]); var elementtype = this.signaturetotype(m[1].slice(0,-1) + m[2]);
return { return {
signature:signature, signature:signature,
arraydims:m[1].length, arraydims:m[1].length,
elementtype: elementtype, elementtype: elementtype,
typename:elementtype.typename+m[1].replace(/\[/g,'[]'), typename:elementtype.typename+'[]',
} }
} }
var primitivetypes = { var primitivetypes = {

View File

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

View File

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

11
src/state.js Normal file
View File

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

View File

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

View File

@@ -79,6 +79,8 @@ class AndroidVariables {
value: x.varinfo.objvar.value, value: x.varinfo.objvar.value,
valid:true, valid:true,
}); });
// create the fully qualified names to use for evaluation
fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`);
x.varinfo.cached = fields; x.varinfo.cached = fields;
return this._local_to_variable(fields); return this._local_to_variable(fields);
}); });
@@ -100,9 +102,10 @@ class AndroidVariables {
return $.Deferred().resolve(variables); return $.Deferred().resolve(variables);
} }
// get the elements for the specified range // get the elements for the specified range
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count) return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo})
.then((elements) => { .then((elements, x) => {
varinfo.cached = elements; elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`);
x.varinfo.cached = elements;
return this._local_to_variable(elements); return this._local_to_variable(elements);
}); });
} }
@@ -158,7 +161,7 @@ class AndroidVariables {
*/ */
_local_to_variable(v) { _local_to_variable(v) {
if (Array.isArray(v)) return v.filter(v => v.valid).map(v => this._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; var varref = 0, objvalue, evaluateName = v.fqname || v.name, formats = {}, typename = v.type.package ? `${v.type.package}.${v.type.typename}` : v.type.typename;
switch(true) { switch(true) {
case v.hasnullvalue && JTYPES.isReference(v.type): case v.hasnullvalue && JTYPES.isReference(v.type):
// null object or array type // null object or array type
@@ -188,7 +191,7 @@ class AndroidVariables {
varref = this._getObjectIdReference(v.type, v.value); varref = this._getObjectIdReference(v.type, v.value);
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] }; 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 objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
break; break;
case JTYPES.isObject(v.type): case JTYPES.isObject(v.type):
// non-null object instance - add another variable reference so the user can expand // non-null object instance - add another variable reference so the user can expand
@@ -207,10 +210,27 @@ class AndroidVariables {
case v.type.signature === 'J': case v.type.signature === 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work // because JS cannot handle 64bit ints, we need a bit of extra work
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,''); var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
objvalue = NumberBaseConverter.hexToDec(v64hex, true); objvalue = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
formats.oct = formats.bin = '';
// 24 bit chunks...
for (var s=v64hex,uint; s; s = s.slice(0,-6)) {
uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
formats.oct = uint.toString(8) + formats.oct;
formats.bin = uint.toString(2) + formats.bin;
}
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
break;
case /^[BIS]$/.test(v.type.signature):
objvalue = formats.dec = v.value.toString();
var uint = (v.value >>> 0);
formats.hex = '0x' + uint.toString(16);
formats.oct = '0c' + uint.toString(8);
formats.bin = '0b' + uint.toString(2);
break; break;
default: default:
// other primitives: int, boolean, etc // other primitives: boolean, etc
objvalue = v.value.toString(); objvalue = v.value.toString();
break; break;
} }
@@ -223,6 +243,7 @@ class AndroidVariables {
name: v.name, name: v.name,
type: typename, type: typename,
value: objvalue, value: objvalue,
evaluateName,
variablesReference: varref, variablesReference: varref,
} }
} }