android-dev-ext initial commit

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

23
.eslintrc.json Normal file
View File

@@ -0,0 +1,23 @@
{
"env": {
"browser": false,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"rules": {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn"
}
}

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint"
]
}

36
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,36 @@
// A launch configuration that launches the extension inside a new window
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false
},
{
"name": "Server",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/debugMain.js",
"args": [ "--server=4711" ]
},
{
"name": "Launch Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ],
"stopOnEntry": false
}
],
"compounds": [
{
"name": "Extension + Server",
"configurations": [ "Launch Extension", "Server" ]
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
// Place your settings in this file to overwrite default and user settings.
{
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
}

7
.vscodeignore Normal file
View File

@@ -0,0 +1,7 @@
.vscode/**
.vscode-test/**
test/**
.gitignore
jsconfig.json
vsc-extension-quickstart.md
.eslintrc.json

11
CHANGELOG.md Normal file
View File

@@ -0,0 +1,11 @@
# Change Log
## version 0.1.0
Initial release
* Support for deploying, launching and debugging Apps via ADB
* Single step debugging (step in, step over, step out)
* Local variable evaluation
* Simple watch expressions
* Breakpoints
* Large array chunking (performance)
* Stale build detection

53
README.md Normal file
View File

@@ -0,0 +1,53 @@
# Android for VS Code
This is a preview version of the Android for VS Code Extension. The extension allows developers to install, launch and debug Android Apps from
within the VS Code environment.
## Requirements
You must have [Android SDK Tools](https://developer.android.com/studio/releases/sdk-tools.html) installed. This extension communicates with your device via the ADB (Android Debug Bridge) interface.
> You are not required to have Android Studio installed - if you have Android Studio installed, make sure there are no active instances of it when using this
extension or you may run into problems with ADB.
## Limitations
* This is a preview version so expect the unexpected. Please log any issues you find on GitHub.
* This extension **will not build your app**.
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing
you to debug it in the normal way.
* Some debugger options are yet to be implemented. You cannot modify local variable values or set conditional breakpoints and watch expressions must be simple variables.
* If you require a must-have feature that isn't there yet, let us know on GitHub.
* This extension does not provide any additional code completion or other editing enhancements.
## Extension Settings
This extension allows you to debug your App by creating a new Android configuration in `launch.json`.
The following settings are used to configure the debugger:
{
"version": "0.2.0",
"configurations": [
{
// configuration type, request and name. "launch" is used to deploy the app to your device and start a debugging session
"type": "android",
"request": "launch",
"name": "Launch App",
// Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)
"appSrcRoot": "${workspaceRoot}/app/src/main",
// Fully qualified path to the built APK (Android Application Package)
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
// Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037
"adbPort": 5037,
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
"staleBuild": "warn",
}
]
}
## Questions / Problems
If you run into any problems, let us know on GitHub or via Twitter.

803
adbclient.js Normal file
View File

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

190
chrome-polyfill.js Normal file
View 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
debugMain.js Normal file

File diff suppressed because it is too large Load Diff

1480
debugger.js Normal file

File diff suppressed because it is too large Load Diff

31
extension.js Normal file
View File

@@ -0,0 +1,31 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
var vscode = require('vscode');
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
function activate(context) {
/* Nothing is done here. The debugger is launched from debugMain.js */
// The commandId parameter must match the command field in package.json
var disposables = [
/*
vscode.commands.registerCommand('extension.doCommand', config => {
return vscode.window.showInputBox({
placeHolder: "Enter a value",
value: "a value to display"
});
})
*/
];
var spliceparams = [context.subscriptions.length,0].concat(disposables);
Array.prototype.splice.apply(context.subscriptions,spliceparams);
}
exports.activate = activate;
// this method is called when your extension is deactivated
function deactivate() {
}
exports.deactivate = deactivate;

1056
jdwp.js Normal file

File diff suppressed because it is too large Load Diff

124
jq-promise.js Normal file
View 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
jsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": [
"es6"
]
},
"exclude": [
"node_modules"
]
}

122
minwebsocket.js Normal file
View 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;

113
package.json Normal file
View File

@@ -0,0 +1,113 @@
{
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "0.1.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
"engines": {
"vscode": "^1.8.0"
},
"categories": [
"Debuggers"
],
"activationEvents": [
],
"main": "./extension",
"contributes": {
"commands": [
],
"breakpoints": [
{
"language": "java"
}
],
"debuggers": [{
"type": "android",
"label": "Android Debug",
"program": "./debugMain.js",
"runtime": "node",
"configurationAttributes": {
"launch": {
"required": ["appSrcRoot","apkFile","adbPort"],
"properties": {
"appSrcRoot": {
"type": "string",
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
"default": "${workspaceRoot}/app/src/main"
},
"apkFile": {
"type": "string",
"description": "Fully qualified path to the built APK (Android Application Package)",
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk"
},
"adbPort": {
"type": "integer",
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
"default": 5037
},
"staleBuild": {
"type": "string",
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
"default": "warn"
},
"targetDevice": {
"type": "string",
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
"default": ""
}
}
}
},
"initialConfigurations": [
{
"type": "android",
"name": "Android Debug",
"request": "launch",
"appSrcRoot": "${workspaceRoot}/app/src/main",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
"adbPort": 5037
}
],
"configurationSnippets": [
{
"label": "Android: Launch Configuration",
"description": "A new configuration for launching an Android app debugging session",
"body": {
"type": "android",
"request": "launch",
"name": "${2:Launch App}",
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
"adbPort": 5037
}
}
],
"variables": {
}
}]
},
"scripts": {
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0",
"xmldom": "^0.1.27",
"xpath": "^0.0.23"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^2.3.3",
"eslint": "^3.6.0",
"@types/node": "^6.0.40",
"@types/mocha": "^2.2.32"
}
}

322
services.js Normal file
View 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
sockets.js Normal file
View 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];
}

24
test/extension.test.js Normal file
View File

@@ -0,0 +1,24 @@
/* global suite, test */
//
// Note: This example test is leveraging the Mocha test framework.
// Please refer to their documentation on https://mochajs.org/ for help.
//
// The module 'assert' provides assertion methods from node
var assert = require('assert');
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
var vscode = require('vscode');
var myExtension = require('../extension');
// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests", function() {
// Defines a Mocha unit test
test("Something 1", function() {
assert.equal(-1, [1, 2, 3].indexOf(5));
assert.equal(-1, [1, 2, 3].indexOf(0));
});
});

22
test/index.js Normal file
View File

@@ -0,0 +1,22 @@
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
var testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.js (suite, test, etc.)
useColors: true // colored output from test results
});
module.exports = testRunner;

424
transport.js Normal file
View 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
util.js Normal file
View 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
wsproxy.js Normal file
View 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;