mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-25 10:58:42 +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:
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;
|
||||
Reference in New Issue
Block a user