mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 09:29:38 +00:00
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:
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
26
extension.js
26
extension.js
@@ -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
2107
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
951
src/adbclient.js
951
src/adbclient.js
@@ -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;
|
||||
|
||||
@@ -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
145
src/apk-file-info.js
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
1424
src/debugMain.js
1424
src/debugMain.js
File diff suppressed because it is too large
Load Diff
772
src/debugger-types.js
Normal file
772
src/debugger-types.js
Normal 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,
|
||||
}
|
||||
3004
src/debugger.js
3004
src/debugger.js
File diff suppressed because it is too large
Load Diff
109
src/expression/assign.js
Normal file
109
src/expression/assign.js
Normal 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
983
src/expression/evaluate.js
Normal 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
323
src/expression/parse.js
Normal 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,
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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
121
src/index.d.js
Normal 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
|
||||
*
|
||||
*/
|
||||
1320
src/jdwp.js
1320
src/jdwp.js
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"target": "es2018",
|
||||
"checkJs": true,
|
||||
"lib": [
|
||||
"es6"
|
||||
"es2018"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
307
src/logcat.js
307
src/logcat.js
@@ -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 => ({ '&': '&', '"': '"', "'": ''', '<': '<', '>': '>' }[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
95
src/manifest.js
Normal 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,
|
||||
}
|
||||
@@ -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
92
src/package-searcher.js
Normal 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
|
||||
}
|
||||
322
src/services.js
322
src/services.js
@@ -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);
|
||||
}
|
||||
}
|
||||
290
src/sockets.js
290
src/sockets.js
@@ -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
143
src/sockets/adbsocket.js
Normal 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;
|
||||
159
src/sockets/androidsocket.js
Normal file
159
src/sockets/androidsocket.js
Normal 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
122
src/sockets/jdwpsocket.js
Normal 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
276
src/stack-frame.js
Normal 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,
|
||||
}
|
||||
254
src/threads.js
254
src/threads.js
@@ -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,
|
||||
}
|
||||
|
||||
424
src/transport.js
424
src/transport.js
@@ -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));
|
||||
});
|
||||
}
|
||||
635
src/util.js
635
src/util.js
@@ -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
53
src/utils/char-decode.js
Normal 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,
|
||||
}
|
||||
@@ -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
51
src/utils/print.js
Normal 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
21
src/utils/source-file.js
Normal 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
11
src/utils/thread.js
Normal 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
166
src/variable-manager.js
Normal 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,
|
||||
}
|
||||
410
src/variables.js
410
src/variables.js
@@ -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;
|
||||
440
src/wsproxy.js
440
src/wsproxy.js
@@ -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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user