var net = require('net') var Promise = require('bluebird') var syrup = require('syrup') var _ = require('lodash') var wire = require('../../../../wire') var logger = require('../../../../util/logger') var lifecycle = require('../../../../util/lifecycle') var streamutil = require('../../../../util/streamutil') var wireutil = require('../../../../wire/util') var ForwardManager = require('./util/manager') module.exports = syrup.serial() .dependency(require('../../support/adb')) .dependency(require('../../support/router')) .dependency(require('../../support/push')) .dependency(require('../../resources/minirev')) .dependency(require('../group')) .define(function(options, adb, router, push, minirev, group) { var log = logger.createLogger('device:plugins:forward') var plugin = Object.create(null) var manager = new ForwardManager() function startService() { log.info('Launching reverse port forwarding service') return adb.shell(options.serial, [ 'exec' , minirev.bin ]) .timeout(10000) .then(function(out) { lifecycle.share('Forward shell', out) streamutil.talk(log, 'Forward shell says: "%s"', out) }) } function connectService(times) { function tryConnect(times, delay) { return adb.openLocal(options.serial, 'localabstract:minirev') .timeout(10000) .catch(function(err) { if (/closed/.test(err.message) && times > 1) { return Promise.delay(delay) .then(function() { return tryConnect(--times, delay * 2) }) } return Promise.reject(err) }) } log.info('Connecting to reverse port forwarding service') return tryConnect(times, 100) } function awaitServer() { return connectService(5) .then(function(conn) { conn.end() return true }) } plugin.createForward = function(id, forward) { log.info( 'Creating reverse port forward "%s" from ":%d" to "%s:%d"' , id , forward.devicePort , forward.targetHost , forward.targetPort ) return connectService(1) .then(function(out) { var header = new Buffer(4) header.writeUInt16LE(0, 0) header.writeUInt16LE(forward.devicePort, 2) out.write(header) return manager.add(id, out, forward) }) } plugin.removeForward = function(id) { log.info('Removing reverse port forward "%s"', id) manager.remove(id) return Promise.resolve() } plugin.connect = function(options) { var resolver = Promise.defer() var conn = net.connect({ host: options.targetHost , port: options.targetPort }) function connectListener() { resolver.resolve(conn) } function errorListener(err) { resolver.reject(err) } conn.on('connect', connectListener) conn.on('error', errorListener) return resolver.promise.finally(function() { conn.removeListener('connect', connectListener) conn.removeListener('error', errorListener) }) } plugin.reset = function() { manager.removeAll() } group.on('leave', plugin.reset) var pushForwards = _.debounce( function() { push.send([ wireutil.global , wireutil.envelope(new wire.ReverseForwardsEvent( options.serial , manager.listAll() )) ]) } , 200 ) manager.on('add', pushForwards) manager.on('remove', pushForwards) return startService() .then(awaitServer) .then(function() { router .on(wire.ForwardTestMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.connect(message) .then(function(conn) { conn.end() push.send([ channel , reply.okay('success') ]) }) .catch(function() { push.send([ channel , reply.fail('fail_connect') ]) }) }) .on(wire.ForwardCreateMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.createForward(message.id, message) .then(function() { push.send([ channel , reply.okay('success') ]) }) .catch(function(err) { log.error('Reverse port forwarding failed', err.stack) push.send([ channel , reply.fail('fail_forward') ]) }) }) .on(wire.ForwardRemoveMessage, function(channel, message) { var reply = wireutil.reply(options.serial) plugin.removeForward(message.id) .then(function() { push.send([ channel , reply.okay('success') ]) }) .catch(function(err) { log.error('Reverse port unforwarding failed', err.stack) push.send([ channel , reply.fail('fail') ]) }) }) }) .return(plugin) })