Version 1 (#83)

* replace jq-promises with native Promises

* updates to use native promises and async await

* Fix variable errors, remove extra parameters and correct export declaratons

* refactor launch request to use async/await

* fix running debugger on custom ADB port

* remove unused files

* move socket_ended check to ensure we don't loop reading 0 bytes

* refactor logcat code and ensure disconnect status is passed on to webview

* Fix warnings

* Clean up util and remove unused functions

* convert Debugger into a class

* update jsconfig target to es2018 and enable checkJS

* more updates to use async/await and more readable refactoring.

- added type definitions and debugger classes
- improved expression evaluation
- refactored expressions into parsing, evaluation and variable assignment
- fixed invoking methods with parameters
- added support for static method invokes
- improved exception display reliability
- refactored launch into smaller functions
- refactored utils into smaller modules
- removed redundant code
- converted JDWP functions to classes

* set version 1.0.0 and update dependencies

* add changelog notes
This commit is contained in:
Dave Holoway
2020-04-20 12:53:08 +01:00
committed by GitHub
parent f92f247ef6
commit 0672e54401
45 changed files with 8050 additions and 9829 deletions

10
.vscode/launch.json vendored
View File

@@ -8,7 +8,10 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false
"stopOnEntry": false,
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Server",
@@ -16,7 +19,10 @@
"request": "launch",
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/src/debugMain.js",
"args": [ "--server=4711" ]
"args": [ "--server=4711" ],
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Launch Tests",

View File

@@ -1,5 +1,9 @@
# Change Log
### version 1.0.0
* Update extension to support minimum version of node v10
* refactoring and improvement of type-checking using jsdocs
### version 0.8.0
* Try to extract Android manifest directly from APK
* Added `manifestFile` launch configuration property

View File

@@ -3,15 +3,6 @@
const vscode = require('vscode');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
const state = require('./src/state');
function getADBPort() {
var defaultPort = 5037;
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
return adbPort;
return defaultPort;
}
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
@@ -20,29 +11,20 @@ function activate(context) {
/* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */
AndroidContentProvider.register(context, vscode.workspace);
// logcat connections require the (fake) websocket proxy to be up
// - take the ADB port from launch.json
const wsproxyserver = require('./src/wsproxy').proxy.Server(6037, getADBPort());
// The commandId parameter must match the command field in package.json
var disposables = [
const disposables = [
// add the view logcat handler
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
openLogcatWindow(vscode);
}),
// watch for changes in the launch config
vscode.workspace.onDidChangeConfiguration(e => {
wsproxyserver.setADBPort(getADBPort());
})
];
var spliceparams = [context.subscriptions.length,0].concat(disposables);
Array.prototype.splice.apply(context.subscriptions,spliceparams);
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);
}
exports.activate = activate;
// this method is called when your extension is deactivated
function deactivate() {
}
exports.activate = activate;
exports.deactivate = deactivate;

2107
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "0.8.0",
"version": "1.0.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
"engines": {
"vscode": "^1.8.0"
"vscode": "^1.24.0"
},
"categories": [
"Debuggers"
@@ -148,15 +148,14 @@
]
},
"scripts": {
"prepare": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"long": "^4.0.0",
"unzipper": "^0.10.4",
"unzipper": "^0.10.10",
"uuid": "^3.3.2",
"vscode-debugadapter": "^1.32.0",
"vscode-debugprotocol": "^1.32.0",
"vscode-debugadapter": "^1.40.0",
"vscode-debugprotocol": "^1.40.0",
"ws": "^7.1.2",
"xmldom": "^0.1.27",
"xpath": "^0.0.27"
@@ -164,9 +163,9 @@
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.5",
"@types/vscode": "1.24.0",
"eslint": "^5.9.0",
"mocha": "^5.2.0",
"typescript": "^3.1.6",
"vscode": "^1.1.26"
"typescript": "^3.8.3"
}
}

View File

