android-dev-ext initial commit

This commit is contained in:
adelphes
2017-01-22 15:40:33 +00:00
commit 82a5a8c414
25 changed files with 7361 additions and 0 deletions

803
adbclient.js Normal file
View File

@@ -0,0 +1,803 @@
/*
ADBClient: class to manage connection and commands to adb (via the Dex plugin) running on the local machine.
*/
const _JDWP = require('./jdwp')._JDWP;
const $ = require('./jq-promise');
const WebSocket = require('./minwebsocket').WebSocketClient;
const { atob,btoa,D } = require('./util');
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/);
lines.sort();
var devicelist = [];
var i=0;
if (extended) {
for (var i=0; i < lines.length; i++) {
try {
var m = JSON.parse(lines[i]);
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]+)/);
if (!m) continue;
devicelist.push({
serial: m[1],
status: m[2],
num:i,
});
}
}
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();
});
},
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: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'});
});
}
// 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
// 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]);
});
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 x.deferred;
},
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;
},
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;
},
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;
},
logcat : function(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) {
// 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]);
});
}
// start the logcat monitor
return this.dexcmd('so', this.fd)
.then(function() {
this.logcatinfo = {
deferred: x.deferred,
buffer: '',
onlog: o.onlog||$.noop,
onlogdata: o.data,
onclose: o.onclose||$.noop,
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;
try {
ws = new WebSocket('ws://127.0.0.1:'+port);
} 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);
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]);
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();
},
};
exports.ADBClient = ADBClient;