Files
android-dev-ext/src/debugMain.js
2017-06-14 15:57:17 +01:00

1571 lines
76 KiB
JavaScript

'use strict'
const {
DebugSession,
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 os = require('os');
const Long = require('long');
const path = require('path');
const xpath = require('xpath');
// our stuff
const { ADBClient } = require('./adbclient');
const { Debugger } = require('./debugger');
const $ = require('./jq-promise');
const { AndroidThread } = require('./threads');
const { D, isEmptyObject } = require('./util');
const { AndroidVariables } = require('./variables');
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');
class AndroidDebugSession extends DebugSession {
/**
* Creates a new debug adapter that is used for one debug session.
*/
constructor() {
super();
// create the Android debugger instance - we proxy requests through this
this.dbgr = new Debugger();
// the base folder of the app (where AndroidManifest.xml and source files should be)
this.app_src_root = '<no appSrcRoot>';
// the filepathname of the built apk
this.apk_fpn = '';
// the apk file content
this._apk_file_data = null;
// the file info, hash and manifest data of the apk
this.apk_file_info = {};
// hashmap of packages we found in the source tree
this.src_packages = {};
// the device we are debugging
this._device = null;
// the threads (we know about from the last refreshThreads call)
// this is implemented as both a hashmap<threadid,AndroidThread> and an array of AndroidThread objects
this._threads = {
array:[],
}
// path to the the ANDROID_HOME/sources/<api> (only set if it's a valid path)
this._android_sources_path = '';
// number of call stack entries to display above the project source
this.callStackDisplaySize = 1;
// the set of variables used for evalution outside of any thread/frame context
this._globals = new AndroidVariables(this, 10000);
// the fifo queue of evaluations (watches, hover, etc)
this._evals_queue = [];
// since we want to send breakpoint events, we will assign an id to every event
// so that the frontend can match events with breakpoints.
this._breakpointId = 1000;
this._sourceRefs = { all:[null] }; // hashmap + array of (non-zero) source references
this._nextVSCodeThreadId = 0; // vscode doesn't like thread id reuse (the Android runtime is OK with it)
// flag to distinguish unexpected disconnection events (initiated from the device) vs user-terminated requests
this._isDisconnecting = false;
// this debugger uses one-based lines and columns
this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true);
}
/**
* 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*/) {
// This debug adapter implements the configurationDoneRequest.
response.body.supportsConfigurationDoneRequest = true;
// we support some exception options
response.body.exceptionBreakpointFilters = [
{ label:'All Exceptions', filter:'all', default:false },
{ label:'Uncaught Exceptions', filter:'uncaught', default:true },
];
// we support modifying variable values
response.body.supportsSetVariable = true;
// we support hit-count conditional breakpoints
response.body.supportsHitConditionalBreakpoints = true;
this.sendResponse(response);
}
LOG(msg) {
D(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));
}
failRequest(msg, response) {
// yeah, it can happen sometimes...
this.WARN(msg);
if (response) {
response.success = false;
this.sendResponse(response);
}
}
failRequestNoThread(requestName, threadId, response) {
this.failRequest(`${requestName} failed. Thread ${threadId} not found`, response);
}
failRequestThreadNotSuspended(requestName, threadId, response) {
this.failRequest(`${requestName} failed. Thread ${threadId} is not suspended`, response);
}
getThread(id) {
var t;
switch(typeof id) {
case 'string':
t = this._threads[id];
if (!t) {
t = new AndroidThread(this, id, ++this._nextVSCodeThreadId);
this._threads[id] = this._threads.array[t.vscode_threadid] = t;
}
break;
case 'number':
t = this._threads.array[id];
break;
}
return t;
}
reportStoppedEvent(reason, location, last_exception) {
var thread = this.getThread(location.threadid);
if (thread.stepTimeout) {
clearTimeout(thread.stepTimeout);
thread.stepTimeout = null;
}
if (thread.paused) {
// this thread is already in the paused state - ignore the notification
thread.paused.reasons.push(reason);
if (last_exception)
thread.paused.last_exception = last_exception;
return;
}
thread.paused = {
when: Date.now(), // when
reasons: [reason], // why
location: Object.assign({},location), // where
last_exception: last_exception || null,
locals_done: {}, // promise to wait on for the stack variables to be evaluated
stack_frame_vars: {}, // hashmap<variablesReference,varinfo> for the stack frame locals
stoppedEvent:null, // event we (eventually) send to vscode
}
this.checkPendingThreadBreaks();
}
refreshThreads(extra) {
return this.dbgr.allthreads(extra)
.then((thread_ids, extra) => this.dbgr.threadinfos(thread_ids, extra))
.then((threadinfos, extra) => {
for (var i=0; i < threadinfos.length; i++) {
var ti = threadinfos[i];
var thread = this.getThread(ti.threadid);
if (thread.name === null) {
thread.name = ti.name;
} else if (thread.name !== ti.name) {
// give the thread a new id for VS code
delete this._threads.array[thread.vscode_threadid];
thread.vscode_threadid = ++this._nextVSCodeThreadId;
this._threads.array[thread.vscode_threadid] = thread;
thread.name = ti.name;
}
}
// remove any threads that are no longer in the system
this._threads.array.reduceRight((threadinfos,t) => {
if (!t) return threadinfos;
var exists = threadinfos.find(ti => ti.threadid === t.threadid);
if (!exists) {
delete this._threads[t.threadid];
delete this._threads.array[t.vscode_threadid];
}
return threadinfos;
},threadinfos);
return extra;
})
}
launchRequest(response/*: DebugProtocol.LaunchResponse*/, args/*: LaunchRequestArguments*/) {
try { D('Launching: ' + JSON.stringify(args)); } catch(ex) {}
// app_src_root must end in a path-separator for correct validation of sub-paths
this.app_src_root = ensure_path_end_slash(args.appSrcRoot);
this.apk_fpn = args.apkFile;
if (typeof args.callStackDisplaySize === 'number' && args.callStackDisplaySize >= 0)
this.callStackDisplaySize = args.callStackDisplaySize|0;
// configure the ADB port - if it's undefined, it will set the default value.
// if it's not a valid port number, any connection request should neatly fail.
ws_proxy.setADBPort(args.adbPort);
try {
// start by scanning the source folder for stuff we need to know about (packages, manifest, etc)
this.src_packages = this.scanSourceSync(this.app_src_root);
// warn if we couldn't find any packages (-> no source -> cannot debug anything)
if (isEmptyObject(this.src_packages.packages))
this.WARN('No source files found. Check the "appSrcRoot" setting in launch.json');
} catch(err) {
// wow, we really didn't make it very far...
this.LOG(err.message);
this.LOG('Check the "appSrcRoot" and "apkFile" entries in launch.json');
this.sendEvent(new TerminatedEvent(false));
return;
}
var fail_launch = (msg) => $.Deferred().rejectWith(this, [new Error(msg)]);
this.LOG('Checking build')
this.getAPKFileInfo()
.then(apk_file_info => {
this.apk_file_info = apk_file_info;
// check if any source file was modified after the apk
if (this.src_packages.last_src_modified >= this.apk_file_info.app_modified) {
switch (args.staleBuild) {
case 'ignore': break;
case 'stop': return fail_launch('Build is not up-to-date');
case 'warn':
default: this.WARN('Build is not up-to-date. Source files may not match execution when debugging.'); break;
}
}
// check we have something to launch - we do this again later, but it's a bit better to do it before we start device comms
var launchActivity = args.launchActivity;
if (!launchActivity)
if (!(launchActivity = this.apk_file_info.launcher))
return fail_launch('No valid launch activity found in AndroidManifest.xml or launch.json');
return new ADBClient().test_adb_connection()
.then(err => {
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
var adbport = ws_proxy.adbport;
if (err && args.autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
var adbargs = ['-P',''+adbport,'start-server'];
try {
this.LOG([adbpath, ...adbargs].join(' '));
var stdout = require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
this.LOG(stdout);
} catch (ex) {} // if we fail, it doesn't matter - the device query will fail and the user will have to work it out themselves
}
})
})
.then(() => this.findSuitableDevice(args.targetDevice))
.then(device => {
this._device = device;
this._device.adbclient = new ADBClient(this._device.serial);
// we've got our device - retrieve the hash of the installed app (or sha1 utility itself if the app is not installed)
const query_app_hash = `/system/bin/sha1sum $(pm path ${this.apk_file_info.package}|grep -o -e '/.*' || echo '/system/bin/sha1sum')`;
return this._device.adbclient.shell_cmd({command: query_app_hash});
})
.then(sha1sum_output => {
const installed_hash = sha1sum_output.match(/^[0-9a-fA-F]*/)[0].toLowerCase();
// does the installed apk hash match the content hash? if, so we don't need to install the app
if (installed_hash === this.apk_file_info.content_hash) {
this.LOG('Current build already installed');
return;
}
return this.copyAndInstallAPK();
})
.then(() => {
// when we reach here, the app should be installed and ready to be launched
// - before we continue, splunk the apk file data because node *still* hangs when evaluating large arrays
this._apk_file_data = null;
// get the API level of the device
return this._device.adbclient.shell_cmd({command:'getprop ro.build.version.sdk'});
})
.then(apilevel => {
apilevel = apilevel.trim();
// look for the android sources folder appropriate for this device
if (process.env.ANDROID_HOME && apilevel) {
var sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+apilevel);
fs.stat(sources_path, (err,stat) => {
if (!err && stat && stat.isDirectory())
this._android_sources_path = sources_path;
});
}
// start the launch
var launchActivity = args.launchActivity;
if (!launchActivity)
if (!(launchActivity = this.apk_file_info.launcher))
return fail_launch('No valid launch activity found in AndroidManifest.xml or launch.json');
var build = {
pkgname:this.apk_file_info.package,
packages:Object.assign({}, this.src_packages.packages),
launchActivity: launchActivity,
};
this.LOG(`Launching ${build.pkgname+'/'+launchActivity} on device ${this._device.serial} [API:${apilevel||'?'}]`);
return this.dbgr.startDebugSession(build, this._device.serial, launchActivity);
})
.then(() => {
// if we get this far, the debugger is connected and waiting for the resume command
// - set up some events
this.dbgr.on('bpstatechange', this, this.onBreakpointStateChange)
.on('bphit', this, this.onBreakpointHit)
.on('step', this, this.onStep)
.on('exception', this, this.onException)
.on('threadchange', this, this.onThreadChange)
.on('disconnect', this, this.onDebuggerDisconnect);
this.waitForConfigurationDone = $.Deferred();
// - tell the client we're initialised and ready for breakpoint info, etc
this.sendEvent(new InitializedEvent());
return this.waitForConfigurationDone;
})
.then(() => {
// get the debugger to tell us about any thread creations/terminations
return this.dbgr.setThreadNotify();
})
.then(() => {
// config is done - we're all set and ready to go!
D('Continuing app start');
this.sendResponse(response);
return this.dbgr.resume();
})
.fail(e => {
// exceptions use message, adbclient uses msg
this.LOG('Launch failed: '+(e.message||e.msg||'No additional information is available'));
// more info for adb connect errors
if (/^ADB server is not running/.test(e.msg)) {
this.LOG('Make sure the Android SDK Platform Tools are installed and run:');
this.LOG(' adb start-server');
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
}
// tell the client we're done
this.sendEvent(new TerminatedEvent(false));
});
}
copyAndInstallAPK() {
// copy the file to the device
this.LOG('Deploying current build...');
return this._device.adbclient.push_file({
filepathname:'/data/local/tmp/debug.apk',
filedata:this._apk_file_data,
filemtime:new Date().getTime(),
})
.then(() => {
// send the install command
this.LOG('Installing...');
return this._device.adbclient.shell_cmd({
command:'pm install -r /data/local/tmp/debug.apk',
untilclosed:true,
})
})
.then((stdout) => {
// failures:
// pkg: x-y-z.apk
// Failure [INSTALL_FAILED_OLDER_SDK]
var m = stdout.match(/Failure\s+\[([^\]]+)\]/g);
if (m) {
return $.Deferred().rejectWith(this, [new Error('Installation failed. ' + m[0])]);
}
})
}
getAPKFileInfo() {
var done = $.Deferred();
done.result = { fpn:this.apk_fpn, app_modified:0, content_hash:'', manifest:'', package:'', activities:[], launcher:'' };
// read the APK
fs.readFile(this.apk_fpn, (err,apk_file_data) => {
if (err) return done.rejectWith(this, [new Error('APK read error. ' + err.message)]);
// debugging is painful when the APK file content is large, so keep the data in a separate field so node
// doesn't have to evaluate it when we're looking at the apk info
this._apk_file_data = apk_file_data;
// save the last modification time of the app
done.result.app_modified = fs.statSync(done.result.fpn).mtime.getTime();
// create a SHA-1 hash as a simple way to see if we need to install/update the app
const h = crypto.createHash('SHA1');
h.update(apk_file_data);
done.result.content_hash = h.digest('hex');
// read the manifest
fs.readFile(path.join(this.app_src_root,'AndroidManifest.xml'), 'utf8', (err,manifest) => {
if (err) return done.rejectWith(this, [new Error('Manifest read error. ' + err.message)]);
done.result.manifest = manifest;
try {
const doc = new dom().parseFromString(manifest);
// extract the package name from the manifest
const pkg_xpath = '/manifest/@package';
done.result.package = xpath.select1(pkg_xpath, doc).value;
const android_select = xpath.useNamespaces({"android": "http://schemas.android.com/apk/res/android"});
// extract a list of all the (named) activities declared in the manifest
const activity_xpath='/manifest/application/activity/@android:name';
var nodes = android_select(activity_xpath, doc);
nodes && (done.result.activities = nodes.map(n => n.value));
// extract the default launcher activity
const launcher_xpath='/manifest/application/activity[intent-filter/action[@android:name="android.intent.action.MAIN"] and intent-filter/category[@android:name="android.intent.category.LAUNCHER"]]/@android:name';
var nodes = android_select(launcher_xpath, doc);
// should we warn if there's more than one?
if (nodes && nodes.length >= 1)
done.result.launcher = nodes[0].value
} catch(err) {
return done.rejectWith(this, [new Error('Manifest parse failed. ' + err.message)]);
}
done.resolveWith(this, [done.result]);
});
});
return done;
}
scanSourceSync(app_root) {
try {
// scan known app folders looking for file changes and package folders
var p, paths = fs.readdirSync(app_root,'utf8'), done=[];
var src_packages = {
last_src_modified: 0,
packages: {},
};
while (paths.length) {
p = paths.shift();
// just in case someone has some crazy circular links going on
if (done.indexOf(p)>=0) continue;
done.push(p);
var subfiles = [], stat, fpn = path.join(app_root,p);
try {
stat = fs.statSync(fpn);
src_packages.last_src_modified = Math.max(src_packages.last_src_modified, stat.mtime.getTime());
if (!stat.isDirectory()) continue;
subfiles = fs.readdirSync(fpn, 'utf8');
}
catch (err) { continue }
// ignore folders not starting with a known top-level Android folder
if (!/^(assets|res|src|main|java)([\\/]|$)/.test(p)) continue;
// is this a package folder
var pkgmatch = p.match(/^(src|main|java)[\\/](.+)/);
if (pkgmatch && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pkgmatch[2].split(/[\\/]/).pop())) {
// looks good - add it to the list
const src_folder = pkgmatch[1]; // src, main or java
const pkgname = pkgmatch[2].replace(/[\\/]/g,'.');
src_packages.packages[pkgname] = {
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])
}
}
// add the subfiles to the list to process
paths = subfiles.map(sf => path.join(p,sf)).concat(paths);
}
return src_packages;
} catch(err) {
throw new Error('Source path error: ' + err.message);
}
}
findSuitableDevice(target_deviceid) {
this.LOG('Searching for devices...');
return this.dbgr.list_devices()
.then(devices => {
this.LOG(`Found ${devices.length} device${devices.length===1?'':'s'}`);
var reject;
if (devices.length === 0) {
reject = 'No devices are connected';
} else if (target_deviceid) {
// check (only one of) the requested device is present
var matching_devices = devices.filter(d => d.serial === target_deviceid);
switch(matching_devices.length) {
case 0: reject = `Target device: '${target_deviceid}' is not connected. Connect it or specify an alternate target device in launch.json`; break;
case 1: return matching_devices[0];
default: reject = `Target device: '${target_deviceid}' has multiple candidates. Connect a single device or specify an alternate target device in launch.json`; break;
}
} else if (devices.length === 1) {
// no specific target device and only one device is connected to adb - use it
return devices[0];
} else {
// more than one device and no specific target - fail the launch
reject = `Multiple devices are connected and no target device is specified in launch.json`;
// be nice and list the devices so the user can easily configure
devices.forEach(d => this.LOG(`\t${d.serial}\t${d.status}`));
}
return $.Deferred().rejectWith(this, [new Error(reject)]);
})
}
configurationDoneRequest(response/*, args*/) {
this.waitForConfigurationDone.resolve();
this.sendResponse(response);
}
onDebuggerDisconnect() {
// called when we manually disconnect, or from an unexpected disconnection (USB cable disconnect, etc)
if (!this._isDisconnecting) {
D('Unexpected disconnection');
// this is a surprise disconnect (initiated from the device) - tell the client we're done
this.LOG(`Device disconnected`);
this.sendEvent(new TerminatedEvent(false));
}
}
disconnectRequest(response/*, args*/) {
D('disconnectRequest');
this._isDisconnecting = true;
// if we're connected, ask ADB to terminate the app
if (this.dbgr.status() === 'connected')
this.dbgr.forcestop();
return this.dbgr.disconnect(response)
.then((state, response) => {
if (/^connect/.test(state))
this.LOG(`Debugger disconnected`);
this.sendResponse(response);
//this.sendEvent(new ExitedEvent(0));
})
}
onBreakpointStateChange(e) {
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));
});
}
onBreakpointHit(e) {
// if we step into a breakpoint, both onBreakpointHit and onStep will be called
D('Breakpoint hit: ' + JSON.stringify(e.stoppedlocation));
this.reportStoppedEvent("breakpoint", e.stoppedlocation);
}
/**
* Called when the user requests a change to breakpoints in a source file
* Note: all breakpoints in a file are always sent in args, even if they are not changing
*/
setBreakPointsRequest(response/*: DebugProtocol.SetBreakpointsResponse*/, args/*: DebugProtocol.SetBreakpointsArguments*/) {
var srcfpn = args.source && args.source.path;
D('setBreakPointsRequest: ' + srcfpn);
const unverified_breakpoint = (src_bp,reason) => {
var bp = new Breakpoint(false,src_bp.line);
bp.id = ++this._breakpointId;
bp.message = reason;
return bp;
}
// 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;
for (var pkg in this.src_packages.packages) {
if ((pkginfo = this.src_packages.packages[pkg]).package_path === srcfolder) break;
pkginfo = null;
}
// if it's not in our source packages, check if it's in the Android source file cache
if (!pkginfo && is_subpath_of(srcfpn, this._android_sources_path)) {
// create a fake pkginfo to use to construct the bp
pkginfo = { srcroot:this._android_sources_path }
}
if (!pkginfo || !/\.java$/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);
return;
}
// our debugger requires a relative fpn beginning with / , rooted at the java source base folder
// - it should look like: /some/package/name/abc.java
var relative_fpn = srcfpn.slice(pkginfo.srcroot.match(/^(.*?)[\\/]?$/)[1].length).replace(/\\/g,'/');
// delete any existing breakpoints not in the list
var src_line_nums = args.breakpoints.map(bp => bp.line);
this.dbgr.clearbreakpoints(javabp => {
var remove = javabp.srcfpn===relative_fpn && !src_line_nums.includes(javabp.linenum);
if (remove) javabp.vsbp = null;
return remove;
});
// return the list of new and existing breakpoints
// - setting a debugger bp is now asynchronous, so we do this as an orderly queue
const _setup_breakpoints = (o, idx, javabp_arr) => {
javabp_arr = javabp_arr || [];
var src_bp = o.args.breakpoints[idx|=0];
if (!src_bp) {
// done
return $.Deferred().resolveWith(this, [javabp_arr]);
}
var dbgline = this.convertClientLineToDebugger(src_bp.line);
var options = {};
if (src_bp.hitCondition) {
// the hit condition is an expression that requires evaluation
// until we get more comprehensive evaluation support, just allow integer literals
var m = src_bp.hitCondition.match(/^\s*(?:0x([0-9a-f]+)|0b([01]+)|0*(\d+([e]\+?\d+)?))\s*$/i);
var hitcount = m && (m[3] ? parseFloat(m[3]) : m[2] ? parseInt(m[2],2) : parseInt(m[1],16));
if (!m || hitcount < 0 || hitcount > 0x7fffffff) return unverified_breakpoint(src_bp, 'The breakpoint is configured with an invalid hit count value');
options.hitcount = hitcount;
}
return this.dbgr.setbreakpoint(o.relative_fpn, dbgline, options)
.then(javabp => {
if (!javabp.vsbp) {
// state is one of: set,notloaded,enabled,removed
var verified = !!javabp.state.match(/set|enabled/);
const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(dbgline));
// the breakpoint *must* have an id field or it won't update properly
bp.id = ++this._breakpointId;
if (javabp.state === 'notloaded')
bp.message = 'The runtime hasn\'t loaded this code location';
javabp.vsbp = bp;
}
javabp.vsbp.order = idx;
javabp_arr.push(javabp);
}).
then((/*javabp*/) => _setup_breakpoints(o, ++idx, javabp_arr));
};
if (!this._set_breakpoints_queue) {
this._set_breakpoints_queue = {
_dbgr:this,
_queue:[],
add(item) {
if (this._queue.push(item) > 1) return;
this._next();
},
_setup_breakpoints: _setup_breakpoints,
_next() {
if (!this._queue.length) return; // done
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);
// .. and do the next one
this._queue.shift();
this._next();
});
},
};
}
this._set_breakpoints_queue.add({args,response,relative_fpn});
}
setExceptionBreakPointsRequest(response /*: SetExceptionBreakpointsResponse*/, args /*: SetExceptionBreakpointsArguments*/) {
this.dbgr.clearBreakOnExceptions({response,args})
.then(x => {
if (x.args.filters.includes('all')) {
x.set = this.dbgr.setBreakOnExceptions('both', x);
} else if (x.args.filters.includes('uncaught')) {
x.set = this.dbgr.setBreakOnExceptions('uncaught', x);
} else {
x.set = $.Deferred().resolveWith(this, [x]);
}
x.set.then(x => this.sendResponse(x.response));
});
}
threadsRequest(response/*: DebugProtocol.ThreadsResponse*/) {
if (this._threads.array.length) {
console.log('threadsRequest: ' + this._threads.array.length);
response.body = {
threads: this._threads.array.filter(x=>x).map(t => {
var javaid = parseInt(t.threadid, 16);
return new Thread(t.vscode_threadid, `Thread (id:${javaid}) ${t.name||'<unnamed>'}`);
})
};
this.sendResponse(response);
return;
}
this.refreshThreads(response)
.then(response => {
response.body = {
threads: this._threads.array.filter(x=>x).map(t => {
var javaid = parseInt(t.threadid, 16);
return new Thread(t.vscode_threadid, `Thread (id:${javaid}) ${t.name}`);
})
};
this.sendResponse(response);
})
.fail(() => {
response.success = false;
this.sendResponse(response);
});
}
/**
* Returns a stack trace for the given threadId
*/
stackTraceRequest(response/*: DebugProtocol.StackTraceResponse*/, args/*: DebugProtocol.StackTraceArguments*/) {
// 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);
// retrieve the (stack) frames from the debugger
this.dbgr.getframes(thread.threadid, {response, args, thread})
.then((frames, x) => {
// first ensure that the line-tables for all the methods are loaded
var defs = frames.map(f => this.dbgr._ensuremethodlines(f.method));
defs.unshift(frames,x);
return $.when.apply($,defs);
})
.then((frames, x) => {
const startFrame = typeof x.args.startFrame === 'number' ? x.args.startFrame : 0;
const maxLevels = typeof x.args.levels === 'number' ? x.args.levels : frames.length-startFrame;
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}';
for (var i= startFrame; i < endFrame; 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}`;
const pkginfo = this.src_packages.packages[frames[i].method.owningclass.type.package];
const srcloc = this.dbgr.line_idx_to_source_location(frames[i].method, frames[i].location.idx);
if (!srcloc && !pkginfo) {
totalFrames--;
continue; // ignore frames which have no location (they're probably synthetic)
}
const linenum = srcloc && this.convertDebuggerLineToClient(srcloc.linenum);
const sourcefile = frames[i].method.owningclass.src.sourcefile || (frames[i].method.owningclass.type.signature.match(/([^\/$]+)[;$]/)[1]+'.java');
var srcRefId = 0;
if (!pkginfo) {
var sig = frames[i].method.owningclass.type.signature, srcInfo = this._sourceRefs[sig];
if (!srcInfo) {
this._sourceRefs.all.push(srcInfo = {
id: this._sourceRefs.all.length,
signature:sig,
filepath:path.join(android_src_path,frames[i].method.owningclass.type.package.replace(/[.]/g,path.sep), sourcefile),
content:null
});
this._sourceRefs[sig] = srcInfo;
}
srcRefId = srcInfo.id;
}
// if this is not a known package, check if android sources is valid
// - if it is, return the expected path - VSCode will auto-load it
// - if not, set the path to null and a sourceRequest will be made.
const srcpath = pkginfo ? path.join(pkginfo.package_path,sourcefile)
: this._android_sources_path ? srcInfo.filepath
: 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));
}
// trim the stack to exclude calls above the known sources
if (this.callStackDisplaySize > 0) {
stack = stack.slice(0,highest_known_source+this.callStackDisplaySize);
totalFrames = stack.length;
}
// return the frames
response.body = {
stackFrames: stack,
totalFrames: totalFrames,
};
this.sendResponse(response);
})
.fail(() => {
this.failRequest('No call stack is available', response);
});
}
scopesRequest(response/*: DebugProtocol.ScopesResponse*/, args/*: DebugProtocol.ScopesArguments*/) {
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);
var scopes = [new Scope("Local", args.frameId, false)];
response.body = {
scopes: scopes
};
var last_exception = thread.paused.last_exception;
if (last_exception && !last_exception.objvar) {
// retrieve the exception object
thread.allocateExceptionScopeReference(args.frameId);
this.dbgr.getExceptionLocal(last_exception.exception, {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);
})
.fail((/*e*/) => { this.sendResponse(response); });
return;
}
this.sendResponse(response);
}
sourceRequest(response/*: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments*/) {
var content =
`/*
The source for this class is unavailable.
Source files for each Android API level can be downloaded using the Android SDK Manager.
To display the file, you must download the sources matching the API level of your device or
emulator and ensure that your ANDROID_HOME environment path is configured correctly.
*/
`;
// don't actually attempt to load the file here - just recheck to see if the sources
// path is valid yet.
if (process.env.ANDROID_HOME && this.dbgr.session.apilevel) {
var sources_path = path.join(process.env.ANDROID_HOME,'sources','android-'+this.dbgr.session.apilevel);
fs.stat(sources_path, (err,stat) => {
if (!err && stat && stat.isDirectory())
this._android_sources_path = sources_path;
});
}
response.body = { content };
this.sendResponse(response);
}
variablesRequest(response/*: DebugProtocol.VariablesResponse*/, args/*: DebugProtocol.VariablesArguments*/) {
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);
thread.getVariables(args.variablesReference)
.then(vars => {
response.body = {
variables: vars,
};
this.sendResponse(response);
});
}
checkPendingThreadBreaks() {
var stepping_thread = this._threads.array.find(t => t && t.stepTimeout);
var paused_threads = this._threads.array.filter(t => t && t.paused);
var stopped_thread = paused_threads.find(t => t.paused.stoppedEvent);
if (!stopped_thread && !stepping_thread && paused_threads.length) {
// prioritise any stepped thread (if it's stopped) or whichever other thread stopped first
var thread;
var paused_step_thread = paused_threads.find(t => t.paused.reasons.includes("step"));
if (paused_step_thread) {
thread = paused_step_thread;
} else {
paused_threads.sort((a,b) => a.paused.when - b.paused.when);
thread = paused_threads[0];
}
// if the break was due to a breakpoint and it has since been removed, just resume the thread
if (thread.paused.reasons.length === 1 && thread.paused.reasons[0] === 'breakpoint') {
var bp = this.dbgr.breakpoints.bysrcloc[thread.paused.location.qtype + ':' + thread.paused.location.linenum];
if (!bp) {
this.doContinue(thread);
return;
}
}
var event = new StoppedEvent(thread.paused.reasons[0], thread.vscode_threadid);
thread.paused.stoppedEvent = event;
this.sendEvent(event);
}
}
doContinue(thread) {
thread.paused = null;
this.checkPendingThreadBreaks();
this.dbgr.resumethread(thread.threadid);
console.log('');
}
continueRequest(response/*: DebugProtocol.ContinueResponse*/, args/*: DebugProtocol.ContinueArguments*/) {
D('Continue');
var t = this.getThread(args.threadId);
if (!t) return this.failRequestNoThread('Continue', args.threadId, response);
if (!t.paused) return this.failRequestThreadNotSuspended('Continue', args.threadId, response);
this.sendResponse(response);
this.doContinue(t);
}
/**
* Called by the debugger after a step operation has completed
*/
onStep(e) {
// if we step into a breakpoint, both onBreakpointHit and onStep will be called
D('step hit: ' + JSON.stringify(e.stoppedlocation));
this.reportStoppedEvent("step", e.stoppedlocation);
}
/**
* Called by the user to start a step operation
*/
doStep(which, response, args) {
D('step '+which);
var t = this.getThread(args.threadId);
if (!t) return this.failRequestNoThread('Step', args.threadId, response);
if (!t.paused) return this.failRequestThreadNotSuspended('Step', args.threadId, response);
t.paused = null;
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);
t.stepTimeout = null;
this.checkPendingThreadBreaks();
}, 2000, t);
t.stepTimeout._begun = process.hrtime();
this.dbgr.step(which, t.threadid);
console.log('');
}
stepInRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepInArguments*/) {
this.doStep('in', response, args);
}
nextRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.NextArguments*/) {
this.doStep('over', response, args);
}
stepOutRequest(response/*: DebugProtocol.NextResponse*/, args/*: DebugProtocol.StepOutArguments*/) {
this.doStep('out', response, args);
}
/**
* Called by the debugger if an exception event is triggered
*/
onException(e) {
// it's possible for the debugger to send multiple exception notifications for the same thread, depending on the package filters
D('exception hit: ' + JSON.stringify(e.throwlocation));
var last_exception = {
exception: e.event.exception,
threadid: e.throwlocation.threadid,
frameId: null, // allocated during scopesRequest
scopeRef: null, // allocated during scopesRequest
};
this.reportStoppedEvent("exception", e.throwlocation, last_exception);
}
/**
* Called by the debugger if a thread start/end event is triggered
*/
onThreadChange(e) {
D(`thread ${e.state}: ${e.threadid}(${parseInt(e.threadid,16)})`);
switch(e.state) {
case 'start':
this.dbgr.threadinfos([e.threadid])
.then((threadinfos) => {
var ti = threadinfos[0], t = this.getThread(ti.threadid), event = new ThreadEvent();
t.name = ti.name;
event.body = { reason:'started', threadId: t.vscode_threadid };
this.sendEvent(event);
})
.always(() => this.dbgr.resumethread(e.threadid));
return;
case 'end':
var t = this._threads[e.threadid];
if (t) {
t.stepTimeout && clearTimeout(t.stepTimeout) && (t.stepTimeout = null);
delete this._threads[e.threadid];
delete this._threads.array[t.vscode_threadid];
var event = new ThreadEvent();
event.body = { reason:'exited', threadId: t.vscode_threadid };
this.sendEvent(event);
this.checkPendingThreadBreaks(); // in case we were stepping this thread
}
break;
}
this.dbgr.resumethread(e.threadid);
}
setVariableRequest(response/*: DebugProtocol.SetVariableResponse*/, args/*: DebugProtocol.SetVariableArguments*/) {
var threadId = variableRefToThreadId(args.variablesReference);
var t = this.getThread(threadId);
if (!t) return this.failRequestNoThread('Set variable', threadId, response);
if (!t.paused) return this.failRequestThreadNotSuspended('Set variable', threadId, response);
t.setVariableValue(args)
.then(function(response,vsvar) {
response.body = {
value: vsvar.value,
type: vsvar.type,
variablesReference: vsvar.variablesReference,
};
this.sendResponse(response);
}.bind(this,response))
.fail(function(response,e) {
response.success = false;
response.message = e.message;
this.sendResponse(response);
}.bind(this,response));
}
/**
* Called by VSCode to perform watch, console and hover evaluations
*/
evaluateRequest(response/*: DebugProtocol.EvaluateResponse*/, args/*: DebugProtocol.EvaluateArguments*/) {
// Some notes to remember:
// annoyingly, during stepping, the step can complete before the resume has called evaluateRequest on watches.
// The order can go: doStep(running=true),onStep(running=false),evaluateRequest(),evaluateRequest()
// so we end up evaluating twice...
// also annoyingly, this method is called before the locals in the current stack frame are evaluated
// and even more annoyingly, Android (or JDWP) seems to get confused on the first request when we're retrieving multiple values, fields, etc
// so we have to queue them or we end up with strange results
// look for a matching entry in the list (other than at index:0)
var previdx = this._evals_queue.findIndex(e => e.args.expression === args.expression);
if (previdx > 0) {
// if we find a match, immediately fail the old one and queue the new one
var prev = this._evals_queue.splice(previdx,1)[0];
prev.response.success = false;
prev.response.message = '(evaluating)';
this.sendResponse(prev.response);
}
// if there's no frameId, we are being asked to evaluate the value in the 'global' context
var getvars;
if (args.frameId) {
var threadId = variableRefToThreadId(args.frameId);
var thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Evaluate',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response);
getvars = thread._ensureLocals(args.frameId).then(frameId => {
var locals = thread.paused.stack_frame_vars[frameId].locals;
return $.Deferred().resolve(thread, locals.variableHandles[frameId].cached, locals);
})
} else {
// global context - no locals
getvars = $.Deferred().resolve(null, [], this._globals);
}
this._evals_queue.push({response,args,getvars,thread});
// if we're currently processing, just wait
if (this._evals_queue.length > 1) {
return;
}
// begin processing
this.doNextEvaluateRequest();
}
doNextEvaluateRequest() {
if (!this._evals_queue.length) {
return;
}
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);
})
.then((value,variablesReference) => {
response.body = { result:value, variablesReference:variablesReference|0 };
})
.fail(e => {
response.success = false;
response.message = e.message;
})
.always(() => {
this.sendResponse(response);
this._evals_queue.shift();
this.doNextEvaluateRequest();
})
}
/*
Asynchronously evaluate an expression
*/
evaluate(expression, thread, locals, vars) {
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 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);
});
}
// the expression is not well-formed
return reject_evaluation('not available');
}
}
DebugSession.run(AndroidDebugSession);