@@ -1,45 +1,30 @@
/*
ADBClient: class to manage connection and commands to adb (via the Dex plugin) running on the local machine.
ADBClient: class to manage commands to ADB
*/
const _JDWP = require('./jdwp')._JDWP;
const $ = require('./jq-promise');
const WebSocket = require('./minwebsocket').WebSocketClient;
const { atob,btoa,D } = require('./util');
const JDWPSocket = require('./sockets/jdwpsocket');
const ADBSocket = require('./sockets/adbsocket');
function ADBClient(deviceid) {
this.deviceid = deviceid;
this.status = 'notinit';
this.reset();
this.JDWP = new _JDWP();
}
ADBClient.prototype = {
reset : function() {
this.ws = null;
this.activepromise={};
this.authdone=false;
this.fd=-1;
this.disconnect_reject_reason=null;
},
_parse_device_list:function(data, extended) {
var lines = atob(data).trim().split(/\r\n?|\n/);
/**
*
* @param {string} data
* @param {boolean} [extended]
*/
function parse_device_list(data, extended = false) {
var lines = data.trim().split(/\r\n?|\n/);
lines.sort();
var devicelist = [];
var i=0;
const devicelist = [];
if (extended) {
for (var i=0; i < lines.length; i++) {
for (let i = 0, m; i < lines.length; i++) {
try {
var m = JSON.parse(lines[i]);
m = JSON.parse(lines[i]);
} catch (e) { continue; }
if (!m) continue;
m.num = i;
} catch(e) {continue;}
devicelist.push(m);
}
} else {
for (var i=0; i < lines.length; i++) {
var m = lines[i].match(/([^\t]+)\t([^\t]+)/);
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/([^\t]+)\t([^\t]+)/);
if (!m) continue;
devicelist.push({
serial: m[1],
@@ -49,778 +34,190 @@ ADBClient.prototype = {
}
}
return devicelist;
},
track_devices_extended : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('track_devices', 'wa', this.fd, 'host:track-devices-extended');
})
.then(function(data) {
return this.dexcmd('ra', this.fd);
})
.then(function(data) {
function nextdeviceinfo(data) {
this.dexcmd('ra', this.fd, null, {notimeout:true})
.then(nextdeviceinfo);
var devicelist = this._parse_device_list(data, true);
x.o.ondevices(devicelist, this);
}
nextdeviceinfo.call(this, data);
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
finish_track_devices : function() {
return this.dexcmd('dc', this.fd)
.then(function() {
return this.proxy_disconnect();
});
},
class ADBClient {
test_adb_connection : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [null, x.o.extra]);
})
.fail(function(err) {
// if we fail, still resolve the deferred, passing the error
x.deferred.resolveWith(x.o.ths||this, [err, x.o.extra]);
});
return x.deferred;
},
list_devices : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('list_devices', 'wa', this.fd, 'host:devices');
})
.then(function(data) {
return this.dexcmd('ra', this.fd);
})
.then(function(data) {
x.devicelist = this._parse_device_list(data);
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.devicelist, x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_list : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('jdwp', 'wa', this.fd, 'jdwp');
})
.then(function(data) {
return this.dexcmd_read_stdout(this.fd);
})
.then(function(data) {
this.stdout = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.stdout.trim().split(/\r?\n|\r/g), x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_forward : function(o) {
// localport:1234
// jdwp:1234
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('forward', 'wa', this.fd, 'host-serial:'+this.deviceid+':forward:tcp:'+x.o.localport+';jdwp:'+x.o.jdwp)
})
.then(function(data) {
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
forward_remove_all : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('forward_remove_all', 'wa', this.fd, 'host:killforward-all');
})
.then(function(data) {
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_connect : function(o) {
// {localport:1234, onreply:fn()}
// note that upon success, this method does not close the connection
var x = {o:o,deferred:$.Deferred()};
this.jdwpinfo = {
o: o,
localport: o.localport,
onreply: o.onreply,
received: [],
};
this.proxy_connect()
.then(function() {
return this.dexcmd('cp', o.localport);
})
.then(function(data) {
this.jdwpfd = data;
return this.dexcmd('wx', this.jdwpfd, 'JDWP-Handshake');
})
.then(function(data) {
return this.dexcmd_read_stdout(this.jdwpfd);
})
.then(function(data) {
if (data!=='JDWP-Handshake') {
// disconnect and fail
return this.dexcmd('dc', this.jdwpfd)
.then(function() {
return this.proxy_disconnect_with_fail({cat:'jdwp', msg:'Invalid handshake response'});
});
/**
* @param {string} [deviceid]
* @param {number} [adbPort] the port number to connect to ADB
*/
constructor(deviceid, adbPort = ADBSocket.ADBPort) {
this.deviceid = deviceid;
this.adbsocket = null;
this.jdwp_socket = null;
this.adbPort = adbPort;
}
// start the monitor - we don't want it terminated on timeout
return this.logsend('rj', 'rj '+this.jdwpfd, {notimeout:true});
})
.then(function() {
// the first rj reply is a blank ok message indicating the monitor
// has started
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
jdwp_command : function(o) {
// cmd: JDWP.Command
// resolveonreply: true/false
async test_adb_connection() {
try {
await this.connect_to_adb();
await this.disconnect_from_adb();
} catch(err) {
// if we fail, still resolve the promise, passing the error
return err;
}
}
// send the raw command over the socket - the reply
// is received via the JDWP monitor
var x = {o:o,deferred:$.Deferred()};
this.dexcmd('wx', this.jdwpfd, o.cmd.toRawString())
.fail(function(err) {
o.cmd.deferred.rejectWith(o.ths||this, [err]);
});
async list_devices() {
await this.connect_to_adb()
const data = await this.adbsocket.cmd_and_reply('host:devices');
const devicelist = parse_device_list(data);
await this.disconnect_from_adb();
return devicelist;
}
o.cmd.deferred
.then(function(decoded,reply,command) {
x.deferred.resolveWith(x.o.ths||this, [decoded,x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
/**
* Return a list of debuggable pids from the device
*/
async jdwp_list() {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
const stdout = await this.adbsocket.cmd_and_read_stdout('jdwp');
await this.disconnect_from_adb();
return stdout.trim().split(/\r?\n|\r/);
}
return x.deferred;
},
/**
* Setup ADB port-forwarding from a local port to a JDWP process
* @param {{localport:number, jdwp:number}} o
*/
async jdwp_forward(o) {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host-serial:${this.deviceid}:forward:tcp:${o.localport};jdwp:${o.jdwp}`);
await this.disconnect_from_adb();
return true;
}
jdwp_disconnect : function(o) {
var x = {o:o,deferred:$.Deferred()};
this.dexcmd('dc', this.jdwpfd)
.then(function() {
delete this.jdwpfd;
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
/**
* remove all port-forwarding configs
*/
async forward_remove_all() {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status('host:killforward-all');
await this.disconnect_from_adb();
return true;
}
readwritesocket : function(o) {
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd('qs', this.fd, ''+o.port+':'+o.readlen+':'+o.data);
})
.then(function(data) {
this.socket_reply = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.socket_reply, x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
/**
* Connect to the JDWP debugging client and perform the handshake
* @param {{localport:number, onreply:()=>void, ondisconnect:()=>void}} o
*/
async jdwp_connect(o) {
// note that upon success, this method does not close the connection (it must be left open for
// future commands to be sent over the jdwp socket)
this.jdwp_socket = new JDWPSocket(o.onreply, o.ondisconnect);
await this.jdwp_socket.connect(o.localport)
await this.jdwp_socket.start();
return true;
}
shell_cmd : function(o) {
// command='ls /'
// untilclosed=true
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('shell_cmd', 'wa', this.fd, 'shell:'+x.o.command);
})
.then(function(data) {
return this.dexcmd_read_stdout(this.fd, !!x.o.untilclosed);
})
.then(function(data) {
this.stdout = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.stdout, x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
/**
* Send a JDWP command to the device
* @param {{cmd}} o
*/
async jdwp_command(o) {
// send the raw command over the socket - the reply is received via the JDWP monitor
const reply = await this.jdwp_socket.cmd_and_reply(o.cmd);
return reply.decoded;
}
logcat : function(o) {
/**
* Disconnect the JDWP socket
*/
async jdwp_disconnect() {
await this.jdwp_socket.disconnect();
return true;
}
/**
* Run a shell command on the connected device
* @param {{command:string}} o
*/
async shell_cmd(o) {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
const stdout = await this.adbsocket.cmd_and_read_stdout(`shell:${o.command}`);
await this.disconnect_from_adb();
return stdout;
}
/**
* Starts the Logcat monitor.
* Logcat lines are passed back via onlog callback. If the device disconnects, onclose is called.
* @param {{onlog:(e)=>void, onclose:()=>void}} o
*/
async startLogcatMonitor(o) {
// onlog:function(e)
// onclose:function(e)
// data:anything
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('shell_cmd', 'wa', this.fd, 'shell:logcat -v time');
})
.then(function(data) {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
await this.adbsocket.cmd_and_status('shell:logcat -v time');
// if there's no handler, just read the complete log and finish
if (!o.onlog) {
return this.dexcmd_read_stdout(this.fd)
.then(function(data) {
this.logcatbuffer = data;
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [this.logcatbuffer, x.o.extra]);
});
const logcatbuffer = await this.adbsocket.read_stdout();
await this.disconnect_from_adb();
return logcatbuffer;
}
// start the logcat monitor
return this.dexcmd('so', this.fd)
.then(function() {
this.logcatinfo = {
deferred: x.deferred,
buffer: '',
onlog: o.onlog||(()=>{}),
onlogdata: o.data,
onclose: o.onclose||(()=>{}),
fd: this.fd,
waitfn:_waitfornextlogcat,
}
this.logcatinfo.waitfn.call(this);
function _waitfornextlogcat() {
// create a new promise for when the next message is received
this.activepromise.so = $.Deferred();
this.activepromise.so
.then(function(data) {
var decodeddata = atob(data);
if (decodeddata === 'eoso:d10d9798-1351-11e5-bdd9-5b316631f026') {
this.logcatinfo.fd=0;
this.proxy_disconnect().always(function() {
var e = {adbclient:this, data:this.logcatinfo.onlogdata};
this.logcatinfo.onclose.call(this, e);
if (this.logcatinfo.end) {
var x = this.logcatinfo.end;
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
}
});
return;
}
var s = this.logcatinfo.buffer + atob(data);
var sp = s.split(/\r\n?|\n/);
if (/[\r\n]$/.test(s)) {
this.logcatinfo.buffer = ''
} else {
this.logcatinfo.buffer = sp.pop();
}
var e = {adbclient:this, data:this.logcatinfo.onlogdata, logs:sp};
this.logcatinfo.onlog.call(this, e);
this.logcatinfo.waitfn.call(this);
});
}
// resolve the promise to indicate that logging has started
return x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
});
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
endlogcat : function(o) {
var x = {o:o||{},deferred:$.Deferred()};
var logcatfd = this.logcatinfo && this.logcatinfo.fd;
if (!logcatfd)
return x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
this.logcatinfo.fd = 0;
this.logcatinfo.end = x;
// close the connection - the monitor callback will resolve the promise
this.dexcmd('dc', logcatfd);
return x.deferred;
},
push_file : function(o) {
// filepathname='/data/local/tmp/fname'
// filedata:<arraybuffer>
// filemtime:12345678
this.push_file_info = o;
var x = {o:o,deferred:$.Deferred()};
this.proxy_connect()
.then(function() {
return this.dexcmd('cn');
})
.then(function(data) {
this.fd = data;
return this.dexcmd_read_status('set_transport', 'wa', this.fd, 'host:transport:'+this.deviceid);
})
.then(function(data) {
return this.dexcmd_read_status('sync', 'wa', this.fd, 'sync:');
})
.then(function() {
var perms = '33204';
var cmddata = this.push_file_info.filepathname+','+perms;
var cmd='SEND'+String.fromCharCode(cmddata.length)+'\0\0\0'+cmddata;
return this.dexcmd('wx', this.fd, cmd)
})
.then(function(data) {
return this.dexcmd_write_data(this.push_file_info.filedata);
})
.then(function(data) {
var cmd='DONE';
var mtime = this.push_file_info.filemtime;
for(var i=0;i < 4; i++)
cmd+= String.fromCharCode((mtime>>(i*8))&255);
return this.dexcmd_read_sync_response('done', 'wx', this.fd, cmd);
})
.then(function(data) {
this.progress = 'quit';
var cmd='QUIT\0\0\0\0';
return this.dexcmd('wx', this.fd, cmd);
})
.then(function(data) {
return this.dexcmd('dc', this.fd);
})
.then(function() {
return this.proxy_disconnect();
})
.then(function() {
x.deferred.resolveWith(x.o.ths||this, [x.o.extra]);
})
.fail(function(err) {
x.deferred.rejectWith(x.o.ths||this, [err]);
});
return x.deferred;
},
do_auth : function(msg) {
var m = msg.match(/^vscadb proxy version 1/);
if (m) {
this.authdone = true;
this.status='connected';
return this.activepromise.auth.resolveWith(this, []);
}
return this.proxy_disconnect_with_fail({cat:"Authentication", msg:"Proxy handshake failed"});
},
proxy_disconnect_with_fail : function(reason) {
this.disconnect_reject_reason = reason;
return this.proxy_disconnect();
},
proxy_disconnect : function() {
this.ws&&this.ws.close();
return this.activepromise.disconnect;
},
proxy_onopen : function() {
this.status='handshake';
this.logsend('auth','vscadb client version 1')
.then(function(){
this.activepromise.connected.resolveWith(this, []);
});
},
proxy_onerror : function() {
var reason;
if (this.status!=='connecting') {
reason= {cat:"Protocol", msg:"Connection fault"};
} else {
reason = {cat:"Connection", msg:"A connection to the Dex debugger could not be established.", nodbgr:true};
}
this.proxy_disconnect_with_fail(reason);
},
proxy_onmessage : function(e) {
if (!this.authdone)
return this.do_auth(e.data);
var cmd = e.data.substring(0, 2);
var msgresult = e.data.substring(3, 5);
if (cmd === 'rj' && this.jdwpinfo) {
// rj is the receive-jdwp reply - it is handled separately
if (this.jdwpinfo.started) {
this.jdwpinfo.received.push(e.data.substring(6));
if (this.jdwpinfo.received.length > 1) return;
process.nextTick(function() {
while (this.jdwpinfo.received.length) {
var nextdata = this.jdwpinfo.received.shift();
this.jdwpinfo.onreply.call(this.jdwpinfo.o.ths||this, atob(nextdata));
}
}.bind(this));
return;
}
if (e.data === 'rj ok')
this.jdwpinfo.started = new Date();
}
var err;
var ap = this.activepromise[cmd], p = ap;
if (Array.isArray(p))
p = p.shift();
if (msgresult === "ok") {
if (p) {
if (!ap.length)
this.activepromise[cmd] = null;
p.resolveWith(this, [e.data.substring(6)]);
return;
}
err = {cat:"Command", msg:'Missing response message: ' + cmd};
} else if (e.data==='cn error connection failed') {
// this is commonly expected, so remap the error to something nice
err = {cat:"Connection", msg:'ADB server is not running or cannot be contacted'};
} else {
err = {cat:"Command", msg:e.data};
}
this.proxy_disconnect_with_fail(err);
},
proxy_onclose : function(e) {
// when disconnecting, reject any pending promises first
var pending = [];
for (var cmd in this.activepromise) {
do {
var p = this.activepromise[cmd];
if (!p) break;
if (Array.isArray(p))
p = p.shift();
if (p !== this.activepromise.disconnect)
if (p.state()==='pending')
pending.push(p);
} while(this.activepromise[cmd].length);
}
if (pending.length) {
var reject_reason = this.disconnect_reject_reason || {cat:'Connection', msg:'Proxy disconnection'};
for (var i=0; i < pending.length; i++)
pending[i].rejectWith(this, [reject_reason]);
}
// reset the object so it can be reused
var dcinfo = {
client: this,
deferred: this.activepromise.disconnect,
reason: this.disconnect_reject_reason
};
this.status='closed';
this.reset();
// resolve the disconnect promise after all others
pending.unshift(dcinfo);
$.when.apply($, pending)
.then(function(dcinfo) {
if (dcinfo.reason)
dcinfo.deferred.rejectWith(dcinfo.client, [dcinfo.reason]);
else
dcinfo.deferred.resolveWith(dcinfo.client);
});
},
proxy_connect : function(o) {
var ws, port=(o&&o.port)||6037;
let logcatbuffer = Buffer.alloc(0);
const next_logcat_lines = async () => {
// read the next data from ADB
let next_data;
try{
ws = new WebSocket('ws://127.0.0.1:'+port);
next_data = await this.adbsocket.read_stdout(null);
} catch(e) {
ws=null;
return $.Deferred().rejectWith(this, [new Error('A connection to the ADB proxy could not be established.')]);
};
this.ws = ws;
this.ws.adbclient = this;
this.status='connecting';
// connected is resolved after auth has completed
this.activepromise.connected = $.Deferred();
// disconnect is resolved when the websocket is closed
this.activepromise.disconnect = $.Deferred();
ws.onopen = function(e) {
this.adbclient.proxy_onopen(e);
}
ws.onerror = function(e) {
clearTimeout(this.commandTimeout);
this.adbclient.proxy_onerror(e);
};
ws.onmessage = function(e) {
clearTimeout(this.commandTimeout);
this.adbclient.proxy_onmessage(e);
};
ws.onclose = function(e) {
clearTimeout(this.commandTimeout);
// safari doesn't call onerror for connection failures
if (this.adbclient.status==='connecting' && !this.adbclient.disconnect_reject_reason)
this.adbclient.proxy_onerror(e);
this.adbclient.proxy_onclose(e);
};
// the first promise is always connected, resolved after auth has completed
return this.activepromise.connected.promise();
},
logsend : function(cmd, msg, opts) {
var def = $.Deferred();
if (this.activepromise[cmd]) {
if (Array.isArray(this.activepromise[cmd])) {
// already a queue - just add it
this.activepromise[cmd].push(def);
} else {
// one pending - turn this into a queue
this.activepromise[cmd] = [this.activepromise[cmd], def];
}
} else {
// no active entry
this.activepromise[cmd] = def;
}
if (!this.ws) {
this.proxy_disconnect_with_fail({cat:'Connection', msg:'Proxy disconnected'});
return def;
}
clearTimeout(this.ws.commandTimeout);
try {
this.ws.send(msg);
} catch (e){
this.proxy_disconnect_with_fail({cat:'Connection', msg:e.toString()});
return def;
}
var docmdtimeout = 0;// !(opts&&opts.notimeout);
// if adb is not active, Windows takes at least 1 second to fail
// the socket connect...
this.ws.commandTimeout = docmdtimeout ?
setTimeout(function(adbclient) {
adbclient.proxy_disconnect_with_fail({cat:'Connection', msg:'Command timeout'});
}, 300*1000, this)
: -1;
return def;
},
dexcmd : function(cmd, fd, data, opts) {
var msg = cmd;
if (fd)
msg = msg + " " + fd;
if (data)
msg = msg + " " + btoa(data);
return this.logsend(cmd, msg, opts);
},
dexcmd_read_status : function(cmdname, cmd, fd, data) {
return this.dexcmd(cmd, fd, data)
.then(function() {
return this.dexcmd('rs', this.fd);
})
.then(function(data) {
if (data !== 'OKAY') {
return this.proxy_disconnect_with_fail({cat:"cmd", msg:"Command "+ cmdname +" failed"});
}
return data;
});
},
dexcmd_read_sync_response : function(cmdname, cmd, fd, data) {
return this.dexcmd(cmd, fd, data)
.then(function() {
return this.dexcmd('rs', this.fd, '4');
})
.then(function(data) {
if (data.slice(0,4) !== 'OKAY') {
return this.proxy_disconnect_with_fail({cat:"cmd", msg:"Command "+ cmdname +" failed"});
}
return data;
});
},
dexcmd_read_stdout : function(fd, untilclosed) {
this.stdoutinfo = {
fd: fd,
result:'',
untilclosed:untilclosed||false,
deferred: $.Deferred(),
}
function readchunk() {
this.dexcmd('rx', this.stdoutinfo.fd)
.then(function(data) {
var eod = data==='nomore';
if (data && data.length && !eod) {
this.stdoutinfo.result += atob(data);
}
if (this.stdoutinfo.untilclosed && !eod) {
readchunk.call(this);
o.onclose();
return;
}
var info = this.stdoutinfo;
delete this.stdoutinfo;
info.deferred.resolveWith(this, [info.result]);
})
.fail(function(err) {
var info = this.stdoutinfo;
delete this.stdoutinfo;
info.deferred.rejectWith(this, [err]);
});
}
readchunk.call(this);
return this.stdoutinfo.deferred.promise();
},
dexcmd_write_data : function(data) {
this.dtinfo = {
transferred: 0,
transferring: 0,
data: data,
deferred: $.Deferred(),
}
function writechunk() {
this.dtinfo.transferred += this.dtinfo.transferring;
var remaining = this.dtinfo.data.byteLength-this.dtinfo.transferred;
if (remaining <= 0 || isNaN(remaining)) {
var info = this.dtinfo;
delete this.dtinfo;
info.deferred.resolveWith(this, [info.transferred]);
logcatbuffer = Buffer.concat([logcatbuffer, next_data]);
const last_newline_index = logcatbuffer.lastIndexOf(10) + 1;
if (last_newline_index === 0) {
// wait for a whole line
next_logcat_lines();
return;
}
var datalen=remaining;
if (datalen > 4000) datalen=4000;
var cmd='DATA';
for(var i=0;i < 4; i++)
cmd+= String.fromCharCode((datalen>>(i*8))&255);
var bytes = new Uint8Array(this.dtinfo.data.slice(this.dtinfo.transferred, this.dtinfo.transferred+datalen));
for(var i=0;i < bytes.length; i++)
cmd+= String.fromCharCode(bytes[i]);
bytes = null;
this.dtinfo.transferring = datalen;
this.dexcmd('wx', this.fd, cmd)
.then(function(data) {
writechunk.call(this);
})
.fail(function(err) {
var info = this.dtinfo;
delete this.dtinfo;
info.deferred.rejectWith(this, [err]);
});
}
writechunk.call(this);
return this.dtinfo.deferred.promise();
},
// split into lines
const logs = logcatbuffer.slice(0, last_newline_index).toString().split(/\r\n?|\n/);
logcatbuffer = logcatbuffer.slice(last_newline_index);
const e = {
adbclient: this,
logs,
};
o.onlog(e);
next_logcat_lines();
}
next_logcat_lines();
}
endLogcatMonitor() {
return this.adbsocket.disconnect();
}
/**
* @param {ADBFileTransferParams} o
*/
async push_file(o) {
await this.connect_to_adb();
await this.adbsocket.cmd_and_status(`host:transport:${this.deviceid}`);
await this.adbsocket.transfer_file(o);
await this.adbsocket.disconnect();
return true;
}
/**
* @param {string} [hostname]
*/
connect_to_adb(hostname = '127.0.0.1') {
this.adbsocket = new ADBSocket();
return this.adbsocket.connect(this.adbPort, hostname);
}
disconnect_from_adb () {
return this.adbsocket.disconnect();
}
};
exports.ADBClient = ADBClient;

View File

@@ -193,13 +193,14 @@ function decode_binary_xml(buf) {
}
case 0x0102: {
// begin node
const node = {};
const node = {
nodes: [],
};
idx += decode_spec(buf, BEGIN_NODE_SPEC, node, node, idx);
node.namespaces = namespaces.slice();
node.namespaces.forEach(ns => {
if (!ns.node) ns.node = node;
});
node.nodes = [];
node_stack[0].nodes.push(node);
node_stack.unshift(node);
break;

145
src/apk-file-info.js Normal file
View File

@@ -0,0 +1,145 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { extractManifestFromAPK, parseManifest } = require('./manifest');
const { D } = require('./utils/print');
class APKFileInfo {
/**
* the full file path to the APK file
*/
fpn = '';
/**
* The APK file data
* @type {Buffer}
*/
file_data = null;
/**
* last modified time of the APK file (in ms)
*/
app_modified = 0;
/**
* SHA-1 (hex) digest of the APK file
*/
content_hash = '';
/**
* Contents of Android Manifest XML file
*/
manifestXml = '';
/**
* Extracted data from the manifest
*/
manifest = {
/**
* Package name of the app
*/
package: '',
/**
* List of all named Activities
* @type {string[]}
*/
activities: [],
/**
* The launcher Activity
*/
launcher: '',
};
constructor(apk_fpn) {
this.fpn = apk_fpn;
}
/**
* Build a new APKFileInfo instance
* @param {*} args
*/
static async from(args) {
const result = new APKFileInfo(args.apkFile);
// read the APK file contents
try {
result.file_data = await readFile(args.apkFile);
} catch(err) {
throw new Error(`APK read error. ${err.message}`);
}
// save the last modification time of the app
result.app_modified = fs.statSync(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(result.file_data);
result.content_hash = h.digest('hex');
// read the manifest
try {
result.manifestXml = await getAndroidManifestXml(args);
} catch (err) {
throw new Error(`Manifest read error. ${err.message}`);
}
// extract the parts we need from the manifest
try {
result.manifest = parseManifest(result.manifestXml);
} catch(err) {
throw new Error(`Manifest parse failed. ${err.message}`);
}
return result;
}
}
/**
* Retrieve the AndroidManifest.xml file content
*
* Because of manifest merging and build-injected properties, the manifest compiled inside
* the APK is frequently different from the AndroidManifest.xml source file.
* We try to extract the manifest from 3 sources (in priority order):
* 1. The 'manifestFile' launch configuration property
* 2. The decoded manifest from the APK
* 3. The AndroidManifest.xml file from the root of the source tree.
*/
async function getAndroidManifestXml(args) {
const {manifestFile, apkFile, appSrcRoot} = args;
let manifest;
// a value from the manifestFile overrides the default manifest extraction
// note: there's no validation that the file is a valid AndroidManifest.xml file
if (manifestFile) {
D(`Reading manifest from ${manifestFile}`);
manifest = await readFile(manifestFile, 'utf8');
return manifest;
}
try {
D(`Reading APK Manifest`);
manifest = await extractManifestFromAPK(apkFile);
} catch(err) {
// if we fail to get manifest from the APK, revert to the source file version
D(`Reading source manifest from ${appSrcRoot}`);
manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8');
}
return manifest;
}
/**
* Promisified fs.readFile()
* @param {string} path
* @param {*} [options]
*/
function readFile(path, options) {
return new Promise((res, rej) => {
fs.readFile(path, options || {}, (err, data) => {
err ? rej(err) : res(data);
})
})
}
module.exports = {
APKFileInfo,
}

View File

@@ -1,198 +0,0 @@
const net = require('net');
const D = require('./util').D;
var sockets_by_id = {};
var last_socket_id = 0;
const chrome = {
storage: {
local: {
q:{},
get(o, cb) {
for (var key in o) {
var x = this.q[key];
if (typeof(x) !== 'undefined') o[key] = x;
}
process.nextTick(cb, o);
},
set(obj, cb) {
for (var key in obj)
this.q[key] = obj[key];
process.nextTick(cb);
}
}
},
runtime: {
lastError:null,
_noError() { this.lastError = null }
},
permissions: {
request(usbPermissions, cb) {
process.nextTick(cb, true);
}
},
socket: {
listen(socketId, host, port, max_connections, cb) {
var s = sockets_by_id[socketId];
s._raw.listen(port, host, max_connections);
process.nextTick(cb => {
chrome.runtime._noError();
cb(0);
}, cb);
},
connect(socketId, host, port, cb) {
var s = sockets_by_id[socketId];
s._raw.connect({port:port,host:host}, function(){
chrome.runtime._noError();
this.s.onerror = null;
this.cb.call(null,0);
}.bind({s:s,cb:cb}));
s.onerror = function(e) {
this.s.onerror = null;
this.cb.call(null,-1);
}.bind({s:s,cb:cb});
},
disconnect(socketId) {
var s = sockets_by_id[socketId];
s._raw.end();
},
setNoDelay(socketId, state, cb) {
var s = sockets_by_id[socketId];
s._raw.setNoDelay(state);
process.nextTick(cb => {
chrome.runtime._noError();
cb(1);
}, cb);
},
read(socketId, bufferSize, onRead) {
if (!onRead && typeof(bufferSize) === 'function')
onRead = bufferSize, bufferSize=-1;
if (!onRead) return;
var s = sockets_by_id[socketId];
if (bufferSize === 0) {
process.nextTick(function(onRead) {
chrome.runtime._noError();
onRead.call(null, {resultCode:1,data:Buffer.alloc(0)});
}, onRead);
return;
}
s.read_requests.push({onRead:onRead, bufferSize:bufferSize});
if (s.read_requests.length > 1) {
return;
}
!s.ondata && s._raw.on('data', s.ondata = function(data) {
this.readbuffer = Buffer.concat([this.readbuffer, data]);
while(this.read_requests.length) {
var amount = this.read_requests[0].bufferSize;
if (amount <= 0) amount = this.readbuffer.length;
if (amount > this.readbuffer.length || this.readbuffer.length === 0)
return; // wait for more data
var readInfo = {
resultCode:1,
data:Buffer.from(this.readbuffer.slice(0,amount)),
};
this.readbuffer = this.readbuffer.slice(amount);
chrome.runtime._noError();
this.read_requests.shift().onRead.call(null,readInfo);
}
this.onerror = this.onclose = null;
}.bind(s));
var on_read_terminated = function(e) {
this.readbuffer = Buffer.alloc(0);
while(this.read_requests.length) {
var readInfo = {
resultCode:-1, // <=0 for error
};
this.read_requests.shift().onRead.call(null,readInfo);
}
this.onerror = this.onclose = null;
}.bind(s);
!s.onerror && (s.onerror = on_read_terminated);
!s.onclose && (s.onclose = on_read_terminated);
if (s.readbuffer.length || bufferSize < 0) {
process.nextTick(s.ondata, Buffer.alloc(0));
}
},
write(socketId, data, cb) {
var s = sockets_by_id[socketId];
if (!(data instanceof Buffer))
data = Buffer.from(data);
s._raw.write(data, function(e,f,g) {
if (this.s.write_cbs.length === 1)
this.s.onerror = null;
var writeInfo = {
bytesWritten: this.len,
};
chrome.runtime._noError();
this.s.write_cbs.shift().call(null, writeInfo);
}.bind({s:s,len:data.length,cb:cb}));
s.write_cbs.push(cb);
if (!s.onerror) {
s.onerror = function(e) {
this.s.onerror = null;
while (this.s.write_cbs.length) {
var writeInfo = {
bytesWritten: 0,
};
this.s.write_cbs.shift().call(null, writeInfo);
}
}.bind({s:s});
}
},
},
create_socket:function(id, type, cb) {
if (!cb && typeof(type) === 'function') {
cb = type, type = null;
}
var socket = type === 'server' ? new net.Server() : new net.Socket();
var socketInfo = {
id: id,
socketId: ++last_socket_id,
_raw: socket,
onerror:null,
onclose:null,
write_cbs:[],
read_requests:[],
readbuffer:Buffer.alloc(0),
};
socketInfo._raw.on('error', function(e) {
chrome.runtime.lastError = e;
this.onerror && this.onerror(e);
}.bind(socketInfo));
socketInfo._raw.on('close', function(e) {
this.onclose && this.onclose(e);
}.bind(socketInfo));
sockets_by_id[socketInfo.socketId] = socketInfo;
process.nextTick(cb, socketInfo);
},
create_chrome_socket(id, type, cb) { return chrome.create_socket(id, type, cb) },
accept_socket:function(id, socketId, cb) {
var s = sockets_by_id[socketId];
if (s.onconnection) {
s.onconnection = cb;
} else {
s.onconnection = cb;
s._raw.on('connection', function(client_socket) {
var acceptInfo = {
socketId: ++last_socket_id,
_raw: client_socket,
}
sockets_by_id[acceptInfo.socketId] = acceptInfo;
this.onconnection(acceptInfo);
}.bind(s));
}
},
accept_chrome_socket(id, socketId, cb) { return chrome.accept_socket(id, socketId, cb) },
destroy_socket:function(socketId) {
var s = sockets_by_id[socketId];
if (!s) return;
s._raw.end();
sockets_by_id[socketId] = null;
},
destroy_chrome_socket(socketId) { return chrome.destroy_socket(socketId) },
}
exports.chrome = chrome;

View File

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

File diff suppressed because it is too large Load Diff

772
src/debugger-types.js Normal file
View File

@@ -0,0 +1,772 @@
const { ADBClient } = require('./adbclient');
const { PackageInfo } = require('./package-searcher');
//const { JavaType } = require('./util');
const { splitSourcePath } = require('./utils/source-file');
class BuildInfo {
/**
* @param {string} pkgname
* @param {Map<string,PackageInfo>} packages
* @param {string} launchActivity
*/
constructor(pkgname, packages, launchActivity) {
this.pkgname = pkgname;
this.packages = packages;
this.launchActivity = launchActivity;
/** the arguments passed to `am start` */
this.startCommandArgs = [
'-D', // enable debugging
'--activity-brought-to-front',
'-a android.intent.action.MAIN',
'-c android.intent.category.LAUNCHER',
`-n ${pkgname}/${launchActivity}`,
];
/**
* the amount of time to wait after 'am start ...' is invoked.
* We need this because invoking JDWP too soon causes a hang.
*/
this.postLaunchPause = 1000;
}
}
/**
* A single debugger session
*/
class DebugSession {
/**
* @param {BuildInfo} build
* @param {string} deviceid
*/
constructor(build, deviceid) {
/**
* Build information for this session
*/
this.build = build;
/**
* The device ID of the device being debugged
*/
this.deviceid = deviceid;
/**
* The ADB connection to the device being debugged
* @type {ADBClient}
*/
this.adbclient = null;
/**
* Location of the last stop event (breakpoint, exception, step)
* @type {SourceLocation}
*/
this.stoppedLocation = null;
/**
* The entire list of retrieved types during the debug session
* @type {DebuggerTypeInfo[]}
*/
this.classList = [];
/**
* Map of type signatures to cached types
* @type {Map<string,DebuggerTypeInfo | Promise<DebuggerTypeInfo>>}
*/
this.classCache = new Map();
/**
* The class-prepare filters set up on the device
* @type {Set<string>}
*/
this.classPrepareFilters = new Set();
/**
* The set of class signatures already prepared
* @type {Set<string>}
*/
this.preparedClasses = new Set();
/**
* Enabled step JDWP IDs for each thread
* @type {Map<JavaThreadID, StepID>}
*/
this.stepIDs = new Map();
/**
* The counts of thread-suspend calls. A thread is only resumed when the
* all suspend calls are matched with resume calls.
* @type {Map<JavaThreadID, number>}
*/
this.threadSuspends = new Map();
/**
* The queue of pending method invoke expressions to be called for each thread.
* Method invokes can only be called sequentially on a per-thread basis.
* @type {Map<JavaThreadID, *[]>}
*/
this.methodInvokeQueues = new Map();
}
}
class JavaTaggedValue {
/**
*
* @param {string|number|boolean} value
* @param {JavaValueType} valuetype
*/
constructor(value, valuetype) {
this.value = value;
this.valuetype = valuetype;
}
static signatureToJavaValueType(s) {
return {
B: 'byte',C:'char',D:'double',F:'float',I:'int',J:'long','S':'short',V:'void',Z:'boolean'
}[s[0]] || 'oref';
}
/**
*
* @param {DebuggerValue} v
* @param {string} [signature]
*/
static from(v, signature) {
return new JavaTaggedValue(v.value, JavaTaggedValue.signatureToJavaValueType(signature || v.type.signature));
}
}
/**
* Base class of Java types
*/
class JavaType {
/**
* @param {string} signature JRE type signature
* @param {string} typename human-readable type name
* @param {boolean} [invalid] true if the type could not be parsed from the signature
*/
constructor(signature, typename, invalid) {
this.signature = signature;
this.typename = typename;
if (invalid) {
this.invalid = invalid;
}
}
fullyQualifiedName() {
return this.typename;
}
/** @type {Map<string, JavaType>} */
static _cache = new Map();
/**
* @param {string} signature
* @returns {JavaType}
*/
static from(signature) {
let type = JavaType._cache.get(signature);
if (!type) {
type = JavaClassType.from(signature)
|| JavaArrayType.from(signature)
|| JavaPrimitiveType.from(signature)
|| new JavaType(signature, signature, true);
JavaType._cache.set(signature, type);
}
return type;
}
static get Object() {
return JavaType.from('Ljava/lang/Object;');
}
static get String() {
return JavaType.from('Ljava/lang/String;');
}
static get byte() {
return JavaType.from('B');
}
static get short() {
return JavaType.from('S');
}
static get int() {
return JavaType.from('I');
}
static get long() {
return JavaType.from('J');
}
static get float() {
return JavaType.from('F');
}
static get double() {
return JavaType.from('D');
}
static get char() {
return JavaType.from('C');
}
static get boolean() {
return JavaType.from('Z');
}
static null = new JavaType('Lnull;', 'null'); // null has no type really, but we need something for literals
/**
* @param {JavaType} t
*/
static isArray(t) { return /^\[/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isByte(t) { return /^B$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isClass(t) { return /^L/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isReference(t) { return /^[L[]/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isPrimitive(t) { return /^[BCIJSFDZ]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isInteger(t) { return /^[BIS]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isLong(t) { return /^J$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isFloat(t) { return /^[FD]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isArrayIndex(t) { return /^[BCIJS]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isString(t) { return t.signature === this.String.signature }
/**
* @param {JavaType} t
*/
static isChar(t) { return t.signature === this.char.signature }
/**
* @param {JavaType} t
*/
static isBoolean(t) { return t.signature === this.boolean.signature }
}
class JavaClassType extends JavaType {
/**
*
* @param {string} signature
* @param {string} package_name
* @param {string} typename
* @param {boolean} anonymous
*/
constructor(signature, package_name, typename, anonymous) {
super(signature, typename);
this.package = package_name;
this.anonymous = anonymous;
}
fullyQualifiedName() {
return this.package ? `${this.package}.${this.typename}` : this.typename;
}
/**
* @param {string} signature
*/
static from(signature) {
const class_match = signature.match(/^L([^$]+)\/([^$\/]+)(\$.+)?;$/);
if (!class_match) {
return null;
}
const package_name = class_match[1].replace(/\//g,'.');
const typename = (class_match[2]+(class_match[3]||'')).replace(/\$(?=[^\d])/g,'.');
const anonymous = /\$\d/.test(class_match[3]);
return new JavaClassType(signature, package_name, typename, anonymous);
}
}
class JavaArrayType extends JavaType {
/**
* @param {string} signature JRE type signature
* @param {number} arraydims number of array dimensions
* @param {JavaType} elementType array element type
*/
constructor(signature, arraydims, elementType) {
super(signature, `${elementType.typename}[]`);
this.arraydims = arraydims;
this.elementType = elementType;
}
fullyQualifiedName() {
return `${this.elementType.fullyQualifiedName()}[]`;
}
static from(signature) {
const array_match = signature.match(/^(\[+)(.+)$/);
if (!array_match) {
return null;
}
const elementType = JavaType.from(array_match[1].slice(0,-1) + array_match[2]);
return new JavaArrayType(signature, array_match[1].length, elementType);
}
}
class JavaPrimitiveType extends JavaType {
/**
* @param {string} signature
* @param {string} typename
*/
constructor(signature, typename) {
super(signature, typename);
}
/**
* @param {string} signature
*/
static from(signature) {
return Object.prototype.hasOwnProperty.call(JavaPrimitiveType.bySignature, signature)
? JavaPrimitiveType.bySignature[signature]
: null;
}
static bySignature = {
B: new JavaPrimitiveType('B', 'byte'),
C: new JavaPrimitiveType('C', 'char'),
F: new JavaPrimitiveType('F', 'float'),
D: new JavaPrimitiveType('D', 'double'),
I: new JavaPrimitiveType('I', 'int'),
J: new JavaPrimitiveType('J', 'long'),
S: new JavaPrimitiveType('S', 'short'),
V: new JavaPrimitiveType('V', 'void'),
Z: new JavaPrimitiveType('Z', 'boolean'),
}
}
class DebuggerValue {
/**
* @param {DebuggerValueType} vtype
* @param {JavaType} type
* @param {*} value
* @param {boolean} valid
* @param {boolean} hasnullvalue
* @param {string} name
* @param {*} data
*/
constructor(vtype, type, value, valid, hasnullvalue, name, data) {
this.vtype = vtype;
this.hasnullvalue = hasnullvalue;
this.name = name;
this.type = type;
this.valid = valid;
this.value = value;
this.data = data;
/** @type {string} */
this.string = null;
/** @type {number} */
this.biglen = null;
/** @type {number} */
this.arraylen = null;
/** @type {string} */
this.fqname = null;
}
}
class LiteralValue extends DebuggerValue {
/**
* @param {JavaType} type
* @param {*} value
* @param {boolean} [hasnullvalue]
* @param {*} [data]
*/
constructor(type, value, hasnullvalue = false, data = null) {
super('literal', type, value, true, hasnullvalue, '', data);
}
static Null = new LiteralValue(JavaType.null, '0000000000000000', true);
}
/**
* The base class of all debugger events invoked by JDWP
*/
class DebuggerEvent {
constructor(event) {
this.event = event;
}
}
class JavaBreakpointEvent extends DebuggerEvent {
/**
*
* @param {*} event
* @param {SourceLocation} stoppedLocation
* @param {DebuggerBreakpoint} breakpoint
*/
constructor(event, stoppedLocation, breakpoint) {
super(event)
this.stoppedLocation = stoppedLocation;
this.bp = breakpoint;
}
}
class JavaExceptionEvent extends DebuggerEvent {
/**
* @param {JavaObjectID} event
* @param {SourceLocation} throwlocation
* @param {SourceLocation} catchlocation
*/
constructor(event, throwlocation, catchlocation) {
super(event);
this.throwlocation = throwlocation;
this.catchlocation = catchlocation;
};
}
class DebuggerException {
/**
* @param {DebuggerValue} exceptionValue
* @param {JavaThreadID} threadid
*/
constructor(exceptionValue, threadid) {
this.exceptionValue = exceptionValue;
this.threadid = threadid;
/** @type {VSCVariableReference} */
this.scopeRef = null;
/** @type {VSCVariableReference} */
this.frameId = null;
}
}
class BreakpointLocation {
/**
* @param {DebuggerBreakpoint} bp
* @param {DebuggerTypeInfo} c
* @param {DebuggerMethodInfo} m
* @param {hex64} l
*/
constructor(bp, c, m, l) {
this.bp = bp;
this.c = c;
this.m = m;
this.l = l;
}
}
class SourceLocation {
/**
* @param {string} qtype
* @param {number} linenum
* @param {boolean} exact
* @param {JavaThreadID} threadid
*/
constructor(qtype, linenum, exact, threadid) {
this.qtype = qtype;
this.linenum = linenum;
this.exact = exact;
this.threadid = threadid;
}
toString() {
return JSON.stringify(this);
}
}
class DebuggerMethodInfo {
/**
* @param {JavaMethod} m
* @param {DebuggerTypeInfo} owningclass
*/
constructor(m, owningclass) {
this._method = m;
this.owningclass = owningclass;
/** @type {JavaVarTable} */
this.vartable = null;
/** @type {JavaLineTable} */
this.linetable = null;
}
get genericsig() { return this._method.genericsig }
get methodid() { return this._method.methodid }
/**
* https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1
*/
get modbits() { return this._method.modbits }
get name() { return this._method.name }
get sig() { return this._method.sig }
get isStatic() {
return (this._method.modbits & 0x0008) !== 0;
}
/**
* @param {JavaLineTable} linetable
*/
setLineTable(linetable) {
return this.linetable = linetable;
}
/**
* @param {JavaVarTable} vartable
*/
setVarTable(vartable) {
return this.vartable = vartable;
}
get returnTypeSignature() {
return (this._method.genericsig || this._method.sig).match(/\)(.+)$/)[1];
}
static NullLineTable = {
start: '0000000000000000',
end: '0000000000000000',
lines: [],
};
}
class DebuggerFrameInfo {
/**
*
* @param {JavaFrame} frame
* @param {DebuggerMethodInfo} method
* @param {JavaThreadID} threadid
*/
constructor(frame, method, threadid) {
this._frame = frame;
this.method = method;
this.threadid = threadid;
}
get frameid() {
return this._frame.frameid;
}
get location() {
return this._frame.location;
}
}
class DebuggerBreakpoint {
/**
* @param {string} srcfpn
* @param {number} linenum
* @param {BreakpointOptions} options
* @param {BreakpointState} initialState
*/
constructor(srcfpn, linenum, options, initialState = 'set') {
const cls = splitSourcePath(srcfpn);
this.id = DebuggerBreakpoint.makeBreakpointID(srcfpn, linenum);
this.srcfpn = srcfpn;
this.qtype = cls.qtype;
this.pkg = cls.pkg;
this.type = cls.type;
this.linenum = linenum;
this.options = options;
this.sigpattern = new RegExp(`^L${cls.qtype}([$][$a-zA-Z0-9_]+)?;$`),
this.state = initialState; // set,notloaded,enabled,removed
this.hitcount = 0; // number of times this bp was hit during execution
this.stopcount = 0; // number of times this bp caused a break into the debugger
this.vsbp = null;
this.enabled = null;
}
/**
* @param {BreakpointLocation} bploc
* @param {number} requestid JDWP request ID for the breakpoint
*/
setEnabled(bploc, requestid) {
this.enabled = {
/** @type {CMLKey} */
cml: `${bploc.c.info.typeid}:${bploc.m.methodid}:${bploc.l}`,
bp: this,
bploc: {
c: bploc.c,
m: bploc.m,
l: bploc.l,
},
requestid,
}
}
setDisabled() {
this.enabled = null;
}
/**
* Constructs a unique breakpoint ID from the source path and line number
* @param {string} srcfpn
* @param {number} line
* @returns {BreakpointID}
*/
static makeBreakpointID(srcfpn, line) {
const cls = splitSourcePath(srcfpn);
return `${line}:${cls.qtype}`;
}
}
class BreakpointOptions {
/**
* Hit-count used for conditional breakpoints
* @type {number|null}
*/
hitcount = null;
}
class DebuggerTypeInfo {
/**
* @param {JavaClassInfo} info
* @param {JavaType} type
*/
constructor(info, type) {
this.info = info;
this.type = type;
/** @type {JavaField[]} */
this.fields = null;
/** @type {DebuggerMethodInfo[]} */
this.methods = null;
/** @type {JavaSource} */
this.src = null;
// if it's not a class type, set super to null
// otherwise, leave super undefined to be updated later
if (info.reftype.string !== 'class' || type.signature[0] !== 'L' || type.signature === JavaType.Object.signature) {
if (info.reftype.string !== 'array') {
/** @type {JavaType} */
this.super = null;
}
}
}
get name() {
return this.type.typename;
}
}
/**
* Dummy type info for when the Java runtime hasn't loaded the class.
*/
class TypeNotAvailable extends DebuggerTypeInfo {
/** @type {JavaClassInfo} */
static info = {
reftype: 0,
status: null,
type: null,
typeid: '',
}
constructor(type) {
super(TypeNotAvailable.info, type);
super.fields = [];
super.methods = [];
}
}
class JavaThreadInfo {
/**
* @param {JavaThreadID} threadid
* @param {string} name
* @param {*} status
*/
constructor(threadid, name, status) {
this.threadid = threadid;
this.name = name;
this.status = status;
}
}
class MethodInvokeArgs {
/**
* @param {JavaObjectID} objectid
* @param {JavaThreadID} threadid
* @param {DebuggerMethodInfo} method
* @param {DebuggerValue[]} args
*/
constructor(objectid, threadid, method, args) {
this.objectid = objectid;
this.threadid = threadid;
this.method = method;
this.args = args;
this.promise = null;
}
}
class VariableValue {
/**
* @param {string} name
* @param {string} value
* @param {string} [type]
* @param {number} [variablesReference]
* @param {string} [evaluateName]
*/
constructor(name, value, type = '', variablesReference = 0, evaluateName = '') {
this.name = name;
this.value = value;
this.type = type;
this.variablesReference = variablesReference;
this.evaluateName = evaluateName;
}
}
module.exports = {
BreakpointLocation,
BreakpointOptions,
BuildInfo,
DebuggerBreakpoint,
DebuggerException,
DebuggerFrameInfo,
DebuggerMethodInfo,
DebuggerTypeInfo,
DebugSession,
DebuggerValue,
LiteralValue,
JavaBreakpointEvent,
JavaExceptionEvent,
JavaTaggedValue,
JavaType,
JavaArrayType,
JavaClassType,
JavaPrimitiveType,
JavaThreadInfo,
MethodInvokeArgs,
SourceLocation,
TypeNotAvailable,
VariableValue,
}

File diff suppressed because it is too large Load Diff

109
src/expression/assign.js Normal file
View File

@@ -0,0 +1,109 @@
const { Debugger } = require('../debugger');
const { DebuggerValue, JavaTaggedValue, JavaType } = require('../debugger-types');
const { NumberBaseConverter } = require('../utils/nbc');
const validmap = {
B: 'BC', // char might not fit into a byte - we special-case this
S: 'BSC',
I: 'BSIC',
J: 'BSIJC',
F: 'BSIJCF',
D: 'BSIJCFD',
C: 'BSC',
Z: 'Z',
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
};
/**
* Checks if the value will fit into a variable with given type
* @param {JavaType} variable_type
* @param {DebuggerValue} value
*/
function checkPrimitiveSize(variable_type, value) {
// variable_type_signature must be a primitive
if (!Object.prototype.hasOwnProperty.call(validmap, variable_type.signature)) {
return false;
}
let value_type_signature = value.type.signature;
if (value.vtype === 'literal' && /[BSI]/.test(value_type_signature)) {
// for integer literals, find the minimum type the value will fit into
if (value.value >= -128 && value.value <= 127) value_type_signature = 'B';
else if (value.value >= -32768 && value.value <= 32767) value_type_signature = 'S';
else if (value.value >= -2147483648 && value.value <= 2147483647) value_type_signature = 'I';
}
let is_in_range = validmap[variable_type.signature].indexOf(value_type_signature) >= 0;
// special check to see if a char value fits into a single byte
if (JavaType.isByte(variable_type) && JavaType.isChar(value.type)) {
is_in_range = validmap.isCharInRangeForByte(value.value);
}
return is_in_range;
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue} destvar
* @param {string} name
* @param {DebuggerValue} result
*/
async function assignVariable(dbgr, destvar, name, result) {
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
throw new Error(`The value is read-only and cannot be updated.`);
}
// non-string reference types can only set to null
if (JavaType.isReference(destvar.type) && !JavaType.isString(destvar.type)) {
if (!result.hasnullvalue) {
throw new Error('Object references can only be set to null');
}
}
// as a nicety, if the destination is a string, stringify any primitive value
if (JavaType.isPrimitive(result.type) && JavaType.isString(destvar.type)) {
result = await dbgr.createJavaStringLiteral(result.value.toString(), { israw:true });
}
if (JavaType.isPrimitive(destvar.type)) {
// if the destination is a primitive, we need to range-check it here
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
// weirdness if we allow primitives to be set with out-of-range values
const is_in_range = checkPrimitiveSize(destvar.type, result);
if (!is_in_range) {
throw new Error(`'${result.value}' is not compatible with variable type: ${destvar.type.typename}`);
}
}
const data = JavaTaggedValue.from(result, destvar.type.signature);
if (JavaType.isLong(destvar.type) && typeof data.value === 'number') {
// convert ints to hex-string longs
data.value = NumberBaseConverter.decToHex(data.value.toString(),16);
}
// convert the debugger value to a JavaTaggedValue
let newlocalvar;
// setxxxvalue sets the new value and then returns a new local for the variable
switch(destvar.vtype) {
case 'field':
newlocalvar = await dbgr.setFieldValue(destvar.data.objvar, destvar.data.field, data);
break;
case 'local':
newlocalvar = await dbgr.setLocalVariableValue(destvar.data.frame, destvar.data.slotinfo, data);
break;
case 'arrelem':
newlocalvar = await dbgr.setArrayElements(destvar.data.array, parseInt(name, 10), 1, data);
newlocalvar = newlocalvar[0];
break;
default:
throw new Error('Unsupported variable type');
}
return newlocalvar;
}
module.exports = {
assignVariable,
}

983
src/expression/evaluate.js Normal file
View File

@@ -0,0 +1,983 @@
const Long = require('long');
const {
ArrayIndexExpression,
BinaryOpExpression,
ExpressionText,
MemberExpression,
MethodCallExpression,
parse_expression,
ParsedExpression,
QualifierExpression,
RootExpression,
TypeCastExpression,
UnaryOpExpression,
} = require('./parse');
const { DebuggerValue, JavaTaggedValue, JavaType, LiteralValue } = require('../debugger-types');
const { Debugger } = require('../debugger');
const { AndroidThread } = require('../threads');
const { D } = require('../utils/print');
const { decodeJavaCharLiteral } = require('../utils/char-decode');
/**
* @param {Long.Long} long
*/
function hex_long(long) {
return long.toUnsigned().toString(16).padStart(64/4, '0');
}
/**
* Determine what type of primitive a decimal value will require
* @param {string} decimal_value
* @returns {'int'|'long'|'float'|'double'}
*/
function get_decimal_number_type(decimal_value) {
if (/^-?0*\d{0,15}(\.0*)?$/.test(decimal_value)) {
const n = parseInt(decimal_value, 10);
if (n >= -2147483648 && n <= 2147483647) {
return 'int';
}
return 'long';
}
// int64: 9223,372036854775807
let m = decimal_value.match(/^(-?)0*(\d*?)(\d{1,4})(\d{15})(\.0+)?$/);
if (m) {
const sign = m[1];
if (!m[2]) {
const x = [parseInt(m[3],10), parseInt(m[4],10)];
if (x[0] < 9223) {
return 'long';
}
if (x[0] > 9223) {
return 'float';
}
let limit = 372036854775807 + (sign ? 1 : 0);
if (x[1] <= limit) {
return 'long';
}
return 'float'
}
// single precision floats allow integers up to +/- 2^127:
// 34028,236692093846346,3374,607431768211455
// but rounded to a power of 2 (not checked here)
let q = m[2].match(/^(\d*?)(\d{0,5}?)(\d{1,15})$/);
if (q[1]) {
return 'double';
}
const x = [parseInt(q[2],10), parseInt(q[3],10), parseInt(m[3],10), parseInt(m[4],10)]
if (x[0] > 34028) {
return 'double';
}
if (x[0] < 34028) {
return 'float';
}
if (x[1] > 236692093846346) {
return 'double';
}
if (x[1] < 236692093846346) {
return 'float';
}
if (x[2] > 3374) {
return 'double';
}
if (x[2] < 3374) {
return 'float';
}
let limit = 607431768211455 + (sign ? 1 : 0);
if (x[3] <= limit) {
return 'float';
}
return 'double';
}
if (/^-?\d{0,38}\./.test(decimal_value))
return 'float';
return 'double'
}
/**
* Convert an exponent-formatted number into a normalised decimal equivilent.
* e.g '1.2345e3' -> '1234.5'
*
* If the number does not include an exponent, it is returned unchanged.
* @param {string} n
*/
function decimalise_exponent_number(n) {
const exp = n.match(/^(\D*)0*(\d+)(?:\.(\d+?)0*)?[eE]([+-]?)0*(\d+)(.*)/);
if (!exp) {
return n;
}
let i = exp[2], frac = (exp[3]||''), sign = exp[4]||'+', pow10 = parseInt(exp[5],10);
if (pow10 > 0) {
if (sign === '+') {
let shifted_digits = Math.min(frac.length, pow10);
i += frac.slice(0, shifted_digits);
frac = frac.slice(shifted_digits);
pow10 -= shifted_digits;
i += '0'.repeat(pow10);
} else {
let shifted_digits = Math.min(i.length, pow10);
frac = i.slice(-shifted_digits) + frac; // move up to pow10 digits from i to frac
i = i.slice(0, -shifted_digits);
pow10 -= shifted_digits;
frac = '0'.repeat(pow10) + frac;
}
}
i = (i || '0').match(/^0*(.+)/)[1];
if (/[1-9]/.test(frac)) i += `.${frac}`;
return `${exp[1]}${i}${exp[6]}`
}
/**
* @param {number|string} number
*/
function evaluate_number(number) {
let n = number.toString();
// normalise exponents into decimal form
n = decimalise_exponent_number(n);
let number_type, base = 10;
const m = n.match(/^([+-]?)0([bBxX0-7])(.+)/);
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)) {
number_type = /[fF]$/.test(n) ? 'float' : 'double';
n = n.slice(0, -1);
} else if (/[lL]$/.test(n)) {
number_type = 'long'
n = n.slice(0, -1);
} else {
number_type = get_decimal_number_type(n);
}
let result;
if (number_type === 'long') {
result = hex_long(Long.fromString(n, false, base));
} else if (/^[fd]/.test(number_type)) {
result = (base === 10) ? parseFloat(n) : parseInt(n, base);
} else {
result = parseInt(n, base) | 0;
}
const iszero = /^[+-]?0+(\.0*)?$/.test(result.toString());
return new LiteralValue(JavaType[number_type], result, iszero);
}
/**
* @param {string} char
*/
function evaluate_char(char) {
// JDWP returns char values as uint16's, so we need to set the value as a number
return new LiteralValue(JavaType.char, char.charCodeAt(0));
}
/**
* Convert a value to a number
* @param {DebuggerValue} local
*/
function numberify(local) {
if (JavaType.isFloat(local.type)) {
return parseFloat(local.value);
}
const radix = JavaType.isLong(local.type) ? 16 : 10;
return parseInt(local.value, radix);
}
/**
* Convert a value to a string
* @param {Debugger} dbgr
* @param {DebuggerValue} local
*/
async function stringify(dbgr, local) {
let s = '';
switch(true) {
case JavaType.isString(local.type):
s = local.string;
break;
case JavaType.isPrimitive(local.type):
s = local.value.toString();
break;
case local.hasnullvalue:
s = '(null)';
break;
case JavaType.isReference(local.type):
// call toString() on the object
const str_literal = await dbgr.invokeToString(local.value, local.data.frame.threadid, local.type.signature);
s = str_literal.string;
break;
}
return s;
}
/**
* @param {string} operator
* @param {boolean} [is_unary]
*/
function invalid_operator(operator, is_unary = false) {
return new Error(`Invalid ${is_unary ? 'type' : 'types'} for operator '${operator}'`);
}
/**
*
*/
function divide_by_zero() {
return new Error('ArithmeticException: divide by zero');
}
/**
*
* @param {*} lhs_local
* @param {*} rhs_local
* @param {string} operator
*/
function evaluate_binary_boolean_expression(lhs_local, rhs_local, operator) {
let a = lhs_local.value, b = rhs_local.value;
switch (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: throw invalid_operator(operator);
}
return new LiteralValue(JavaType.boolean, a);
}
/**
*
* @param {*} lhs_local
* @param {*} rhs_local
* @param {string} operator
*/
function evaluate_binary_float_expression(lhs_local, rhs_local, operator) {
/** @type {number|boolean} */
let a = numberify(lhs_local), b = numberify(rhs_local);
switch (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: throw invalid_operator(operator);
}
/** @type {number|boolean|string} */
let value = a, result_type = 'boolean'
if (typeof a !== 'boolean') {
result_type = (lhs_local.type.signature === 'D' || rhs_local.type.signature === 'D') ? 'double' : 'float';
}
return new LiteralValue(JavaType[result_type], value);
}
/**
*
* @param {DebuggerValue} lhs
* @param {DebuggerValue} rhs
* @param {string} operator
*/
function evaluate_binary_int_expression(lhs, rhs, operator) {
/** @type {number|boolean} */
let a = numberify(lhs), b = numberify(rhs);
// dividend cannot be zero for / and %
if (/[\/%]/.test(operator) && b === 0) {
throw divide_by_zero();
}
switch (operator) {
case '+': a += b; break;
case '-': a -= b; break;
case '*': a *= b; break;
case '/': a = Math.trunc(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 |= 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: throw invalid_operator(operator);
}
/** @type {number|boolean|string} */
let value = a, result_type = 'boolean'
if (typeof a !== 'boolean') {
result_type = 'int';
}
return new LiteralValue(JavaType[result_type], value);
}
/**
* @param {DebuggerValue} lhs
* @param {DebuggerValue} rhs
* @param {string} operator
*/
function evaluate_binary_long_expression(lhs, rhs, operator) {
function longify(local) {
const radix = JavaType.isLong(local.type) ? 16 : 10;
return Long.fromString(`${local.value}`, false, radix);
}
/** @type {Long.Long|boolean} */
let a = longify(lhs), b = longify(rhs);
// dividend cannot be zero for / and %
if (/[\/%]/.test(operator) && b.isZero()) {
throw divide_by_zero();
}
switch (operator) {
case '+': a = a.add(b); break;
case '-': a = a.subtract(b); break;
case '*': a = a.multiply(b); break;
case '/': a = a.divide(b); break;
case '%': a = a.mod(b); break;
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: throw invalid_operator(operator);
}
/** @type {boolean|Long.Long|string} */
let value = a, result_type = 'boolean';
if (typeof a !== 'boolean') {
value = hex_long(a);
result_type = 'long';
}
return new LiteralValue(JavaType[result_type], value);
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {ParsedExpression} lhs
* @param {ParsedExpression} rhs
*/
async function evaluate_assignment_expression(dbgr, locals, thread, lhs, rhs) {
if (!(lhs instanceof RootExpression)) {
throw new Error('Cannot assign value: left-hand-side is not a variable');
}
// if there are any qualifiers, the last qualifier must not be a method call
const qualified_terms = lhs.qualified_terms.slice();
const last_qualifier = qualified_terms.pop();
if ((lhs.root_term_type !== 'ident') || (last_qualifier instanceof MethodCallExpression)) {
throw new Error('Cannot assign value: left-hand-side is not a variable');
}
let lhs_value = locals.find(local => local.name === lhs.root_term);
if (!lhs_value) {
throw new Error(`Cannot assign value: variable '${lhs.root_term}' not found`);
}
// evaluate the qualified terms, until the last qualifier
lhs_value = await evaluate_qualifiers(dbgr, locals, thread, lhs_value, qualified_terms);
// evaluate the rhs
const value = await evaluate_expression(dbgr, locals, thread, rhs);
// assign the value
if (last_qualifier instanceof ArrayIndexExpression) {
const array_index = await evaluate_expression(dbgr, locals, thread, last_qualifier);
await dbgr.setArrayElements(lhs_value, numberify(array_index), 1, JavaTaggedValue.from(value));
}
else if (last_qualifier instanceof MemberExpression) {
const field = (await dbgr.findNamedFields(lhs_value.type.signature, last_qualifier.name, true))[0]
await dbgr.setFieldValue(lhs_value, field, JavaTaggedValue.from(value));
} else {
//await dbgr.setLocalVariableValue(lhs_value, JavaTaggedValue.from(value));
}
return value;
}
/**
*
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {ParsedExpression} lhs
* @param {ParsedExpression} rhs
* @param {string} operator
*/
async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operator) {
if (operator === '=') {
return evaluate_assignment_expression(dbgr, locals, thread, lhs, rhs);
}
const [lhs_value, rhs_value] = await Promise.all([
evaluate_expression(dbgr, locals, thread, lhs),
evaluate_expression(dbgr, locals, thread, rhs)
]);
const types_key = `${lhs_value.type.signature}#${rhs_value.type.signature}`
if (/[BCIJS]#[BCIJS]/.test(types_key) && /J/.test(types_key)) {
// both expressions are integers - one is a long
return evaluate_binary_long_expression(lhs_value, rhs_value, operator);
}
if (/[BCIS]#[BCIS]/.test(types_key)) {
// both expressions are (non-long) integer types
return evaluate_binary_int_expression(lhs_value, rhs_value, operator);
}
if (/[BCIJSFD]#[BCIJSFD]/.test(types_key)) {
// both expressions are number types - one is a float or double
return evaluate_binary_float_expression(lhs_value, rhs_value, operator);
}
if (/Z#Z/.test(types_key)) {
// both expressions are boolean types
return evaluate_binary_boolean_expression(lhs_value, rhs_value, operator);
}
// any + operator with a lhs of type String is coerced into a string append
if (JavaType.isString(lhs_value.type) && operator === '+') {
const rhs_str = await stringify(dbgr, rhs_value);
return dbgr.createJavaStringLiteral(lhs_value.string + rhs_str, { israw: true });
}
// anything else is an invalid combination
throw invalid_operator(operator);
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} operator
* @param {*} expr
*/
async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) {
/** @type {DebuggerValue} */
let local = await evaluate_expression(dbgr, locals, thread, expr);
const key = `${operator}${local.type.signature}`;
switch(true) {
case /!Z/.test(key):
return new LiteralValue(JavaType.boolean, !local.value);
case /~C/.test(key):
return evaluate_number(~local.value.charCodeAt(0));
case /~[BIS]/.test(key):
return evaluate_number(~local.value);
case /~J/.test(key):
return new LiteralValue(JavaType.long, hex_long(Long.fromString(local.value, false, 16).not()));
case /-C/.test(key):
return evaluate_number(-local.value.charCodeAt(0));
case /-[BCIS]/.test(key):
return evaluate_number(-local.value);
case /-J/.test(key):
return new LiteralValue(JavaType.long, hex_long(Long.fromString(local.value, false, 16).neg()));
case /\+[BCIJS]/.test(key):
return local;
default:
throw invalid_operator(operator, true);
}
}
/**
*
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {string} identifier
* @returns {Promise<DebuggerValue>}
*/
async function evaluate_identifier(dbgr, locals, identifier) {
const local = locals.find(l => l.name === identifier);
if (local) {
return local;
}
// if it's not a local, it could be the start of a package name or a type
const classes = await dbgr.getAllClasses();
return evaluate_qualified_type_name(dbgr, identifier, classes);
}
/**
*
* @param {Debugger} dbgr
* @param {string} dotted_name
* @param {*[]} classes
*/
async function evaluate_qualified_type_name(dbgr, dotted_name, classes) {
const exact_class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace(/\./g,'[$/]')};$`);
const exact_class = classes.find(c => exact_class_matcher.test(c.type.signature));
if (exact_class) {
return dbgr.getTypeValue(exact_class.type.signature);
}
const class_matcher = new RegExp(`^L(java/lang/)?${dotted_name.replace('.','[$/]')}/`);
const matching_classes = classes.filter(c => class_matcher.test(c.type.signature));
if (matching_classes.length === 0) {
// the dotted name doesn't match any packages
throw new Error(`'${dotted_name}' is not a package, type or variable name`);
}
return new DebuggerValue('package', null, dotted_name, true, false, 'package', {matching_classes});
}
/**
*
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {RootExpression} expr
* @returns {Promise<DebuggerValue>}
*/
async function evaluate_root_term(dbgr, locals, expr) {
switch (expr.root_term_type) {
case 'boolean':
return new LiteralValue(JavaType.boolean, expr.root_term === 'true');
case 'null':
return LiteralValue.Null;
case 'ident':
return evaluate_identifier(dbgr, locals, expr.root_term);
case 'hexint':
case 'octint':
case 'decint':
case 'decfloat':
return evaluate_number(expr.root_term);
case 'char':
case 'echar':
case 'uchar':
return evaluate_char(decodeJavaCharLiteral(expr.root_term))
case 'string':
// we must get the runtime to create string instances
return await dbgr.createJavaStringLiteral(expr.root_term);
default:
return null;
}
}
/**
*
* @param {Debugger} dbgr
* @param {DebuggerValue} value
* @param {QualifierExpression[]} qualified_terms
* @returns {Promise<[number, DebuggerValue]>}
*/
async function evaluate_package_qualifiers(dbgr, value, qualified_terms) {
let i = 0;
for (;;) {
// while the value is a package identifier...
if (value.vtype !== 'package') {
break;
}
// ... and the next term is a member expression...
const term = qualified_terms[i];
if (term instanceof MemberExpression) {
// search for a valid type
value = await evaluate_qualified_type_name(dbgr, `${value.value}.${term.name}`, value.data.matching_classes);
i++;
continue;
}
break;
}
if (value.vtype === 'package') {
throw new Error('not available');
}
// return the number of qualified terms we used and the resulting value
return [i, value];
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {DebuggerValue} value
* @param {QualifierExpression[]} qualified_terms
*/
async function evaluate_qualifiers(dbgr, locals, thread, value, qualified_terms) {
let pkg_members;
[pkg_members, value] = await evaluate_package_qualifiers(dbgr, value, qualified_terms);
for (let i = pkg_members; i < qualified_terms.length; i++) {
const term = qualified_terms[i];
if (term instanceof MemberExpression) {
// if this term is a member name, check if it's really a method call
const next_term = qualified_terms[i + 1];
if (next_term instanceof MethodCallExpression) {
value = await evaluate_methodcall(dbgr, locals, thread, term.name, next_term, value);
i++;
continue;
}
value = await evaluate_member(dbgr, locals, thread, term, value);
continue;
}
if (term instanceof ArrayIndexExpression) {
value = await evaluate_array_element(dbgr, locals, thread, term.indexExpression, value);
continue;
}
throw new Error('not available');
}
return value;
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {RootExpression} expr
*/
async function evaluate_root_expression(dbgr, locals, thread, expr) {
let value = await evaluate_root_term(dbgr, locals, expr);
if (!value || !value.valid) {
throw new Error('not available');
}
// we've evaluated the root term variable - work out the rest
value = await evaluate_qualifiers(dbgr, locals, thread, value, expr.qualified_terms);
return value;
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {ParsedExpression} expr
* @returns {Promise<DebuggerValue>}
*/
function evaluate_expression(dbgr, locals, thread, expr) {
if (expr instanceof RootExpression) {
return evaluate_root_expression(dbgr, locals, thread, expr);
}
if (expr instanceof BinaryOpExpression) {
return evaluate_binary_expression(dbgr, locals, thread, expr.lhs, expr.rhs, expr.operator);
}
if (expr instanceof UnaryOpExpression) {
return evaluate_unary_expression(dbgr, locals, thread, expr.operator, expr.rhs);
}
if (expr instanceof TypeCastExpression) {
return evaluate_cast(dbgr, locals, thread, expr.cast_type, expr.rhs);
}
throw new Error('not available');
}
/**
*
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} index_expr
* @param {DebuggerValue} arr_local
*/
async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) {
if (arr_local.type.signature[0] !== '[') {
throw new Error(`TypeError: cannot apply array index to non-array type '${arr_local.type.typename}'`);
}
if (arr_local.hasnullvalue) {
throw new Error('NullPointerException');
}
const idx_local = await evaluate_expression(dbgr, locals, thread, index_expr);
if (!JavaType.isArrayIndex(idx_local.type)) {
throw new Error('TypeError: array index is not an integer value');
}
const idx = numberify(idx_local);
if (idx < 0 || idx >= arr_local.arraylen) {
throw new Error(`BoundsError: array index (${idx}) out of bounds. Array length = ${arr_local.arraylen}`);
}
const element_values = await dbgr.getArrayElementValues(arr_local, idx, 1);
return element_values[0];
}
/**
* Build a regular expression which matches the possible parameter types for a value
* @param {Debugger} dbgr
* @param {DebuggerValue} v
*/
async function getParameterSignatureRegex(dbgr, v) {
if (v.type.signature == 'Lnull;') {
return /^[LT[]/; // null matches any reference type
}
if (/^L/.test(v.type.signature)) {
// for class reference types, retrieve a list of inherited classes
// since subclass instances can be passed as arguments
const sigs = await dbgr.getClassInheritanceList(v.type.signature);
const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$'));
return new RegExp(`(^${re_sigs.join('$)|(^')}$)`);
}
if (/^\[/.test(v.type.signature)) {
// for array types, only an exact array match or Object is allowed
return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`);
}
switch(v.type.signature) {
case 'I':
// match bytes/shorts/ints/longs/floats/doubles literals within range
if (v.value >= -128 && v.value <= 127)
return /^[BSIJFD]$/
if (v.value >= -32768 && v.value <= 32767)
return /^[SIJFD]$/
return /^[IJFD]$/;
case 'F':
return /^[FD]$/; // floats can be assigned to floats or doubles
default:
// anything else must be an exact match (no implicit cast is valid)
return new RegExp(`^${v.type.signature}$`);
}
}
/**
* @param {Debugger} dbgr
* @param {*} type
* @param {string} method_name
* @param {DebuggerValue[]} args
*/
async function findCompatibleMethod(dbgr, type, method_name, args) {
// find any methods matching the member name with any parameters in the signature
const methods = await dbgr.findNamedMethods(type.signature, method_name, /^/, false);
if (!methods[0]) {
throw new Error(`Error: method '${type.name}.${method_name}' not found`);
}
// filter the method based upon the types of parameters
const arg_type_matchers = [];
for (let arg of args) {
arg_type_matchers.push(await getParameterSignatureRegex(dbgr, arg));
}
// find the first method where the argument types match the parameter types
const matching_method = methods.find(method => {
// extract a list of parameter types from the method signature
const param_type_re = /\[*([BSIJFDCZ]|([LT][^;]+;))/g;
const parameter_types = [];
for (let x; x = param_type_re.exec(method.sig); ) {
parameter_types.push(x[0]);
}
// the last type is always the return value
parameter_types.pop();
// check if the arguments and parameters match
if (parameter_types.length !== arg_type_matchers.length) {
return false;
}
// are there any argument types that don't match the corresponding parameter type?
if (arg_type_matchers.find((m, idx) => !m.test(parameter_types[idx]))) {
return false;
}
// we found a match
return true;
});
if (!matching_method) {
throw new Error(`Error: incompatible parameters for method '${method_name}'`);
}
return matching_method;
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} method_name
* @param {MethodCallExpression} m
* @param {DebuggerValue} obj_local
*/
async function evaluate_methodcall(dbgr, locals, thread, method_name, m, obj_local) {
if (obj_local.hasnullvalue) {
throw new Error('NullPointerException');
}
// evaluate any parameters
const param_values = await Promise.all(m.arguments.map(arg => evaluate_expression(dbgr, locals, thread, arg)));
// find a method in the object type matching the name and argument types
const method = await findCompatibleMethod(dbgr, obj_local.type, method_name, param_values);
return dbgr.invokeMethod(
obj_local.value,
thread.threadid,
method,
param_values
);
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {MemberExpression} member
* @param {DebuggerValue} value
*/
async function evaluate_member(dbgr, locals, thread, member, value) {
if (!JavaType.isReference(value.type)) {
throw new Error('TypeError: value is not a reference type');
}
if (value.hasnullvalue) {
throw new Error('NullPointerException');
}
if (JavaType.isArray(value.type)) {
// length is a 'fake' field of arrays, so special-case it
if (member.name === 'length') {
return evaluate_number(value.arraylen);
}
}
// we also special-case :super (for object instances)
if (member.name === ':super' && JavaType.isClass(value.type)) {
return dbgr.getSuperInstance(value);
}
// check if the value is an enclosed type
const enclosed_type = await dbgr.getTypeValue(`${value.type.signature.replace(/;$/,'')}$${member.name};`);
if (enclosed_type.valid) {
return enclosed_type;
}
// anything else must be a real field
return dbgr.getFieldValue(value, member.name, true)
}
/**
* @param {*} type
* @param {*} local
*/
function incompatible_cast(type, local) {
return new Error(`Incompatible cast from ${local.type.typename} to ${type}`);
}
/**
* @param {Long.Long} value
* @param {8|16|32} bits
*/
function signed_from_long(value, bits) {
return (parseInt(value.toString(16).slice(-bits >> 3),16) << (32-bits)) >> (32-bits);
}
/**
* @param {string} type
* @param {DebuggerValue} local
*/
function cast_from_long(type, local) {
const value = Long.fromString(local.value, true, 16);
switch (true) {
case (type === 'byte'):
return evaluate_number(signed_from_long(value, 8));
case (type === 'short'):
return evaluate_number(signed_from_long(value, 16));
case (type === 'int'):
return evaluate_number(signed_from_long(value, 32));
case (type === 'char'):
return evaluate_char(String.fromCharCode(signed_from_long(value, 16) & 0xffff));
case (type === 'float'):
return evaluate_number(value.toSigned().toNumber() + 'F');
case (type === 'double'):
return evaluate_number(value.toSigned().toNumber() + 'D');
default:
throw incompatible_cast(type, local);
}
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} cast_type
* @param {ParsedExpression} rhs
*/
async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
let local = await evaluate_expression(dbgr, locals, thread, rhs);
// check if a conversion is unnecessary
if (cast_type === local.type.typename) {
return local;
}
// boolean cannot be converted from anything else
if (cast_type === 'boolean' || local.type.typename === 'boolean') {
throw incompatible_cast(cast_type, local);
}
switch (true) {
case local.type.typename === 'long':
// conversion from long to something else
local = cast_from_long(cast_type, local);
break;
case (cast_type === 'byte'):
local = evaluate_number((local.value << 24) >> 24);
break;
case (cast_type === 'short'):
local = evaluate_number((local.value << 16) >> 16);
break;
case (cast_type === 'int'):
local = evaluate_number((local.value | 0));
break;
case (cast_type === 'long'):
local = evaluate_number(local.value + 'L');
break;
case (cast_type === 'char'):
local = evaluate_char(String.fromCharCode(local.value | 0));
break;
case (cast_type === 'float'):
case (cast_type === 'double'):
break;
default:
throw incompatible_cast(cast_type, local);
}
local.type = JavaType[cast_type];
return local;
}
/**
* @param {string} expression
* @param {AndroidThread} thread
* @param {DebuggerValue[]} locals
* @param {Debugger} dbgr
*/
async function evaluate(expression, thread, locals, dbgr) {
D('evaluate: ' + expression);
await dbgr.ensureConnected();
// the thread must be in the paused state
if (thread && !thread.paused) {
throw new Error('not available');
}
// parse the expression
const e = new ExpressionText(expression.trim())
if (!e.expr) {
return null;
}
const parsed_expression = parse_expression(e);
// if there's anything left, it's an error
if (!parsed_expression || e.expr) {
// the expression is not well-formed
throw new Error(`Invalid expression: ${expression.trim()}`);
}
// the expression is well-formed - start the (asynchronous) evaluation
const value = await evaluate_expression(dbgr, locals, thread, parsed_expression);
return value;
}
module.exports = {
evaluate,
}

323
src/expression/parse.js Normal file
View File

@@ -0,0 +1,323 @@
/**
* Operator precedence levels.
* Lower number = higher precedence.
* Operators with equal precedence are evaluated left-to-right.
*/
const operator_precedences = {
'*': 1, '%': 1, '/': 1,
'+': 2, '-': 2,
'<<': 3, '>>': 3, '>>>': 3,
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
'==': 5, '!=': 5,
'&': 6, '^': 7, '|': 8,
'&&': 9, '||': 10,
'?': 11,
'=': 12,
}
const lowest_precedence = 13;
class ExpressionText {
/**
* @param {string} text
*/
constructor(text) {
this.expr = text;
this.precedence_stack = [lowest_precedence];
}
get current_precedence() {
return this.precedence_stack[0];
}
}
class ParsedExpression {
}
class RootExpression extends ParsedExpression {
/**
* @param {string} root_term
* @param {string} root_term_type
* @param {QualifierExpression[]} qualified_terms
*/
constructor(root_term, root_term_type, qualified_terms) {
super();
this.root_term = root_term;
this.root_term_type = root_term_type;
this.qualified_terms = qualified_terms;
}
}
class TypeCastExpression extends ParsedExpression {
/**
*
* @param {string} cast_type
* @param {ParsedExpression} rhs
*/
constructor(cast_type, rhs) {
super();
this.cast_type = cast_type;
this.rhs = rhs;
}
}
class BinaryOpExpression extends ParsedExpression {
/**
* @param {ParsedExpression} lhs
* @param {string} operator
* @param {ParsedExpression} rhs
*/
constructor(lhs, operator, rhs) {
super();
this.lhs = lhs;
this.operator = operator;
this.rhs = rhs;
}
}
class UnaryOpExpression extends ParsedExpression {
/**
* @param {string} operator
* @param {ParsedExpression} rhs
*/
constructor(operator, rhs) {
super();
this.operator = operator;
this.rhs = rhs;
}
}
class TernaryExpression extends ParsedExpression {
constructor(condition) {
super();
this.condition = condition;
this.ternary_true = null;
this.ternary_false = null;
}
}
class QualifierExpression extends ParsedExpression {
}
class ArrayIndexExpression extends QualifierExpression {
constructor(e) {
super();
this.indexExpression = e;
}
}
class MethodCallExpression extends QualifierExpression {
arguments = [];
}
class MemberExpression extends QualifierExpression {
constructor(name) {
super();
this.name = name;
}
}
/**
* Remove characters from the expression followed by any leading whitespace/comments
* @param {ExpressionText} e
* @param {number|string} length_or_text
*/
function strip(e, length_or_text) {
if (typeof length_or_text === 'string') {
if (!e.expr.startsWith(length_or_text)) {
return false;
}
length_or_text = length_or_text.length;
}
e.expr = e.expr.slice(length_or_text).trimLeft();
for (;;) {
const comment = e.expr.match(/(^\/\/.+)|(^\/\*[\d\D]*?\*\/)/);
if (!comment) break;
e.expr = e.expr.slice(comment[0].length).trimLeft();
}
return true;
}
/**
* @param {ExpressionText} e
* @returns {(MemberExpression|ArrayIndexExpression|MethodCallExpression)[]}
*/
function parse_qualified_terms(e) {
const res = [];
while (/^[([.]/.test(e.expr)) {
if (strip(e, '.')) {
// member access
const name_match = e.expr.match(/^:?[a-zA-Z_$][a-zA-Z0-9_$]*/); // allow : at start for :super and :msg
if (!name_match) {
return null;
}
const member = new MemberExpression(name_match[0]);
strip(e, member.name.length)
res.push(member);
}
else if (strip(e, '(')) {
// method call
const call = new MethodCallExpression();
if (!strip(e, ')')) {
for (let arg; ;) {
if ((arg = parse_expression(e)) === null) {
return null;
}
call.arguments.push(arg);
if (strip(e, ',')) continue;
if (strip(e, ')')) break;
return null;
}
}
res.push(call);
}
else if (strip(e, '[')) {
// array index
const index_expr = parse_expression(e);
if (index_expr === null) {
return null;
}
if (!strip(e, ']')) {
return null;
}
res.push(new ArrayIndexExpression(index_expr));
}
}
return res;
}
/**
* @param {ExpressionText} e
*/
function parseBracketOrCastExpression(e) {
if (!strip(e, '(')) {
return null;
}
let res = parse_expression(e);
if (!res) {
return null;
}
if (!strip(e, ')')) {
return null;
}
if (res instanceof RootExpression) {
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.qualified_terms.length) {
// primitive typecast
const castexpr = parse_expression_term(e);
if (!castexpr) {
return null;
}
res = new TypeCastExpression(res.root_term, castexpr);
}
}
return res;
}
/**
*
* @param {ExpressionText} e
* @param {string} unop
*/
function parseUnaryExpression(e, unop) {
strip(e, unop.length);
let res = parse_expression_term(e);
if (!res) {
return null;
}
const op = unop.replace(/\s+/g, '');
for (let i = op.length - 1; i >= 0; --i) {
res = new UnaryOpExpression(op[i], res);
}
return res;
}
/**
* @param {ExpressionText} e
*/
function parse_expression_term(e) {
if (e.expr[0] === '(') {
return parseBracketOrCastExpression(new ExpressionText(e.expr));
}
const unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
if (unop) {
return parseUnaryExpression(e, unop[0]);
}
const root_term_types = ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'];
const 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+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
if (!root_term) {
return null;
}
strip(e, root_term[0].length);
const root_term_type = root_term_types[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].find(x => root_term[x]) - 1];
const qualified_terms = parse_qualified_terms(e);
if (qualified_terms === null) {
return null;
}
// the root term is not allowed to be a method call
if (qualified_terms[0] instanceof MethodCallExpression) {
return null;
}
return new RootExpression(root_term[0], root_term_type, qualified_terms);
}
/**
* @param {string} s
*/
function getBinaryOperator(s) {
const binary_op_match = s.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
return binary_op_match ? binary_op_match[0] : null;
}
/**
* @param {ExpressionText} e
* @returns {ParsedExpression}
*/
function parse_expression(e) {
let res = parse_expression_term(e);
for (; ;) {
const binary_operator = getBinaryOperator(e.expr);
if (!binary_operator) {
break;
}
const prec_diff = operator_precedences[binary_operator] - e.current_precedence;
if (prec_diff > 0) {
// bigger number -> lower precendence -> end of (sub)expression
break;
}
if (prec_diff === 0 && binary_operator !== '?') {
// equal precedence, ltr evaluation
break;
}
// higher or equal precendence
e.precedence_stack.unshift(e.current_precedence + prec_diff);
strip(e, binary_operator.length);
if (binary_operator === '?') {
res = new TernaryExpression(res);
res.ternary_true = parse_expression(e);
if (!strip(e, ':')) {
return null;
}
res.ternary_false = parse_expression(e);
} else {
res = new BinaryOpExpression(res, binary_operator, parse_expression(e));
}
e.precedence_stack.shift();
}
return res;
}
module.exports = {
ArrayIndexExpression,
BinaryOpExpression,
ExpressionText,
MemberExpression,
MethodCallExpression,
parse_expression,
ParsedExpression,
QualifierExpression,
RootExpression,
TypeCastExpression,
UnaryOpExpression,
}

View File

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

View File

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

121
src/index.d.js Normal file
View File

@@ -0,0 +1,121 @@
/**
* @typedef {string} hex64
* @typedef {hex64} JavaRefID
* @typedef {number} VSCThreadID
* @typedef {number} VSCVariableReference
* A variable reference is a number, encoding the thread, stack level and variable index, using:
*
* variableReference = {threadid * 1e9} + {level * 1e6} + varindex
*
* This allows 1M variables (locals, fields, array elements) per call stack frame
* and 1000 frames per call stack
* @typedef {number} byte
*
* @typedef {JavaRefID} JavaFrameID
* @typedef {JavaRefID} JavaThreadID
* @typedef {JavaRefID} JavaClassID
* @typedef {JavaRefID} JavaMethodID
* @typedef {JavaRefID} JavaFieldID
* @typedef {JavaRefID} JavaObjectID
* @typedef {JavaRefID} JavaTypeID
*
* @typedef JavaFrame
* @property {JavaFrameID} frameid
* @property {JavaLocation} location
*
* @typedef JavaClassInfo
* @property {*} reftype
* @property {*} status
* @property {JavaType} type
* @property {JavaTypeID} typeid
*
* @typedef JavaMethod
* @property {string} genericsig
* @property {JavaMethodID} methodid
* @property {byte} modbits
* @property {string} name
* @property {string} sig
*
* @typedef JavaSource
* @property {string} sourcefile
*
* @typedef JavaLocation
* @property {JavaClassID} cid
* @property {hex64} idx
* @property {JavaMethodID} mid
* @property {1} type
*
* @typedef JavaLineTable
* @property {hex64} start
* @property {hex64} end
* @property {JavaLineTableEntry[]} lines
*
* @typedef JavaLineTableEntry
* @property {hex64} linecodeidx
* @property {number} linenum
*
*
* @typedef JavaField
* @property {JavaFieldID} fieldid
* @property {string} name
* @property {JavaType} type
* @property {string} genericsig
* @property {number} modbits
*
* @typedef JavaVar
* @property {*} codeidx
* @property {string} name
* @property {JavaType} type
* @property {string} genericsig
* @property {number} length
* @property {number} slot
*
* @typedef JavaVarTable
* @property {number} argCnt
* @property {JavaVar[]} vars
*
* @typedef {'byte'|'short'|'int'|'long'|'boolean'|'char'|'float'|'double'|'void'|'oref'} JavaValueType
*
* @typedef HitMod
* @property {1} modkind
* @property {number} count
* @property {() => void} encode
*
* @typedef ClassMatchMod
* @property {5} modkind
* @property {string} pattern
*
* @typedef LocMod
* @property {7} modkind
* @property {*} loc
* @property {() => void} encode
*
* @typedef ExOnlyMod
* @property {8} modkind
* @property {*} reftypeid
* @property {boolean} caught
* @property {boolean} uncaught
**/
/**
* @typedef {"local" | "literal" | "field" | "exception" | "return" | "arrelem" | "super" | "class" | "package"} DebuggerValueType
* @typedef {'in'|'over'|'out'} DebuggerStepType
* @typedef {'set'|'notloaded'|'enabled'|'removed'} BreakpointState
* @typedef {string} BreakpointID
* @typedef {string} CMLKey
* @typedef {number} JDWPRequestID
* @typedef {JDWPRequestID} StepID
* @typedef {'caught'|'uncaught'|'both'} ExceptionBreakMode
*
*/
/**
* @typedef ADBFileTransferParams
* @property {string} pathname
* @property {Buffer} data
* @property {number} mtime
* @property {number} perms
*
*/

File diff suppressed because it is too large Load Diff

View File

@@ -1,137 +0,0 @@
// a very stripped down polyfill implementation of jQuery's promise methods
const util = require('util'); // for util.inspect
var $ = this;
// Deferred wraps a Promise into a jQuery-like object
var Deferred = exports.Deferred = function(p, parent) {
var o = {
_isdeferred:true,
_original:null,
_promise:null,
_fns:null,
_context:null,
_parent:null,
_root:null,
promise() {
return this;
},
then(fn) {
var thendef = $.Deferred(null, this);
var p = this._promise.then(function(a) {
var res = this.fn.apply(a._ctx, a._args);
if (res === undefined)
return a;
if (res && res._isdeferred)
return res._promise;
return {_ctx:a._ctx, _args:[res]}
}.bind({def:thendef,fn:fn}));
thendef._promise = thendef._original = p;
return thendef;
},
always(fn) {
var thendef = this.then(fn);
this.fail(function() {
// we cannot bind thendef to the function because we need the caller's this to resolve the thendef
return thendef.resolveWith(this, Array.prototype.map.call(arguments,x=>x))._promise;
});
return thendef;
},
fail(fn) {
var faildef = $.Deferred(null, this);
var p = this._promise.catch(function(a) {
if (a.stack) {
util.E(a.stack);
a = [a];
}
if (this.def._context === null && this.def._parent)
this.def._context = this.def._parent._context;
if (this.def._context === null && this.def._root)
this.def._context = this.def._root._context;
var res = this.fn.apply(this.def._context,a);
if (res === undefined)
return a;
if (res && res._isdeferred)
return res._promise;
return res;
}.bind({def:faildef,fn:fn}));
faildef._promise = faildef._original = p;
return faildef;
},
state() {
var m = util.inspect(this._original).match(/^Promise\s*\{\s*<(\w+)>/); // urgh!
// anything that's not pending or rejected is resolved
return m ? m[1] : 'resolved';
},
resolve:function() {
return this.resolveWith(null, Array.prototype.map.call(arguments,x=>x));
},
resolveWith:function(ths, args) {
if (typeof(args) === 'undefined') args = [];
if (!Array.isArray(args))
throw new Error('resolveWith must be passed an array of arguments');
if (this._root) {
this._root.resolveWith(ths, args);
return this;
}
if (ths === null || ths === undefined) ths = this;
this._fns[0]({_ctx:ths,_args:args});
return this;
},
reject:function() {
return this.rejectWith(null, Array.prototype.map.call(arguments,x=>x));
},
rejectWith:function(ths,args) {
if (typeof(args) === 'undefined') args = [];
if (!Array.isArray(args))
throw new Error('rejectWith must be passed an array of arguments');
if (this._root) {
this._root.rejectWith(ths, args);
return this;
}
this._context = ths;
this._fns[1](args);
return this;
},
}
if (parent) {
o._original = o._promise = p;
o._parent = parent;
o._root = parent._root || parent;
} else {
o._original = o._promise = new Promise((res,rej) => {
o._fns = [res,rej];
});
}
return o;
}
// $.when() is jQuery's version of Promise.all()
// - this version just scans the array of arguments waiting on any Deferreds in turn before finally resolving the return Deferred
var when = exports.when = function() {
if (arguments.length === 1 && Array.isArray(arguments[0])) {
return when.apply(this,...arguments).then(() => [...arguments]);
}
var x = {
def: $.Deferred(),
args: Array.prototype.map.call(arguments,x=>x),
idx:0,
next(x) {
if (x.idx >= x.args.length) {
return process.nextTick(x => {
x.def.resolveWith(null, x.args);
}, x);
}
if ((x.args[x.idx]||{})._isdeferred) {
x.args[x.idx].then(function() {
var x = this, result = Array.prototype.map.call(arguments,x=>x);
x.args[x.idx] = result;
x.idx++; x.next(x);
}.bind(x));
return;
}
x.idx++; x.next(x);
},
};
x.next(x);
return x.def;
}

View File

@@ -1,9 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"target": "es2018",
"checkJs": true,
"lib": [
"es6"
"es2018"
]
},
"exclude": [

View File

@@ -7,88 +7,134 @@ const WebSocketServer = require('ws').Server;
// our stuff
const { ADBClient } = require('./adbclient');
const { AndroidContentProvider } = require('./contentprovider');
const $ = require('./jq-promise');
const { D } = require('./util');
const { D } = require('./utils/print');
/*
Class to setup and store logcat data
/**
* WebSocketServer instance
* @type {WebSocketServer}
*/
let Server = null;
/**
* Promise resolved once the WebSocketServer is listening
* @type {Promise}
*/
let wss_inited;
/**
* hashmap of all LogcatContent instances, keyed on device id
* @type {Map<string, LogcatContent>}
*/
const LogcatInstances = new Map();
/**
* Class to manage logcat data transferred between device and a WebView.
*
* Each LogcatContent instance receives logcat lines via ADB, formats them into
* HTML and sends them to a WebSocketClient running within a WebView page.
*
* The order goes:
* - a new LogcatContent instance is created
* - if this is the first instance, create the WebSocketServer
* - set up handlers to receive logcat messages from ADB
* - upon the first get content(), return the templated HTML page - this is designed to bootstrap the view and create a WebSocket client.
* - when the client connects, start sending logcat messages over the websocket
*/
class LogcatContent {
/**
* @param {string} deviceid
*/
constructor(deviceid) {
this._logcatid = deviceid;
this._logs = [];
this._htmllogs = [];
this._oldhtmllogs = [];
this._prevlogs = null;
this._notifying = 0;
this._refreshRate = 200; // ms
this._state = '';
this._state = 'connecting';
this._htmltemplate = '';
this._adbclient = new ADBClient(deviceid);
this._initwait = new Promise((resolve, reject) => {
this._state = 'connecting';
LogcatContent.initWebSocketServer()
.then(() => {
return this._adbclient.logcat({
this._initwait = this.initialise();
LogcatInstances.set(this._logcatid, this);
}
/**
* Ensures the websocket server is initialised and sets up
* logcat handlers for ADB.
* Once everything is ready, returns the initial HTML bootstrap content
* @returns {Promise<string>}
*/
async initialise() {
try {
// create the WebSocket server instance
await initWebSocketServer();
// register handlers for logcat
await this._adbclient.startLogcatMonitor({
onlog: this.onLogcatContent.bind(this),
onclose: this.onLogcatDisconnect.bind(this),
});
}).then(() => {
this._state = 'connected';
this._initwait = null;
resolve(this.content);
}).fail(e => {
this._state = 'connect_failed';
reject(e);
})
});
LogcatContent.byLogcatID[this._logcatid] = this;
} catch (err) {
return `Logcat initialisation failed. ${err.message}`;
}
get content() {
// retrieve the initial content
return this.content();
}
/**
* @returns {Promise<string>}
*/
async content() {
if (this._initwait) return this._initwait;
if (this._state !== 'disconnected')
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 = this.tryReconnect();
}
async tryReconnect() {
// 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 };
const prevlogs = {_logs: this._logs, _htmllogs: this._htmllogs, _oldhtmllogs: this._oldhtmllogs };
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
this._adbclient.logcat({
try {
await this._adbclient.startLogcatMonitor({
onlog: this.onLogcatContent.bind(this),
onclose: this.onLogcatDisconnect.bind(this),
}).then(() => {
})
// we successfully reconnected
this._state = 'connected';
this._prevlogs = null;
this._initwait = null;
resolve(this.content);
}).fail((/*e*/) => {
return this.content();
} catch(err) {
// reconnection failed - put the logs back and return the cached info
this._logs = this._prevlogs._logs;
this._htmllogs = this._prevlogs._htmllogs;
this._oldhtmllogs = this._prevlogs._oldhtmllogs;
this._prevlogs = null;
this._logs = prevlogs._logs;
this._htmllogs = prevlogs._htmllogs;
this._oldhtmllogs = prevlogs._oldhtmllogs;
this._initwait = null;
var cached_content = this.htmlBootstrap({connected:false, status:'Device disconnected',oldlogs: this._oldhtmllogs.join(os.EOL)});
resolve(cached_content);
})
const cached_content = this.htmlBootstrap({
connected: false,
status: 'Device disconnected',
oldlogs: this._oldhtmllogs.join(os.EOL),
});
return cached_content;
}
}
sendClientMessage(msg) {
LogcatContent._wss.clients.forEach(client => {
if (client._logcatid === this._logcatid) {
client.send(msg + '\n'); // include a newline to try and persuade a buffer write
}
})
const clients = [...Server.clients].filter(client => client['_logcatid'] === this._logcatid);
clients.forEach(client => client.send(msg+'\n')); // include a newline to try and persuade a buffer write
}
sendDisconnectMsg() {
this.sendClientMessage(':disconnect');
}
onClientConnect(client) {
if (this._oldhtmllogs.length) {
var lines = '<div class="logblock">' + this._oldhtmllogs.join(os.EOL) + '</div>';
const lines = '<div class="logblock">' + this._oldhtmllogs.join(os.EOL) + '</div>';
client.send(lines);
}
// if the window is tabbed away and then returned to, vscode assumes the content
@@ -98,6 +144,7 @@ class LogcatContent {
if (this._state === 'disconnected')
this.sendDisconnectMsg();
}
onClientMessage(client, message) {
if (message === 'cmd:clear_logcat') {
if (this._state !== 'connected') return;
@@ -107,31 +154,33 @@ class LogcatContent {
this._logs = []; this._htmllogs = []; this._oldhtmllogs = [];
this.sendClientMessage(':logcat_cleared');
})
.fail(e => {
.catch(e => {
D('Clear logcat command failed: ' + e.message);
})
}
}
updateLogs() {
// no point in formatting the data if there are no connected clients
var clients = [...LogcatContent._wss.clients].filter(client => client._logcatid === this._logcatid);
const clients = [...Server.clients].filter(client => client['_logcatid'] === this._logcatid);
if (clients.length) {
var lines = '<div class="logblock">' + this._htmllogs.join('') + '</div>';
const lines = '<div class="logblock">' + this._htmllogs.join('') + '</div>';
clients.forEach(client => client.send(lines));
}
// once we've updated all the clients, discard the info
this._oldhtmllogs = this._htmllogs.concat(this._oldhtmllogs).slice(0, 10000);
this._htmllogs = [], this._logs = [];
}
htmlBootstrap(vars) {
if (!this._htmltemplate)
this._htmltemplate = fs.readFileSync(path.join(__dirname,'res/logcat.html'), 'utf8');
vars = Object.assign({
logcatid: this._logcatid,
wssport: LogcatContent._wssport,
wssport: Server.options.port,
}, vars);
// simple value replacement using !{name} as the placeholder
var html = this._htmltemplate.replace(/!\{(.*?)\}/g, (match,expr) => ''+(vars[expr.trim()]||''));
const html = this._htmltemplate.replace(/!\{(.*?)\}/g, (match,expr) => ''+(vars[expr.trim()]||''));
return html;
}
renotify() {
@@ -146,13 +195,13 @@ class LogcatContent {
}
onLogcatContent(e) {
if (e.logs.length) {
var mrlast = e.logs.slice();
const mrlast = e.logs.slice();
this._logs = this._logs.concat(mrlast);
mrlast.forEach(log => {
if (!(log = log.trim())) return;
// replace html-interpreted chars
var m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/);
var style = (m && m[1]) || '';
const m = log.match(/^\d\d-\d\d\s+?\d\d:\d\d:\d\d\.\d+?\s+?(.)/);
const style = (m && m[1]) || '';
log = log.replace(/[&"'<>]/g, c => ({ '&': '&amp;', '"': '&quot;', "'": '&#39;', '<': '&lt;', '>': '&gt;' }[c]));
this._htmllogs.unshift(`<div class="log ${style}">${log}</div>`);
@@ -167,73 +216,85 @@ class LogcatContent {
}
}
// hashmap of all LogcatContent instances, keyed on device id
LogcatContent.byLogcatID = {};
LogcatContent.initWebSocketServer = function () {
if (LogcatContent._wssdone) {
function initWebSocketServer() {
if (wss_inited) {
// already inited
return LogcatContent._wssdone;
return wss_inited;
}
// retrieve the logcat websocket port
var default_wssport = 7038;
var wssport = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport);
if (typeof wssport !== 'number' || wssport <= 0 || wssport >= 65536 || wssport !== (wssport|0))
wssport = default_wssport;
const default_wssport = 7038;
let start_port = AndroidContentProvider.getLaunchConfigSetting('logcatPort', default_wssport);
if (typeof start_port !== 'number' || start_port <= 0 || start_port >= 65536 || start_port !== (start_port|0)) {
start_port = default_wssport;
}
LogcatContent._wssdone = $.Deferred();
({
wss: null,
startport: wssport,
port: wssport,
retries: 0,
tryCreateWSS() {
wss_inited = new Promise((resolve, reject) => {
let retries = 100;
tryCreateWebSocketServer(start_port, retries, (err, server) => {
if (err) {
wss_inited = null;
reject(err);
} else {
Server = server;
resolve();
}
});
});
return wss_inited;
}
/**
*
* @param {number} port
* @param {number} retries
* @param {(err,server?) => void} cb
*/
function tryCreateWebSocketServer(port, retries, cb) {
const wsopts = {
host: '127.0.0.1',
port: this.port,
port,
clientTracking: true,
};
this.wss = new WebSocketServer(wsopts, () => {
// success - save the info and resolve the deferred
LogcatContent._wssport = this.port;
LogcatContent._wssstartport = this.startport;
LogcatContent._wss = this.wss;
this.wss.on('connection', (client, req) => {
// the client uses the url path to signify which logcat data it wants
client._logcatid = req.url.match(/^\/?(.*)$/)[1];
var lc = LogcatContent.byLogcatID[client._logcatid];
if (lc) lc.onClientConnect(client);
else client.close();
client.on('message', function(message) {
var lc = LogcatContent.byLogcatID[this._logcatid];
if (lc) lc.onClientMessage(this, message);
}.bind(client));
/*client.on('close', e => {
console.log('client close');
});*/
// try and make sure we don't delay writes
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
});
this.wss = null;
LogcatContent._wssdone.resolveWith(LogcatContent, []);
});
this.wss.on('error', (/*err*/) => {
if (!LogcatContent._wss) {
// listen failed -try the next port
this.retries++ , this.port++;
this.tryCreateWSS();
new WebSocketServer(wsopts)
.on('listening', function() {
cb(null, this);
})
.on('connection', (client, req) => {
onWebSocketClientConnection(client, req);
})
.on('error', err => {
if (retries <= 0) {
cb(err);
} else {
tryCreateWebSocketServer(port + 1, retries - 1, cb);
}
})
}
}).tryCreateWSS();
return LogcatContent._wssdone;
function onWebSocketClientConnection(client, req) {
// the client uses the url path to signify which logcat data it wants
client._logcatid = req.url.match(/^\/?(.*)$/)[1];
const lc = LogcatInstances.get(client._logcatid);
if (!lc) {
client.close();
return;
}
lc.onClientConnect(client);
client.on('message', function(message) {
const lc = LogcatInstances.get(this._logcatid);
if (lc) {
lc.onClientMessage(this, message);
}
}.bind(client));
// try and make sure we don't delay writes
client._socket && typeof(client._socket.setNoDelay)==='function' && client._socket.setNoDelay(true);
}
function getADBPort() {
var defaultPort = 5037;
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
const defaultPort = 5037;
const adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
return adbPort;
return defaultPort;
@@ -243,13 +304,13 @@ function openLogcatWindow(vscode) {
new ADBClient().test_adb_connection()
.then(err => {
// if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number)
var adbport = getADBPort();
var autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
const adbport = getADBPort();
const autoStartADB = AndroidContentProvider.getLaunchConfigSetting('autoStartADB', true);
if (err && autoStartADB!==false && process.env.ANDROID_HOME && typeof adbport === 'number' && adbport > 0 && adbport < 65536) {
var adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
var adbargs = ['-P',''+adbport,'start-server'];
const adbpath = path.join(process.env.ANDROID_HOME, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb');
const adbargs = ['-P',''+adbport,'start-server'];
try {
/*var stdout = */require('child_process').execFileSync(adbpath, adbargs, {cwd:process.env.ANDROID_HOME, encoding:'utf8'});
/*const 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
}
})
@@ -262,22 +323,26 @@ function openLogcatWindow(vscode) {
case 1:
return devices; // only one device - just show it
}
var multidevicewait = $.Deferred(), prefix = 'Android: View Logcat - ', all = '[ Display All ]';
var devicelist = devices.map(d => prefix + d.serial);
const prefix = 'Android: View Logcat - ', all = '[ Display All ]';
const devicelist = devices.map(d => prefix + d.serial);
//devicelist.push(prefix + all);
vscode.window.showQuickPick(devicelist)
return vscode.window.showQuickPick(devicelist)
.then(which => {
if (!which) return; // user cancelled
which = which.slice(prefix.length);
new ADBClient().list_devices()
return new ADBClient().list_devices()
.then(devices => {
if (which === all) return multidevicewait.resolveWith(this,[devices]);
var found = devices.find(d => d.serial===which);
if (found) return multidevicewait.resolveWith(this,[[found]]);
if (which === all) {
return devices
}
const found = devices.find(d => d.serial === which);
if (found) {
return [found];
}
vscode.window.showInformationMessage('Logcat cannot be displayed. The device is disconnected');
return null;
});
});
return multidevicewait;
}, () => null);
})
.then(devices => {
if (!Array.isArray(devices)) return; // user cancelled (or no devices connected)
@@ -292,19 +357,21 @@ function openLogcatWindow(vscode) {
}
);
const logcat = new LogcatContent(device.serial);
logcat.content.then(html => {
logcat.content().then(html => {
panel.webview.html = html;
});
return;
}
var uri = AndroidContentProvider.getReadLogcatUri(device.serial);
return vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
const uri = AndroidContentProvider.getReadLogcatUri(device.serial);
vscode.commands.executeCommand("vscode.previewHtml",uri,vscode.ViewColumn.Two);
});
})
.fail((/*e*/) => {
.catch((/*e*/) => {
vscode.window.showInformationMessage('Logcat cannot be displayed. Querying the connected devices list failed. Is ADB running?');
});
}
exports.LogcatContent = LogcatContent;
exports.openLogcatWindow = openLogcatWindow;
module.exports = {
LogcatContent,
openLogcatWindow,
}

95
src/manifest.js Normal file
View File

@@ -0,0 +1,95 @@
const fs = require('fs');
const dom = require('xmldom').DOMParser;
const unzipper = require('unzipper');
const xpath = require('xpath');
const { decode_binary_xml } = require('./apk-decoder');
/**
* Extracts and decodes the compiled AndroidManifest.xml from an APK
* @param {string} apk_fpn file path to APK
* @returns {Promise<string>}
*/
async function extractManifestFromAPK(apk_fpn) {
const data = await extractFileFromAPK(apk_fpn, /^AndroidManifest\.xml$/);
return decode_binary_xml(data);
}
/**
* Extracts a single file from an APK
* @param {string} apk_fpn
* @param {RegExp} file_match
*/
function extractFileFromAPK(apk_fpn, file_match) {
return new Promise((resolve, reject) => {
const file_chunks = [];
let cb_once = (err, data) => {
cb_once = () => {};
err ? reject(err) : resolve(data);
}
fs.createReadStream(apk_fpn)
.pipe(unzipper.ParseOne(file_match))
.on('data', chunk => {
file_chunks.push(chunk);
})
.once('error', err => {
cb_once(err);
})
.once('end', () => {
cb_once(null, Buffer.concat(file_chunks));
});
})
}
/**
* Parses a manifest file to extract package, activities and launch activity
* @param {string} xml AndroidManifest XML text
*/
function parseManifest(xml) {
const result = {
/**
* The package name
*/
package: '',
/**
* the list of Activities stored in the manifest
* @type {string[]}
*/
activities: [],
/**
* the name of the Activity with:
* - intent-filter action = android.intent.action.MAIN and
* - intent-filter category = android.intent.category.LAUNCHER
*/
launcher: '',
}
const doc = new dom().parseFromString(xml);
// extract the package name from the manifest
const pkg_xpath = '/manifest/@package';
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';
const activity_nodes = android_select(activity_xpath, doc);
if (activity_nodes) {
result.activities = activity_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';
const launcher_nodes = android_select(launcher_xpath, doc);
// should we warn if there's more than one?
if (launcher_nodes && launcher_nodes.length >= 1) {
result.launcher = launcher_nodes[0].value
}
return result;
}
module.exports = {
extractManifestFromAPK,
parseManifest,
}

View File

@@ -1,122 +0,0 @@
/*
A dummy websocket implementation for passing messages internally using a WS-like protocol
*/
var Servers = {};
function isfn(x) { return typeof(x) === 'function' }
function WebSocketClient(url) {
// we only support localhost addresses in this implementation
var match = url.match(/^ws:\/\/127\.0\.0\.1:(\d+)$/);
var port = match && parseInt(match[1],10);
if (!port || port <= 0 || port >= 65536)
throw new Error('Invalid websocket url');
var server = Servers[port];
if (!server) throw new Error('Connection refused'); // 'port' already in use :)
server.addClient(this);
this._ws = {
port: port,
server: server,
outgoing:[],
};
}
WebSocketClient.prototype.send = function(message) {
this._ws.outgoing.push(message);
if (this._ws.outgoing.length > 1) return;
process.nextTick(function(client) {
if (!client || !client._ws || !client._ws.server)
return;
client._ws.server.receive(client, client._ws.outgoing);
client._ws.outgoing = [];
}, this);
}
WebSocketClient.prototype.receive = function(messages) {
if (isfn(this.onmessage))
messages.forEach(m => {
this.onmessage({
data:m
});
});
}
WebSocketClient.prototype.close = function() {
process.nextTick(() => {
this._ws.server.rmClient(this);
this._ws.server = null;
if (isfn(this.onclose))
this.onclose(this);
this._ws = null;
});
}
function WebSocketServer(port) {
if (typeof(port) !== 'number' || port <= 0 || port >= 65536)
throw new Error('Invalid websocket server port');
if (Servers[''+port])
throw new Error('Address in use');
this.port = port;
this.clients = [];
Servers[''+port] = this;
}
WebSocketServer.prototype.addClient = function(client) {
var status;
this.clients.push(status = {
server:this,
client: client,
onmessage:null,
onclose:null,
outgoing:[],
send: function(message) {
this.outgoing.push(message);
if (this.outgoing.length > 1) return;
process.nextTick(function(status) {
if (!status || !status.client)
return;
status.client.receive(status.outgoing);
status.outgoing = [];
}, this);
}
});
process.nextTick((status) => {
if (isfn(this.onconnection))
this.onconnection({
status: status,
accept:function() {
process.nextTick((status) => {
if (isfn(status.client.onopen))
status.client.onopen(status.client);
}, this.status);
return this.status;
}
});
}, status);
}
WebSocketServer.prototype.rmClient = function(client) {
for (var i = this.clients.length-1; i >= 0; --i) {
if (this.clients[i].client === client) {
if (isfn(this.clients[i].onclose))
this.clients[i].onclose();
this.clients.splice(i, 1);
}
}
}
WebSocketServer.prototype.receive = function(client, messages) {
var status = this.clients.filter(c => c.client === client)[0];
if (!status) return;
if (!isfn(status.onmessage)) return;
messages.forEach(m => {
status.onmessage({
data: m,
});
});
}
exports.WebSocketClient = WebSocketClient;
exports.WebSocketServer = WebSocketServer;

92
src/package-searcher.js Normal file
View File

@@ -0,0 +1,92 @@
const fs = require('fs');
const path = require('path');
const { hasValidSourceFileExtension } = require('./utils/source-file');
class PackageInfo {
/**
*
* @param {string} app_root
* @param {string} src_folder
* @param {string[]} files
* @param {string} pkg_name
* @param {string} package_path
*/
constructor(app_root, src_folder, files, pkg_name, package_path) {
this.package = pkg_name;
this.package_path = package_path;
this.srcroot = path.join(app_root, src_folder),
this.public_classes = files.reduce(
(classes, f) => {
// any file with a Java-identifier-compatible name and a valid extension
const m = f.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\.\w+$/);
if (m && hasValidSourceFileExtension(f)) {
classes.push(m[1]);
}
return classes;
}, []);
}
/**
* Scan known app folders looking for file changes and package folders
* @param {string} app_root app root directory path
*/
static scanSourceSync(app_root) {
try {
let subpaths = fs.readdirSync(app_root,'utf8');
const done_subpaths = new Set();
const src_packages = {
/**
* most recent modification time of a source file
*/
last_src_modified: 0,
/**
* Map of packages detected
* @type {Map<string,PackageInfo>}
*/
packages: new Map(),
};
while (subpaths.length) {
const subpath = subpaths.shift();
// just in case someone has some crazy circular links going on
if (done_subpaths.has(subpath)) {
continue;
}
done_subpaths.add(subpath);
let subfiles = [];
const package_path = path.join(app_root, subpath);
try {
const stat = fs.statSync(package_path);
src_packages.last_src_modified = Math.max(src_packages.last_src_modified, stat.mtime.getTime());
if (!stat.isDirectory()) {
continue;
}
subfiles = fs.readdirSync(package_path, 'utf8');
}
catch (err) {
continue;
}
// ignore folders not starting with a known top-level Android folder
if (!(/^(assets|res|src|main|java|kotlin)([\\/]|$)/.test(subpath))) {
continue;
}
// is this a package folder
const pkgmatch = subpath.match(/^(src|main|java|kotlin)[\\/](.+)/);
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, java or kotlin
const package_name = pkgmatch[2].replace(/[\\/]/g,'.');
src_packages.packages.set(package_name, new PackageInfo(app_root, src_folder, subfiles, package_name, package_path));
}
// add the subfiles to the list to process
subpaths = subfiles.map(sf => path.join(subpath,sf)).concat(subpaths);
}
return src_packages;
} catch(err) {
throw new Error('Source path error: ' + err.message);
}
}
}
module.exports = {
PackageInfo
}

View File

@@ -1,322 +0,0 @@
const chrome = require('./chrome-polyfill').chrome;
const { new_socketfd } = require('./sockets');
const { create_chrome_socket, accept_chrome_socket, destroy_chrome_socket } = chrome;
var start_request = function(fd) {
if (fd.closeState) return;
// read service passed from client
D('waiting for adb request...');
readx_with_data(fd, function(err, data) {
if (err) {
D('SS: error %o', err);
return;
}
handle_request(fd, data.asString());
start_request(fd);
});
}
var handle_request = exports.handle_request = function(fd, service) {
if (!service){
D('SS: no service');
sendfailmsg(fd, 'No service received');
return false;
}
D('adb request: %s', service);
if (service.slice(0,4) === 'host') {
// trim 'host:'
return handle_host_request(service.slice(5), 'kTransportAny', null, fd);
}
if (!fd.transport) {
D('No transport configured - using any found');
var t = acquire_one_transport('CS_DEVICE', 'kTransportAny', null);
t = check_one_transport(t, '', fd);
if (!t) return false;
fd.transport = t;
}
// once we call open_device_service, the fd belongs to the transport
open_device_service(fd.transport, fd, service, function(err, serviceinfo) {
if (err) {
sendfailmsg(fd, 'Device connection failed');
return;
}
D('device service opened: %o', serviceinfo);
send_okay(fd);
});
return true;
}
var sendfailmsg = function(fd, reason) {
reason = reason.slice(0, 0xffff);
var msg = 'FAIL' + intToHex(reason.length,4) + reason;
writex(fd, msg);
}
var handle_host_request = function(service, ttype, serial, replyfd) {
var transport;
if (service === 'kill') {
cl('service kill request');
send_okay(replyfd);
killall_devices();
//window.close();
return false;
}
if (service.slice(0,9) === 'transport') {
var t,serialmatch;
switch(service.slice(9)) {
case '-any':
t = acquire_one_transport('CS_ANY','kTransportAny',null);
break;
case '-local':
t = acquire_one_transport('CS_ANY','kTransportLocal',null);
break;
case '-usb':
t = acquire_one_transport('CS_ANY','kTransportUsb',null);
break;
default:
if (serialmatch = service.slice(9).match(/^:(.+)/))
t = acquire_one_transport('CS_ANY','kTransportAny',serialmatch[1]);
break;
}
t = check_one_transport(t, serialmatch&&serialmatch[1], replyfd);
if (!t) return false;
// set the transport in the fd - the client can use it
// to send raw data directly to the device
D('transport configured: %o', t);
replyfd.transport = t;
adb_writebytes(replyfd, "OKAY");
return false;
}
if (service.slice(0,7) === 'devices') {
var use_long = service.slice(7)==='-l';
D('Getting device list');
var transports = list_transports(use_long);
D('Wrote device list');
send_msg_with_okay(replyfd, transports);
return false;
}
if (service === 'version') {
var version = intToHex(ADB_SERVER_VERSION, 4);
send_msg_with_okay(replyfd, version);
return false;
}
if (service.slice(0,9) === 'emulator:') {
var port = service.slice(9);
port = port&&parseInt(port, 10)||0;
if (!port || port <= 0 || port >= 65536) {
D('Invalid emulator port: %s', service);
return false;
}
local_connect(port, function(err) {
});
// no reply needed
return false;
}
if (service.slice(0,9) === 'get-state') {
transport = acquire_one_transport('CS_ANY', ttype, serial, null);
transport = check_one_transport(transport, serial, replyfd);
if (!transport) return false;
var state = connection_state_name(transport);
send_msg_with_okay(replyfd, state);
return false;
}
if (service === 'killforward-all') {
remove_all_forward_listeners();
writex(replyfd, 'OKAY');
return false;
}
var fwdmatch = service.match(/^forward:(tcp:\d+);(jdwp:\d+)/);
if (fwdmatch) {
transport = acquire_one_transport('CS_ANY', ttype, serial, null);
transport = check_one_transport(transport, serial, replyfd);
if (!transport) return false;
install_forward_listener(fwdmatch[1], fwdmatch[2], transport, function(err) {
if (err) return sendfailmsg(replyfd, err.msg);
// on the host, 1st OKAY is connect, 2nd OKAY is status
writex(replyfd, 'OKAY');
writex(replyfd, 'OKAY');
});
return false;
}
if (service === 'track-devices') {
writex(replyfd, 'OKAY');
add_device_tracker(replyfd);
// fd now belongs to the tracker
return true;
}
if (service === 'track-devices-extended') {
writex(replyfd, 'OKAY');
add_device_tracker(replyfd, true);
// fd now belongs to the tracker
return true;
}
cl('Ignoring host service request: %s', service);
return false;
}
var check_one_transport = function(t, serial, replyfd) {
var which = serial||'(null)';
switch((t||[]).length) {
case 0:
sendfailmsg(replyfd, "device '"+which+"' not found");
return null;
case 1: t = t[0];
break;
default:
sendfailmsg(replyfd, 'more than one device/emulator');
return null;
}
switch(t.connection_state) {
case 'CS_DEVICE': break;
case 'CS_UNAUTHORIZED':
sendfailmsg(replyfd, 'device unauthorized.\r\nCheck for a confirmation dialog on your device or reconnect the device.');
return null;
default:
sendfailmsg(replyfd, 'Device not ready');
return null;
}
return t;
}
var forward_listeners = {};
var install_forward_listener = function(local, remote, t, cb) {
var localport = parseInt(local.split(':').pop(), 10);
var socket = chrome.socket;
create_chrome_socket('forward listener:'+localport, function(socketInfo) {
if (chrome.runtime.lastError) {
return cb({msg:chrome.runtime.lastError.message||'socket creation failed'});
}
socket.listen(socketInfo.socketId, '127.0.0.1', localport, 5,
function(result) {
if (chrome.runtime.lastError) {
var err = {msg:chrome.runtime.lastError.message||'socket listen failed'};
destroy_setup(socketInfo);
return cb(err);
}
if (result < 0) {
destroy_setup(socketInfo);
return cb({msg:'Cannot bind to socket'});
}
forward_listeners[localport] = {
port:localport,
socketId: socketInfo.socketId,
connectors_fd: null,
connect_cb:function(){},
};
accept_chrome_socket('forward server:'+localport, socketInfo.socketId, function(acceptInfo) {
accept_forward_connection(socketInfo.socketId, acceptInfo, localport, local, remote, t);
});
// listener is ready
D('started forward listener on port %d: %d', localport, socketInfo.socketId);
cb();
}
);
});
function destroy_setup(socketInfo) {
destroy_chrome_socket(socketInfo.socketId);
}
}
var connect_forward_listener = exports.connect_forward_listener = function(port, opts, cb) {
// if we're implementing the adb service, this will already be created
// if we're connecting via the adb executable, we need to create a dummy entry
if (!forward_listeners[port]) {
if (opts && opts.create) {
forward_listeners[port] = {
is_external_adb: true,
port:port,
socketId: null,
connectors_fd: null,
connect_cb:function(){},
}
} else {
D('Refusing forward connection request - forwarder for port %d does not exist', port);
return cb();
}
}
create_chrome_socket('forward client:'+port, function(createInfo) {
// save the receiver info
forward_listeners[port].connectors_fd = new_socketfd(createInfo.socketId);
forward_listeners[port].connect_cb = cb;
// do the connect - everything from here on is handled in the accept routine
chrome.socket.connect(createInfo.socketId, '127.0.0.1', port, function(result) {
chrome.socket.setNoDelay(createInfo.socketId, true, function(result) {
var x = forward_listeners[port];
if (x.is_external_adb) {
delete forward_listeners[port];
x.connect_cb(x.connectors_fd);
}
});
});
});
}
var accept_forward_connection = exports.accept_forward_connection = function(listenerSocketId, acceptInfo, port, local, remote, t) {
if (chrome.runtime.lastError) {
D('Forward port socket accept failed: '+port);
var listener = remove_forward_listener(listenerSocketId);
return listener.connect_cb();
}
// on accept - create the remote connection to the device
D('Binding forward port connection to remote port %s', remote);
var sfd = new_socketfd(acceptInfo.socketId);
// remove the listener
var listener = remove_forward_listener(listenerSocketId);
chrome.socket.setNoDelay(acceptInfo.socketId, true, function(result) {
// start the connection as a service
open_device_service(t, sfd, remote, function(err) {
listener.connect_cb(listener.connectors_fd);
});
});
}
var remove_forward_listener = exports.remove_forward_listener = function(socketId) {
for (var port in forward_listeners) {
if (forward_listeners[port].socketId === socketId) {
var x = forward_listeners[port];
delete forward_listeners[port];
destroy_chrome_socket(x.socketId);
D('removed forward listener: %d', x.socketId);
return x;
}
}
}
var remove_all_forward_listeners = exports.remove_all_forward_listeners = function() {
var ports = Object.keys(forward_listeners);
while (ports.length) {
remove_forward_listener(forward_listeners[ports.pop()].socketId);
}
}

View File

@@ -1,290 +0,0 @@
const chrome = require('./chrome-polyfill').chrome;
const { create_chrome_socket, destroy_chrome_socket } = chrome;
const { D, remove_from_list } = require('./util');
// array of local_sockets
var _local_sockets = [];
var _new_local_socket_id = 1000;
var new_local_socket = function(t, fd, close_fd_on_local_socket_close) {
var x = {
id:++_new_local_socket_id,
fd:fd,
close_fd_on_local_socket_close: !!close_fd_on_local_socket_close,
transport:t,
enqueue: local_socket_enqueue,
ready: local_socket_ready_notify,
close: local_socket_close,
peer:null,
//socketbuffer: [],
}
_local_sockets.push(x);
return x;
}
var find_local_socket = function(local_socket_id, peer_socket_id) {
for (var i=0; i < _local_sockets.length; i++) {
var ls = _local_sockets[i];
if (ls.id === local_socket_id) {
if (!peer_socket_id) return ls;
if (!ls.peer) continue;
if (ls.peer.id === peer_socket_id) return ls;
}
}
return null;
}
var local_socket_ready = function(s) {
D("LS(%d): ready()\n", s.id);
}
var local_socket_ready_notify = function(s) {
s.ready = local_socket_ready;
send_okay(s.fd);
s.ready(s);
}
var local_socket_enqueue = function(s, p) {
D("LS(%d): enqueue()\n", s.id, p.len);
if (s.fd.closed) return false;
D("LS: enqueue() - writing %d bytes to fd:%d %o\n", p.len, s.fd.n, s.fd);
adb_writebytes(s.fd, p.data, p.len);
//s.socketbuffer.push({data:p.data, len:p.len});
return true;
}
var local_socket_close = function(s) {
// flush the data to the output socket
/*var totallen = s.socketbuffer.reduce(function(n, x) { return n+x.len },0);
adb_writebytes(s.fd, intToHex(totallen,4));
s.socketbuffer.forEach(function(x) {
adb_writebytes(s.fd, x.data, x.len);
});*/
if (s.peer) {
s.peer.peer = null;
s.peer.close(s.peer);
s.peer = null;
}
if (s.fd && s.close_fd_on_local_socket_close) {
s.fd.close();
}
var id = s.id;
var idx = _local_sockets.indexOf(s);
if (idx >= 0) _local_sockets.splice(idx, 1);
D("LS(%d): closed()\n", id);
}
var local_socket_force_close_all = function(t) {
// called when a transport disconnects without a clean finish
var lsarr = _local_sockets.reduce(function(res, ls) {
if (ls && ls.transport === t) res.push(ls);
return res;
}, []);
lsarr.forEach(function(ls) {
D('force closing socket: %o', ls);
local_socket_close(ls);
});
}
var remote_socket_ready = function(s, cb) {
D("entered remote_socket_ready RS(%d) OKAY fd=%d peer.fd=%d\n",
s.id, s.fd, s.peer.fd);
p = get_apacket();
p.msg.command = A_OKAY;
p.msg.arg0 = s.peer.id;
p.msg.arg1 = s.id;
send_packet(p, s.transport, cb);
}
var remote_socket_close = function(s) {
if (s.peer) {
s.peer.peer = null;
s.peer.close(s.peer);
}
D("RS(%d): closed\n", s.id);
}
var create_remote_socket = function(id, t) {
var s = {
id: id,
transport: t,
peer:null,
ready: remote_socket_ready,
close: remote_socket_close,
// a remote socket is a normal socket with an extra disconnect function
disconnect:null,
}
D("RS(%d): created\n", s.id);
// when a
return s;
}
var loopback_clients = [];
var get_socket_fd_from_fdn = exports.get_socket_fd_from_fdn = function(n) {
for (var i=0; i < loopback_clients.length; i++) {
if (loopback_clients[i].n === n)
return loopback_clients[i];
}
return null;
}
var socket_loopback_client = exports.socket_loopback_client = function(port, cb) {
create_chrome_socket('socket_loopback_client', function(createInfo) {
chrome.socket.connect(createInfo.socketId, '127.0.0.1', port, function(result) {
if (result < 0) {
destroy_chrome_socket(createInfo.socketId);
return cb();
}
chrome.socket.setNoDelay(createInfo.socketId, true, function(result) {
var x = new_socketfd(createInfo.socketId);
return cb(x);
});
});
});
}
var new_socketfd = exports.new_socketfd = function(socketId) {
var x = {
n: socketId,
isSocket:true,
connected:true,
closed:false,
// readbytes and writebytes are used by readx and writex
readbytes:function(len, cb) {
slc_read(this, len, function(err, data){
cb(err, data);
});
},
writebytes:function(data, cb) {
slc_write(this, data, cb||function(){});
},
close:function() {
slc_close(this, function(){});
}
};
loopback_clients.push(x);
return x;
}
var slc_readwithkick = function(sfd, cb) {
/*if (sfd.reader_cb_stack.length) {
return cb(null, new Uint8Array(0));
}*/
//var readinfo = {cb:cb, expired:false};
//sfd.reader_cb_stack.push(readinfo);
var kicker = setTimeout(function() {
if (!kicker) return;
kicker = null;
D('reader kick expired - retuning nothing');
//readinfo.expired = true;
cb(null, new Uint8Array(0));
}, 100);
slc_read_stacked_(sfd, function(err, data) {
if (!kicker) {
D('Discarding data recevied after kick expired');
return;
}
clearTimeout(kicker);
kicker = null;
cb(err, data);
});
};
var slc_read = function(sfd, minlen, cb) {
//sfd.reader_cb_stack.push({cb:cb, expired:false});
slc_read_stacked_(sfd, minlen, cb);
}
var slc_read_stacked_ = function(sfd, minlen, cb) {
var params = [sfd.n];
switch(typeof(minlen)) {
case 'number': params.push(minlen); break;
case 'function': cb = minlen; // fall through
default: minlen = 'any';
};
var buffer = new Uint8Array(minlen==='any'?65536:minlen);
var buffer_offset = 0;
var onread = function(readInfo) {
if (chrome.runtime.lastError) {
slc_close(sfd, function() {
cb({msg: 'socket read error. Terminating socket'});
});
return;
}
if (readInfo.resultCode < 0) return cb(readInfo);
buffer.set(new Uint8Array(readInfo.data), buffer_offset);
buffer_offset += readInfo.data.byteLength;
if (typeof(minlen)==='number' &&buffer_offset < minlen) {
// read more
params[1] = minlen - buffer_offset;
chrome.socket.read.apply(chrome.socket, params);
return;
}
buffer = buffer.subarray(0, buffer_offset);
buffer.asString = function() { return arrayBufferToString(this); }
return cb(null, buffer);
};
params.push(onread);
chrome.socket.read.apply(chrome.socket, params);
}
var slc_write = function(sfd, data, cb) {
var buf = data.buffer;
if (buf.byteLength !== data.byteLength) {
buf = buf.slice(0, data.byteLength);
}
chrome.socket.write(sfd.n, buf, function(writeInfo) {
if (chrome.runtime.lastError) {
slc_close(sfd, function() {
cb({msg: 'socket write error. Terminating socket'});
});
return;
}
if (writeInfo.bytesWritten !== data.byteLength)
return cb({msg: 'socket write mismatch. wanted:'+data.byteLength+', sent:'+writeInfo.bytesWritten});
cb();
});
}
var slc_shutdown = function(sfd, cb) {
if (sfd.connected) {
sfd.connected = false;
chrome.socket.disconnect(sfd.n);
}
cb();
}
var slc_close = function(sfd, cb) {
if (sfd.connected) {
sfd.connected = false;
chrome.socket.disconnect(sfd.n);
}
sfd.closed = true;
destroy_chrome_socket(sfd.n);
remove_from_list(loopback_clients, sfd);
cb();
}
var fd_loopback_client = function() {
var s = [];
adb_socketpair(s, 'fd_loopback_client', true);
D('fd_loopback_client created. server fd:%d, client fd:%d', s[1].n, s[0].n);
// return one side and pass the other side to the request handler
start_request(s[1]);
return s[0];
}

143
src/sockets/adbsocket.js Normal file
View File

@@ -0,0 +1,143 @@
const AndroidSocket = require('./androidsocket');
/**
* Manages a socket connection to Android Debug Bridge
*/
class ADBSocket extends AndroidSocket {
/**
* The port number to run ADB on.
* The value can be overriden by the adbPort value in each configuration.
*/
static ADBPort = 5037;
constructor() {
super('ADBSocket');
}
/**
* Reads and checks the reply from an ADB command
* @param {boolean} [throw_on_fail] true if the function should throw on non-OKAY status
*/
async read_adb_status(throw_on_fail = true) {
// read back the status
const status = await this.read_bytes(4, 'latin1')
if (status !== 'OKAY' && throw_on_fail) {
throw new Error(`ADB command failed. Status: '${status}'`);
}
return status;
}
/**
* Reads and decodes an ADB reply. The reply is always in the form XXXXnnnn where XXXX is a 4 digit ascii hex length
*/
async read_adb_reply() {
const hexlen = await this.read_bytes(4, 'latin1');
if (/[^\da-fA-F]/.test(hexlen)) {
throw new Error('Bad ADB reply - invalid length data');
}
return this.read_bytes(parseInt(hexlen, 16), 'latin1');
}
/**
* Writes a command to the ADB socket
* @param {string} command
*/
write_adb_command(command) {
const command_bytes = Buffer.from(command);
const command_length = Buffer.from(('000' + command_bytes.byteLength.toString(16)).slice(-4));
return this.write_bytes(Buffer.concat([command_length, command_bytes]));
}
/**
* Sends an ADB command and checks the returned status
* @param {String} command ADB command to send
* @returns {Promise<string>} OKAY status or rejected
*/
async cmd_and_status(command) {
await this.write_adb_command(command);
return this.read_adb_status();
}
/**
* Sends an ADB command, checks the returned status and then reads the return reply
* @param {String} command ADB command to send
* @returns {Promise<string>} reply string or rejected if the status is not OKAY
*/
async cmd_and_reply(command) {
await this.cmd_and_status(command);
return this.read_adb_reply();
}
/**
* Sends an ADB command, checks the returned status and then reads raw data from the socket
* @param {string} command
*/
async cmd_and_read_stdout(command) {
await this.cmd_and_status(command);
return this.read_stdout();
}
/**
* Copies a file to the device, setting the file time and permissions
* @param {ADBFileTransferParams} file file parameters
*/
async transfer_file(file) {
await this.cmd_and_status('sync:');
// initiate the file send
const filename_and_perms = `${file.pathname},${file.perms}`;
const send_and_fileinfo = Buffer.from(`SEND\0\0\0\0${filename_and_perms}`);
send_and_fileinfo.writeUInt32LE(filename_and_perms.length, 4);
await this.write_bytes(send_and_fileinfo);
// send the file data
await this.write_file_data(file.data);
// send the DONE message with the new filetime
const done_and_mtime = Buffer.from('DONE\0\0\0\0');
done_and_mtime.writeUInt32LE(file.mtime, 4);
await this.write_bytes(done_and_mtime);
// read the final status and any error message
const result = await this.read_adb_status(false);
const failmsg = await this.read_le_length_data('latin1');
// finish the transfer mode
await this.write_bytes('QUIT\0\0\0\0');
if (result !== 'OKAY') {
throw new Error(`File transfer failed. ${failmsg}`);
}
return true;
}
/**
* @param {Buffer} data
*/
async write_file_data(data) {
const dtinfo = {
transferred: 0,
transferring: 0,
chunk_size: 10240,
};
for (;;) {
dtinfo.transferred += dtinfo.transferring;
const remaining = data.byteLength - dtinfo.transferred;
if (remaining <= 0 || isNaN(remaining)) {
return dtinfo.transferred;
}
const datalen = Math.min(remaining, dtinfo.chunk_size);
const cmd = Buffer.concat([Buffer.from(`DATA\0\0\0\0`), data.slice(dtinfo.transferred, dtinfo.transferred + datalen)]);
cmd.writeUInt32LE(datalen, 4);
dtinfo.transferring = datalen;
await this.write_bytes(cmd);
}
}
}
module.exports = ADBSocket;

View File

@@ -0,0 +1,159 @@
const net = require('net');
const EventEmitter = require('events');
/**
* Common socket class for ADBSocket and JDWPSocket
*/
class AndroidSocket extends EventEmitter {
constructor(which) {
super()
this.which = which;
this.socket = null;
this.socket_error = null;
this.socket_ended = false;
this.readbuffer = Buffer.alloc(0);
}
connect(port, hostname) {
return new Promise((resolve, reject) => {
if (this.socket) {
return reject(new Error(`${this.which} Socket connect failed. Socket already connected.`));
}
const connection_error = err => {
return reject(new Error(`${this.which} Socket connect failed. ${err.message}.`));
}
const post_connection_error = err => {
this.socket_error = err;
this.socket.end();
}
let error_handler = connection_error;
this.socket = new net.Socket()
.once('connect', () => {
error_handler = post_connection_error;
this.socket
.on('data', buffer => {
this.readbuffer = Buffer.concat([this.readbuffer, buffer]);
this.emit('data-changed');
})
.once('end', () => {
this.socket_ended = true;
this.emit('socket-ended');
if (!this.socket_disconnecting) {
this.socket_disconnecting = this.socket_error ? Promise.reject(this.socket_error) : Promise.resolve();
}
});
resolve();
})
.on('error', err => error_handler(err));
this.socket.connect(port, hostname);
});
}
disconnect() {
if (!this.socket_disconnecting) {
this.socket_disconnecting = new Promise(resolve => {
this.socket.end();
this.socket = null;
this.once('socket-ended', resolve);
});
}
return this.socket_disconnecting;
}
/**
*
* @param {number|'length+data'|undefined} length
* @param {string} [format]
*/
async read_bytes(length, format) {
//D(`reading ${length} bytes`);
let actual_length = length;
if (typeof actual_length === 'undefined') {
if (this.readbuffer.byteLength > 0 || this.socket_ended) {
actual_length = this.readbuffer.byteLength;
}
}
if (actual_length < 0) {
throw new Error(`${this.which} socket read failed. Attempt to read ${actual_length} bytes.`);
}
if (length === 'length+data' && this.readbuffer.byteLength >= 4) {
length = actual_length = this.readbuffer.readUInt32BE(0);
}
if (this.socket_ended) {
if (actual_length <= 0 || (this.readbuffer.byteLength < actual_length)) {
this.check_socket_active('read');
}
}
// do we have enough data in the buffer?
if (this.readbuffer.byteLength >= actual_length) {
//D(`got ${actual_length} bytes`);
let data = this.readbuffer.slice(0, actual_length);
this.readbuffer = this.readbuffer.slice(actual_length);
if (format) {
data = data.toString(format);
}
return Promise.resolve(data);
}
// wait for the socket to update and then retry the read
await this.wait_for_socket_data();
return this.read_bytes(length, format);
}
wait_for_socket_data() {
return new Promise((resolve, reject) => {
let done = 0;
let onDataChanged = () => {
if ((done += 1) !== 1) return;
this.off('socket-ended', onSocketEnded);
resolve();
}
let onSocketEnded = () => {
if ((done += 1) !== 1) return;
this.off('data-changed', onDataChanged);
reject(new Error(`${this.which} socket read failed. Socket closed.`));
}
this.once('data-changed', onDataChanged);
this.once('socket-ended', onSocketEnded);
});
}
async read_le_length_data(format) {
const len = await this.read_bytes(4);
return this.read_bytes(len.readUInt32LE(0), format);
}
read_stdout(format = 'latin1') {
return this.read_bytes(undefined, format);
}
/**
* Writes a raw command to the socket
* @param {string|Buffer} bytes
*/
write_bytes(bytes) {
return new Promise((resolve, reject) => {
this.check_socket_active('write');
try {
const flushed = this.socket.write(bytes, () => {
flushed ? resolve() : this.socket.once('drain', resolve);
});
} catch (e) {
this.socket_error = e;
reject(new Error(`${this.which} socket write failed. ${e.message}`));
}
});
}
/**
*
* @param {'read'|'write'} action
*/
check_socket_active(action) {
if (this.socket_ended) {
throw new Error(`${this.which} socket ${action} failed. Socket closed.`);
}
}
}
module.exports = AndroidSocket;

122
src/sockets/jdwpsocket.js Normal file
View File

@@ -0,0 +1,122 @@
const AndroidSocket = require('./androidsocket');
/**
* Manages a JDWP connection to the device
* The debugger uses ADB to setup JDWP port forwarding to the device - this class
* connects to the local forwarding port
*/
class JDWPSocket extends AndroidSocket {
/**
* @param {(data)=>*} decode_reply function used for decoding raw JDWP data
* @param {()=>void} on_disconnect function called when the socket disconnects
*/
constructor(decode_reply, on_disconnect) {
super('JDWP')
this.decode_reply = decode_reply;
this.on_disconnect = on_disconnect;
/** @type {Map<*,function>} */
this.cmds_in_progress = new Map();
this.cmd_queue = [];
}
/**
* Performs the JDWP handshake and begins reading the socket for JDWP events/replies
*/
async start() {
const handshake = 'JDWP-Handshake';
await this.write_bytes(handshake);
const handshake_reply = await this.read_bytes(handshake.length, 'latin1');
if (handshake_reply !== handshake) {
throw new Error('JDWP handshake failed');
}
this.start_jdwp_reply_reader();
return true;
}
/**
* Continuously reads replies from the JDWP socket. After each reply is read,
* it's matched up with its corresponding command using the request ID.
*/
async start_jdwp_reply_reader() {
for (;;) {
let data;
try {
data = await this.read_bytes('length+data'/* , 'latin1' */)
} catch (e) {
// ignore socket closed errors (sent when the debugger disconnects)
if (!/socket closed/i.test(e.message))
throw e;
if (typeof this.on_disconnect === 'function') {
this.on_disconnect();
}
return;
}
const reply = this.decode_reply(data);
const on_reply = this.cmds_in_progress.get(reply.command);
if (on_reply) {
on_reply(reply);
}
}
}
/**
* Send a single command to the device and wait for the reply
* @param {*} command
*/
process_cmd(command) {
return new Promise(resolve => {
// add the command to the in-progress set
this.cmds_in_progress.set(command, reply => {
// once the command has completed, delete it from in-progress and resolve the promise
this.cmds_in_progress.delete(command);
resolve(reply);
});
// send the raw command bytes to the device
this.write_bytes(command.toBuffer());
});
}
/**
* Drain the queue of JDWP commands waiting to be sent to the device
*/
async run_cmd_queue() {
for (;;) {
if (this.cmd_queue.length === 0) {
return;
}
const { command, resolve, reject } = this.cmd_queue[0];
const reply = await this.process_cmd(command);
if (reply.errorcode) {
class JDWPCommandError extends Error {
constructor(reply) {
super(`JDWP command failed '${reply.command.name}'. Error ${reply.errorcode}`);
this.command = reply.command;
this.errorcode = reply.errorcode;
}
}
reject(new JDWPCommandError(reply));
} else {
resolve(reply);
}
this.cmd_queue.shift();
}
}
/**
* Queue a command to be sent to the device and wait for the reply
* @param {*} command
*/
async cmd_and_reply(command) {
return new Promise((resolve, reject) => {
const queuelen = this.cmd_queue.push({
command,
resolve, reject
})
if (queuelen === 1) {
this.run_cmd_queue();
}
})
}
}
module.exports = JDWPSocket;

276
src/stack-frame.js Normal file
View File

@@ -0,0 +1,276 @@
const { Debugger } = require('./debugger');
const { DebuggerFrameInfo, DebuggerValue, JavaType, LiteralValue, VariableValue } = require('./debugger-types');
const { assignVariable } = require('./expression/assign');
const { NumberBaseConverter } = require('./utils/nbc');
const { VariableManager } = require('./variable-manager');
/**
* @param {DebuggerValue[]} variables
* @param {boolean} thisFirst
* @param {boolean} allCapsLast
*/
function sortVariables(variables, thisFirst, allCapsLast) {
return variables.sort((a,b) => {
if (a.name === b.name) return 0;
if (thisFirst) {
if (a.name === 'this') return -1;
if (b.name === 'this') return +1;
}
if (allCapsLast) {
const acaps = !/[a-z]/.test(a.name);
const bcaps = !/[a-z]/.test(b.name);
if (acaps !== bcaps) {
return acaps ? +1 : -1;
}
}
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
});
}
class DebuggerStackFrame extends VariableManager {
/**
* @param {Debugger} dbgr
* @param {DebuggerFrameInfo} frame
* @param {VSCVariableReference} frame_variable_reference
*/
constructor(dbgr, frame, frame_variable_reference) {
super(frame_variable_reference );
this.variableReference = frame_variable_reference;
this.dbgr = dbgr;
this.frame = frame;
/** @type {DebuggerValue[]} */
this.locals = null;
}
/**
* Return the list of local values for this stack frame
* @returns {Promise<DebuggerValue[]>}
*/
async getLocals() {
if (this.locals) {
return this.locals;
}
const fetch_locals = async () => {
const values = await this.dbgr.getLocals(this.frame);
// display the variables in (case-insensitive) alphabetical order, with 'this' first and all-caps last
return this.locals = sortVariables(values, true, false);
}
// @ts-ignore
return this.locals = fetch_locals();
}
async getLocalVariables() {
const values = await this.getLocals();
return values.map(value => this.makeVariableValue(value));
}
/**
* @param {VSCVariableReference} variablesReference
* @param {string} name
* @param {DebuggerValue} value
*/
async setVariableValue(variablesReference, name, value) {
/** @type {DebuggerValue[]} */
let variables;
if (variablesReference === this.variableReference) {
variables = this.locals;
} else {
const varinfo = this.variableValues.get(variablesReference);
if (!varinfo || !varinfo.cached) {
throw new Error(`Variable '${name}' not found`);
}
variables = varinfo.cached;
}
const var_idx = variables.findIndex(v => v.name === name);
try {
const updated_value = await assignVariable(this.dbgr, variables[var_idx], name, value);
variables[var_idx] = updated_value;
return this.makeVariableValue(updated_value);
} catch(e) {
throw new Error(`Variable update failed. ${e.message}`);
}
}
/**
* @param {VSCVariableReference} variablesReference
* @returns {Promise<VariableValue[]>}
*/
async getExpandableValues(variablesReference) {
const varinfo = this.variableValues.get(variablesReference);
if (!varinfo) {
return [];
}
if (varinfo.cached) {
// return the cached version
return varinfo.cached.map(v => this.makeVariableValue(v));
}
if (varinfo.primitive) {
// convert the primitive value into alternate formats
return this.getPrimitive(varinfo);
}
/** @type {DebuggerValue[]} */
let values = [];
if (varinfo.objvar) {
// object fields request
values = sortVariables(await this.getObjectFields(varinfo), false, true);
}
else if (varinfo.arrvar) {
// array elements request
const arr = await this.getArrayElements(varinfo);
if (arr.isSubrange) {
// @ts-ignore
return arr.values;
}
// @ts-ignore
values = arr.values;
}
else if (varinfo.bigstring) {
values = [await this.getBigString(varinfo)];
}
return (varinfo.cached = values).map(v => this.makeVariableValue(v));
}
async getObjectFields(varinfo) {
const supertype = await this.dbgr.getSuperType(varinfo.objvar);
const fields = await this.dbgr.getFieldValues(varinfo.objvar);
// add an extra msg field for exceptions
if (varinfo.exception) {
const call = await this.dbgr.invokeToString(varinfo.objvar.value, varinfo.threadid, varinfo.objvar.type.signature);
call.name = ":message";
fields.unshift(call);
}
// add a ":super" member, unless the super is Object
if (supertype && supertype.signature !== JavaType.Object.signature) {
fields.unshift(new DebuggerValue('super', supertype, varinfo.objvar.value, true, false, ':super', null));
}
return fields;
}
async getArrayElements(varinfo) {
const range = varinfo.range,
count = range[1] - range[0];
// should always have a +ve count, but just in case...
if (count <= 0) {
return null;
}
// counts over 110 are shown as subranges
if (count > 110) {
return {
isSubrange: true,
values: this.getArraySubrange(varinfo.arrvar, count, range),
};
}
// get the elements for the specified range
const elements = await this.dbgr.getArrayElementValues(varinfo.arrvar, range[0], count);
return {
isSubrange: false,
values: elements,
}
}
/**
*
* @param {*} arrvar
* @param {number} count
* @param {[number,number]} range
*/
getArraySubrange(arrvar, count, range) {
// create subranges in the sub-power of 10
const subrangelen = Math.max(Math.pow(10, (Math.log10(count)|0)-1),100);
/** @type {VariableValue[]} */
const variables = [];
for (let i = range[0]; i < range[1]; i+= subrangelen) {
const varinfo = {
varref: 0,
arrvar,
range: [i, Math.min(i+subrangelen, range[1])],
};
const varref = this._addVariable(varinfo);
const variable = new VariableValue(`[${varinfo.range[0]}..${varinfo.range[1]-1}]`, '', null, varref, '');
variables.push(variable);
}
return variables;
}
async getBigString(varinfo) {
const string = await this.dbgr.getStringText(varinfo.bigstring.value);
const res = new LiteralValue(JavaType.String, string);
res.name = '<value>';
res.string = string;
return res;
}
getPrimitive(varinfo) {
/** @type {VariableValue[]} */
const variables = [];
const bits = {
J:64,
I:32,
S:16,
B:8,
}[varinfo.signature];
/**
*
* @param {number|hex64} n
* @param {number} base
* @param {number} len
*/
function convert(n, base, len) {
let converted;
if (typeof n === 'string') {
converted = {
2: () => n.replace(/./g, c => parseInt(c,16).toString(2)),
10: () => NumberBaseConverter.hexToDec(n, false),
16: () => n,
}[base]();
} else {
converted = n.toString(base);
}
return converted.padStart(len, '0');
}
/**
* @param {number|hex64} u
* @param {8|16|32|64} bits
*/
function getIntFormats(u, bits) {
const bases = [2, 10, 16];
const min_lengths = [bits, 1, bits/4];
const base_names = ['<binary>', '<decimal>', '<hex>'];
return base_names.map((name, i) => new VariableValue(name, convert(u, bases[i], min_lengths[i])));
}
switch(varinfo.signature) {
case 'Ljava/lang/String;':
variables.push(new VariableValue('<length>', varinfo.value.toString()));
break;
case 'C':
variables.push(new VariableValue('<charCode>', varinfo.value.charCodeAt(0).toString()));
break;
case 'J':
// because JS cannot handle 64bit ints, we need a bit of extra work
const v64hex = varinfo.value.replace(/[^0-9a-fA-F]/g,'');
variables.push(...getIntFormats(v64hex, 64));
break;
default:// integer/short/byte value
const u = varinfo.value >>> 0;
variables.push(...getIntFormats(u, bits));
break;
}
return variables;
}
}
module.exports = {
DebuggerStackFrame,
}

View File

@@ -1,23 +1,78 @@
'use strict'
const { Debugger } = require('./debugger');
const { DebuggerException, DebuggerFrameInfo, SourceLocation } = require('./debugger-types');
const { DebuggerStackFrame } = require('./stack-frame');
const { VariableManager } = require('./variable-manager');
const { AndroidVariables } = require('./variables');
const $ = require('./jq-promise');
// vscode doesn't like thread id reuse (the Android runtime is OK with it)
let nextVSCodeThreadId = 0;
/**
* Scales used to build VSCVariableReferences.
* Each reference contains a thread id, frame id and variable index.
* eg. VariableReference 1005000000 has thread:1 and frame:5
*
* The variable index is the bottom 1M values.
* - A 0 value is used for locals scope
* - A 1 value is used for exception scope
* - Values above 10 are used for variables
*/
const var_ref_thread_scale = 1e9;
const var_ref_frame_scale = 1e6;
const var_ref_global_frame = 999e6;
class ThreadPauseInfo {
/**
* @param {string} reason
* @param {SourceLocation} location
* @param {DebuggerException} last_exception
*/
constructor(reason, location, last_exception) {
this.when = Date.now(); // when
this.reasons = [reason]; // why
this.location = location; // where
this.last_exception = last_exception;
/**
* @type {Map<VSCVariableReference,DebuggerStackFrame>}
*/
this.stack_frames = new Map();
/**
* instance used to manage variables created for expressions evaluated in the global context
* @type {VariableManager}
*/
this.global_vars = null;
this.stoppedEvent = null; // event we (eventually) send to vscode
}
/**
* @param {number} frameId
*/
getLocals(frameId) {
return this.stack_frames.get(frameId).locals;
}
}
/*
Class used to manage a single thread reported by JDWP
*/
class AndroidThread {
constructor(session, threadid, vscode_threadid) {
// the AndroidDebugSession instance
this.session = session;
/**
*
* @param {Debugger} dbgr
* @param {string} name
* @param {JavaThreadID} threadid
*/
constructor(dbgr, name, threadid) {
// the Android debugger instance
this.dbgr = session.dbgr;
this.dbgr = dbgr;
// the java thread id (hex string)
this.threadid = threadid;
// the vscode thread id (number)
this.vscode_threadid = vscode_threadid;
this.vscode_threadid = (nextVSCodeThreadId += 1);
// the (Java) name of the thread
this.name = null;
this.name = name;
// the thread break info
this.paused = null;
// the timeout during a step which, if it expires, we allow other threads to break
@@ -28,102 +83,101 @@ class AndroidThread {
return new Error(`Thread ${this.vscode_threadid} not suspended`);
}
addStackFrameVariable(frame, level) {
if (!this.paused) throw this.threadNotSuspendedError();
var frameId = (this.vscode_threadid * 1e9) + (level * 1e6);
var stack_frame_var = {
frame, frameId,
locals: null,
}
return this.paused.stack_frame_vars[frameId] = stack_frame_var;
}
allocateExceptionScopeReference(frameId) {
if (!this.paused) return;
if (!this.paused.last_exception) return;
this.paused.last_exception.frameId = frameId;
this.paused.last_exception.scopeRef = frameId + 1;
}
getVariables(variablesReference) {
if (!this.paused)
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
// is this reference a stack frame
var stack_frame_var = this.paused.stack_frame_vars[variablesReference];
if (stack_frame_var) {
// frame locals request
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(varref));
}
// is this refrence an exception scope
if (this.paused.last_exception && variablesReference === this.paused.last_exception.scopeRef) {
var stack_frame_var = this.paused.stack_frame_vars[this.paused.last_exception.frameId];
return this._ensureLocals(stack_frame_var).then(varref => this.paused.stack_frame_vars[varref].locals.getVariables(this.paused.last_exception.scopeRef));
}
// work out which stack frame this reference is for
var frameId = Math.trunc(variablesReference/1e6) * 1e6;
var stack_frame_var = this.paused.stack_frame_vars[frameId];
return stack_frame_var.locals.getVariables(variablesReference);
}
_ensureLocals(varinfo) {
if (!this.paused)
return $.Deferred().rejectWith(this, [this.threadNotSuspendedError()]);
// evaluate can call this using frameId as the argument
if (typeof varinfo === 'number')
return this._ensureLocals(this.paused.stack_frame_vars[varinfo]);
// if we're currently processing it (or we've finished), just return the promise
if (this.paused.locals_done[varinfo.frameId])
return this.paused.locals_done[varinfo.frameId];
// create a new promise
var def = this.paused.locals_done[varinfo.frameId] = $.Deferred();
this.dbgr.getlocals(this.threadid, varinfo.frame, {def:def,varinfo:varinfo})
.then((locals,x) => {
// make sure we are still paused...
if (!this.paused)
/**
* @param {DebuggerFrameInfo} frame
* @param {number} call_stack_level
*/
createStackFrameVariable(frame, call_stack_level) {
if (!this.paused) {
throw this.threadNotSuspendedError();
// sort the locals by name, except for 'this' which always goes first
locals.sort((a,b) => {
if (a.name === b.name) return 0;
if (a.name === 'this') return -1;
if (b.name === 'this') return +1;
return a.name.localeCompare(b.name);
})
// create a new local variable with the results and resolve the promise
var varinfo = x.varinfo;
varinfo.cached = locals;
x.varinfo.locals = new AndroidVariables(this.session, x.varinfo.frameId + 2); // 0 = stack frame, 1 = exception, 2... others
x.varinfo.locals.setVariable(varinfo.frameId, varinfo);
var last_exception = this.paused.last_exception;
if (last_exception) {
x.varinfo.locals.setVariable(last_exception.scopeRef, last_exception);
}
const frameId = AndroidThread.makeFrameVariableReference(this.vscode_threadid, call_stack_level) ;
const stack_frame = new DebuggerStackFrame(this.dbgr, frame, frameId);
this.paused.stack_frames.set(frameId, stack_frame);
return stack_frame;
}
x.def.resolveWith(this, [varinfo.frameId]);
})
.fail(e => {
x.def.rejectWith(this, [e]);
})
return def;
/**
* Retrieve the variable manager used to maintain variableReferences for
* expressions evaluated in the global context for this thread.
*/
getGlobalVariableManager() {
if (!this.paused) {
throw this.threadNotSuspendedError();
}
if (!this.paused.global_vars) {
const globalFrameId = AndroidThread.makeGlobalVariableReference(this.vscode_threadid) ;
this.paused.global_vars = new VariableManager(globalFrameId);
}
return this.paused.global_vars;
}
setVariableValue(args) {
var frameId = Math.trunc(args.variablesReference/1e6) * 1e6;
var stack_frame_var = this.paused.stack_frame_vars[frameId];
return this._ensureLocals(stack_frame_var).then(varref => {
return this.paused.stack_frame_vars[varref].locals.setVariableValue(args);
});
/**
* set a new VSCode thread ID for this thread
*/
allocateNewThreadID() {
this.vscode_threadid = (nextVSCodeThreadId += 1);
}
clearStepTimeout() {
if (this.stepTimeout) {
clearTimeout(this.stepTimeout);
this.stepTimeout = null;
}
}
exports.AndroidThread = AndroidThread;
/**
* @param {VSCVariableReference} variablesReference
*/
findStackFrame(variablesReference) {
if (!this.paused) {
return null;
}
const stack_frame_ref = AndroidThread.variableRefToFrameId(variablesReference);
return this.paused.stack_frames.get(stack_frame_ref);
}
/**
* @param {string} reason
* @param {SourceLocation} location
* @param {DebuggerException} last_exception
*/
setPaused(reason, location, last_exception) {
this.paused = new ThreadPauseInfo(reason, location, last_exception);
this.clearStepTimeout();
}
/**
* @param {VSCThreadID} vscode_threadid
* @param {number} call_stack_level
* @returns {VSCVariableReference}
*/
static makeFrameVariableReference(vscode_threadid, call_stack_level) {
return (vscode_threadid * var_ref_thread_scale) + (call_stack_level * var_ref_frame_scale)
}
static makeGlobalVariableReference(vscode_threadid) {
return (vscode_threadid * var_ref_thread_scale) + var_ref_global_frame;
}
/**
* Convert a variable reference ID to a VSCode thread ID
* @param {VSCVariableReference} variablesReference
*/
static variableRefToThreadId(variablesReference) {
return Math.trunc(variablesReference / var_ref_thread_scale);
}
/**
* Convert a variable reference ID to a frame ID
* @param {VSCVariableReference} variablesReference
*/
static variableRefToFrameId(variablesReference) {
return Math.trunc(variablesReference / var_ref_frame_scale) * var_ref_frame_scale;
}
}
module.exports = {
AndroidThread,
}

View File

@@ -1,424 +0,0 @@
const D = function(){};// require('./util').D;
var transport_list = [];
var next_connect_device_service_id = 1;
var open_device_service = exports.open_device_service = function(t, fd, service, cb) {
D('open_device_service %s on device %s', service, t.serial);
var p = get_apacket();
p.msg.command = A_OPEN;
p.msg.arg0 = ++next_connect_device_service_id;
p.msg.data_length = service.length+1;
p.data.set(str2u8arr(service));
var serviceinfo = {
service: service,
transport: t,
localid: p.msg.arg0,
remoteid: 0,
state: 'init',
nextokay:null,
nextwrte:null,
nextclse:on_device_close_reply,
clientfd: fd,
isjdwp: /^jdwp\:\d+/.test(service),
islogcat: /^(shell:)?logcat/.test(service),
};
t.open_services.push(serviceinfo);
serviceinfo.nextokay = on_device_open_okay;
serviceinfo.state = 'talking';
send_packet(p, t, function(err) {
if (err) {
serviceinfo.state = 'init-error';
remove_device_service(serviceinfo);
return cb(err);
}
});
function ignore_response(err, p, serviceinfo, receivecb) {
D('ignore_response, p=%o', p);
receivecb();
}
function on_device_open_okay(err, p, serviceinfo, receivecb) {
D('on_device_open_okay: %s, err:%o', serviceinfo.service, err);
if (err) {
receivecb();
cb(err);
return;
}
serviceinfo.state = 'ready';
serviceinfo.nextokay = ignore_response;
serviceinfo.nextwrte = on_device_write_reply;
// ack the packet receive callback
receivecb();
// ack the open_device_service callback
cb(null, serviceinfo);
// start reading from the client
read_from_client(serviceinfo);
}
function read_from_client(serviceinfo) {
D('Waiting for client data');
serviceinfo.clientfd.readbytes(function(err, data) {
if (err) {
// read error - the client probably closed the connection
send_close_device_service(serviceinfo, function(err) {
remove_device_service(serviceinfo);
});
return;
}
D('client WRTE %d bytes to device', data.byteLength);
// send the data to the device
var p = get_apacket();
p.msg.command = A_WRTE;
p.msg.arg0 = serviceinfo.localid;
p.msg.arg1 = serviceinfo.remoteid;
p.msg.data_length = data.byteLength;
p.data.set(data);
if (serviceinfo.isjdwp)
print_jdwp_data('out',data);
serviceinfo.nextokay = function(err, p, serviceinfo, receivecb) {
if (err) {
// if we fail to write, just abort
remove_device_service(serviceinfo);
receivecb();
return;
}
D('client WRTE - got OKAY');
serviceinfo.nextokay = ignore_response;
receivecb();
// read and send more
read_from_client(serviceinfo);
}
send_packet(p, t, function(err) {
if (err) {
// if we fail to write, just abort
remove_device_service(serviceinfo);
return;
}
// we must wait until the next OKAY until we can write more
D('client WRTE - waiting for OKAY');
});
});
}
function on_device_write_reply(err, p, serviceinfo, receivecb) {
D('device WRTE received');
if (err) {
serviceinfo.state = 'write reply error';
remove_device_service(serviceinfo);
receivecb();
return;
};
// when we receive a WRTE, we must reply with an OKAY as the very next packet.
// - we can't wait for the data to be forwarded because the reader might post
// something in between
D('sending OKAY');
send_ready(serviceinfo.localid, serviceinfo.remoteid, serviceinfo.transport, function(err){
if (err) {
serviceinfo.state = 'write okay error';
remove_device_service(serviceinfo);
return;
}
D('sent OKAY');
});
if (serviceinfo.isjdwp)
print_jdwp_data('dev', p.data);
// write the data to the client
serviceinfo.clientfd.writebytes(new Uint8Array(p.data.buffer.slice(0, p.msg.data_length)), function(err) {
// ack the packet receive callback
receivecb();
});
}
function on_device_close_reply(err, p, serviceinfo, receivecb) {
var t = serviceinfo.transport;
D('on_device_close_reply %s (by device) on device %s', serviceinfo.service, t.serial);
serviceinfo.state = 'closed (by device)';
remove_device_service(serviceinfo);
// ack the packet receive callback
receivecb();
}
}
var find_open_device_service = exports.find_open_device_service = function(t, localid, remoteid) {
for (var i=0; i < t.open_services.length; i++) {
var s = t.open_services[i];
if (s.localid === localid && (!remoteid ||(s.remoteid === remoteid))) {
return s;
}
}
return null;
}
var send_close_device_service = exports.send_close_device_service = function(serviceinfo, cb) {
D('send_close_device_service: %s, device:%s', serviceinfo.service, serviceinfo.transport.serial);
var p = get_apacket();
p.msg.command = A_CLSE;
p.msg.arg0 = serviceinfo.localid;
p.msg.arg1 = serviceinfo.remoteid;
serviceinfo.nextreply = on_close_request_reply;
serviceinfo.state = 'talking';
send_packet(p, serviceinfo.transport, function(err) {
if (err) {
serviceinfo.state = 'error';
} else {
serviceinfo.state = 'closed';
}
// ack the close_device_service request as soon as we
// send the packet - don't wait for the reply
return cb(err);
});
function on_close_request_reply(which, serviceinfo, receivecb) {
// ack the packet receive callback
receivecb();
}
}
var remove_device_service = exports.remove_device_service = function(serviceinfo) {
var fd;
if (fd=serviceinfo.clientfd) {
serviceinfo.clientfd=null;
fd.close();
}
remove_from_list(serviceinfo.transport.open_services, serviceinfo);
}
var register_transport = exports.register_transport = function(t, cb) {
t.terminated = false;
t.open_services = [];
transport_list.push(t);
// start the reader
function read_next_packet_from_transport(t, packetcount) {
var p = new_apacket();
t.read_from_remote(p, t, function(err, p) {
if (t.terminated) {
return;
}
if (err) {
D('Error reading next packet from transport:%s - terminating.', t.serial);
kick_transport(t);
unregister_transport(t);
return;
}
p.which = intToCharString(p.msg.command);
D('Read packet:%d (%s) from transport:%s', packetcount, p.which, t.serial);
var pc = packetcount++;
handle_packet(p, t, function(err) {
D('packet:%d handled, err:%o', pc, err);
read_next_packet_from_transport(t, packetcount);
});
});
}
read_next_packet_from_transport(t, 0);
D("transport: %s registered\n", t.serial);
D('new transport list: %o', transport_list.slice());
update_transports();
ui.update_device_property(t.deviceinfo, 'status', 'Connecting...');
send_connect(t, cb);
}
var unregister_transport = exports.unregister_transport = function(t) {
if (t.fd)
t.fd.close();
// kill any connected services
while (t.open_services.length) {
remove_device_service(t.open_services.pop());
}
remove_from_list(transport_list, t);
D("transport: %s unregistered\n", t.serial);
D('remaining transports: %o', transport_list.slice());
t.serial = 'REMOVED:' + t.serial;
t.terminated = true;
update_transports();
ui.update_device_property(t.deviceinfo, 'status', 'Disconnected', '#8B0E0E');
ui.remove_disconnected_device(t.deviceinfo);
}
var kick_transport = exports.kick_transport = function(t) {
if (t && !t.kicked) {
t.kicked = true;
t.kick(t);
}
}
var write_packet_to_transport = exports.write_packet_to_transport = function(t, p, cb) {
if (t.terminated) {
D('Refusing to write packet to terminated transport: %s', t.serial);
return cb({msg:'device not found'});
}
t.write_to_remote(p, t, function(err) {
cb(err);
});
}
var send_packet = exports.send_packet = function(p, t, cb) {
p.msg.magic = p.msg.command ^ 0xffffffff;
var count = p.msg.data_length;
var x = new Uint8Array(p.data);
var sum = 0, i=0;
while(count-- > 0){
sum += x[i++];
}
p.msg.data_check = sum;
write_packet_to_transport(t, p, cb);
}
var acquire_one_transport = exports.acquire_one_transport = function(connection_state, transport_type, serial) {
var candidates = [];
for (var i=0, tl=transport_list; i < tl.length; i++) {
if (connection_state !== 'CS_ANY' && tl[i].connection_state !== connection_state)
continue;
if (transport_type !== 'kTransportAny' && tl[i].transport_type !== transport_type)
continue;
if (serial && tl[i].serial !== serial)
continue;
candidates.push(tl[i]);
}
return candidates;
}
var statename = exports.statename = function(t) {
if (/^CS_.+/.test(t.connection_state))
return t.connection_state.slice(3).toLowerCase();
return 'unknown state: ' + t.connection_state;
}
var typename = exports.typename = function(t) {
if (/^kTransport.+/.test(t.type))
return t.type.slice(10).toLowerCase();
return 'unknown type: ' + t.type;
}
var format_transport = exports.format_transport = function(t, format) {
var serial = t.serial || '???????????';
if (!format) {
return serial+'\t'+statename(t);
} else if (format === 'extended') {
return '{'+[
'"device":'+JSON.stringify(t.device),
'"model":'+JSON.stringify(t.model||t.deviceinfo.productName),
'"product":'+JSON.stringify(t.product),
'"serial":'+JSON.stringify(serial),
'"status":'+JSON.stringify(statename(t)),
'"type":'+JSON.stringify(typename(t)),
].join(',') + '}';
} else {
return [
serial+'\t'+statename(t),
t.devpath||'',
t.product?'product:'+t.product.replace(/\s+/,'_'):'',
t.model?'model:'+t.model.replace(/\s+/,'_'):'',
t.device?'device:'+t.device.replace(/\s+/,'_'):''
].join(' ');
}
}
var list_transports = exports.list_transports = function(format) {
return transport_list.map(function(t) {
return format_transport(t, format);
}).join('\n')+'\n';
}
var update_transports = exports.update_transports = function() {
write_transports_to_trackers(_device_trackers.normal);
write_transports_to_trackers(_device_trackers.extended, null, true);
}
var readx_with_data = exports.readx_with_data = function(fd, cb) {
readx(fd, 4, function(err, buf) {
if (err) return cb(err);
var dlen = buf.intFromHex();
if (dlen < 0 || dlen > 0xffff)
return cb({msg:'Invalid data len: ' + dlen});
readx(fd, dlen, function(err, buf) {
if (err) return cb(err);
return cb(null, buf);
});
});
}
var readx = exports.readx = function(fd, len, cb) {
D('readx: fd:%o wanted=%d', fd, len);
fd.readbytes(len, function(err, buf) {
if (err) return cb(err);
cb(err, buf);
});
}
var writex = exports.writex = function(fd, bytes, len) {
if (typeof(bytes) === 'string') {
var buf = new Uint8Array(bytes.length);
for (var i=0; i < bytes.length; i++)
buf[i] = bytes.charCodeAt(i);
bytes = buf;
}
if (typeof(len) !== 'number')
len = bytes.byteLength;
D('writex: fd:%o writing=%d', fd, len);
fd.writebytes(bytes.subarray(0,len));
}
var writex_with_data = exports.writex_with_data = function(fd, data, len) {
if (typeof(len) === 'undefined');
len = data.byteLength||data.length||0;
writex(fd, intToHex(len, 4));
writex(fd, data, len);
}
var _device_trackers = {
normal:[],
extended:[],
}
var add_device_tracker = exports.add_device_tracker = function(fd, extended) {
_device_trackers[extended?'extended':'normal'].push(fd);
write_transports_to_trackers([fd], null, extended);
readtracker(fd, extended);
D('Device tracker added. Trackers: %o', _device_trackers);
function readtracker(fd, extended) {
chrome.socket.read(fd.n, function(readInfo) {
if (chrome.runtime.lastError || readInfo.resultCode < 0) {
remove_from_list(_device_trackers[extended?'extended':'normal'], fd);
D('Device tracker socket read failed - closing. Trackers: %o', _device_trackers);
fd.close();
return;
}
D('Ignoring data read from device tracker socket');
readtracker(fd, extended);
});
}
}
var write_transports_to_trackers = exports.write_transports_to_trackers = function(fds, transports, extended) {
if (!fds || !fds.length)
return;
if (!transports) {
return write_transports_to_trackers(fds, list_transports(extended?'extended':''), extended);
}
D('Writing transports: %s', transports);
fds.slice().forEach(function(fd) {
writex_with_data(fd, str2u8arr(transports));
});
}

View File

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

53
src/utils/char-decode.js Normal file
View File

@@ -0,0 +1,53 @@
const BACKSLASH_ESCAPE_MAP = {
b: '\b',
f: '\f',
r: '\r',
n: '\n',
t: '\t',
v: '\v',
'0': '\0',
'\\': '\\',
};
/**
* De-escape backslash escaped characters
* @param {string} c
*/
function decode_char(c) {
switch(true) {
case /^\\u[0-9a-fA-F]{4}$/.test(c):
// unicode escape
return String.fromCharCode(parseInt(c.slice(2),16));
case /^\\.$/.test(c):
// backslash escape
const char = BACKSLASH_ESCAPE_MAP[c[1]];
return char || c[1];
case c.length === 1:
return c;
}
throw new Error('Invalid character value');
}
/**
* Convert a Java string literal to a raw string
* @param {string} s
*/
function decodeJavaStringLiteral(s) {
return s.slice(1, -1).replace(/\\u[0-9a-fA-F]{4}|\\./g, decode_char);
}
/**
* Convert a Java char literal to a raw character
* @param {string} s
*/
function decodeJavaCharLiteral(s) {
return decode_char(s.slice(1, -1));
}
module.exports = {
decode_char,
decodeJavaCharLiteral,
decodeJavaStringLiteral,
}

View File

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

51
src/utils/print.js Normal file
View File

@@ -0,0 +1,51 @@
/**
* Set of callbacks to be called when any message is output to the console
* @type {Set<Function>}
* */
const messagePrintCallbacks = new Set();
function callMessagePrintCallbacks(args) {
messagePrintCallbacks.forEach(cb => cb(...args));
}
/**
* print a debug message to the console
* @param {...any} args
*/
function D(...args) {
console.log(...args);
callMessagePrintCallbacks(args);
}
/**
* print an error message to the console
* @param {...any} args
*/
function E(...args) {
console.error(...args);
callMessagePrintCallbacks(args);
}
/**
* print a warning message to the console
* @param {...any} args
*/
function W(...args) {
console.warn(...args);
callMessagePrintCallbacks(args);
}
/**
* Adds a callback to be called when any message is output
* @param {Function} cb
*/
function onMessagePrint(cb) {
messagePrintCallbacks.add(cb);
}
module.exports = {
D,
E,
W,
onMessagePrint,
}

21
src/utils/source-file.js Normal file
View File

@@ -0,0 +1,21 @@
/**
* Returns true if the string has an extension we recognise as a source file
* @param {string} s
*/
function hasValidSourceFileExtension(s) {
return /\.(java|kt)$/i.test(s);
}
function splitSourcePath(filepath) {
const m = filepath.match(/^\/([^/]+(?:\/[^/]+)*)?\/([^./]+)\.(java|kt)$/);
return {
pkg: m[1].replace(/\/+/g, '.'),
type: m[2],
qtype: `${m[1]}/${m[2]}`,
}
}
module.exports = {
hasValidSourceFileExtension,
splitSourcePath,
}

11
src/utils/thread.js Normal file
View File

@@ -0,0 +1,11 @@
/**
* Returns a Promise which resolves after the specified period.
* @param {number} ms wait time in milliseconds
*/
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
module.exports = {
sleep,
}

166
src/variable-manager.js Normal file
View File

@@ -0,0 +1,166 @@
const { DebuggerValue, JavaType, VariableValue } = require('./debugger-types');
const { NumberBaseConverter } = require('./utils/nbc');
/**
* Class to manage variable references used by VS code.
*
* This class is primarily used to manage references to variables created in stack frames, but is
* also used in 'standalone' mode for repl expressions evaluated in the global context.
*/
class VariableManager {
/**
* @param {VSCVariableReference} base_variable_reference The reference value for values stored by this manager
*/
constructor(base_variable_reference) {
// expandable variables get allocated new variable references.
this._expandable_prims = false;
/** @type {VSCVariableReference} */
this.nextVariableRef = base_variable_reference + 10;
/** @type {Map<VSCVariableReference,*>} */
this.variableValues = new Map();
/** @type {Map<JavaObjectID,VSCVariableReference>} */
this.objIdCache = new Map();
}
_addVariable(varinfo) {
varinfo.varref = this.nextVariableRef += 1;
this._setVariable(varinfo.varref, varinfo)
return varinfo.varref;
}
/**
*
* @param {VSCVariableReference} variablesReference
* @param {*} value
*/
_setVariable(variablesReference, value) {
this.variableValues.set(variablesReference, value);
}
_getObjectIdReference(type, objvalue) {
// we need the type signature because we must have different id's for
// an instance and it's supertype instance (which obviously have the same objvalue)
const key = type.signature + objvalue;
let value = this.objIdCache.get(key);
if (!value) {
this.objIdCache.set(key, value = this.nextVariableRef += 1);
}
return value;
}
/**
* Convert to a VariableValue object used by VSCode
* @param {DebuggerValue} v
*/
makeVariableValue(v) {
let varref = 0;
let value = '';
const evaluateName = v.fqname || v.name;
const formats = {};
const full_typename = v.type.fullyQualifiedName();
switch(true) {
case v.hasnullvalue && JavaType.isReference(v.type):
// null object or array type
value = 'null';
break;
case v.vtype === 'class':
value = full_typename;
break;
case v.type.signature === JavaType.Object.signature:
// Object doesn't really have anything worth seeing, so just treat it as unexpandable
value = v.type.typename;
break;
case v.type.signature === JavaType.String.signature:
value = JSON.stringify(v.string);
if (v.biglen) {
// since this is a big string - make it viewable on expand
varref = this._addVariable({
bigstring: v,
});
value = `String (length:${v.biglen})`;
}
else if (this._expandable_prims) {
// as a courtesy, allow strings to be expanded to see their length
varref = this._addVariable({
signature: v.type.signature,
primitive: true,
value: v.string.length
});
}
break;
case JavaType.isArray(v.type):
// non-null array type - if it's not zero-length add another variable reference so the user can expand
if (v.arraylen) {
varref = this._getObjectIdReference(v.type, v.value);
this._setVariable(varref, {
varref,
arrvar: v,
range:[0, v.arraylen],
});
}
value = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
break;
case JavaType.isClass(v.type):
// non-null object instance - add another variable reference so the user can expand
varref = this._getObjectIdReference(v.type, v.value);
this._setVariable(varref, {
varref,
objvar: v,
});
value = v.type.typename;
break;
case v.type.signature === JavaType.char.signature:
// character types have a integer value
const char = String.fromCodePoint(v.value);
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\','\0':'0'};
if (cmap[char]) {
value = `'\\${cmap[char]}'`;
} else if (v.value < 32) {
value = `'\\u${v.value.toString(16).padStart(4,'0')}'`;
} else value = `'${char}'`;
break;
case v.type.signature === JavaType.long.signature:
// because JS cannot handle 64bit ints, we need a bit of extra work
const v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
value = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
formats.oct = formats.bin = '';
// 24 bit chunks...
for (let s = v64hex; s; s = s.slice(0,-6)) {
const 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 JavaType.isInteger(v.type):
value = formats.dec = v.value.toString();
const 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: boolean, etc
value = v.value.toString();
break;
}
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
varref = this._addVariable({
signature: v.type.signature,
primitive: true,
value: v.value,
});
}
return new VariableValue(v.name, value, full_typename, varref, evaluateName);
}
}
module.exports = {
VariableManager,
}

View File

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

View File

@@ -1,440 +0,0 @@
const WebSocketServer = require('./minwebsocket').WebSocketServer;
const { atob, btoa, ab2str, str2u8arr, arrayBufferToString, intFromHex, intToHex, D,E,W, get_file_fd_from_fdn } = require('./util');
const { connect_forward_listener } = require('./services');
const { get_socket_fd_from_fdn, socket_loopback_client } = require('./sockets');
const { readx, writex } = require('./transport');
var dprintfln = ()=>{};//D;
WebSocketServer.DEFAULT_ADB_PORT = 5037;
var proxy = {
Server: function(port, adbport) {
// Listen for websocket connections.
var wsServer = new WebSocketServer(port);
wsServer.adbport = adbport;
wsServer.setADBPort = function(port) {
if (typeof(port) === 'undefined')
return this.adbport = WebSocketServer.DEFAULT_ADB_PORT;
return this.adbport = port;
}
// A list of connected websockets.
var connectedSockets = [];
function indexof_connected_socket(socketinfo) {
if (!socketinfo) return -1;
for (var i=0; i < connectedSockets.length; i++)
if (connectedSockets[i] === socketinfo)
return i;
return -1;
}
wsServer.onconnection = function(req) {
var ws = req.accept();
var si = {
wsServer: wsServer,
ws: ws,
fn: check_client_version,
fdarr: [],
};
connectedSockets.push(si);
ws.onmessage = function(e) {
si.fn(si, e);
};
// When a socket is closed, remove it from the list of connected sockets.
ws.onclose = function() {
while (si.fdarr.length) {
si.fdarr.pop().close();
}
var idx = indexof_connected_socket(si);
if (idx>=0) connectedSockets.splice(idx, 1);
else D('Cannot find disconnected socket in connectedSockets');
};
return true;
};
D('WebSocketServer started. Listening on port: %d', port);
return wsServer;
}
}
var check_client_version = function(si, e) {
if (e.data !== 'vscadb client version 1') {
D('Wrong client version: ', e.data);
return end_of_connection(si);
}
si.fn = handle_proxy_command;
si.ws.send('vscadb proxy version 1');
}
var end_of_connection = function(si) {
if (!si || !si.ws) return;
si.ws.close();
}
var handle_proxy_command = function(si, e) {
if (!e || !e.data || e.data.length<2) return end_of_connection(si);
var cmd = e.data.slice(0,2);
var fn = proxy_command_fns[cmd];
if (!fn) {
E('Unknown command: %s', e.data);
return end_of_connection(si);
}
fn(si, e);
}
function end_of_command(si, respfmt) {
if (!si || !si.ws || !respfmt) return;
// format the response - we allow %s, %d and %xX
var response = respfmt;
var fmtidx = 0;
for (var i=2; i < arguments.length; i++) {
var fmt = response.slice(fmtidx).match(/%([sdxX])/);
if (!fmt) break;
response = [response.slice(0,fmt.index),arguments[i],response.slice(fmt.index+2)];
switch(fmt[1]) {
case 'x': response[1] = response[1].toString(16).toLowerCase(); break;
case 'X': response[1] = response[1].toString(16).toUpperCase(); break;
}
response = response.join('');
fmtidx = fmt.index + arguments[i].length;
}
si.ws.send(response);
}
function readsckt(fd, n, cb) {
readx(fd, n, cb);
}
function write_adb_command(fd, cmd) {
dprintfln('write_adb_command: %s',cmd);
// write length in hex first
writex(fd, intToHex(cmd.length, 4));
// then the command
writex(fd, cmd);
}
function read_adb_status(adbfd, extra, cb) {
// read back the status
readsckt(adbfd, 4+extra, function(err, data) {
if (err) return cb();
var status = ab2str(data);
dprintfln("adb status: %s", status);
cb(status);
});
}
function read_adb_reply(adbfd, b64encode, cb) {
// read reply length
readsckt(adbfd, 4, function(err, data) {
if (err) return cb();
var n = intFromHex(ab2str(data));
dprintfln("adb expected reply: %d bytes", n);
// read reply
readsckt(adbfd, n, function(err, data) {
if (err) return cb();
var n = data.byteLength;
dprintfln("adb reply: %d bytes", n);
var response = ab2str(data);
if (n === 0) response = '\n'; // always send something
dprintfln("%s",response);
if (b64encode) response = btoa(response);
return cb(response);
});
});
}
const min_fd_num = 1000;
var fdn_to_fd = function(n) {
var fd;
if (n >= min_fd_num) fd = get_file_fd_from_fdn(n);
else fd = get_socket_fd_from_fdn(n);
if (!fd) throw new Error('Invalid file descriptor number: '+n);
return fd;
}
var retryread = function(fd, len, cb) {
fd.readbytes(len, cb);
}
var retryreadfill = function(fd, len, cb) {
var buf = new Uint8Array(len);
var totalread = 0;
var readmore = function(amount) {
fd.readbytes(amount, function(err, data) {
if (err) return cb(err);
buf.set(data, totalread);
totalread += data.byteLength;
var diff = len - totalread;
if (diff > 0) return readmore(diff);
cb(err, buf);
});
};
readmore(len);
}
var be2le = function(buf) {
var x = new Uint8Array(buf);
var a = x[0];
a = (a<<8)+x[1];
a = (a<<8)+x[2];
a = (a<<8)+x[3];
return a;
}
var jdwpReplyMonitor = function(fd, si, packets) {
if (!packets) {
packets = 0;
dprintfln("jdwpReplyMonitor thread started. jdwpfd:%d.", fd.n);
}
//dprintfln("WAITING FOR JDWP DATA....");
//int* pjdwpdatalen = (int*)&buffer[0];
//*pjdwpdatalen=0;
retryread(fd, 4, function(err, data) {
if (err) return terminate();
var m = data.byteLength;
if (m != 4) {
dprintfln("rj %d len read", m);
return terminate();
}
m = be2le(data.buffer.slice(0,4));
//dprintfln("STARTING JDWP DATA: %.8x....", m);
var lenstr = arrayBufferToString(data.buffer);
retryreadfill(fd, m-4, function(err, data) {
if (err) return terminate();
var n = data.byteLength + 4;
if (n != m) {
dprintfln("rj read incomplete %d/%d", (n+4),m);
return terminate();
}
//dprintfln("GOT JDWP DATA....");
dprintfln("rj encoding %d bytes", n);
var response = "rj ok ";
response += btoa(lenstr + arrayBufferToString(data.buffer));
si.ws.send(response);
//dprintfln("SENT JDWP REPLY....");
packets++;
jdwpReplyMonitor(fd, si, packets);
});
});
function terminate() {
// try and send a final event reply indicating the VM has disconnected
var vmdisconnect = [
0,0,0,17, // len
100,100,100,100, // id
0, //flags
0x40,0x64, // errcode = composite event
0, //suspend
0,0,0,1, // eventcount
100, // eventkind=VM_DISCONNECTED
];
var response = "rj ok ";
response += btoa(ab2str(new Uint8Array(vmdisconnect)));
si.ws.send(response);
dprintfln("jdwpReplyMonitor thread finished. Sent:%d packets.", packets);
}
}
var stdoutMonitor = function(fd, si, packets) {
if (!packets) {
packets = 0;
dprintfln("stdoutMonitor thread started. jdwpfd:%d, wsfd:%o.", fd.n, si);
}
retryread(fd, function(err, data) {
if (err) return terminate();
var response = 'so ok '+btoa(ab2str(new Uint8Array(data)));
si.ws.send(response);
packets++;
stdoutMonitor(fd, si, packets);
});
function terminate() {
// send a unique terminating string to indicate the stdout monitor has finished
var eoso = "eoso:d10d9798-1351-11e5-bdd9-5b316631f026";
var response = "so ok " + btoa(eoso);
si.ws.send(response);
dprintfln("stdoutMonitor thread finished. Sent:%d packets.", packets);
}
}
// commands are:
// cn - create adb socket
// cp <port> - create custom-port socket
// wa <fd> <base64cmd> - write_adb_command
// rs <fd> [extra] - read_adb_status
// ra <fd> - read_adb_reply
// rj <fd> - read jdwp-formatted reply
// rx <fd> <len> - read raw data from adb socket
// wx <fd> <base64data> - write raw data to adb socket
// dc <fd|all> - disconnect adb sockets
var proxy_command_fns = {
cn:function(si, e) {
// create adb socket
socket_loopback_client(si.wsServer.adbport, function(fd) {
if (!fd) {
return end_of_command(si, 'cn error connection failed');
}
si.fdarr.push(fd);
return end_of_command(si, 'cn ok %d', fd.n);
});
},
cp:function(si, e) {
var x = e.data.split(' '), port;
port = parseInt(x[1], 10);
connect_forward_listener(port, {create:true}, function(sfd) {
return end_of_command(si, 'cp ok %d', sfd.n);
});
},
wa:function(si, e) {
var x = e.data.split(' '), fd, buffer;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
buffer = atob(x[2]);
} catch(err) {
return end_of_command(si, 'wa error wrong parameters');
}
write_adb_command(fd, buffer);
return end_of_command(si, 'wa ok');
},
// rs fd [extra]
rs:function(si, e) {
var x = e.data.split(' '), fd, extra;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
// optional additional bytes - used for sync-responses which
// send status+length as 8 bytes
extra = parseInt(atob(x[2]||'MA=='));
} catch(err) {
return end_of_command(si, 'rs error wrong parameters');
}
read_adb_status(fd, extra, function(status) {
return end_of_command(si, 'rs ok %s', status||'');
})
},
ra:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'ra error wrong parameters');
}
read_adb_reply(fd, true, function(b64adbreply) {
if (!b64adbreply) {
return end_of_command('ra error read failed');
}
return end_of_command(si, 'ra ok %s', b64adbreply);
});
},
rj:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'rj error wrong parameters');
}
jdwpReplyMonitor(fd, si);
return end_of_command(si, 'rj ok');
},
rx:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'rx error wrong parameters');
}
if (fd.isSocket) {
fd.readbytes(doneread);
} else {
fd.readbytes(fd.readpipe.byteLength, doneread);
}
function doneread(err, data) {
if (err) {
return end_of_command(si, 'rx ok nomore');
}
end_of_command(si, 'rx ok ' + btoa(ab2str(data)));
}
},
so:function(si, e) {
var x = e.data.split(' '), fd;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
} catch(err) {
return end_of_command(si, 'so error wrong parameters');
}
stdoutMonitor(fd, si);
return end_of_command(si, 'so ok');
},
wx:function(si, e) {
var x = e.data.split(' '), fd, buffer;
try {
var fdn = parseInt(x[1], 10);
fd = fdn_to_fd(fdn);
buffer = atob(x[2]);
} catch(err) {
return end_of_command(si, 'wx error wrong parameters');
}
fd.writebytes(str2u8arr(buffer), function(err) {
if (err)
return end_of_command(si, 'wx error device write failed');
end_of_command(si, 'wx ok');
});
},
dc:function(si, e) {
var x = e.data.split(' ');
if (x[1] === 'all') {
while (si.fdarr.length) {
si.fdarr.pop().close();
}
return end_of_command(si, 'dc ok');
}
var n = parseInt(x[1],10);
for (var i=0; i < si.fdarr.length; i++) {
if (si.fdarr[i].n === n) {
var fd = si.fdarr.splice(i,1)[0];
fd.close();
break;
}
}
return end_of_command(si, 'dc ok');
}
}
exports.proxy = proxy;

View File

@@ -6,12 +6,12 @@
//
// The module 'assert' provides assertion methods from node
var assert = require('assert');
const assert = require('assert');
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
var vscode = require('vscode');
var myExtension = require('../extension');
//const vscode = require('vscode');
//const myExtension = require('../extension');
// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests", function() {

View File

@@ -10,7 +10,7 @@
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
var testRunner = require('vscode/lib/testrunner');
const testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info