mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 09:59:25 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1e7c86092 | ||
|
|
690f9dc23a | ||
|
|
27ecd41b68 | ||
|
|
756a1cea29 | ||
|
|
fc2ce97a23 | ||
|
|
de8abc62bc | ||
|
|
8cc31476b3 | ||
|
|
494bb83cbf | ||
|
|
9fca5cbe8c | ||
|
|
5f0a02b17f | ||
|
|
da36e8e457 | ||
|
|
3dbfd8ef2a | ||
|
|
4a31b83eb9 | ||
|
|
261c06f1d6 | ||
|
|
130d79f6c2 | ||
|
|
8baf894fc9 | ||
|
|
92bd003122 | ||
|
|
13f116b3b3 | ||
|
|
140e48cbd1 | ||
|
|
7e8f471df4 | ||
|
|
09905eb85a | ||
|
|
e76773e8e4 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Change Log
|
||||
|
||||
### version 0.6.0
|
||||
* Fix issue with breakpoints not enabling correctly
|
||||
* Fix issue with JDWP failure on breakpoint hit
|
||||
* Added support for diagnostic logs using trace configuration option
|
||||
* Updated default apkFile path to match current releases of Android Studio
|
||||
* Updated package dependencies
|
||||
|
||||
### version 0.5.0
|
||||
* Debugger support for Kotlin source files
|
||||
* Exception UI
|
||||
* Fixed some console display issues
|
||||
|
||||
### version 0.4.1
|
||||
* One day I will learn to update the changelog **before** I hit publish
|
||||
* Updated changelog
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const vscode = require('vscode');
|
||||
const { AndroidContentProvider } = require('./src/contentprovider');
|
||||
const { openLogcatWindow } = require('./src/logcat');
|
||||
const state = require('./src/state');
|
||||
|
||||
function getADBPort() {
|
||||
var defaultPort = 5037;
|
||||
@@ -38,6 +39,7 @@ function activate(context) {
|
||||
var spliceparams = [context.subscriptions.length,0].concat(disposables);
|
||||
Array.prototype.splice.apply(context.subscriptions,spliceparams);
|
||||
}
|
||||
|
||||
exports.activate = activate;
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
|
||||
3091
package-lock.json
generated
Normal file
3091
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "android-dev-ext",
|
||||
"displayName": "Android",
|
||||
"description": "Android debugging support for VS Code",
|
||||
"version": "0.4.1",
|
||||
"version": "0.6.0",
|
||||
"publisher": "adelphes",
|
||||
"preview": true,
|
||||
"license": "MIT",
|
||||
@@ -35,12 +35,15 @@
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "java"
|
||||
},
|
||||
{
|
||||
"language": "kotlin"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "android",
|
||||
"label": "Android Debug",
|
||||
"label": "Android",
|
||||
"program": "./src/debugMain.js",
|
||||
"runtime": "node",
|
||||
"configurationAttributes": {
|
||||
@@ -59,7 +62,7 @@
|
||||
"apkFile": {
|
||||
"type": "string",
|
||||
"description": "Fully qualified path to the built APK (Android Application Package)",
|
||||
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk"
|
||||
"default": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk"
|
||||
},
|
||||
"adbPort": {
|
||||
"type": "integer",
|
||||
@@ -90,6 +93,11 @@
|
||||
"type": "string",
|
||||
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
|
||||
"default": ""
|
||||
},
|
||||
"trace": {
|
||||
"type": "boolean",
|
||||
"description": "Set to true to output debugging logs for diagnostics",
|
||||
"default": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,10 +105,10 @@
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "android",
|
||||
"name": "Android Debug",
|
||||
"name": "Android",
|
||||
"request": "launch",
|
||||
"appSrcRoot": "${workspaceRoot}/app/src/main",
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
|
||||
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
|
||||
"adbPort": 5037
|
||||
}
|
||||
],
|
||||
@@ -113,7 +121,7 @@
|
||||
"request": "launch",
|
||||
"name": "${2:Launch App}",
|
||||
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
|
||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
|
||||
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
|
||||
"adbPort": 5037
|
||||
}
|
||||
}
|
||||
@@ -127,19 +135,19 @@
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-debugprotocol": "^1.15.0",
|
||||
"vscode-debugadapter": "^1.15.0",
|
||||
"long": "^3.2.0",
|
||||
"vscode-debugprotocol": "^1.32.0",
|
||||
"vscode-debugadapter": "^1.32.0",
|
||||
"long": "^4.0.0",
|
||||
"ws": "^1.1.1",
|
||||
"xmldom": "^0.1.27",
|
||||
"xpath": "^0.0.23"
|
||||
"xpath": "^0.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^2.0.3",
|
||||
"vscode": "^1.0.0",
|
||||
"mocha": "^2.3.3",
|
||||
"eslint": "^3.6.0",
|
||||
"@types/node": "^6.0.40",
|
||||
"@types/mocha": "^2.2.32"
|
||||
"typescript": "^3.1.6",
|
||||
"vscode": "^1.1.21",
|
||||
"mocha": "^5.2.0",
|
||||
"eslint": "^5.9.0",
|
||||
"@types/node": "^10.12.5",
|
||||
"@types/mocha": "^5.2.5"
|
||||
}
|
||||
}
|
||||
640
src/debugMain.js
640
src/debugMain.js
@@ -1,14 +1,14 @@
|
||||
'use strict'
|
||||
const {
|
||||
DebugSession,
|
||||
ContinuedEvent, InitializedEvent, ExitedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent, Event,
|
||||
Thread, StackFrame, Scope, Source, Handles, Breakpoint } = require('vscode-debugadapter');
|
||||
InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, ThreadEvent, OutputEvent,
|
||||
Thread, StackFrame, Scope, Source, Breakpoint } = require('vscode-debugadapter');
|
||||
|
||||
// node and external modules
|
||||
const crypto = require('crypto');
|
||||
const dom = require('xmldom').DOMParser;
|
||||
const fs = require('fs');
|
||||
const Long = require('long');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const xpath = require('xpath');
|
||||
|
||||
@@ -16,12 +16,12 @@ const xpath = require('xpath');
|
||||
const { ADBClient } = require('./adbclient');
|
||||
const { Debugger } = require('./debugger');
|
||||
const $ = require('./jq-promise');
|
||||
const NumberBaseConverter = require('./nbc');
|
||||
const { AndroidThread } = require('./threads');
|
||||
const { D, isEmptyObject } = require('./util');
|
||||
const { D, onMessagePrint, isEmptyObject } = require('./util');
|
||||
const { AndroidVariables } = require('./variables');
|
||||
const { evaluate } = require('./expressions');
|
||||
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 {
|
||||
|
||||
@@ -73,6 +73,9 @@ class AndroidDebugSession extends DebugSession {
|
||||
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
|
||||
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.setDebuggerLinesStartAt1(true);
|
||||
this.setDebuggerColumnsStartAt1(true);
|
||||
@@ -82,7 +85,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
* The 'initialize' request is the first request called by the frontend
|
||||
* 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.
|
||||
response.body.supportsConfigurationDoneRequest = true;
|
||||
@@ -99,22 +102,39 @@ class AndroidDebugSession extends DebugSession {
|
||||
// we support hit-count conditional breakpoints
|
||||
response.body.supportsHitConditionalBreakpoints = true;
|
||||
|
||||
// we support the new ExceptionInfoRequest
|
||||
response.body.supportsExceptionInfoRequest = true;
|
||||
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
LOG(msg) {
|
||||
if (!this.trace) {
|
||||
D(msg);
|
||||
this.sendEvent(new OutputEvent(msg));
|
||||
}
|
||||
// VSCode no longer auto-newlines output
|
||||
this.sendEvent(new OutputEvent(msg + os.EOL));
|
||||
}
|
||||
|
||||
WARN(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) {
|
||||
// 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) {
|
||||
response.success = false;
|
||||
this.sendResponse(response);
|
||||
@@ -129,6 +149,11 @@ class AndroidDebugSession extends DebugSession {
|
||||
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) {
|
||||
var t;
|
||||
switch(typeof id) {
|
||||
@@ -206,6 +231,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
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) {}
|
||||
// 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);
|
||||
return this.dbgr.resume();
|
||||
})
|
||||
.then(() => {
|
||||
this.LOG('Application started');
|
||||
})
|
||||
.fail(e => {
|
||||
// exceptions use message, adbclient uses msg
|
||||
this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available'));
|
||||
@@ -462,7 +494,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
package: pkgname,
|
||||
package_path: fpn,
|
||||
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
|
||||
@@ -503,7 +535,8 @@ class AndroidDebugSession extends DebugSession {
|
||||
})
|
||||
}
|
||||
|
||||
configurationDoneRequest(response, args) {
|
||||
configurationDoneRequest(response/*, args*/) {
|
||||
D('configurationDoneRequest');
|
||||
this.waitForConfigurationDone.resolve();
|
||||
this.sendResponse(response);
|
||||
}
|
||||
@@ -518,7 +551,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
disconnectRequest(response, args) {
|
||||
disconnectRequest(response/*, args*/) {
|
||||
D('disconnectRequest');
|
||||
this._isDisconnecting = true;
|
||||
// if we're connected, ask ADB to terminate the app
|
||||
@@ -534,13 +567,14 @@ class AndroidDebugSession extends DebugSession {
|
||||
}
|
||||
|
||||
onBreakpointStateChange(e) {
|
||||
D('onBreakpointStateChange');
|
||||
e.breakpoints.forEach(javabp => {
|
||||
// if there's no associated vsbp we're deleting it, so just ignore the update
|
||||
if (!javabp.vsbp) return;
|
||||
var verified = !!javabp.state.match(/set|enabled/);
|
||||
javabp.vsbp.verified = verified;
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
var srcfolder = path.dirname(srcfpn);
|
||||
var pkginfo;
|
||||
@@ -577,13 +619,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
// create a fake pkginfo to use to construct the bp
|
||||
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
|
||||
// just send back a list of unverified breakpoints
|
||||
response.body = {
|
||||
breakpoints: args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid'))
|
||||
};
|
||||
this.sendResponse(response);
|
||||
sendBPResponse(response, args.breakpoints.map(bp => unverified_breakpoint(bp, 'The breakpoint location is not valid')));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -633,7 +672,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
javabp.vsbp.order = idx;
|
||||
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) {
|
||||
@@ -650,10 +689,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this._setup_breakpoints(this._queue[0]).then(javabp_arr => {
|
||||
// send back the VS Breakpoint instances
|
||||
var response = this._queue[0].response;
|
||||
response.body = {
|
||||
breakpoints: javabp_arr.map(javabp => javabp.vsbp)
|
||||
};
|
||||
this._dbgr.sendResponse(response);
|
||||
sendBPResponse(response, javabp_arr.map(javabp => javabp.vsbp));
|
||||
// .. and do the next one
|
||||
this._queue.shift();
|
||||
this._next();
|
||||
@@ -681,7 +717,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
|
||||
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
|
||||
if (this._threads.array.length) {
|
||||
console.log('threadsRequest: ' + this._threads.array.length);
|
||||
D('threadsRequest: ' + this._threads.array.length);
|
||||
response.body = {
|
||||
threads: this._threads.array.filter(x=>x).map(t => {
|
||||
var javaid = parseInt(t.threadid, 16);
|
||||
@@ -702,7 +738,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
};
|
||||
this.sendResponse(response);
|
||||
})
|
||||
.fail(e => {
|
||||
.fail(() => {
|
||||
response.success = false;
|
||||
this.sendResponse(response);
|
||||
});
|
||||
@@ -716,7 +752,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
// debugger threadid's are a padded 64bit hex string
|
||||
var thread = this.getThread(args.threadId);
|
||||
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
|
||||
this.dbgr.getframes(thread.threadid, {response, args, thread})
|
||||
@@ -732,8 +768,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
const endFrame = Math.min(startFrame + maxLevels, frames.length);
|
||||
var stack = [], totalFrames = frames.length, highest_known_source=0;
|
||||
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; i++) {
|
||||
for (var i = startFrame; (i < endFrame) && x.thread.paused; i++) {
|
||||
// the stack_frame_id must be unique across all threads
|
||||
const stack_frame_id = x.thread.addStackFrameVariable(frames[i], i).frameId;
|
||||
const name = `${frames[i].method.owningclass.name}.${frames[i].method.name}`;
|
||||
@@ -767,7 +802,10 @@ class AndroidDebugSession extends DebugSession {
|
||||
: null;
|
||||
const src = new Source(sourcefile, srcpath, srcpath ? 0 : srcRefId);
|
||||
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
|
||||
if (this.callStackDisplaySize > 0) {
|
||||
@@ -781,7 +819,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
};
|
||||
this.sendResponse(response);
|
||||
})
|
||||
.fail((e,x) => {
|
||||
.fail(() => {
|
||||
this.failRequest('No call stack is available', response);
|
||||
});
|
||||
}
|
||||
@@ -790,7 +828,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
var threadId = variableRefToThreadId(args.frameId);
|
||||
var thread = this.getThread(threadId);
|
||||
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)];
|
||||
response.body = {
|
||||
@@ -801,21 +839,30 @@ class AndroidDebugSession extends DebugSession {
|
||||
if (last_exception && !last_exception.objvar) {
|
||||
// retrieve the exception object
|
||||
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) => {
|
||||
var {response,scopes,last_exception} = x;
|
||||
last_exception.objvar = ex_local;
|
||||
// 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);
|
||||
x.last_exception.objvar = ex_local;
|
||||
return $.when(x, x.thread.getVariables(x.last_exception.scopeRef));
|
||||
})
|
||||
.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;
|
||||
}
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
sourceRequest(response/*: DebugProtocol.SourceResponse*/, args/*: DebugProtocol.SourceArguments*/) {
|
||||
sourceRequest(response/*: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments*/) {
|
||||
var content =
|
||||
`/*
|
||||
The source for this class is unavailable.
|
||||
@@ -844,7 +891,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
var threadId = variableRefToThreadId(args.variablesReference);
|
||||
var thread = this.getThread(threadId);
|
||||
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)
|
||||
.then(vars => {
|
||||
@@ -877,7 +924,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
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;
|
||||
this.sendEvent(event);
|
||||
}
|
||||
@@ -926,7 +973,7 @@ class AndroidDebugSession extends DebugSession {
|
||||
this.sendResponse(response);
|
||||
// we time the step - if it takes more than 2 seconds, we switch to any other threads that are waiting
|
||||
t.stepTimeout = setTimeout(t => {
|
||||
console.log('Step timeout on thread:'+t.threadid);
|
||||
D('Step timeout on thread:'+t.threadid);
|
||||
t.stepTimeout = null;
|
||||
this.checkPendingThreadBreaks();
|
||||
}, 2000, t);
|
||||
@@ -1070,11 +1117,11 @@ class AndroidDebugSession extends DebugSession {
|
||||
if (!this._evals_queue.length) {
|
||||
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
|
||||
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) => {
|
||||
response.body = { result:value, variablesReference:variablesReference|0 };
|
||||
@@ -1090,479 +1137,52 @@ class AndroidDebugSession extends DebugSession {
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Asynchronously evaluate an expression
|
||||
*/
|
||||
evaluate(expression, thread, locals, vars) {
|
||||
D('evaluate: ' + expression);
|
||||
exceptionInfoRequest(response /*DebugProtocol.ExceptionInfoResponse*/, args /**/) {
|
||||
var thread = this.getThread(args.threadId);
|
||||
if (!thread) return this.failRequestNoThread('Exception info', args.threadId, response);
|
||||
if (!thread.paused) return this.cancelRequestThreadNotSuspended('Exception info', args.threadId, response);
|
||||
if (!thread.paused.last_exception) return this.failRequest('No exception available', response);
|
||||
|
||||
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(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();
|
||||
if (!thread.paused.last_exception.objvar || !thread.paused.last_exception.cached) {
|
||||
// we must wait for the exception object to be retreived as a local (along with the message field)
|
||||
if (!thread.paused.last_exception.waitForExObject) {
|
||||
thread.paused.last_exception.waitForExObject = $.Deferred().then(() => {
|
||||
// redo the request
|
||||
this.exceptionInfoRequest(response, args);
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
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) || '';
|
||||
|
||||
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();
|
||||
response.body = {
|
||||
/** 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: [],
|
||||
}
|
||||
}
|
||||
local.type = JTYPES[type];
|
||||
return local;
|
||||
this.sendResponse(response);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -525,7 +525,7 @@ Debugger.prototype = {
|
||||
},
|
||||
|
||||
_splitsrcfpn: function (srcfpn) {
|
||||
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.java$/);
|
||||
var m = srcfpn.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/);
|
||||
return {
|
||||
pkg: m[1].replace(/\/+/g, '.'),
|
||||
type: m[2],
|
||||
@@ -1054,6 +1054,50 @@ Debugger.prototype = {
|
||||
return this.invokeMethod(objectid, threadid, type_signature || 'Ljava/lang/Object;', 'toString', '()Ljava/lang/String;', [], extra);
|
||||
},
|
||||
|
||||
findNamedMethods(type_signature, name, method_signature) {
|
||||
var x = { type_signature, name, method_signature }
|
||||
const ismatch = function(x, y) {
|
||||
if (!x || (x === y)) return true;
|
||||
return (x instanceof RegExp) && x.test(y);
|
||||
}
|
||||
return this.gettypedebuginfo(x.type_signature)
|
||||
.then(dbgtype => this._ensuremethods(dbgtype[x.type_signature]))
|
||||
.then(typeinfo => ({
|
||||
// resolving the methods only resolves the non-inherited methods
|
||||
// if we can't find a matching method, we need to search the super types
|
||||
dbgr: this,
|
||||
def: $.Deferred(),
|
||||
matches:[],
|
||||
find_methods(typeinfo) {
|
||||
for (var mid in typeinfo.methods) {
|
||||
var m = typeinfo.methods[mid];
|
||||
// does the name match
|
||||
if (!ismatch(x.name, m.name)) continue;
|
||||
// does the signature match
|
||||
if (!ismatch(x.method_signature, m.genericsig || m.sig)) continue;
|
||||
// add it to the results
|
||||
this.matches.push(m);
|
||||
}
|
||||
// search the supertype
|
||||
if (typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||
this.def.resolveWith(this.dbgr, [this.matches]);
|
||||
return this;
|
||||
}
|
||||
this.dbgr._ensuresuper(typeinfo)
|
||||
.then(typeinfo => {
|
||||
return this.dbgr.gettypedebuginfo(typeinfo.super.signature, typeinfo.super.signature)
|
||||
})
|
||||
.then((dbgtype, sig) => {
|
||||
return this.dbgr._ensuremethods(dbgtype[sig])
|
||||
})
|
||||
.then(typeinfo => {
|
||||
this.find_methods(typeinfo)
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}).find_methods(typeinfo).def)
|
||||
},
|
||||
|
||||
getstringchars: function (stringref, extra) {
|
||||
return this.session.adbclient.jdwp_command({
|
||||
ths: this,
|
||||
@@ -1269,9 +1313,11 @@ Debugger.prototype = {
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
}
|
||||
if (typeinfo.info.reftype.string !== 'class' || typeinfo.type.signature[0] !== 'L' || typeinfo.type.signature === 'Ljava/lang/Object;') {
|
||||
if (typeinfo.info.reftype.string !== 'array') {
|
||||
typeinfo.super = null;
|
||||
return $.Deferred().resolveWith(this, [typeinfo]);
|
||||
}
|
||||
}
|
||||
|
||||
typeinfo.super = $.Deferred();
|
||||
this.session.adbclient.jdwp_command({
|
||||
@@ -1379,6 +1425,15 @@ Debugger.prototype = {
|
||||
cmd: this.JDWP.Commands.lineTable(methodinfo.owningclass, methodinfo),
|
||||
})
|
||||
.then(function (linetable, methodinfo) {
|
||||
// if the request failed, just return a blank table
|
||||
if (linetable.errorcode) {
|
||||
linetable = {
|
||||
errorcode: linetable.errorcode,
|
||||
start: '00000000000000000000000000000000',
|
||||
end: '00000000000000000000000000000000',
|
||||
lines:[],
|
||||
}
|
||||
}
|
||||
// the linetable does not correlate code indexes with line numbers
|
||||
// - location searching relies on the table being ordered by code indexes
|
||||
linetable.lines.sort(function (a, b) {
|
||||
|
||||
535
src/expressions.js
Normal file
535
src/expressions.js
Normal file
@@ -0,0 +1,535 @@
|
||||
'use strict'
|
||||
const Long = require('long');
|
||||
const $ = require('./jq-promise');
|
||||
const { D } = require('./util');
|
||||
const { JTYPES, exmsg_var_name, decode_char, createJavaString } = require('./globals');
|
||||
|
||||
/*
|
||||
Asynchronously evaluate an expression
|
||||
*/
|
||||
exports.evaluate = function(expression, thread, locals, vars, dbgr) {
|
||||
D('evaluate: ' + expression);
|
||||
|
||||
const reject_evaluation = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
|
||||
const resolve_evaluation = (value, variablesReference) => $.Deferred().resolveWith(this, [value, variablesReference]);
|
||||
|
||||
if (thread && !thread.paused)
|
||||
return reject_evaluation('not available');
|
||||
|
||||
// special case for evaluating exception messages
|
||||
// - this is called if the user tries to evaluate ':msg' from the locals
|
||||
if (expression === exmsg_var_name) {
|
||||
if (thread && thread.paused.last_exception && thread.paused.last_exception.cached) {
|
||||
var msglocal = thread.paused.last_exception.cached.find(v => v.name === exmsg_var_name);
|
||||
if (msglocal) {
|
||||
return resolve_evaluation(vars._local_to_variable(msglocal).value);
|
||||
}
|
||||
}
|
||||
return reject_evaluation('not available');
|
||||
}
|
||||
|
||||
const parse_array_or_fncall = function (e) {
|
||||
var arg, res = { arr: [], call: null };
|
||||
// pre-call array indexes
|
||||
while (e.expr[0] === '[') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.arr.push(arg);
|
||||
if (e.expr[0] !== ']') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
if (res.arr.length) return res;
|
||||
// method call
|
||||
if (e.expr[0] === '(') {
|
||||
res.call = []; e.expr = e.expr.slice(1).trim();
|
||||
if (e.expr[0] !== ')') {
|
||||
for (; ;) {
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.call.push(arg);
|
||||
if (e.expr[0] === ')') break;
|
||||
if (e.expr[0] !== ',') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
}
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
// post-call array indexes
|
||||
while (e.expr[0] === '[') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
if ((arg = parse_expression(e)) === null) return null;
|
||||
res.arr.push(arg);
|
||||
if (e.expr[0] !== ']') return null;
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const parse_expression_term = function (e) {
|
||||
if (e.expr[0] === '(') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
var subexpr = { expr: e.expr };
|
||||
var res = parse_expression(subexpr);
|
||||
if (res) {
|
||||
if (subexpr.expr[0] !== ')') return null;
|
||||
e.expr = subexpr.expr.slice(1).trim();
|
||||
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.members.length && !res.array_or_fncall.call && !res.array_or_fncall.arr.length) {
|
||||
// primitive typecast
|
||||
var castexpr = parse_expression_term(e);
|
||||
if (castexpr) castexpr.typecast = res.root_term;
|
||||
res = castexpr;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
|
||||
if (unop) {
|
||||
var op = unop[0].replace(/\s/g, '');
|
||||
e.expr = e.expr.slice(unop[0].length).trim();
|
||||
var res = parse_expression_term(e);
|
||||
if (res) {
|
||||
for (var i = op.length - 1; i >= 0; --i)
|
||||
res = { operator: op[i], rhs: res };
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.\d+(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
|
||||
if (!root_term) return null;
|
||||
var res = {
|
||||
root_term: root_term[0],
|
||||
root_term_type: ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'][[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1],
|
||||
array_or_fncall: null,
|
||||
members: [],
|
||||
typecast: ''
|
||||
}
|
||||
e.expr = e.expr.slice(res.root_term.length).trim();
|
||||
if ((res.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||
// the root term is not allowed to be a method call
|
||||
if (res.array_or_fncall.call) return null;
|
||||
while (e.expr[0] === '.') {
|
||||
// member expression
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
var m, member_name = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
|
||||
if (!member_name) return null;
|
||||
res.members.push(m = { member: member_name[0], array_or_fncall: null })
|
||||
e.expr = e.expr.slice(m.member.length).trim();
|
||||
if ((m.array_or_fncall = parse_array_or_fncall(e)) === null) return null;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const prec = {
|
||||
'*': 1, '%': 1, '/': 1,
|
||||
'+': 2, '-': 2,
|
||||
'<<': 3, '>>': 3, '>>>': 3,
|
||||
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
|
||||
'==': 5, '!=': 5,
|
||||
'&': 6, '^': 7, '|': 8, '&&': 9, '||': 10, '?': 11,
|
||||
}
|
||||
const parse_expression = function (e) {
|
||||
var res = parse_expression_term(e);
|
||||
|
||||
if (!e.currprec) e.currprec = [12];
|
||||
for (; ;) {
|
||||
var binary_operator = e.expr.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
|
||||
if (!binary_operator) break;
|
||||
var precdiff = (prec[binary_operator[0]] || 12) - e.currprec[0];
|
||||
if (precdiff > 0) {
|
||||
// bigger number -> lower precendence -> end of (sub)expression
|
||||
break;
|
||||
}
|
||||
if (precdiff === 0 && binary_operator[0] !== '?') {
|
||||
// equal precedence, ltr evaluation
|
||||
break;
|
||||
}
|
||||
// higher or equal precendence
|
||||
e.currprec.unshift(e.currprec[0] + precdiff);
|
||||
e.expr = e.expr.slice(binary_operator[0].length).trim();
|
||||
// current or higher precendence
|
||||
if (binary_operator[0] === '?') {
|
||||
res = { condition: res, operator: binary_operator[0], ternary_true: null, ternary_false: null };
|
||||
res.ternary_true = parse_expression(e);
|
||||
if (e.expr[0] === ':') {
|
||||
e.expr = e.expr.slice(1).trim();
|
||||
res.ternary_false = parse_expression(e);
|
||||
}
|
||||
} else {
|
||||
res = { lhs: res, operator: binary_operator[0], rhs: parse_expression(e) };
|
||||
}
|
||||
e.currprec.shift();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
const hex_long = long => ('000000000000000' + long.toUnsigned().toString(16)).slice(-16);
|
||||
const evaluate_number = (n) => {
|
||||
n += '';
|
||||
var numtype, m = n.match(/^([+-]?)0([bBxX0-7])(.+)/), base = 10;
|
||||
if (m) {
|
||||
switch (m[2]) {
|
||||
case 'b': base = 2; n = m[1] + m[3]; break;
|
||||
case 'x': base = 16; n = m[1] + m[3]; break;
|
||||
default: base = 8; break;
|
||||
}
|
||||
}
|
||||
if (base !== 16 && /[fFdD]$/.test(n)) {
|
||||
numtype = /[fF]$/.test(n) ? 'float' : 'double';
|
||||
n = n.slice(0, -1);
|
||||
} else if (/[lL]$/.test(n)) {
|
||||
numtype = 'long'
|
||||
n = n.slice(0, -1);
|
||||
} else {
|
||||
numtype = /\./.test(n) ? 'double' : 'int';
|
||||
}
|
||||
if (numtype === 'long') n = hex_long(Long.fromString(n, false, base));
|
||||
else if (/^[fd]/.test(numtype)) n = (base === 10) ? parseFloat(n) : parseInt(n, base);
|
||||
else n = parseInt(n, base) | 0;
|
||||
|
||||
const iszero = /^[+-]?0+(\.0*)?$/.test(n);
|
||||
return { vtype: 'literal', name: '', hasnullvalue: iszero, type: JTYPES[numtype], value: n, valid: true };
|
||||
}
|
||||
const evaluate_char = (char) => {
|
||||
return { vtype: 'literal', name: '', char: char, hasnullvalue: false, type: JTYPES.char, value: char.charCodeAt(0), valid: true };
|
||||
}
|
||||
const numberify = (local) => {
|
||||
//if (local.type.signature==='C') return local.char.charCodeAt(0);
|
||||
if (/^[FD]$/.test(local.type.signature))
|
||||
return parseFloat(local.value);
|
||||
if (local.type.signature === 'J')
|
||||
return parseInt(local.value, 16);
|
||||
return parseInt(local.value, 10);
|
||||
}
|
||||
const stringify = (local) => {
|
||||
var s;
|
||||
if (JTYPES.isString(local.type)) s = local.string;
|
||||
else if (JTYPES.isChar(local.type)) s = local.char;
|
||||
else if (JTYPES.isPrimitive(local.type)) s = '' + local.value;
|
||||
else if (local.hasnullvalue) s = '(null)';
|
||||
if (typeof s === 'string')
|
||||
return $.Deferred().resolveWith(this, [s]);
|
||||
return dbgr.invokeToString(local.value, local.info.frame.threadid, local.type.signature)
|
||||
.then(s => s.string);
|
||||
}
|
||||
const evaluate_expression = (expr) => {
|
||||
var q = $.Deferred(), local;
|
||||
if (expr.operator) {
|
||||
const invalid_operator = (unary) => reject_evaluation(`Invalid ${unary ? 'type' : 'types'} for operator '${expr.operator}'`),
|
||||
divide_by_zero = () => reject_evaluation('ArithmeticException: divide by zero');
|
||||
var lhs_local;
|
||||
return !expr.lhs
|
||||
? // unary operator
|
||||
evaluate_expression(expr.rhs)
|
||||
.then(rhs_local => {
|
||||
if (expr.operator === '!' && JTYPES.isBoolean(rhs_local.type)) {
|
||||
rhs_local.value = !rhs_local.value;
|
||||
return rhs_local;
|
||||
}
|
||||
else if (expr.operator === '~' && JTYPES.isInteger(rhs_local.type)) {
|
||||
switch (rhs_local.type.typename) {
|
||||
case 'long': rhs_local.value = rhs_local.value.replace(/./g, c => (15 - parseInt(c, 16)).toString(16)); break;
|
||||
default: rhs_local = evaluate_number('' + ~rhs_local.value); break;
|
||||
}
|
||||
return rhs_local;
|
||||
}
|
||||
else if (/[+-]/.test(expr.operator) && JTYPES.isInteger(rhs_local.type)) {
|
||||
if (expr.operator === '+') return rhs_local;
|
||||
switch (rhs_local.type.typename) {
|
||||
case 'long': rhs_local.value = hex_long(Long.fromString(rhs_local.value, false, 16).neg()); break;
|
||||
default: rhs_local = evaluate_number('' + (-rhs_local.value)); break;
|
||||
}
|
||||
return rhs_local;
|
||||
}
|
||||
return invalid_operator('unary');
|
||||
})
|
||||
: // binary operator
|
||||
evaluate_expression(expr.lhs)
|
||||
.then(x => (lhs_local = x) && evaluate_expression(expr.rhs))
|
||||
.then(rhs_local => {
|
||||
if ((lhs_local.type.signature === 'J' && JTYPES.isInteger(rhs_local.type))
|
||||
|| (rhs_local.type.signature === 'J' && JTYPES.isInteger(lhs_local.type))) {
|
||||
// one operand is a long, the other is an integer -> the result is a long
|
||||
var a, b, lbase, rbase;
|
||||
lbase = lhs_local.type.signature === 'J' ? 16 : 10;
|
||||
rbase = rhs_local.type.signature === 'J' ? 16 : 10;
|
||||
a = Long.fromString('' + lhs_local.value, false, lbase);
|
||||
b = Long.fromString('' + rhs_local.value, false, rbase);
|
||||
switch (expr.operator) {
|
||||
case '+': a = a.add(b); break;
|
||||
case '-': a = a.subtract(b); break;
|
||||
case '*': a = a.multiply(b); break;
|
||||
case '/': if (!b.isZero()) { a = a.divide(b); break } return divide_by_zero();
|
||||
case '%': if (!b.isZero()) { a = a.mod(b); break; } return divide_by_zero();
|
||||
case '<<': a = a.shl(b); break;
|
||||
case '>>': a = a.shr(b); break;
|
||||
case '>>>': a = a.shru(b); break;
|
||||
case '&': a = a.and(b); break;
|
||||
case '|': a = a.or(b); break;
|
||||
case '^': a = a.xor(b); break;
|
||||
case '==': a = a.eq(b); break;
|
||||
case '!=': a = !a.eq(b); break;
|
||||
case '<': a = a.lt(b); break;
|
||||
case '<=': a = a.lte(b); break;
|
||||
case '>': a = a.gt(b); break;
|
||||
case '>=': a = a.gte(b); break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.long, value: hex_long(a), valid: true };
|
||||
}
|
||||
else if (JTYPES.isInteger(lhs_local.type) && JTYPES.isInteger(rhs_local.type)) {
|
||||
// both are (non-long) integer types
|
||||
var a = numberify(lhs_local), b = numberify(rhs_local);
|
||||
switch (expr.operator) {
|
||||
case '+': a += b; break;
|
||||
case '-': a -= b; break;
|
||||
case '*': a *= b; break;
|
||||
case '/': if (b) { a = Math.trunc(a / b); break } return divide_by_zero();
|
||||
case '%': if (b) { a %= b; break; } return divide_by_zero();
|
||||
case '<<': a <<= b; break;
|
||||
case '>>': a >>= b; break;
|
||||
case '>>>': a >>>= b; break;
|
||||
case '&': a &= b; break;
|
||||
case '|': a |= b; break;
|
||||
case '^': a ^= b; break;
|
||||
case '==': a = a === b; break;
|
||||
case '!=': a = a !== b; break;
|
||||
case '<': a = a < b; break;
|
||||
case '<=': a = a <= b; break;
|
||||
case '>': a = a > b; break;
|
||||
case '>=': a = a >= b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.int, value: '' + a, valid: true };
|
||||
}
|
||||
else if (JTYPES.isNumber(lhs_local.type) && JTYPES.isNumber(rhs_local.type)) {
|
||||
var a = numberify(lhs_local), b = numberify(rhs_local);
|
||||
switch (expr.operator) {
|
||||
case '+': a += b; break;
|
||||
case '-': a -= b; break;
|
||||
case '*': a *= b; break;
|
||||
case '/': a /= b; break;
|
||||
case '==': a = a === b; break;
|
||||
case '!=': a = a !== b; break;
|
||||
case '<': a = a < b; break;
|
||||
case '<=': a = a <= b; break;
|
||||
case '>': a = a > b; break;
|
||||
case '>=': a = a >= b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
if (typeof a === 'boolean')
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
// one of them must be a float or double
|
||||
var result_type = 'float double'.split(' ')[Math.max("FD".indexOf(lhs_local.type.signature), "FD".indexOf(rhs_local.type.signature))];
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES[result_type], value: '' + a, valid: true };
|
||||
}
|
||||
else if (lhs_local.type.signature === 'Z' && rhs_local.type.signature === 'Z') {
|
||||
// boolean operands
|
||||
var a = lhs_local.value, b = rhs_local.value;
|
||||
switch (expr.operator) {
|
||||
case '&': case '&&': a = a && b; break;
|
||||
case '|': case '||': a = a || b; break;
|
||||
case '^': a = !!(a ^ b); break;
|
||||
case '==': a = a === b; break;
|
||||
case '!=': a = a !== b; break;
|
||||
default: return invalid_operator();
|
||||
}
|
||||
return { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: a, valid: true };
|
||||
}
|
||||
else if (expr.operator === '+' && JTYPES.isString(lhs_local.type)) {
|
||||
return stringify(rhs_local).then(rhs_str => createJavaString(dbgr, lhs_local.string + rhs_str, { israw: true }));
|
||||
}
|
||||
return invalid_operator();
|
||||
});
|
||||
}
|
||||
switch (expr.root_term_type) {
|
||||
case 'boolean':
|
||||
local = { vtype: 'literal', name: '', hasnullvalue: false, type: JTYPES.boolean, value: expr.root_term !== 'false', valid: true };
|
||||
break;
|
||||
case 'null':
|
||||
const nullvalue = '0000000000000000'; // null reference value
|
||||
local = { vtype: 'literal', name: '', hasnullvalue: true, type: JTYPES.null, value: nullvalue, valid: true };
|
||||
break;
|
||||
case 'ident':
|
||||
local = locals && locals.find(l => l.name === expr.root_term);
|
||||
break;
|
||||
case 'hexint':
|
||||
case 'octint':
|
||||
case 'decint':
|
||||
case 'decfloat':
|
||||
local = evaluate_number(expr.root_term);
|
||||
break;
|
||||
case 'char':
|
||||
case 'echar':
|
||||
case 'uchar':
|
||||
local = evaluate_char(decode_char(expr.root_term.slice(1, -1)))
|
||||
break;
|
||||
case 'string':
|
||||
// we must get the runtime to create string instances
|
||||
q = createJavaString(dbgr, expr.root_term);
|
||||
local = { valid: true }; // make sure we don't fail the evaluation
|
||||
break;
|
||||
}
|
||||
if (!local || !local.valid) return reject_evaluation('not available');
|
||||
// we've got the root term variable - work out the rest
|
||||
q = expr.array_or_fncall.arr.reduce((q, index_expr) => {
|
||||
return q.then(function (index_expr, local) { return evaluate_array_element.call(this, index_expr, local) }.bind(this, index_expr));
|
||||
}, q);
|
||||
q = expr.members.reduce((q, m) => {
|
||||
return q.then(function (m, local) { return evaluate_member.call(this, m, local) }.bind(this, m));
|
||||
}, q);
|
||||
if (expr.typecast) {
|
||||
q = q.then(function (type, local) { return evaluate_cast.call(this, type, local) }.bind(this, expr.typecast))
|
||||
}
|
||||
// if it's a string literal, we are already waiting for the runtime to create the string
|
||||
// - otherwise, start the evalaution...
|
||||
if (expr.root_term_type !== 'string')
|
||||
q.resolveWith(this, [local]);
|
||||
return q;
|
||||
}
|
||||
const evaluate_array_element = (index_expr, arr_local) => {
|
||||
if (arr_local.type.signature[0] !== '[') return reject_evaluation(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
|
||||
if (arr_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||
return evaluate_expression(index_expr)
|
||||
.then(function (arr_local, idx_local) {
|
||||
if (!JTYPES.isInteger(idx_local.type)) return reject_evaluation('TypeError: array index is not an integer value');
|
||||
var idx = numberify(idx_local);
|
||||
if (idx < 0 || idx >= arr_local.arraylen) return reject_evaluation(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
|
||||
return dbgr.getarrayvalues(arr_local, idx, 1)
|
||||
}.bind(this, arr_local))
|
||||
.then(els => els[0])
|
||||
}
|
||||
const evaluate_methodcall = (m, obj_local) => {
|
||||
// until we can figure out why method invokes with parameters crash the debugger, disallow parameterised calls
|
||||
if (m.array_or_fncall.call.length)
|
||||
return reject_evaluation('Error: method calls with parameter values are not supported');
|
||||
|
||||
// find any methods matching the member name with any parameters in the signature
|
||||
return dbgr.findNamedMethods(obj_local.type.signature, m.member, /^/)
|
||||
.then(methods => {
|
||||
if (!methods[0])
|
||||
return reject_evaluation(`Error: method '${m.member}()' not found`);
|
||||
// evaluate any parameters (and wait for the results)
|
||||
return $.when({methods},...m.array_or_fncall.call.map(evaluate_expression));
|
||||
})
|
||||
.then((x,...paramValues) => {
|
||||
// filter the method based upon the types of parameters - note that null types and integer literals can match multiple types
|
||||
paramValues = paramValues = paramValues.map(p => p[0]);
|
||||
var matchers = paramValues.map(p => {
|
||||
switch(true) {
|
||||
case p.type.signature === 'I':
|
||||
// match bytes/shorts/ints/longs/floats/doubles within range
|
||||
if (p.value >= -128 && p.value <= 127) return /^[BSIJFD]$/
|
||||
if (p.value >= -32768 && p.value <= 32767) return /^[SIJFD]$/
|
||||
return /^[IJFD]$/;
|
||||
case p.type.signature === 'F':
|
||||
return /^[FD]$/;
|
||||
case p.type.signature === 'Lnull;':
|
||||
return /^[LT\[]/; // any reference type
|
||||
default:
|
||||
// anything else must be an exact signature match (for now - in reality we should allow subclassed type)
|
||||
return new RegExp(`^${p.type.signature.replace(/[$]/g,x=>'\\'+x)}$`);
|
||||
}
|
||||
});
|
||||
var methods = x.methods.filter(m => {
|
||||
// extract a list of parameter types
|
||||
var paramtypere = /\[*([BSIJFDCZ]|([LT][^;]+;))/g;
|
||||
for (var x, ptypes=[]; x = paramtypere.exec(m.sig); ) {
|
||||
ptypes.push(x[0]);
|
||||
}
|
||||
// the last paramter type is the return value
|
||||
ptypes.pop();
|
||||
// check if they match
|
||||
if (ptypes.length !== paramValues.length)
|
||||
return;
|
||||
return matchers.filter(m => {
|
||||
return !m.test(ptypes.shift())
|
||||
}).length === 0;
|
||||
});
|
||||
if (!methods[0])
|
||||
return reject_evaluation(`Error: incompatible parameters for method '${m.member}'`);
|
||||
// convert the parameters to exact debugger-compatible values
|
||||
paramValues = paramValues.map(p => {
|
||||
if (p.type.signature.length === 1)
|
||||
return { type: p.type.typename, value: p.value};
|
||||
return { type: 'oref', value: p.value };
|
||||
})
|
||||
return dbgr.invokeMethod(obj_local.value, thread.threadid, obj_local.type.signature, m.member, methods[0].genericsig || methods[0].sig, paramValues, {});
|
||||
});
|
||||
}
|
||||
const evaluate_member = (m, obj_local) => {
|
||||
if (!JTYPES.isReference(obj_local.type)) return reject_evaluation('TypeError: value is not a reference type');
|
||||
if (obj_local.hasnullvalue) return reject_evaluation('NullPointerException');
|
||||
var chain;
|
||||
if (m.array_or_fncall.call) {
|
||||
chain = evaluate_methodcall(m, obj_local);
|
||||
}
|
||||
// length is a 'fake' field of arrays, so special-case it
|
||||
else if (JTYPES.isArray(obj_local.type) && m.member === 'length') {
|
||||
chain = $.Deferred().resolve(evaluate_number(obj_local.arraylen));
|
||||
}
|
||||
// we also special-case :super (for object instances)
|
||||
else if (JTYPES.isObject(obj_local.type) && m.member === ':super') {
|
||||
chain = dbgr.getsuperinstance(obj_local);
|
||||
}
|
||||
// anything else must be a real field
|
||||
else {
|
||||
chain = dbgr.getFieldValue(obj_local, m.member, true)
|
||||
}
|
||||
|
||||
return chain.then(local => {
|
||||
if (m.array_or_fncall.arr.length) {
|
||||
var q = $.Deferred();
|
||||
m.array_or_fncall.arr.reduce((q, index_expr) => {
|
||||
return q.then(function (index_expr, local) { return evaluate_array_element(index_expr, local) }.bind(this, index_expr));
|
||||
}, q);
|
||||
return q.resolveWith(this, [local]);
|
||||
}
|
||||
});
|
||||
}
|
||||
const evaluate_cast = (type, local) => {
|
||||
if (type === local.type.typename) return local;
|
||||
const incompatible_cast = () => reject_evaluation(`Incompatible cast from ${local.type.typename} to ${type}`);
|
||||
// boolean cannot be converted from anything else
|
||||
if (type === 'boolean' || local.type.typename === 'boolean') return incompatible_cast();
|
||||
if (local.type.typename === 'long') {
|
||||
// long to something else
|
||||
var value = Long.fromString(local.value, true, 16);
|
||||
switch (true) {
|
||||
case (type === 'byte'): local = evaluate_number((parseInt(value.toString(16).slice(-2), 16) << 24) >> 24); break;
|
||||
case (type === 'short'): local = evaluate_number((parseInt(value.toString(16).slice(-4), 16) << 16) >> 16); break;
|
||||
case (type === 'int'): local = evaluate_number((parseInt(value.toString(16).slice(-8), 16) | 0)); break;
|
||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(parseInt(value.toString(16).slice(-4), 16))); break;
|
||||
case (type === 'float'): local = evaluate_number(value.toSigned().toNumber() + 'F'); break;
|
||||
case (type === 'double'): local = evaluate_number(value.toSigned().toNumber() + 'D'); break;
|
||||
default: return incompatible_cast();
|
||||
}
|
||||
} else {
|
||||
switch (true) {
|
||||
case (type === 'byte'): local = evaluate_number((local.value << 24) >> 24); break;
|
||||
case (type === 'short'): local = evaluate_number((local.value << 16) >> 16); break;
|
||||
case (type === 'int'): local = evaluate_number((local.value | 0)); break;
|
||||
case (type === 'long'): local = evaluate_number(local.value + 'L'); break;
|
||||
case (type === 'char'): local = evaluate_char(String.fromCharCode(local.value | 0)); break;
|
||||
case (type === 'float'): break;
|
||||
case (type === 'double'): break;
|
||||
default: return incompatible_cast();
|
||||
}
|
||||
}
|
||||
local.type = JTYPES[type];
|
||||
return local;
|
||||
}
|
||||
|
||||
var e = { expr: expression.trim() };
|
||||
var parsed_expression = parse_expression(e);
|
||||
// if there's anything left, it's an error
|
||||
if (parsed_expression && !e.expr) {
|
||||
// the expression is well-formed - start the (asynchronous) evaluation
|
||||
return evaluate_expression(parsed_expression)
|
||||
.then(local => {
|
||||
var v = vars._local_to_variable(local);
|
||||
return resolve_evaluation(v.value, v.variablesReference);
|
||||
});
|
||||
}
|
||||
|
||||
// the expression is not well-formed
|
||||
return reject_evaluation('not available');
|
||||
}
|
||||
@@ -27,6 +27,22 @@ const JTYPES = {
|
||||
fromPrimSig(sig) { return JTYPES['byte,short,int,long,float,double,char,boolean'.split(',')['BSIJFDCZ'.indexOf(sig)]] },
|
||||
}
|
||||
|
||||
function signatureToFullyQualifiedType(sig) {
|
||||
var arr = sig.match(/^\[+/) || '';
|
||||
if (arr) {
|
||||
arr = '[]'.repeat(arr[0].length);
|
||||
sig = sig.slice(0, arr.length/2);
|
||||
}
|
||||
var m = sig.match(/^((L([^<;]+).)|T([^;]+).|.)/);
|
||||
if (!m) return '';
|
||||
if (m[3]) {
|
||||
return m[3].replace(/[/$]/g,'.') + arr;
|
||||
} else if (m[4]) {
|
||||
return m[4].replace(/[/$]/g, '.') + arr;
|
||||
}
|
||||
return JTYPES.fromPrimSig(sig[0]) + arr;
|
||||
}
|
||||
|
||||
// the special name given to exception message fields
|
||||
const exmsg_var_name = ':msg';
|
||||
|
||||
@@ -67,5 +83,5 @@ function variableRefToThreadId(variablesReference) {
|
||||
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
15
src/jdwp.js
15
src/jdwp.js
@@ -1,5 +1,5 @@
|
||||
const $ = require('./jq-promise');
|
||||
const { atob,btoa,D,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
|
||||
const { btoa,D,E,getutf8bytes,fromutf8bytes,intToHex } = require('./util');
|
||||
/*
|
||||
JDWP - The Java Debug Wire Protocol
|
||||
*/
|
||||
@@ -96,8 +96,8 @@ function _JDWP() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.errorcode != 0) {
|
||||
console.error("Command failed: error " + this.errorcode, this);
|
||||
if (this.errorcode !== 0) {
|
||||
E(`JDWP command failed '${this.command.name}'. Error ${this.errorcode}`, this);
|
||||
}
|
||||
|
||||
if (!this.errorcode && this.command && this.command.replydecodefn) {
|
||||
@@ -109,7 +109,10 @@ function _JDWP() {
|
||||
return;
|
||||
}
|
||||
|
||||
this.decoded = {empty:true};
|
||||
this.decoded = {
|
||||
empty: true,
|
||||
errorcode: this.errorcode,
|
||||
};
|
||||
}
|
||||
|
||||
this.decodereply = function(ths,s) {
|
||||
@@ -537,12 +540,12 @@ function _JDWP() {
|
||||
}
|
||||
m = signature.match(/^(\[+)(.+)$/);
|
||||
if (m) {
|
||||
var elementtype = this.signaturetotype(m[2]);
|
||||
var elementtype = this.signaturetotype(m[1].slice(0,-1) + m[2]);
|
||||
return {
|
||||
signature:signature,
|
||||
arraydims:m[1].length,
|
||||
elementtype: elementtype,
|
||||
typename:elementtype.typename+m[1].replace(/\[/g,'[]'),
|
||||
typename:elementtype.typename+'[]',
|
||||
}
|
||||
}
|
||||
var primitivetypes = {
|
||||
|
||||
@@ -40,7 +40,7 @@ var Deferred = exports.Deferred = function(p, parent) {
|
||||
var faildef = $.Deferred(null, this);
|
||||
var p = this._promise.catch(function(a) {
|
||||
if (a.stack) {
|
||||
console.error(a.stack);
|
||||
util.E(a.stack);
|
||||
a = [a];
|
||||
}
|
||||
if (this.def._context === null && this.def._parent)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
'use strict'
|
||||
// vscode stuff
|
||||
const { EventEmitter, Uri } = require('vscode');
|
||||
// node and external modules
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -38,7 +36,7 @@ class LogcatContent {
|
||||
onlog: this.onLogcatContent.bind(this),
|
||||
onclose: this.onLogcatDisconnect.bind(this),
|
||||
});
|
||||
}).then(x => {
|
||||
}).then(() => {
|
||||
this._state = 'connected';
|
||||
this._initwait = null;
|
||||
resolve(this.content);
|
||||
@@ -55,20 +53,20 @@ class LogcatContent {
|
||||
return this.htmlBootstrap({connected:true, status:'',oldlogs:''});
|
||||
// if we're in the disconnected state, and this.content is called, it means the user has requested
|
||||
// this logcat again - check if the device has reconnected
|
||||
return this._initwait = new Promise((resolve, reject) => {
|
||||
return this._initwait = new Promise((resolve/*, reject*/) => {
|
||||
// clear the logs first - if we successfully reconnect, we will be retrieving the entire logcat again
|
||||
this._prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
|
||||
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
|
||||
this._adbclient.logcat({
|
||||
onlog: this.onLogcatContent.bind(this),
|
||||
onclose: this.onLogcatDisconnect.bind(this),
|
||||
}).then(x => {
|
||||
}).then(() => {
|
||||
// we successfully reconnected
|
||||
this._state = 'connected';
|
||||
this._prevlogs = null;
|
||||
this._initwait = null;
|
||||
resolve(this.content);
|
||||
}).fail(e => {
|
||||
}).fail((/*e*/) => {
|
||||
// reconnection failed - put the logs back and return the cached info
|
||||
this._logs = this._prevlogs._logs;
|
||||
this._htmllogs = this._prevlogs._htmllogs;
|
||||
@@ -161,7 +159,7 @@ class LogcatContent {
|
||||
this.renotify();
|
||||
}
|
||||
}
|
||||
onLogcatDisconnect(e) {
|
||||
onLogcatDisconnect(/*e*/) {
|
||||
if (this._state === 'disconnected') return;
|
||||
this._state = 'disconnected';
|
||||
this.sendDisconnectMsg();
|
||||
@@ -215,7 +213,7 @@ LogcatContent.initWebSocketServer = function () {
|
||||
this.wss = null;
|
||||
LogcatContent._wssdone.resolveWith(LogcatContent, []);
|
||||
});
|
||||
this.wss.on('error', err => {
|
||||
this.wss.on('error', (/*err*/) => {
|
||||
if (!LogcatContent._wss) {
|
||||
// listen failed -try the next port
|
||||
this.retries++ , this.port++;
|
||||
@@ -245,7 +243,7 @@ function openLogcatWindow(vscode) {
|
||||
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
|
||||
var adbargs = ['-P',''+adbport,'start-server'];
|
||||
try {
|
||||
var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
/*var stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
|
||||
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
|
||||
}
|
||||
})
|
||||
@@ -282,7 +280,7 @@ function openLogcatWindow(vscode) {
|
||||
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
|
||||
});
|
||||
})
|
||||
.fail(e => {
|
||||
.fail((/*e*/) => {
|
||||
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
|
||||
});
|
||||
}
|
||||
|
||||
11
src/state.js
Normal file
11
src/state.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const vscode = require('vscode');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
var adext = {};
|
||||
try {
|
||||
Object.assign(adext, JSON.parse(fs.readFileSync(path.join(path.dirname(__dirname),'package.json'),'utf8')));
|
||||
} catch (ex) { }
|
||||
|
||||
exports.adext = adext;
|
||||
14
src/util.js
14
src/util.js
@@ -1,13 +1,18 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
var nofn = function () { };
|
||||
var D=exports.D=console.log.bind(console);
|
||||
var E=exports.E=console.error.bind(console);
|
||||
var W=exports.W=console.warn.bind(console);
|
||||
const messagePrintCallbacks = new Set();
|
||||
var D = exports.D = (...args) => (console.log(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||
var E = exports.E = (...args) => (console.error(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||
var W = exports.W = (...args) => (console.warn(...args), messagePrintCallbacks.forEach(cb => cb(...args)))
|
||||
var DD = nofn, cl = D, printf = D;
|
||||
var print_jdwp_data = nofn;// _print_jdwp_data;
|
||||
var print_packet = nofn;//_print_packet;
|
||||
|
||||
exports.onMessagePrint = function(cb) {
|
||||
messagePrintCallbacks.add(cb);
|
||||
}
|
||||
|
||||
Array.first = function (arr, fn, defaultvalue) {
|
||||
var idx = Array.indexOfFirst(arr, fn);
|
||||
return idx < 0 ? defaultvalue : arr[idx];
|
||||
@@ -551,8 +556,7 @@ exports.fromutf8bytes = function(array) {
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
c = array[i++];
|
||||
switch(c >> 4)
|
||||
{
|
||||
switch (c >> 4) {
|
||||
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
|
||||
// 0xxxxxxx
|
||||
out += String.fromCharCode(c);
|
||||
|
||||
@@ -79,6 +79,8 @@ class AndroidVariables {
|
||||
value: x.varinfo.objvar.value,
|
||||
valid:true,
|
||||
});
|
||||
// create the fully qualified names to use for evaluation
|
||||
fields.forEach(f => f.fqname = `${x.varinfo.objvar.fqname || x.varinfo.objvar.name}.${f.name}`);
|
||||
x.varinfo.cached = fields;
|
||||
return this._local_to_variable(fields);
|
||||
});
|
||||
@@ -100,9 +102,10 @@ class AndroidVariables {
|
||||
return $.Deferred().resolve(variables);
|
||||
}
|
||||
// get the elements for the specified range
|
||||
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count)
|
||||
.then((elements) => {
|
||||
varinfo.cached = elements;
|
||||
return this.dbgr.getarrayvalues(varinfo.arrvar, range[0], count, {varinfo})
|
||||
.then((elements, x) => {
|
||||
elements.forEach(el => el.fqname = `${x.varinfo.arrvar.fqname || x.varinfo.arrvar.name}[${el.name}]`);
|
||||
x.varinfo.cached = elements;
|
||||
return this._local_to_variable(elements);
|
||||
});
|
||||
}
|
||||
@@ -158,7 +161,7 @@ class AndroidVariables {
|
||||
*/
|
||||
_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) {
|
||||
case v.hasnullvalue && JTYPES.isReference(v.type):
|
||||
// null object or array type
|
||||
@@ -188,7 +191,7 @@ class AndroidVariables {
|
||||
varref = this._getObjectIdReference(v.type, v.value);
|
||||
this.variableHandles[varref] = { varref:varref, arrvar:v, range:[0,v.arraylen] };
|
||||
}
|
||||
objvalue = v.type.typename.replace(/]$/, v.arraylen+']'); // insert len as the final array bound
|
||||
objvalue = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
|
||||
break;
|
||||
case JTYPES.isObject(v.type):
|
||||
// non-null object instance - add another variable reference so the user can expand
|
||||
@@ -207,10 +210,27 @@ class AndroidVariables {
|
||||
case v.type.signature === 'J':
|
||||
// because JS cannot handle 64bit ints, we need a bit of extra work
|
||||
var v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
|
||||
objvalue = NumberBaseConverter.hexToDec(v64hex, true);
|
||||
objvalue = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
|
||||
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
|
||||
formats.oct = formats.bin = '';
|
||||
// 24 bit chunks...
|
||||
for (var s=v64hex,uint; s; s = s.slice(0,-6)) {
|
||||
uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
|
||||
formats.oct = uint.toString(8) + formats.oct;
|
||||
formats.bin = uint.toString(2) + formats.bin;
|
||||
}
|
||||
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
|
||||
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
|
||||
break;
|
||||
case /^[BIS]$/.test(v.type.signature):
|
||||
objvalue = formats.dec = v.value.toString();
|
||||
var uint = (v.value >>> 0);
|
||||
formats.hex = '0x' + uint.toString(16);
|
||||
formats.oct = '0c' + uint.toString(8);
|
||||
formats.bin = '0b' + uint.toString(2);
|
||||
break;
|
||||
default:
|
||||
// other primitives: int, boolean, etc
|
||||
// other primitives: boolean, etc
|
||||
objvalue = v.value.toString();
|
||||
break;
|
||||
}
|
||||
@@ -223,6 +243,7 @@ class AndroidVariables {
|
||||
name: v.name,
|
||||
type: typename,
|
||||
value: objvalue,
|
||||
evaluateName,
|
||||
variablesReference: varref,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user