mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-22 17:39:19 +00:00
Relocate code into src folder
This commit is contained in:
803
src/adbclient.js
Normal file
803
src/adbclient.js
Normal 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;
|
||||
190
src/chrome-polyfill.js
Normal file
190
src/chrome-polyfill.js
Normal file
@@ -0,0 +1,190 @@
|
||||
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:[],
|
||||
get lastError() { return this._lastError.pop() },
|
||||
set lastError(e) { this._lastError.push(e) }
|
||||
},
|
||||
permissions: {
|
||||
request(usbPermissions, cb) {
|
||||
process.nextTick(cb, true);
|
||||
}
|
||||
},
|
||||
socket: {
|
||||
listen(socketId, host, port, max_connections, cb) {
|
||||
var result = 0;
|
||||
var s = sockets_by_id[socketId];
|
||||
s._raw.listen(port, host, max_connections);
|
||||
process.nextTick(cb, result);
|
||||
},
|
||||
connect(socketId, host, port, cb) {
|
||||
var s = sockets_by_id[socketId];
|
||||
s._raw.connect({port:port,host:host}, function(){
|
||||
this.s.onerror = null;
|
||||
this.cb.call(null,0);
|
||||
}.bind({s:s,cb:cb}));
|
||||
s.onerror = function(e) {
|
||||
this.s.onerror = null;
|
||||
chrome.runtime.lastError = e;
|
||||
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, 1);
|
||||
},
|
||||
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) {
|
||||
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);
|
||||
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,
|
||||
};
|
||||
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) {
|
||||
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;
|
||||
1135
src/debugMain.js
Normal file
1135
src/debugMain.js
Normal file
File diff suppressed because it is too large
Load Diff
1480
src/debugger.js
Normal file
1480
src/debugger.js
Normal file
File diff suppressed because it is too large
Load Diff
1056
src/jdwp.js
Normal file
1056
src/jdwp.js
Normal file
File diff suppressed because it is too large
Load Diff
124
src/jq-promise.js
Normal file
124
src/jq-promise.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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;
|
||||
},
|
||||
fail(fn) {
|
||||
var faildef = $.Deferred(null, this);
|
||||
var p = this._promise.catch(function(a) {
|
||||
if (a.stack) {
|
||||
console.error(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;
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
12
src/jsconfig.json
Normal file
12
src/jsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"es6"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
122
src/minwebsocket.js
Normal file
122
src/minwebsocket.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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;
|
||||
322
src/services.js
Normal file
322
src/services.js
Normal file
@@ -0,0 +1,322 @@
|
||||
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
Normal file
290
src/sockets.js
Normal file
@@ -0,0 +1,290 @@
|
||||
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];
|
||||
}
|
||||
424
src/transport.js
Normal file
424
src/transport.js
Normal file
@@ -0,0 +1,424 @@
|
||||
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));
|
||||
});
|
||||
}
|
||||
631
src/util.js
Normal file
631
src/util.js
Normal file
@@ -0,0 +1,631 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
var nofn=function(){};
|
||||
var D=exports.D=console.log.bind(console);
|
||||
var E=exports.E=console.error.bind(console);
|
||||
var W=exports.W=console.warn.bind(console);
|
||||
var DD=nofn,cl=D,printf=D;
|
||||
var print_jdwp_data = nofn;// _print_jdwp_data;
|
||||
var print_packet = nofn;//_print_packet;
|
||||
|
||||
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 new Buffer(arr,'binary').toString('base64');
|
||||
}
|
||||
|
||||
exports.atob = function(base64) {
|
||||
return new Buffer(base64, 'base64').toString('binary');
|
||||
}
|
||||
440
src/wsproxy.js
Normal file
440
src/wsproxy.js
Normal file
@@ -0,0 +1,440 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user