Rename "roles" to "units". Put units in their own folders.

This commit is contained in:
Simo Kinnunen
2014-08-26 14:34:34 +09:00
parent 7d9d64ddcb
commit 3a9b193f68
63 changed files with 105 additions and 105 deletions

215
lib/units/app/index.js Normal file
View File

@@ -0,0 +1,215 @@
var http = require('http')
var express = require('express')
var validator = require('express-validator')
var cookieSession = require('cookie-session')
var bodyParser = require('body-parser')
var serveFavicon = require('serve-favicon')
var serveStatic = require('serve-static')
var csrf = require('csurf')
var Promise = require('bluebird')
var httpProxy = require('http-proxy')
var compression = require('compression')
var logger = require('../../util/logger')
var pathutil = require('../../util/pathutil')
var dbapi = require('../../db/api')
var datautil = require('../../util/datautil')
var auth = require('./middleware/auth')
var deviceIconMiddleware = require('./middleware/device-icons')
var browserIconMiddleware = require('./middleware/browser-icons')
var appstoreIconMiddleware = require('./middleware/appstore-icons')
var webpackServerConfig = require('./../../../webpack.config').webpackServer
module.exports = function(options) {
var log = logger.createLogger('app')
, app = express()
, server = http.createServer(app)
, proxy = httpProxy.createProxyServer()
proxy.on('error', function(err) {
log.error('Proxy had an error', err.stack)
})
app.set('view engine', 'jade')
app.set('views', pathutil.resource('app/views'))
app.set('strict routing', true)
app.set('case sensitive routing', true)
if (options.disableWatch) {
app.use(compression())
app.use('/static/app/build/entry',
serveStatic(pathutil.resource('build/entry')))
app.use('/static/app/build', serveStatic(pathutil.resource('build'), {
maxAge: '10d'
}))
}
else {
app.use('/static/app/build',
require('./middleware/webpack')(webpackServerConfig))
}
app.use('/static/bower_components',
serveStatic(pathutil.resource('bower_components')))
app.use('/intro',
serveStatic(pathutil.resource('bower_components/stf-site/intro')))
app.use('/manual-basic',
serveStatic(pathutil.resource('bower_components/stf-site/manual/basic')))
app.use('/manual-advanced',
serveStatic(pathutil.resource('bower_components/stf-site/manual/advanced')))
app.use('/v2-features',
serveStatic(pathutil.resource('bower_components/stf-site/v2-features')))
app.use('/static/app/data', serveStatic(pathutil.resource('data')))
app.use('/static/app/status', serveStatic(pathutil.resource('common/status')))
app.use('/static/app/browsers', browserIconMiddleware())
app.use('/static/app/appstores', appstoreIconMiddleware())
app.use('/static/app/devices', deviceIconMiddleware())
app.use('/static/app', serveStatic(pathutil.resource('app')))
app.use(serveFavicon(pathutil.resource(
'bower_components/stf-graphics/logo/exports/STF-128.png')))
app.use(cookieSession({
name: options.ssid
, keys: [options.secret]
}))
app.use(auth({
secret: options.secret
, authUrl: options.authUrl
}))
// Proxied requests must come before any body parsers. These proxies are
// here mainly for convenience, they should be replaced with proper reverse
// proxies in production.
app.all('/api/v1/s/image/*', function(req, res) {
proxy.web(req, res, {
target: options.storagePluginImageUrl
})
})
app.all('/api/v1/s/apk/*', function(req, res) {
proxy.web(req, res, {
target: options.storagePluginApkUrl
})
})
app.all('/api/v1/s/*', function(req, res) {
proxy.web(req, res, {
target: options.storageUrl
})
})
app.use(bodyParser.json())
app.use(csrf())
app.use(validator())
app.get('/', function(req, res) {
res.render('index')
})
app.get('/api/v1/appstate.js', function(req, res) {
res.type('application/javascript')
res.send('var GLOBAL_APPSTATE = ' + JSON.stringify({
config: {
websocketUrl: options.websocketUrl
}
, user: req.user
})
)
})
app.get('/api/v1/angular-appstate.js', function(req, res) {
res.type('application/javascript')
res.send('angular.module("stf.app-state")' +
'.config(function(AppState){AppState.set(' +
JSON.stringify({
config: {
websocketUrl: options.websocketUrl
}
, user: req.user
}) +
')})')
})
app.get('/api/v1/app/user', function(req, res) {
res.json({
success: true
, user: req.user
})
})
app.get('/api/v1/app/group', function(req, res) {
dbapi.loadGroup(req.user.email)
.then(function(cursor) {
return Promise.promisify(cursor.toArray, cursor)()
.then(function(list) {
list.forEach(function(device) {
datautil.normalize(device, req.user)
})
res.json({
success: true
, devices: list
})
})
})
.catch(function(err) {
log.error('Failed to load group: ', err.stack)
res.json(500, {
success: false
})
})
})
app.get('/api/v1/app/devices', function(req, res) {
dbapi.loadDevices()
.then(function(cursor) {
return Promise.promisify(cursor.toArray, cursor)()
.then(function(list) {
list.forEach(function(device) {
datautil.normalize(device, req.user)
})
res.json({
success: true
, devices: list
})
})
})
.catch(function(err) {
log.error('Failed to load device list: ', err.stack)
res.json(500, {
success: false
})
})
})
app.get('/api/v1/app/devices/:serial', function(req, res) {
dbapi.loadDevice(req.params.serial)
.then(function(device) {
if (device) {
datautil.normalize(device, req.user)
res.json({
success: true
, device: device
})
}
else {
res.json(404, {
success: false
})
}
})
.catch(function(err) {
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
res.json(500, {
success: false
})
})
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -0,0 +1,12 @@
var serveStatic = require('serve-static')
var pathutil = require('../../../util/pathutil')
module.exports = function() {
return serveStatic(
pathutil.root('node_modules/stf-appstore-db/dist')
, {
maxAge: '30d'
}
)
}

View File

@@ -0,0 +1,50 @@
var jwtutil = require('../../../util/jwtutil')
var urlutil = require('../../../util/urlutil')
var dbapi = require('../../../db/api')
module.exports = function(options) {
return function(req, res, next) {
if (req.query.jwt) {
// Coming from auth client
var data = jwtutil.decode(req.query.jwt, options.secret)
, redir = urlutil.removeParam(req.url, 'jwt')
if (data) {
// Redirect once to get rid of the token
dbapi.saveUserAfterLogin({
name: data.name
, email: data.email
, ip: req.ip
})
.then(function() {
req.session.jwt = data
res.redirect(redir)
})
.catch(next)
}
else {
// Invalid token, forward to auth client
res.redirect(options.authUrl)
}
}
else if (req.session && req.session.jwt) {
dbapi.loadUser(req.session.jwt.email)
.then(function(user) {
if (user) {
// Continue existing session
req.user = user
next()
}
else {
// We no longer have the user in the database
res.redirect(options.authUrl)
}
})
.catch(next)
}
else {
// No session, forward to auth client
res.redirect(options.authUrl)
}
}
}

View File

@@ -0,0 +1,12 @@
var serveStatic = require('serve-static')
var pathutil = require('../../../util/pathutil')
module.exports = function() {
return serveStatic(
pathutil.root('node_modules/stf-browser-db/dist')
, {
maxAge: '30d'
}
)
}

View File

@@ -0,0 +1,12 @@
var serveStatic = require('serve-static')
var pathutil = require('../../../util/pathutil')
module.exports = function() {
return serveStatic(
pathutil.root('node_modules/stf-device-db/dist')
, {
maxAge: '30d'
}
)
}

View File

@@ -0,0 +1,109 @@
var path = require('path')
var url = require('url')
var webpack = require('webpack')
var mime = require('mime')
var Promise = require('bluebird')
var _ = require('lodash')
var MemoryFileSystem = require('webpack/node_modules/memory-fs')
var logger = require('../../../util/logger')
var lifecycle = require('../../../util/lifecycle')
var globalOptions = require('../../../../webpack.config').webpack
// Similar to webpack-dev-middleware, but integrates with our custom
// lifecycle, behaves more like normal express middleware, and removes
// all unnecessary features.
module.exports = function(options) {
var log = logger.createLogger('middleware:webpack')
options = _.defaults(options || {}, globalOptions)
var compiler = webpack(options)
var fs = compiler.outputFileSystem = new MemoryFileSystem()
var valid = false
var queue = []
log.info('Creating bundle')
var watching = compiler.watch(options.watchDelay, function(err) {
if (err) {
log.fatal('Webpack had an error', err.stack)
lifecycle.fatal()
}
})
lifecycle.observe(function() {
if (watching.watcher) {
watching.watcher.close()
}
})
function doneListener(stats) {
process.nextTick(function() {
if (valid) {
log.info(stats.toString(options.stats))
if (stats.hasErrors()) {
log.error('Bundle has errors')
}
else if (stats.hasWarnings()) {
log.warn('Bundle has warnings')
}
else {
log.info('Bundle is now valid')
}
queue.forEach(function(resolver) {
resolver.resolve()
})
}
})
valid = true
}
function invalidate() {
if (valid) {
log.info('Bundle is now invalid')
valid = false
}
}
compiler.plugin('done', doneListener)
compiler.plugin('invalid', invalidate)
compiler.plugin('compile', invalidate)
function bundle() {
if (valid) {
return Promise.resolve()
}
else {
log.info('Waiting for bundle to finish')
var resolver = Promise.defer()
queue.push(resolver)
return resolver.promise
}
}
return function(req, res, next) {
var parsedUrl = url.parse(req.url)
var target = path.join(
compiler.outputPath
, parsedUrl.pathname
)
bundle()
.then(function() {
try {
var body = fs.readFileSync(target)
res.set('Content-Type', mime.lookup(target))
res.end(body)
}
catch (err) {
return next()
}
})
.catch(next)
}
}

140
lib/units/auth/ldap.js Normal file
View File

@@ -0,0 +1,140 @@
var http = require('http')
var express = require('express')
var validator = require('express-validator')
var cookieSession = require('cookie-session')
var bodyParser = require('body-parser')
var serveStatic = require('serve-static')
var csrf = require('csurf')
var Promise = require('bluebird')
var logger = require('../../util/logger')
var requtil = require('../../util/requtil')
var ldaputil = require('../../util/ldaputil')
var jwtutil = require('../../util/jwtutil')
var pathutil = require('../../util/pathutil')
var urlutil = require('../../util/urlutil')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
var log = logger.createLogger('auth-ldap')
, app = express()
, server = Promise.promisifyAll(http.createServer(app))
lifecycle.observe(function() {
log.info('Waiting for client connections to end')
return server.closeAsync()
.catch(function() {
// Okay
})
})
app.set('view engine', 'jade')
app.set('views', pathutil.resource('auth/ldap/views'))
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.use(cookieSession({
name: options.ssid
, keys: [options.secret]
}))
app.use(bodyParser.json())
app.use(csrf())
app.use(validator())
app.use('/static/bower_components',
serveStatic(pathutil.resource('bower_components')))
app.use('/static/auth/ldap', serveStatic(pathutil.resource('auth/ldap')))
app.use(function(req, res, next) {
res.cookie('XSRF-TOKEN', req.csrfToken());
next()
})
app.get('/static/auth/ldap/views/partials/:name.html', function(req, res) {
var whitelist = {
'signin': true
}
if (whitelist[req.params.name]) {
res.render('partials/' + req.params.name)
}
else {
res.send(404)
}
})
app.get('/', function(req, res) {
res.redirect('/auth/ldap/')
})
app.get('/auth/ldap/', function(req, res) {
res.render('index')
})
app.post('/api/v1/auth/ldap', function(req, res) {
var log = logger.createLogger('auth-ldap')
log.setLocalIdentifier(req.ip)
switch (req.accepts(['json'])) {
case 'json':
requtil.validate(req, function() {
req.checkBody('username').notEmpty()
req.checkBody('password').notEmpty()
})
.then(function() {
return ldaputil.login(
options.ldap
, req.body.username
, req.body.password
)
})
.then(function(user) {
log.info('Authenticated "%s"', ldaputil.email(user))
var token = jwtutil.encode({
payload: {
email: ldaputil.email(user)
, name: user.cn
}
, secret: options.secret
})
res.status(200)
.json({
success: true
, redirect: urlutil.addParams(options.appUrl, {
jwt: token
})
})
})
.catch(requtil.ValidationError, function(err) {
res.status(400)
.json({
success: false
, error: 'ValidationError'
, validationErrors: err.errors
})
})
.catch(ldaputil.InvalidCredentialsError, function(err) {
log.warn('Authentication failure for "%s"', err.user)
res.status(400)
.json({
success: false
, error: 'InvalidCredentialsError'
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
break
default:
res.send(406)
break
}
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

124
lib/units/auth/mock.js Normal file
View File

@@ -0,0 +1,124 @@
var http = require('http')
var express = require('express')
var validator = require('express-validator')
var cookieSession = require('cookie-session')
var bodyParser = require('body-parser')
var serveStatic = require('serve-static')
var csrf = require('csurf')
var Promise = require('bluebird')
var logger = require('../../util/logger')
var requtil = require('../../util/requtil')
var jwtutil = require('../../util/jwtutil')
var pathutil = require('../../util/pathutil')
var urlutil = require('../../util/urlutil')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
var log = logger.createLogger('auth-mock')
, app = express()
, server = Promise.promisifyAll(http.createServer(app))
lifecycle.observe(function() {
log.info('Waiting for client connections to end')
return server.closeAsync()
.catch(function() {
// Okay
})
})
app.set('view engine', 'jade')
app.set('views', pathutil.resource('auth/mock/views'))
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.use(cookieSession({
name: options.ssid
, keys: [options.secret]
}))
app.use(bodyParser.json())
app.use(csrf())
app.use(validator())
app.use('/static/bower_components',
serveStatic(pathutil.resource('bower_components')))
app.use('/static/auth/mock', serveStatic(pathutil.resource('auth/mock')))
app.use(function(req, res, next) {
res.cookie('XSRF-TOKEN', req.csrfToken());
next()
})
app.get('/static/auth/mock/views/partials/:name.html', function(req, res) {
var whitelist = {
'signin': true
}
if (whitelist[req.params.name]) {
res.render('partials/' + req.params.name)
}
else {
res.send(404)
}
})
app.get('/', function(req, res) {
res.redirect('/auth/mock/')
})
app.get('/auth/mock/', function(req, res) {
res.render('index')
})
app.post('/api/v1/auth/mock', function(req, res) {
var log = logger.createLogger('auth-mock')
log.setLocalIdentifier(req.ip)
switch (req.accepts(['json'])) {
case 'json':
requtil.validate(req, function() {
req.checkBody('name').notEmpty()
req.checkBody('email').isEmail()
})
.then(function() {
log.info('Authenticated "%s"', req.body.email)
var token = jwtutil.encode({
payload: {
email: req.body.email
, name: req.body.name
}
, secret: options.secret
})
res.status(200)
.json({
success: true
, redirect: urlutil.addParams(options.appUrl, {
jwt: token
})
})
})
.catch(requtil.ValidationError, function(err) {
res.status(400)
.json({
success: false
, error: 'ValidationError'
, validationErrors: err.errors
})
})
.catch(function(err) {
log.error('Unexpected error', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
break
default:
res.send(406)
break
}
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

54
lib/units/device/index.js Normal file
View File

@@ -0,0 +1,54 @@
var syrup = require('syrup')
var logger = require('../../util/logger')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
// Show serial number in logs
logger.setGlobalIdentifier(options.serial)
var log = logger.createLogger('device')
return syrup.serial()
// We want to send logs before anything else starts happening
.dependency(require('./plugins/logger'))
.define(function(options) {
var log = logger.createLogger('device')
log.info('Preparing device')
return syrup.serial()
.dependency(require('./plugins/solo'))
.dependency(require('./plugins/screenshot'))
.dependency(require('./plugins/http'))
.dependency(require('./plugins/service'))
.dependency(require('./plugins/display'))
.dependency(require('./plugins/browser'))
.dependency(require('./plugins/store'))
.dependency(require('./plugins/clipboard'))
.dependency(require('./plugins/logcat'))
.dependency(require('./plugins/shell'))
.dependency(require('./plugins/touch'))
.dependency(require('./plugins/install'))
.dependency(require('./plugins/forward'))
.dependency(require('./plugins/group'))
.dependency(require('./plugins/reboot'))
.dependency(require('./plugins/connect'))
.dependency(require('./plugins/account'))
.dependency(require('./plugins/ringer'))
.dependency(require('./plugins/wifi'))
.dependency(require('./plugins/sd'))
.define(function(options, solo) {
if (process.send) {
// Only if we have a parent process
process.send('ready')
}
log.info('Fully operational')
return solo.poke()
})
.consume(options)
})
.consume(options)
.catch(function(err) {
log.fatal('Setup had an error', err.stack)
lifecycle.fatal()
})
}

View File

@@ -0,0 +1,324 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('./identity'))
.dependency(require('./touch'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/adb'))
.define(function(options, service, identity, touch, router, push, adb) {
var log = logger.createLogger('device:plugins:account')
function checkAccount(type, account) {
return service.getAccounts({type: type})
.timeout(30000)
.then(function(accounts) {
if(accounts.indexOf(account) >= 0) {
return true
}
throw new Error('The account is not added')
})
}
router.on(wire.AccountCheckMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Checking if account "%s" is added',message.account)
checkAccount(message.type, message.account)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Account check failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountGetMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Getting account(s)')
service.getAccounts(message)
.timeout(30000)
.then(function(accounts) {
push.send([
channel
, reply.okay('success',accounts)
])
})
.catch(function(err) {
log.error('Account get failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountRemoveMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Removing "%s" account(s)', message.type)
service.removeAccount(message)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Account removal failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountAddMenuMessage, function(channel) {
var reply = wireutil.reply(options.serial)
log.info('Showing add account menu for Google Account')
service.addAccountMenu()
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Add account menu failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.AccountAddMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
var type = "com.google"
var account = message.user + "@gmail.com";
log.info('Adding Google Account automatedly')
var version = identity.version.substring(0,3)
function automation() {
switch (version) {
case '2.3': // tested: 2.3.3-2.3.6
return service.pressKey('dpad_down').delay(1000)
.then(function() {
return service.pressKey('dpad_down')
}).delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.pressKey('dpad_down')
}).delay(2000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.0': // tested: 4.0.3 and 4.0.4
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.1': // tested: 4.1.1 and 4.1.2
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.2': // tested: 4.2.2
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
case '4.3': // tested: 4.3
case '4.4': // tested: 4.4.2
default:
return service.pressKey('tab').delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(2000)
.then(function() {
return service.type(message.user)
}).delay(1000)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('switch_charset')
}).delay(100)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.type(message.password)
}).delay(1000)
.then(function() {
return service.pressKey('enter')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('tab')
}).delay(1000)
.then(function() {
return service.pressKey('enter')
})
}
}
// First check if the account is already added so we don't continue
return checkAccount(type, account)
.then(function() {
push.send([
channel
, reply.fail('Add account failed: account was already added')
])
})
.catch(function() {
return adb.clear(options.serial, 'com.google.android.gsf.login')
.catch(function() {
// The package name is different in 2.3, so let's try the old name
// if the new name fails.
return adb.clear(options.serial, 'com.google.android.gsf')
})
.then(function() {
return service.addAccountMenu()
})
.delay(5000)
.then(function() {
// Just in case the add account menu has any button focused
return touch.tap({x:0, y:0.9})
})
.delay(500)
.then(function() {
return automation()
})
.delay(3000)
.then(function () {
return service.pressKey('home')
})
.then(function () {
return checkAccount(type, account)
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Add account failed', err.stack)
push.send([
channel
, reply.fail('Add account failed: ' + err.message)
])
})
})
})
})

View File

@@ -0,0 +1,122 @@
var util = require('util')
var syrup = require('syrup')
var browsers = require('stf-browser-db')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var mapping = (function() {
var list = Object.create(null)
Object.keys(browsers).forEach(function(id) {
var browser = browsers[id]
if (browser.platforms.android) {
list[browser.platforms.android.package] = id
}
})
return list
})()
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/adb'))
.dependency(require('./service'))
.define(function(options, router, push, adb, service) {
var log = logger.createLogger('device:plugins:browser')
function pkg(component) {
return component.split('/', 1)[0]
}
function processApp(app) {
var packageName = pkg(app.component)
var browserId = mapping[packageName]
if (!browserId) {
throw new Error(util.format('Unmapped browser "%s"', packageName))
}
return {
id: app.component
, type: browserId
, name: browsers[browserId].name
, selected: app.selected
, system: app.system
}
}
function updateBrowsers(data) {
log.info('Updating browser list')
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceBrowserMessage(
options.serial
, data.selected
, data.apps.map(function(app) {
return new wire.DeviceBrowserAppMessage(processApp(app))
})
))
])
}
function loadBrowsers() {
log.info('Loading browser list')
return service.getBrowsers()
.then(updateBrowsers)
}
service.on('browserPackageChange', updateBrowsers)
router.on(wire.BrowserOpenMessage, function(channel, message) {
if (message.browser) {
log.info('Opening "%s" in "%s"', message.url, message.browser)
}
else {
log.info('Opening "%s"', message.url)
}
var reply = wireutil.reply(options.serial)
adb.startActivity(options.serial, {
action: 'android.intent.action.VIEW'
, component: message.browser
, data: message.url
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Browser could not be opened', err.stack)
push.send([
channel
, reply.fail()
])
})
})
router.on(wire.BrowserClearMessage, function(channel, message) {
log.info('Clearing "%s"', message.browser)
var reply = wireutil.reply(options.serial)
adb.clear(options.serial, pkg(message.browser))
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Browser could not be cleared', err.stack)
push.send([
channel
, reply.fail()
])
})
})
return loadBrowsers()
})

View File

@@ -0,0 +1,51 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('./service'))
.define(function(options, router, push, service) {
var log = logger.createLogger('device:plugins:clipboard')
router.on(wire.PasteMessage, function(channel, message) {
log.info('Pasting "%s" to clipboard', message.text)
var reply = wireutil.reply(options.serial)
service.paste(message.text)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Paste failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.CopyMessage, function(channel) {
log.info('Copying clipboard contents')
var reply = wireutil.reply(options.serial)
service.copy()
.then(function(content) {
push.send([
channel
, reply.okay(content)
])
})
.catch(function(err) {
log.error('Copy failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,111 @@
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var lifecycle = require('../../../util/lifecycle')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('./group'))
.define(function(options, adb, router, push, group) {
var log = logger.createLogger('device:plugins:connect')
, plugin = Object.create(null)
, activeServer = null
plugin.port = options.ports.pop()
plugin.url = util.format('%s:%s', options.publicIp, plugin.port)
plugin.start = function() {
return new Promise(function(resolve, reject) {
if (plugin.isRunning()) {
return resolve(plugin.url)
}
var server = adb.createTcpUsbBridge(options.serial)
server.on('listening', function() {
resolve(plugin.url)
})
server.on('connection', function(conn) {
log.info('New remote ADB connection from %s', conn.remoteAddress)
conn.on('userActivity', function() {
group.keepalive()
})
})
server.on('error', reject)
log.info(util.format('Listening on port %d', plugin.port))
server.listen(plugin.port)
activeServer = server
lifecycle.share('Remote ADB', activeServer)
})
}
plugin.stop = Promise.method(function() {
if (plugin.isRunning()) {
activeServer.close()
activeServer.end()
}
})
plugin.end = Promise.method(function() {
if (plugin.isRunning()) {
activeServer.end()
}
})
plugin.isRunning = function() {
return !!activeServer
}
lifecycle.observe(plugin.stop)
group.on('leave', plugin.end)
router
.on(wire.ConnectStartMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.start()
.then(function(url) {
push.send([
channel
, reply.okay(url)
])
})
.catch(function(err) {
log.error('Unable to start remote connect service', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
.on(wire.ConnectStopMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.end()
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Failed to stop connect service', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
return plugin.start()
.return(plugin)
})

View File

@@ -0,0 +1,20 @@
var syrup = require('syrup')
var deviceData = require('stf-device-db')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./identity'))
.define(function(options, identity) {
var log = logger.createLogger('device:plugins:data')
function find() {
var data = deviceData.find(identity)
if (!data) {
log.warn('Unable to find device data')
}
return data
}
return find()
})

View File

@@ -0,0 +1,25 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('./http'))
.define(function(options, service, http) {
var log = logger.createLogger('device:plugins:display')
function fetch() {
log.info('Fetching display info')
return service.getDisplay(0)
.catch(function() {
log.info('Falling back to HTTP API')
return http.getDisplay(0)
})
.then(function(display) {
display.url = http.getDisplayUrl(display.id)
return display
})
}
return fetch()
})

View File

@@ -0,0 +1,237 @@
var net = require('net')
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var split = require('split')
var logger = require('../../../util/logger')
var devutil = require('../../../util/devutil')
var streamutil = require('../../../util/streamutil')
var lifecycle = require('../../../util/lifecycle')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../resources/remote'))
.define(function(options, adb, router, push, remote) {
var log = logger.createLogger('device:plugins:forward')
var service = {
port: 2810
, privatePorts: (function() {
var ports = []
for (var i = 2520; i <= 2540; ++i) {
ports.push(i)
}
return ports
})()
, forwards: Object.create(null)
}
function openService() {
log.info('Launching reverse port forwarding service')
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-forward', service.port
])
.timeout(10000)
.then(function(out) {
lifecycle.share('Forward shell', out)
streamutil.talk(log, 'Forward shell says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(10000)
})
.then(function(conn) {
conn.end()
})
})
}
function createForward(data) {
log.info(
'Reverse port forwarding port %d to %s:%d'
, data.devicePort
, data.targetHost
, data.targetPort
)
var forward = service.forwards[data.devicePort]
if (forward) {
if (forward.targetHost === data.targetHost &&
forward.targetPort === data.targetPort) {
return Promise.resolve()
}
else if (forward.system) {
return Promise.reject(new Error('Cannot rebind system port'))
}
else {
removeForward(forward)
}
}
return adb.openTcp(options.serial, service.port)
.timeout(10000)
.then(function(conn) {
var resolver = Promise.defer()
var forward = {
devicePort: data.devicePort
, targetHost: data.targetHost
, targetPort: data.targetPort
, system: !!data.system
, privatePort: service.privatePorts.pop()
, connection: conn
}
var parser = conn.pipe(split())
parser.on('data', function(chunk) {
var cmd = chunk.toString().trim()
switch (cmd) {
case 'OKAY':
resolver.resolve(forward)
break
case 'FAIL':
resolver.reject(new Error('Remote replied with FAIL'))
break
case 'CNCT':
adb.openTcp(options.serial, forward.privatePort)
.done(function(dstream) {
return tryConnect(forward)
.then(function(ustream) {
ustream.pipe(dstream)
dstream.pipe(ustream)
})
})
break
}
})
// Keep this around
function endListener() {
removeForward(forward)
}
conn.on('end', endListener)
conn.write(util.format(
'FRWD %d %d\n'
, forward.devicePort
, forward.privatePort
))
return resolver.promise
})
}
function removeForward(data) {
log.info('Removing reverse port forwarding on port %d', data.devicePort)
var forward = service.forwards[data.devicePort]
if (forward) {
forward.connection.end()
delete service.forwards[data.devicePort]
}
}
function tryConnect(data) {
var resolver = Promise.defer()
var conn = net.connect({
host: data.targetHost
, port: data.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)
})
}
function resetForwards() {
Object.keys(service.forwards).forEach(function(privatePort) {
service.forwards[privatePort].connection.end()
delete service.forwards[privatePort]
})
}
function listForwards() {
return Object.keys(service.forwards).map(function(privatePort) {
var forward = service.forwards[privatePort]
return {
devicePort: forward.devicePort
, targetHost: forward.targetHost
, targetPort: forward.targetPort
, system: !!forward.system
}
})
}
return openService()
.then(function() {
router
.on(wire.ForwardTestMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
tryConnect(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)
createForward(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)
removeForward(message)
push.send([
channel
, reply.okay('success')
])
})
})
})

View File

@@ -0,0 +1,170 @@
var events = require('events')
var Promise = require('bluebird')
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var grouputil = require('../../../util/grouputil')
var lifecycle = require('../../../util/lifecycle')
module.exports = syrup.serial()
.dependency(require('./solo'))
.dependency(require('./identity'))
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/sub'))
.dependency(require('../support/channels'))
.define(function(options, solo, ident, service, router, push, sub, channels) {
var log = logger.createLogger('device:plugins:group')
, currentGroup = null
, plugin = new events.EventEmitter()
plugin.get = Promise.method(function() {
if (!currentGroup) {
throw new grouputil.NoGroupError()
}
return currentGroup
})
plugin.join = function(newGroup, timeout) {
return plugin.get()
.then(function() {
if (currentGroup.group !== newGroup.group) {
throw new grouputil.AlreadyGroupedError()
}
return currentGroup
})
.catch(grouputil.NoGroupError, function() {
currentGroup = newGroup
log.important('Now owned by "%s"', currentGroup.email)
log.info('Subscribing to group channel "%s"', currentGroup.group)
channels.register(currentGroup.group, {
timeout: timeout || options.groupTimeout
, alias: solo.channel
})
sub.subscribe(currentGroup.group)
push.send([
wireutil.global
, wireutil.envelope(new wire.JoinGroupMessage(
options.serial
, currentGroup
))
])
plugin.emit('join', currentGroup)
return currentGroup
})
}
plugin.keepalive = function() {
if (currentGroup) {
channels.keepalive(currentGroup.group)
}
}
plugin.leave = function(reason) {
return plugin.get()
.then(function(group) {
log.important('No longer owned by "%s"', group.email)
log.info('Unsubscribing from group channel "%s"', group.group)
channels.unregister(group.group)
sub.unsubscribe(group.group)
push.send([
wireutil.global
, wireutil.envelope(new wire.LeaveGroupMessage(
options.serial
, group
, reason
))
])
currentGroup = null
plugin.emit('leave', group)
return group
})
}
plugin.on('join', function() {
service.acquireWakeLock()
service.unlock()
})
plugin.on('leave', function() {
service.releaseWakeLock()
service.lock()
})
router
.on(wire.GroupMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
grouputil.match(ident, message.requirements)
.then(function() {
return plugin.join(message.owner, message.timeout)
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(grouputil.RequirementMismatchError, function(err) {
push.send([
channel
, reply.fail(err.message)
])
})
.catch(grouputil.AlreadyGroupedError, function(err) {
push.send([
channel
, reply.fail(err.message)
])
})
})
.on(wire.UngroupMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
grouputil.match(ident, message.requirements)
.then(function() {
return plugin.leave('ungroup_request')
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(grouputil.NoGroupError, function(err) {
push.send([
channel
, reply.fail(err.message)
])
})
})
channels.on('timeout', function(channel) {
if (currentGroup && channel === currentGroup.group) {
plugin.leave('automatic_timeout')
}
})
lifecycle.observe(function() {
return plugin.leave('device_absent')
.catch(grouputil.NoGroupError, function() {
return true
})
})
return plugin
})

View File

@@ -0,0 +1,143 @@
var util = require('util')
var assert = require('assert')
var http = require('http')
var Promise = require('bluebird')
var syrup = require('syrup')
var request = Promise.promisifyAll(require('request'))
var httpProxy = require('http-proxy')
var logger = require('../../../util/logger')
var devutil = require('../../../util/devutil')
var lifecycle = require('../../../util/lifecycle')
var streamutil = require('../../../util/streamutil')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../resources/remote'))
.define(function(options, adb, remote) {
var log = logger.createLogger('device:plugins:http')
var service = {
port: 2870
, privateUrl: null
, publicUrl: null
}
function openService() {
log.info('Launching HTTP API')
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-http', service.port
])
.timeout(10000)
.then(function(out) {
lifecycle.share('Remote shell', out)
streamutil.talk(log, 'Remote shell says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(20000)
})
.then(function(conn) {
var ours = options.ports.pop()
, everyones = options.ports.pop()
, url = util.format('http://127.0.0.1:%d', ours)
// Don't need the connection
conn.end()
log.info('Opening device HTTP API forwarder on "%s"', url)
service.privateUrl = url
service.publicUrl = util.format(
'http://%s:%s'
, options.publicIp
, everyones
)
return adb.forward(
options.serial
, util.format('tcp:%d', ours)
, util.format('tcp:%d', service.port)
)
.timeout(10000)
.then(function() {
log.info(
'Opening HTTP API proxy on "http://%s:%s"'
, options.publicIp
, everyones
)
var resolver = Promise.defer()
function resolve() {
lifecycle.share('Proxy server', proxyServer, {
end: false
})
resolver.resolve()
}
function reject(err) {
resolver.reject(err)
}
var proxy = httpProxy.createProxyServer({
target: url
, ws: false
, xfwd: false
})
var proxyServer = http.createServer(proxy.web)
.listen(everyones)
proxyServer.on('listening', resolve)
proxyServer.on('error', reject)
return resolver.promise.finally(function() {
proxyServer.removeListener('listening', resolve)
proxyServer.removeListener('error', reject)
})
})
})
})
}
return openService()
.then(function() {
return {
getDisplay: function(id) {
return request.getAsync({
url: util.format(
'%s/api/v1/displays/%d'
, service.privateUrl
, id
)
, json: true
})
.timeout(10000)
.then(function(args) {
var display = args[1]
assert.ok('id' in display, 'Invalid response from HTTP API')
// Fix rotation's old name
if ('orientation' in display) {
display.rotation = display.orientation
delete display.orientation
}
return display
})
}
, getDisplayUrl: function(id) {
return util.format(
'%s/api/v1/displays/%d/screenshot.jpg'
, service.publicUrl
, id
)
}
}
})
})

View File

@@ -0,0 +1,22 @@
var syrup = require('syrup')
var devutil = require('../../../util/devutil')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('../support/properties'))
.dependency(require('./display'))
.dependency(require('./phone'))
.define(function(options, properties, display, phone) {
var log = logger.createLogger('device:plugins:identity')
function solve() {
log.info('Solving identity')
var identity = devutil.makeIdentity(options.serial, properties)
identity.display = display
identity.phone = phone
return identity
}
return solve()
})

View File

@@ -0,0 +1,189 @@
var stream = require('stream')
var url = require('url')
var util = require('util')
var syrup = require('syrup')
var request = require('request')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var promiseutil = require('../../../util/promiseutil')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, adb, router, push) {
var log = logger.createLogger('device:plugins:install')
router.on(wire.InstallMessage, function(channel, message) {
var manifest = JSON.parse(message.manifest)
, pkg = manifest.package
log.info('Installing package "%s" from "%s"', pkg, message.href)
var reply = wireutil.reply(options.serial)
function sendProgress(data, progress) {
push.send([
channel
, reply.progress(data, progress)
])
}
function pushApp() {
var req = request({
url: url.resolve(options.storageUrl, message.href)
})
// We need to catch the Content-Length on the fly or we risk
// losing some of the initial chunks.
var contentLength = null
req.on('response', function(res) {
contentLength = parseInt(res.headers['content-length'], 10)
})
var source = new stream.Readable().wrap(req)
var target = '/data/local/tmp/_app.apk'
return adb.push(options.serial, source, target)
.timeout(10000)
.then(function(transfer) {
var resolver = Promise.defer()
function progressListener(stats) {
if (contentLength) {
// Progress 0% to 70%
sendProgress(
'pushing_app'
, 50 * Math.max(0, Math.min(
50
, stats.bytesTransferred / contentLength
))
)
}
}
function errorListener(err) {
resolver.reject(err)
}
function endListener() {
resolver.resolve(target)
}
transfer.on('progress', progressListener)
transfer.on('error', errorListener)
transfer.on('end', endListener)
return resolver.promise.finally(function() {
transfer.removeListener('progress', progressListener)
transfer.removeListener('error', errorListener)
transfer.removeListener('end', endListener)
})
})
}
// Progress 0%
sendProgress('pushing_app', 0)
pushApp()
.then(function(apk) {
var start = 50
, end = 90
, guesstimate = start
sendProgress('installing_app', guesstimate)
return promiseutil.periodicNotify(
adb.installRemote(options.serial, apk)
.timeout(60000)
.catch(function(err) {
switch (err.code) {
case 'INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES':
log.info(
'Uninstalling "%s" first due to inconsistent certificates'
, pkg
)
return adb.uninstall(options.serial, pkg)
.timeout(15000)
.then(function() {
return adb.installRemote(options.serial, apk)
.timeout(60000)
})
default:
return Promise.reject(err)
}
})
, 250
)
.progressed(function() {
guesstimate = Math.min(
end
, guesstimate + 1.5 * (end - guesstimate) / (end - start)
)
sendProgress('installing_app', guesstimate)
})
})
.then(function() {
if (message.launch) {
if (manifest.application.launcherActivities.length) {
var launchActivity = {
action: 'android.intent.action.MAIN'
, component: util.format(
'%s/%s'
, pkg
, manifest.application.launcherActivities[0].name
)
, category: ['android.intent.category.LAUNCHER']
, flags: 0x10200000
}
log.info(
'Launching activity with action "%s" on component "%s"'
, launchActivity.action
, launchActivity.component
)
// Progress 90%
sendProgress('launching_app', 90)
return adb.startActivity(options.serial, launchActivity)
.timeout(15000)
}
}
})
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Installation of package "%s" failed', pkg, err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
router.on(wire.UninstallMessage, function(channel, message) {
log.info('Uninstalling "%s"', message.packageName)
var reply = wireutil.reply(options.serial)
adb.uninstall(options.serial, message.packageName)
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Uninstallation failed', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
})

View File

@@ -0,0 +1,141 @@
var syrup = require('syrup')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var lifecycle = require('../../../util/lifecycle')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('./group'))
.define(function(options, adb, router, push, group) {
var log = logger.createLogger('device:plugins:logcat')
, plugin = Object.create(null)
, activeLogcat = null
plugin.start = function(filters) {
return group.get()
.then(function(group) {
return plugin.stop()
.then(function() {
log.info('Starting logcat')
return adb.openLogcat(options.serial, {
clear: true
})
})
.timeout(10000)
.then(function(logcat) {
activeLogcat = logcat
function entryListener(entry) {
push.send([
group.group
, wireutil.envelope(new wire.DeviceLogcatEntryMessage(
options.serial
, entry.date.getTime() / 1000
, entry.pid
, entry.tid
, entry.priority
, entry.tag
, entry.message
))
])
}
logcat.on('entry', entryListener)
return plugin.reset(filters)
})
})
}
plugin.stop = Promise.method(function() {
if (plugin.isRunning()) {
log.info('Stopping logcat')
activeLogcat.end()
activeLogcat = null
}
})
plugin.reset = Promise.method(function(filters) {
if (plugin.isRunning()) {
activeLogcat
.resetFilters()
if (filters.length) {
activeLogcat.excludeAll()
filters.forEach(function(filter) {
activeLogcat.include(filter.tag, filter.priority)
})
}
}
else {
throw new Error('Logcat is not running')
}
})
plugin.isRunning = function() {
return !!activeLogcat
}
lifecycle.observe(plugin.stop)
group.on('leave', plugin.stop)
router
.on(wire.LogcatStartMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
plugin.start(message.filters)
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Unable to open logcat', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
.on(wire.LogcatApplyFiltersMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
plugin.reset(message.filters)
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Failed to apply logcat filters', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
.on(wire.LogcatStopMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.stop()
.then(function() {
push.send([
channel
, reply.okay('success')
])
})
.catch(function(err) {
log.error('Failed to stop logcat', err.stack)
push.send([
channel
, reply.fail('fail')
])
})
})
return plugin
})

View File

@@ -0,0 +1,27 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/push'))
.define(function(options, push) {
// Forward all logs
logger.on('entry', function(entry) {
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceLogMessage(
options.serial
, entry.timestamp / 1000
, entry.priority
, entry.tag
, entry.pid
, entry.message
, entry.identifier
))
])
})
return logger
})

View File

@@ -0,0 +1,21 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./service'))
.define(function(options, service) {
var log = logger.createLogger('device:plugins:phone')
function fetch() {
log.info('Fetching phone info')
return service.getProperties([
'imei'
, 'phoneNumber'
, 'iccid'
, 'network'
])
}
return fetch()
})

View File

@@ -0,0 +1,35 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, adb, router, push) {
var log = logger.createLogger('device:plugins:reboot')
router.on(wire.RebootMessage, function(channel) {
var reply = wireutil.reply(options.serial)
log.important('Rebooting')
adb.reboot(options.serial)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.error(function(err) {
log.error('Reboot failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,57 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, service, router, push) {
var log = logger.createLogger('device:plugins:ringer')
router.on(wire.RingerSetMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Setting ringer mode to mode "%s"', message.mode)
service.setRingerMode(message.mode)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Setting ringer mode failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.RingerGetMessage, function(channel) {
var reply = wireutil.reply(options.serial)
log.info('Getting ringer mode')
service.getRingerMode()
.timeout(30000)
.then(function(mode) {
push.send([
channel
, reply.okay('success', mode)
])
})
.catch(function(err) {
log.error('Getting ringer mode failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,66 @@
var http = require('http')
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/storage'))
.dependency(require('./display'))
.define(function(options, router, push, storage, display) {
var log = logger.createLogger('device:plugins:screenshot')
var plugin = Object.create(null)
plugin.capture = function() {
log.info('Capturing screenshot from %s', display.url)
return new Promise(function(resolve, reject) {
var req = http.get(display.url)
function responseListener(res) {
if (res.statusCode !== 200) {
reject(new Error(util.format(
'Screenshot capture failed: HTTP %d'
, res.statusCode
)))
}
else {
resolve(storage.store('image', res, {
filename: util.format('%s.jpg', options.serial)
, contentType: 'image/jpeg'
, knownLength: +res.headers['content-length']
}))
}
}
req.on('response', responseListener)
req.on('error', reject)
})
}
router.on(wire.ScreenCaptureMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.capture()
.then(function(file) {
push.send([
channel
, reply.okay('success', file)
])
})
.catch(function(err) {
log.error('Screen capture failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
return plugin
})

View File

@@ -0,0 +1,33 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, service, router, push) {
var log = logger.createLogger('device:plugins:sd')
router.on(wire.SdStatusMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Getting SD card status')
service.getSdStatus(message)
.timeout(30000)
.then(function(mounted) {
push.send([
channel
, reply.okay(mounted ? 'sd_mounted' : 'sd_unmounted')
])
})
.catch(function(err) {
log.error('Getting SD card Status', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,695 @@
var util = require('util')
var events = require('events')
var syrup = require('syrup')
var Promise = require('bluebird')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
var devutil = require('../../../util/devutil')
var keyutil = require('../../../util/keyutil')
var streamutil = require('../../../util/streamutil')
var logger = require('../../../util/logger')
var ms = require('../../../wire/messagestream')
var lifecycle = require('../../../util/lifecycle')
function MessageResolver() {
this.resolvers = Object.create(null)
this.await = function(id, resolver) {
this.resolvers[id] = resolver
return resolver.promise
}
this.resolve = function(id, value) {
var resolver = this.resolvers[id]
delete this.resolvers[id]
resolver.resolve(value)
return resolver.promise
}
}
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../resources/service'))
.define(function(options, adb, router, push, apk) {
var log = logger.createLogger('device:plugins:service')
var messageResolver = new MessageResolver()
var plugin = new events.EventEmitter()
var agent = {
socket: null
, writer: null
, port: 1090
}
var service = {
socket: null
, writer: null
, reader: null
, port: 1100
}
function openAgent() {
log.info('Launching agent')
return stopAgent()
.timeout(15000)
.then(function() {
return devutil.ensureUnusedPort(adb, options.serial, agent.port)
.timeout(10000)
})
.then(function() {
return adb.shell(options.serial, util.format(
"export CLASSPATH='%s'; exec app_process /system/bin '%s'"
, apk.path
, apk.main
))
.timeout(10000)
})
.then(function(out) {
lifecycle.share('Agent shell', out)
streamutil.talk(log, 'Agent says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, agent.port)
.timeout(10000)
})
.then(function(conn) {
agent.socket = conn
agent.writer = new ms.DelimitingStream()
agent.writer.pipe(conn)
lifecycle.share('Agent connection', conn)
})
}
function stopAgent() {
return devutil.killProcsByComm(
adb
, options.serial
, 'stf.agent'
, 'stf.agent'
)
}
function callService(intent) {
return adb.shell(options.serial, util.format(
'am startservice --user 0 %s'
, intent
))
.timeout(15000)
.then(function(out) {
return streamutil.findLine(out, /^Error/)
.finally(function() {
out.end()
})
.timeout(10000)
.then(function(line) {
if (line.indexOf('--user') !== -1) {
return adb.shell(options.serial, util.format(
'am startservice %s'
, intent
))
.timeout(15000)
.then(function() {
return streamutil.findLine(out, /^Error/)
.finally(function() {
out.end()
})
.timeout(10000)
.then(function(line) {
throw new Error(util.format(
'Service had an error: "%s"'
, line
))
})
.catch(streamutil.NoSuchLineError, function() {
return true
})
})
}
else {
throw new Error(util.format(
'Service had an error: "%s"'
, line
))
}
})
.catch(streamutil.NoSuchLineError, function() {
return true
})
})
}
// The APK should be up to date at this point. If it was reinstalled, the
// service should have been automatically stopped while it was happening.
// So, we should be good to go.
function openService() {
log.info('Launching service')
return callService(util.format(
"-a '%s' -n '%s'"
, apk.startIntent.action
, apk.startIntent.component
))
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(15000)
})
.then(function(conn) {
service.socket = conn
service.reader = conn.pipe(new ms.DelimitedStream())
service.reader.on('data', handleEnvelope)
service.writer = new ms.DelimitingStream()
service.writer.pipe(conn)
lifecycle.share('Service connection', conn)
})
}
function handleEnvelope(data) {
var envelope = apk.wire.Envelope.decode(data)
, message
if (envelope.id !== null) {
messageResolver.resolve(envelope.id, envelope.message)
}
else {
switch (envelope.type) {
case apk.wire.MessageType.EVENT_AIRPLANE_MODE:
message = apk.wire.AirplaneModeEvent.decode(envelope.message)
push.send([
wireutil.global
, wireutil.envelope(new wire.AirplaneModeEvent(
options.serial
, message.enabled
))
])
plugin.emit('airplaneModeChange', message)
break
case apk.wire.MessageType.EVENT_BATTERY:
message = apk.wire.BatteryEvent.decode(envelope.message)
push.send([
wireutil.global
, wireutil.envelope(new wire.BatteryEvent(
options.serial
, message.status
, message.health
, message.source
, message.level
, message.scale
, message.temp
, message.voltage
))
])
plugin.emit('batteryChange', message)
break
case apk.wire.MessageType.EVENT_BROWSER_PACKAGE:
message = apk.wire.BrowserPackageEvent.decode(envelope.message)
plugin.emit('browserPackageChange', message)
break
case apk.wire.MessageType.EVENT_CONNECTIVITY:
message = apk.wire.ConnectivityEvent.decode(envelope.message)
push.send([
wireutil.global
, wireutil.envelope(new wire.ConnectivityEvent(
options.serial
, message.connected
, message.type
, message.subtype
, message.failover
, message.roaming
))
])
plugin.emit('connectivityChange', message)
break
case apk.wire.MessageType.EVENT_PHONE_STATE:
message = apk.wire.PhoneStateEvent.decode(envelope.message)
push.send([
wireutil.global
, wireutil.envelope(new wire.PhoneStateEvent(
options.serial
, message.state
, message.manual
, message.operator
))
])
plugin.emit('phoneStateChange', message)
break
case apk.wire.MessageType.EVENT_ROTATION:
message = apk.wire.RotationEvent.decode(envelope.message)
push.send([
wireutil.global
, wireutil.envelope(new wire.RotationEvent(
options.serial
, message.rotation
))
])
plugin.emit('rotationChange', message)
break
}
}
}
function keyEvent(data) {
return runAgentCommand(
apk.wire.MessageType.DO_KEYEVENT
, new apk.wire.KeyEventRequest(data)
)
}
plugin.type = function(text) {
return runAgentCommand(
apk.wire.MessageType.DO_TYPE
, new apk.wire.DoTypeRequest(text)
)
}
plugin.paste = function(text) {
return plugin.setClipboard(text)
.then(function() {
keyEvent({
event: apk.wire.KeyEvent.PRESS
, keyCode: adb.Keycode.KEYCODE_V
, ctrlKey: true
})
})
}
plugin.copy = function() {
// @TODO Not sure how to force the device to copy the current selection
// yet.
return plugin.getClipboard()
}
plugin.getDisplay = function(id) {
return runServiceCommand(
apk.wire.MessageType.GET_DISPLAY
, new apk.wire.GetDisplayRequest(id)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.GetDisplayResponse.decode(data)
if (response.success) {
return {
id: id
, width: response.width
, height: response.height
, xdpi: response.xdpi
, ydpi: response.ydpi
, fps: response.fps
, density: response.density
, rotation: response.rotation
, secure: response.secure
}
}
throw new Error('Unable to retrieve display information')
})
}
plugin.wake = function() {
return runAgentCommand(
apk.wire.MessageType.DO_WAKE
, new apk.wire.DoWakeRequest()
)
}
plugin.rotate = function(rotation) {
return runAgentCommand(
apk.wire.MessageType.SET_ROTATION
, new apk.wire.SetRotationRequest(rotation, false)
)
}
plugin.freezeRotation = function(rotation) {
return runAgentCommand(
apk.wire.MessageType.SET_ROTATION
, new apk.wire.SetRotationRequest(rotation, true)
)
}
plugin.thawRotation = function() {
return runAgentCommand(
apk.wire.MessageType.SET_ROTATION
, new apk.wire.SetRotationRequest(0, false)
)
}
plugin.version = function() {
return runServiceCommand(
apk.wire.MessageType.GET_VERSION
, new apk.wire.GetVersionRequest()
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.GetVersionResponse.decode(data)
if (response.success) {
return response.version
}
throw new Error('Unable to retrieve version')
})
}
plugin.unlock = function() {
return runServiceCommand(
apk.wire.MessageType.SET_KEYGUARD_STATE
, new apk.wire.SetKeyguardStateRequest(false)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetKeyguardStateResponse.decode(data)
if (!response.success) {
throw new Error('Unable to unlock device')
}
})
}
plugin.lock = function() {
return runServiceCommand(
apk.wire.MessageType.SET_KEYGUARD_STATE
, new apk.wire.SetKeyguardStateRequest(true)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetKeyguardStateResponse.decode(data)
if (!response.success) {
throw new Error('Unable to lock device')
}
})
}
plugin.acquireWakeLock = function() {
return runServiceCommand(
apk.wire.MessageType.SET_WAKE_LOCK
, new apk.wire.SetWakeLockRequest(true)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetWakeLockResponse.decode(data)
if (!response.success) {
throw new Error('Unable to acquire WakeLock')
}
})
}
plugin.releaseWakeLock = function() {
return runServiceCommand(
apk.wire.MessageType.SET_WAKE_LOCK
, new apk.wire.SetWakeLockRequest(false)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetWakeLockResponse.decode(data)
if (!response.success) {
throw new Error('Unable to release WakeLock')
}
})
}
plugin.identity = function() {
return runServiceCommand(
apk.wire.MessageType.DO_IDENTIFY
, new apk.wire.DoIdentifyRequest(options.serial)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.DoIdentifyResponse.decode(data)
if (!response.success) {
throw new Error('Unable to identify device')
}
})
}
plugin.setClipboard = function(text) {
return runServiceCommand(
apk.wire.MessageType.SET_CLIPBOARD
, new apk.wire.SetClipboardRequest(
apk.wire.ClipboardType.TEXT
, text
)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetClipboardResponse.decode(data)
if (!response.success) {
throw new Error('Unable to set clipboard')
}
})
}
plugin.getClipboard = function() {
return runServiceCommand(
apk.wire.MessageType.GET_CLIPBOARD
, new apk.wire.GetClipboardRequest(
apk.wire.ClipboardType.TEXT
)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.GetClipboardResponse.decode(data)
if (response.success) {
switch (response.type) {
case apk.wire.ClipboardType.TEXT:
return response.text
}
}
throw new Error('Unable to get clipboard')
})
}
plugin.getBrowsers = function() {
return runServiceCommand(
apk.wire.MessageType.GET_BROWSERS
, new apk.wire.GetBrowsersRequest()
)
.timeout(15000)
.then(function(data) {
var response = apk.wire.GetBrowsersResponse.decode(data)
if (response.success) {
delete response.success
return response
}
throw new Error('Unable to get browser list')
})
}
plugin.getProperties = function(properties) {
return runServiceCommand(
apk.wire.MessageType.GET_PROPERTIES
, new apk.wire.GetPropertiesRequest(properties)
)
.timeout(15000)
.then(function(data) {
var response = apk.wire.GetPropertiesResponse.decode(data)
if (response.success) {
var mapped = Object.create(null)
response.properties.forEach(function(property) {
mapped[property.name] = property.value
})
return mapped
}
throw new Error('Unable to get properties')
})
}
plugin.getAccounts = function(data) {
return runServiceCommand(
apk.wire.MessageType.GET_ACCOUNTS
, new apk.wire.GetAccountsRequest({type: data.type})
)
.timeout(15000)
.then(function(data) {
var response = apk.wire.GetAccountsResponse.decode(data)
if (response.success) {
return response.accounts
}
throw new Error('No accounts returned')
})
}
plugin.removeAccount = function(data) {
return runServiceCommand(
apk.wire.MessageType.DO_REMOVE_ACCOUNT
, new apk.wire.DoRemoveAccountRequest({
type: data.type
, account: data.account
})
)
.timeout(15000)
.then(function(data) {
var response = apk.wire.DoRemoveAccountResponse.decode(data)
if (response.success) {
return true
}
throw new Error('Unable to remove account')
})
}
plugin.addAccountMenu = function() {
return runServiceCommand(
apk.wire.MessageType.DO_ADD_ACCOUNT_MENU
, new apk.wire.DoAddAccountMenuRequest()
)
.timeout(15000)
.then(function(data) {
var response = apk.wire.DoAddAccountMenuResponse.decode(data)
if (response.success) {
return true
}
throw new Error('Unable to show add account menu')
})
}
plugin.setRingerMode = function(mode) {
return runServiceCommand(
apk.wire.MessageType.SET_RINGER_MODE
, new apk.wire.SetRingerModeRequest(mode)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetRingerModeResponse.decode(data)
if (!response.success) {
throw new Error('Unable to set ringer mode')
}
})
}
plugin.getRingerMode = function() {
return runServiceCommand(
apk.wire.MessageType.GET_RINGER_MODE
, new apk.wire.GetRingerModeRequest()
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.GetRingerModeResponse.decode(data)
// Reflection to decode enums to their string values, otherwise
// we only get an integer
var ringerMode = apk.builder.lookup('jp.co.cyberagent.stf.proto.RingerMode')
.children[response.mode].name
if (response.success) {
return ringerMode
}
throw new Error('Unable to get ringer mode')
})
}
plugin.setWifiEnabled = function(enabled) {
return runServiceCommand(
apk.wire.MessageType.SET_WIFI_ENABLED
, new apk.wire.SetWifiEnabledRequest(enabled)
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.SetWifiEnabledResponse.decode(data)
if (!response.success) {
throw new Error('Unable to set Wifi')
}
})
}
plugin.getWifiStatus = function() {
return runServiceCommand(
apk.wire.MessageType.GET_WIFI_STATUS
, new apk.wire.GetWifiStatusRequest()
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.GetWifiStatusResponse.decode(data)
if (response.success) {
return response.status
}
throw new Error('Unable to get Wifi status')
})
}
plugin.getSdStatus = function () {
return runServiceCommand(
apk.wire.MessageType.GET_SD_STATUS
, new apk.wire.GetSdStatusRequest()
)
.timeout(10000)
.then(function(data) {
var response = apk.wire.GetSdStatusResponse.decode(data)
if (response.success) {
return response.mounted
}
throw new Error('Unable to get SD card status')
})
}
plugin.pressKey = function(key) {
keyEvent({event: apk.wire.KeyEvent.PRESS, keyCode: keyutil.namedKey(key)})
return Promise.resolve(true)
}
function runServiceCommand(type, cmd) {
var resolver = Promise.defer()
var id = Math.floor(Math.random() * 0xFFFFFF)
service.writer.write(new apk.wire.Envelope(
id
, type
, cmd.encodeNB()
).encodeNB())
return messageResolver.await(id, resolver)
}
function runAgentCommand(type, cmd) {
agent.writer.write(new apk.wire.Envelope(
null
, type
, cmd.encodeNB()
).encodeNB())
}
return openAgent()
.then(openService)
.then(function() {
router
.on(wire.PhysicalIdentifyMessage, function(channel) {
var reply = wireutil.reply(options.serial)
plugin.identity()
push.send([
channel
, reply.okay()
])
})
.on(wire.KeyDownMessage, function(channel, message) {
try {
keyEvent({
event: apk.wire.KeyEvent.DOWN
, keyCode: keyutil.namedKey(message.key)
})
}
catch(e) {
log.warn(e.message)
}
})
.on(wire.KeyUpMessage, function(channel, message) {
try {
keyEvent({
event: apk.wire.KeyEvent.UP
, keyCode: keyutil.namedKey(message.key)
})
}
catch(e) {
log.warn(e.message)
}
})
.on(wire.KeyPressMessage, function(channel, message) {
try {
keyEvent({
event: apk.wire.KeyEvent.PRESS
, keyCode: keyutil.namedKey(message.key)
})
}
catch(e) {
log.warn(e.message)
}
})
.on(wire.TypeMessage, function(channel, message) {
plugin.type(message.text)
})
.on(wire.RotateMessage, function(channel, message) {
plugin.rotate(message.rotation)
})
})
.return(plugin)
})

View File

@@ -0,0 +1,86 @@
var Promise = require('bluebird')
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/sub'))
.define(function(options, adb, router, push, sub) {
var log = logger.createLogger('device:plugins:shell')
router.on(wire.ShellCommandMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Running shell command "%s"', message.command)
adb.shell(options.serial, message.command)
.timeout(10000)
.then(function(stream) {
var resolver = Promise.defer()
, timer
function keepAliveListener(channel, message) {
clearTimeout(timer)
timer = setTimeout(forceStop, message.timeout)
}
function readableListener() {
var chunk
while ((chunk = stream.read())) {
push.send([
channel
, reply.progress(chunk)
])
}
}
function endListener() {
push.send([
channel
, reply.okay(null)
])
resolver.resolve()
}
function errorListener(err) {
resolver.reject(err)
}
function forceStop() {
stream.end()
}
stream.setEncoding('utf8')
stream.on('readable', readableListener)
stream.on('end', endListener)
stream.on('error', errorListener)
sub.subscribe(channel)
router.on(wire.ShellKeepAliveMessage, keepAliveListener)
timer = setTimeout(forceStop, message.timeout)
return resolver.promise.finally(function() {
stream.removeListener('readable', readableListener)
stream.removeListener('end', endListener)
stream.removeListener('error', errorListener)
sub.unsubscribe(channel)
router.removeListener(wire.ShellKeepAliveMessage, keepAliveListener)
clearTimeout(timer)
})
})
.error(function(err) {
log.error('Shell command "%s" failed', message.command, err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,50 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/sub'))
.dependency(require('../support/push'))
.dependency(require('../support/router'))
.dependency(require('./identity'))
.define(function(options, sub, push, router, identity) {
var log = logger.createLogger('device:plugins:solo')
var channel = wireutil.makePrivateChannel()
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
router.on(wire.ProbeMessage, function() {
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceIdentityMessage(
options.serial
, identity.platform
, identity.manufacturer
, identity.operator
, identity.model
, identity.version
, identity.abi
, identity.sdk
, new wire.DeviceDisplayMessage(identity.display)
, new wire.DevicePhoneMessage(identity.phone)
, identity.product
))
])
})
return {
channel: channel
, poke: function() {
push.send([
wireutil.global
, wireutil.envelope(new wire.DevicePokeMessage(
options.serial
, channel
))
])
}
}
})

View File

@@ -0,0 +1,46 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var devutil = require('../../../util/devutil')
var lifecycle = require('../../../util/lifecycle')
var streamutil = require('../../../util/streamutil')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../resources/remote'))
.define(function(options, adb, remote) {
var log = logger.createLogger('device:plugins:stats')
var service = {
port: 2830
}
function openService() {
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-stats', service.port
])
.timeout(10000)
.then(function(out) {
lifecycle.share('Stats shell', out)
streamutil.talk(log, 'Stats shell says: "%s"', out)
})
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(15000)
})
.then(function(conn) {
return lifecycle.share('Stats connection', conn)
})
}
return openService()
.then(function() {
return {}
})
})

View File

@@ -0,0 +1,40 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/adb'))
.define(function(options, router, push, adb) {
var log = logger.createLogger('device:plugins:store')
router.on(wire.StoreOpenMessage, function(channel) {
log.info('Opening Play Store')
var reply = wireutil.reply(options.serial)
adb.startActivity(options.serial, {
action: 'android.intent.action.MAIN'
, component: 'com.android.vending/.AssetBrowserActivity'
// FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
// FLAG_ACTIVITY_BROUGHT_TO_FRONT
// FLAG_ACTIVITY_NEW_TASK
, flags: 0x10600000
})
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Play Store could not be opened', err.stack)
push.send([
channel
, reply.fail()
])
})
})
})

View File

@@ -0,0 +1,142 @@
var Promise = require('bluebird')
var syrup = require('syrup')
var monkey = require('adbkit-monkey')
var wire = require('../../../wire')
var devutil = require('../../../util/devutil')
var logger = require('../../../util/logger')
var lifecycle = require('../../../util/lifecycle')
var streamutil = require('../../../util/streamutil')
var SeqQueue = require('../../../wire/seqqueue')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../resources/remote'))
.dependency(require('./display'))
.dependency(require('./data'))
.define(function(options, adb, router, remote, display, data) {
var log = logger.createLogger('device:plugins:touch')
var plugin = Object.create(null)
var service = {
port: 2820
}
function openService() {
log.info('Launching touch service')
return devutil.ensureUnusedPort(adb, options.serial, service.port)
.timeout(10000)
.then(function() {
return adb.shell(options.serial, [
remote.bin
, '--lib', remote.lib
, '--listen-input', service.port
])
.timeout(10000)
})
.then(function(out) {
lifecycle.share('Touch shell', out)
streamutil.talk(log, 'Touch shell says: "%s"', out)
})
.then(function() {
return devutil.waitForPort(adb, options.serial, service.port)
.timeout(15000)
})
.then(function(conn) {
return Promise.promisifyAll(monkey.connectStream(conn))
})
.then(function(monkey) {
return lifecycle.share('Touch monkey', monkey)
})
}
function modifyCoords(message) {
message.x = Math.floor(message.x * display.width)
message.y = Math.floor(message.y * display.height)
}
return openService()
.then(function(monkey) {
var queue = new SeqQueue()
, pressure = (data && data.touch && data.touch.defaultPressure) || 50
log.info('Setting default pressure to %d', pressure)
plugin.touchDown = function(point) {
modifyCoords(point)
monkey.sendAsync([
'touch down'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('touchDown failed', err.stack)
})
}
plugin.touchMove = function(point) {
modifyCoords(point)
monkey.sendAsync([
'touch move'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('touchMove failed', err.stack)
})
}
plugin.touchUp = function(point) {
modifyCoords(point)
monkey.sendAsync([
'touch up'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('touchUp failed', err.stack)
})
}
plugin.tap = function(point) {
modifyCoords(point)
monkey.sendAsync([
'tap'
, point.x
, point.y
, pressure
].join(' '))
.catch(function(err) {
log.error('tap failed', err.stack)
})
}
router
.on(wire.TouchDownMessage, function(channel, message) {
queue.push(message.seq, function() {
plugin.touchDown(message)
})
})
.on(wire.TouchMoveMessage, function(channel, message) {
queue.push(message.seq, function() {
plugin.touchMove(message)
})
})
.on(wire.TouchUpMessage, function(channel, message) {
queue.push(message.seq, function() {
plugin.touchUp(message)
})
// Reset queue
queue = new SeqQueue()
})
.on(wire.TapMessage, function(channel, message) {
plugin.tap(message)
})
})
.return(plugin)
})

View File

@@ -0,0 +1,53 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('./service'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.define(function(options, service, router, push) {
var log = logger.createLogger('device:plugins:wifi')
router.on(wire.WifiSetEnabledMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
log.info('Setting Wifi "%s"', message.enabled)
service.setWifiEnabled(message.enabled)
.timeout(30000)
.then(function() {
push.send([
channel
, reply.okay()
])
})
.catch(function(err) {
log.error('Setting Wifi enabled failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.WifiGetStatusMessage, function(channel){
var reply = wireutil.reply(options.serial)
log.info('Getting Wifi status')
service.getWifiStatus()
.timeout(30000)
.then(function(enabled) {
push.send([
channel
, reply.okay(enabled ? 'wifi_enabled' : 'wifi_disabled')
])
})
.catch(function(err) {
log.error('Getting Wifi status failed', err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
})

View File

@@ -0,0 +1,103 @@
var util = require('util')
var Promise = require('bluebird')
var syrup = require('syrup')
var logger = require('../../../util/logger')
var pathutil = require('../../../util/pathutil')
var devutil = require('../../../util/devutil')
var streamutil = require('../../../util/streamutil')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/properties'))
.define(function(options, adb, properties) {
var log = logger.createLogger('device:resources:remote')
var resources = {
bin: {
src: pathutil.vendor(util.format(
'remote/libs/%s/remote'
, properties['ro.product.cpu.abi']
))
, dest: '/data/local/tmp/remote'
, comm: 'remote'
, mode: 0755
}
, lib: {
src: pathutil.vendor(util.format(
'remote/external/android-%d/remote_external.so'
, properties['ro.build.version.sdk']
))
, dest: '/data/local/tmp/remote_external.so'
, mode: 0755
}
}
function removeResource(res) {
return adb.shell(options.serial, ['rm', res.dest])
.timeout(10000)
.then(function(out) {
return streamutil.readAll(out)
})
.return(res)
}
function installResource(res) {
return adb.push(options.serial, res.src, res.dest, res.mode)
.timeout(10000)
.then(function(transfer) {
return new Promise(function(resolve, reject) {
transfer.on('error', reject)
transfer.on('end', resolve)
})
})
.return(res)
}
function ensureNotBusy(res) {
return adb.shell(options.serial, [res.dest, '--help'])
.timeout(10000)
.then(function(out) {
// Can be "Text is busy", "text busy"
return streamutil.findLine(out, (/busy/i))
.timeout(10000)
.then(function() {
log.info('Binary is busy, will retry')
return Promise.delay(1000)
})
.then(function() {
return ensureNotBusy(res)
})
.catch(streamutil.NoSuchLineError, function() {
return res
})
})
}
function installAll() {
return Promise.all([
removeResource(resources.bin).then(installResource).then(ensureNotBusy)
, removeResource(resources.lib).then(installResource)
])
}
function stop() {
return devutil.killProcsByComm(
adb
, options.serial
, resources.bin.comm
, resources.bin.dest
)
.timeout(15000)
}
return stop()
.then(installAll)
.then(function() {
return {
bin: resources.bin.dest
, lib: resources.lib.dest
}
})
})

View File

@@ -0,0 +1,104 @@
var util = require('util')
var syrup = require('syrup')
var ProtoBuf = require('protobufjs')
var semver = require('semver')
var pathutil = require('../../../util/pathutil')
var streamutil = require('../../../util/streamutil')
var promiseutil = require('../../../util/promiseutil')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.define(function(options, adb) {
var log = logger.createLogger('device:resources:service')
var builder = ProtoBuf.loadProtoFile(pathutil.vendor('STFService/wire.proto'))
var resource = {
requiredVersion: '0.7.21'
, pkg: 'jp.co.cyberagent.stf'
, main: 'jp.co.cyberagent.stf.Agent'
, apk: pathutil.vendor('STFService/STFService.apk')
, wire: builder.build().jp.co.cyberagent.stf.proto
, builder: builder
, startIntent: {
action: 'jp.co.cyberagent.stf.ACTION_START'
, component: 'jp.co.cyberagent.stf/.Service'
}
}
function getPath() {
return adb.shell(options.serial, ['pm', 'path', resource.pkg])
.timeout(10000)
.then(function(out) {
return streamutil.findLine(out, (/^package:/))
.timeout(15000)
.then(function(line) {
return line.substr(8)
})
})
}
function install() {
log.info('Checking whether we need to install STFService')
return getPath()
.then(function(installedPath) {
log.info('Running version check')
return adb.shell(options.serial, util.format(
"export CLASSPATH='%s';" +
" exec app_process /system/bin '%s' --version"
, installedPath
, resource.main
))
.timeout(10000)
.then(function(out) {
return streamutil.readAll(out)
.timeout(10000)
.then(function(buffer) {
var version = buffer.toString()
if (semver.satisfies(version, resource.requiredVersion)) {
return installedPath
}
else {
throw new Error(util.format(
'Incompatible version %s'
, version
))
}
})
})
})
.catch(function() {
log.info('Installing STFService')
// Uninstall first to make sure we don't have any certificate
// issues.
return adb.uninstall(options.serial, resource.pkg)
.timeout(15000)
.then(function() {
return promiseutil.periodicNotify(
adb.install(options.serial, resource.apk)
, 10000
)
.timeout(60000)
})
.progressed(function() {
log.warn(
'STFService installation is taking a long time; '
+ 'perhaps you have to accept 3rd party app installation '
+ 'on the device?'
)
})
.then(function() {
return getPath()
})
})
}
return install()
.then(function(path) {
log.info('STFService up to date')
resource.path = path
return resource
})
})

View File

@@ -0,0 +1,30 @@
var syrup = require('syrup')
var adbkit = require('adbkit')
var logger = require('../../../util/logger')
var promiseutil = require('../../../util/promiseutil')
module.exports = syrup.serial()
.define(function(options) {
var log = logger.createLogger('device:support:adb')
var adb = adbkit.createClient({
host: options.adbHost
, port: options.adbPort
})
adb.Keycode = adbkit.Keycode
function ensureBootComplete() {
return promiseutil.periodicNotify(
adb.waitBootComplete(options.serial)
, 1000
)
.progressed(function() {
log.info('Waiting for boot to complete')
})
.timeout(60000)
}
return ensureBootComplete()
.return(adb)
})

View File

@@ -0,0 +1,14 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
var ChannelManager = require('../../../wire/channelmanager')
module.exports = syrup.serial()
.define(function() {
var log = logger.createLogger('device:support:channels')
var channels = new ChannelManager()
channels.on('timeout', function(channel) {
log.info('Channel "%s" timed out', channel)
})
return channels
})

View File

@@ -0,0 +1,29 @@
var http = require('http')
var util = require('util')
var syrup = require('syrup')
var express = require('express')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.define(function(options) {
var log = logger.createLogger('device:support:http')
, port = options.ports.pop()
, app = express()
, server = http.createServer(app)
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.set('public url', util.format(
'http://%s:%s'
, options.publicIp
, port
))
server.listen(port)
log.info('Listening on %s', app.get('public url'))
return app
})

View File

@@ -0,0 +1,17 @@
var syrup = require('syrup')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.dependency(require('./adb'))
.define(function(options, adb) {
var log = logger.createLogger('device:support:properties')
function load() {
log.info('Loading properties')
return adb.getProperties(options.serial)
.timeout(10000)
}
return load()
})

View File

@@ -0,0 +1,19 @@
var syrup = require('syrup')
var zmq = require('zmq')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.define(function(options) {
var log = logger.createLogger('device:support:push')
// Output
var push = zmq.socket('push')
options.endpoints.push.forEach(function(endpoint) {
log.info('Sending output to %s', endpoint)
push.connect(endpoint)
})
return push
})

View File

@@ -0,0 +1,19 @@
var syrup = require('syrup')
var wirerouter = require('../../../wire/router')
module.exports = syrup.serial()
.dependency(require('./sub'))
.dependency(require('./channels'))
.define(function(options, sub, channels) {
var router = wirerouter()
sub.on('message', router.handler())
// Special case, we're hooking into a message that's not actually routed.
router.on({$code: 'message'}, function(channel) {
channels.keepalive(channel)
})
return router
})

View File

@@ -0,0 +1,53 @@
var util = require('util')
var syrup = require('syrup')
var Promise = require('bluebird')
var request = require('request')
var logger = require('../../../util/logger')
module.exports = syrup.serial()
.define(function(options) {
var log = logger.createLogger('device:support:storage')
var plugin = Object.create(null)
plugin.store = function(type, stream, meta) {
var resolver = Promise.defer()
var req = request.post({
url: util.format('%sapi/v1/s/%s', options.storageUrl, type)
}
, function(err, res, body) {
if (err) {
log.error('Upload failed', err.stack)
resolver.reject(err)
}
else if (res.statusCode !== 201) {
log.error('Upload failed: HTTP %d', res.statusCode)
resolver.reject(new Error(util.format(
'Upload failed: HTTP %d'
, res.statusCode
)))
}
else {
try {
var result = JSON.parse(body)
log.info('Uploaded to %s', result.resources.file.href)
resolver.resolve(result.resources.file)
}
catch (err) {
log.error('Invalid JSON in response', err.stack, body)
resolver.reject(err)
}
}
}
)
req.form()
.append('file', stream, meta)
return resolver.promise
}
return plugin
})

View File

@@ -0,0 +1,26 @@
var syrup = require('syrup')
var zmq = require('zmq')
var logger = require('../../../util/logger')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.define(function(options) {
var log = logger.createLogger('device:support:sub')
// Input
var sub = zmq.socket('sub')
options.endpoints.sub.forEach(function(endpoint) {
log.info('Receiving input from %s', endpoint)
sub.connect(endpoint)
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
})
return sub
})

View File

@@ -0,0 +1,77 @@
var util = require('util')
var Hipchatter = require('hipchatter')
var Promise = require('bluebird')
var zmq = require('zmq')
var logger = require('../../util/logger')
var wire = require('../../wire')
var wirerouter = require('../../wire/router')
var wireutil = require('../../wire/util')
var lifecycle = require('../../util/lifecycle')
var COLORS = {
1: 'gray'
, 2: 'gray'
, 3: 'green'
, 4: 'purple'
, 5: 'yellow'
, 6: 'red'
, 7: 'red'
}
module.exports = function(options) {
var log = logger.createLogger('notify-hipchat')
var client = Promise.promisifyAll(new Hipchatter(options.token))
var buffer = []
, timer
// Input
var sub = zmq.socket('sub')
options.endpoints.sub.forEach(function(endpoint) {
log.info('Receiving input from %s', endpoint)
sub.connect(endpoint)
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
})
sub.on('message', wirerouter()
.on(wire.DeviceLogMessage, function(channel, message) {
if (message.priority >= options.priority) {
buffer.push(message)
clearTimeout(timer)
timer = setTimeout(push, 1000)
}
})
.handler())
function push() {
buffer.splice(0).forEach(function(entry) {
client.notifyAsync(options.room, {
message: util.format(
'<strong>%s</strong>/<strong>%s</strong> %d [<strong>%s</strong>] %s'
, logger.LevelLabel[entry.priority]
, entry.tag
, entry.pid
, entry.identifier
, entry.message
)
, color: COLORS[entry.priority]
, notify: entry.priority >= options.notifyPriority
, 'message_format': 'html'
, token: options.token
})
})
}
log.info('Listening for %s (or higher) level log messages',
logger.LevelLabel[options.priority])
lifecycle.observe(function() {
sub.close()
})
}

View File

@@ -0,0 +1,131 @@
var zmq = require('zmq')
var logger = require('../../util/logger')
var wire = require('../../wire')
var wirerouter = require('../../wire/router')
var wireutil = require('../../wire/util')
var dbapi = require('../../db/api')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
var log = logger.createLogger('processor')
if (options.name) {
logger.setGlobalIdentifier(options.name)
}
// App side
var appDealer = zmq.socket('dealer')
options.endpoints.appDealer.forEach(function(endpoint) {
log.info('App dealer connected to %s', endpoint)
appDealer.connect(endpoint)
})
appDealer.on('message', function(channel, data) {
devDealer.send([channel, data])
})
// Device side
var devDealer = zmq.socket('dealer')
options.endpoints.devDealer.forEach(function(endpoint) {
log.info('Device dealer connected to %s', endpoint)
devDealer.connect(endpoint)
})
devDealer.on('message', wirerouter()
// Provider messages
.on(wire.ProviderHeartbeatMessage, function(channel, message) {
dbapi.updateProviderHeartbeat(message.channel)
})
// Initial device message
.on(wire.DevicePresentMessage, function(channel, message, data) {
dbapi.saveDevice(message.serial, message)
.then(function() {
devDealer.send([
message.provider.channel
, wireutil.envelope(new wire.DeviceRegisteredMessage(
message.serial
))
])
appDealer.send([channel, data])
})
})
// Workerless messages
.on(wire.DeviceAbsentMessage, function(channel, message, data) {
dbapi.setDeviceAbsent(message.serial)
appDealer.send([channel, data])
})
.on(wire.DeviceStatusMessage, function(channel, message, data) {
dbapi.saveDeviceStatus(message.serial, message.status)
appDealer.send([channel, data])
})
// Worker initialized
.on(wire.DevicePokeMessage, function(channel, message) {
dbapi.setDeviceChannel(message.serial, message.channel)
.then(function() {
devDealer.send([
message.channel
, wireutil.envelope(new wire.ProbeMessage())
])
})
})
// Worker messages
.on(wire.JoinGroupMessage, function(channel, message, data) {
dbapi.setDeviceOwner(message.serial, message.owner)
appDealer.send([channel, data])
})
.on(wire.LeaveGroupMessage, function(channel, message, data) {
dbapi.unsetDeviceOwner(message.serial, message.owner)
appDealer.send([channel, data])
})
.on(wire.DeviceLogMessage, function(channel, message, data) {
dbapi.saveDeviceLog(message.serial, message)
appDealer.send([channel, data])
})
.on(wire.DeviceIdentityMessage, function(channel, message, data) {
dbapi.saveDeviceIdentity(message.serial, message)
appDealer.send([channel, data])
})
.on(wire.TransactionProgressMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.TransactionDoneMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.DeviceLogcatEntryMessage, function(channel, message, data) {
appDealer.send([channel, data])
})
.on(wire.AirplaneModeEvent, function(channel, message, data) {
dbapi.setDeviceAirplaneMode(message.serial, message.enabled)
appDealer.send([channel, data])
})
.on(wire.BatteryEvent, function(channel, message, data) {
dbapi.setDeviceBattery(message.serial, message)
appDealer.send([channel, data])
})
.on(wire.DeviceBrowserMessage, function(channel, message, data) {
dbapi.setDeviceBrowser(message.serial, message)
appDealer.send([channel, data])
})
.on(wire.ConnectivityEvent, function(channel, message, data) {
dbapi.setDeviceConnectivity(message.serial, message)
appDealer.send([channel, data])
})
.on(wire.PhoneStateEvent, function(channel, message, data) {
dbapi.setDevicePhoneState(message.serial, message)
appDealer.send([channel, data])
})
.on(wire.RotationEvent, function(channel, message, data) {
dbapi.setDeviceRotation(message.serial, message.rotation)
appDealer.send([channel, data])
})
.handler())
lifecycle.observe(function() {
try {
appDealer.close()
devDealer.close()
}
catch (err) {}
})
}

409
lib/units/provider/index.js Normal file
View File

@@ -0,0 +1,409 @@
var events = require('events')
var adb = require('adbkit')
var Promise = require('bluebird')
var zmq = require('zmq')
var _ = require('lodash')
var logger = require('../../util/logger')
var wire = require('../../wire')
var wireutil = require('../../wire/util')
var wirerouter = require('../../wire/router')
var procutil = require('../../util/procutil')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
var log = logger.createLogger('provider')
var client = adb.createClient({
host: options.adbHost
, port: options.adbPort
})
var workers = {}
var solo = wireutil.makePrivateChannel()
var lists = {
all: []
, ready: []
, waiting: []
}
var totalsTimer
// To make sure that we always bind the same type of service to the same
// port, we must ensure that we allocate ports in fixed groups.
var ports = options.ports.slice(
0
, options.ports.length - options.ports.length % 4
)
// Information about total devices
var delayedTotals = (function() {
function totals() {
if (lists.waiting.length) {
log.info(
'Providing %d of %d device(s); waiting for "%s"'
, lists.ready.length
, lists.all.length
, lists.waiting.join('", "')
)
delayedTotals()
}
else if (lists.ready.length < lists.all.length) {
log.info(
'Providing all %d of %d device(s); ignoring "%s"'
, lists.ready.length
, lists.all.length
, _.difference(lists.all, lists.ready).join('", "')
)
}
else {
log.info(
'Providing all %d device(s)'
, lists.all.length
)
}
}
return function() {
clearTimeout(totalsTimer)
totalsTimer = setTimeout(totals, 10000)
}
})()
// Output
var push = zmq.socket('push')
options.endpoints.push.forEach(function(endpoint) {
log.info('Sending output to %s', endpoint)
push.connect(endpoint)
})
// Input
var sub = zmq.socket('sub')
options.endpoints.sub.forEach(function(endpoint) {
log.info('Receiving input from %s', endpoint)
sub.connect(endpoint)
})
// Establish always-on channels
;[solo].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
})
// Track and manage devices
client.trackDevices().then(function(tracker) {
log.info('Tracking devices')
// This can happen when ADB doesn't have a good connection to
// the device
function isWeirdUnusableDevice(device) {
return device.id === '????????????'
}
// Helper for ignoring unwanted devices
function filterDevice(listener) {
return function(device) {
if (isWeirdUnusableDevice(device)) {
log.warn('ADB lists a weird device: "%s"', device.id)
return false
}
if (options.filter && !options.filter(device)) {
return false
}
return listener(device)
}
}
// To make things easier, we're going to cheat a little, and make all
// device events go to their own EventEmitters. This way we can keep all
// device data in the same scope.
var flippedTracker = new events.EventEmitter()
tracker.on('add', filterDevice(function(device) {
log.info('Found device "%s" (%s)', device.id, device.type)
var privateTracker = new events.EventEmitter()
, willStop = false
, timer
, worker
// Wait for others to acknowledge the device
var register = new Promise(function(resolve) {
// Tell others we found a device
push.send([
wireutil.global
, wireutil.envelope(new wire.DevicePresentMessage(
device.id
, wireutil.toDeviceStatus(device.type)
, new wire.ProviderMessage(
solo
, options.name
)
))
])
privateTracker.once('register', resolve)
})
register.then(function() {
log.info('Registered device "%s"', device.id)
check()
})
// Statistics
lists.all.push(device.id)
delayedTotals()
// Will be set to false when the device is removed
_.assign(device, {
present: true
})
// When any event occurs on the added device
function deviceListener(type, updatedDevice) {
// Okay, this is a bit unnecessary but it allows us to get rid of an
// ugly switch statement and return to the original style.
privateTracker.emit(type, updatedDevice)
}
// When the added device changes
function changeListener(updatedDevice) {
register.then(function() {
log.info(
'Device "%s" is now "%s" (was "%s")'
, device.id
, updatedDevice.type
, device.type
)
_.assign(device, {
type: updatedDevice.type
})
// Tell others the device changed
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceStatusMessage(
device.id
, wireutil.toDeviceStatus(device.type)
))
])
check()
})
}
// When the added device gets removed
function removeListener() {
register.then(function() {
log.info('Lost device "%s" (%s)', device.id, device.type)
clearTimeout(timer)
flippedTracker.removeListener(device.id, deviceListener)
_.pull(lists.all, device.id)
delayedTotals()
// Tell others the device is gone
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceAbsentMessage(
device.id
))
])
_.assign(device, {
present: false
})
check()
})
}
// Check if we can do anything with the device
function check() {
clearTimeout(timer)
if (device.present) {
// We might get multiple status updates in rapid succession,
// so let's wait for a while
switch (device.type) {
case 'device':
case 'emulator':
willStop = false
timer = setTimeout(work, 100)
break
default:
willStop = true
timer = setTimeout(stop, 100)
break
}
}
else {
stop()
}
}
// Starts a device worker and keeps it alive
function work() {
return (worker = workers[device.id] = spawn())
.then(function() {
log.info('Device worker "%s" has retired', device.id)
delete workers[device.id]
worker = null
})
.catch(procutil.ExitError, function(err) {
if (!willStop) {
log.error(
'Device worker "%s" died with code %s'
, device.id
, err.code
)
log.info('Restarting device worker "%s"', device.id)
return Promise.delay(500)
.then(function() {
return work()
})
}
})
}
// No more work required
function stop() {
if (worker) {
log.info('Shutting down device worker "%s"', device.id)
worker.cancel()
}
}
// Spawn a device worker
function spawn() {
var allocatedPorts = ports.splice(0, 4)
, proc = options.fork(device, allocatedPorts)
, resolver = Promise.defer()
function exitListener(code, signal) {
if (signal) {
log.warn(
'Device worker "%s" was killed with signal %s, assuming ' +
'deliberate action and not restarting'
, device.id
, signal
)
resolver.resolve()
}
else if (code === 0) {
log.info('Device worker "%s" stopped cleanly', device.id)
resolver.resolve()
}
else {
resolver.reject(new procutil.ExitError(code))
}
}
function errorListener(err) {
log.error(
'Device worker "%s" had an error: %s'
, device.id
, err.message
)
}
function messageListener(message) {
switch (message) {
case 'ready':
_.pull(lists.waiting, device.id)
lists.ready.push(device.id)
break
default:
log.warn(
'Unknown message from device worker "%s": "%s"'
, device.id
, message
)
break
}
}
proc.on('exit', exitListener)
proc.on('error', errorListener)
proc.on('message', messageListener)
lists.waiting.push(device.id)
return resolver.promise
.finally(function() {
log.info('Cleaning up device worker "%s"', device.id)
proc.removeListener('exit', exitListener)
proc.removeListener('error', errorListener)
proc.removeListener('message', messageListener)
// Return used ports to the main pool
Array.prototype.push.apply(ports, allocatedPorts)
// Update lists
_.pull(lists.ready, device.id)
_.pull(lists.waiting, device.id)
})
.cancellable()
.catch(Promise.CancellationError, function() {
log.info('Gracefully killing device worker "%s"', device.id)
return procutil.gracefullyKill(proc, options.killTimeout)
})
.catch(Promise.TimeoutError, function(err) {
log.error(
'Device worker "%s" did not stop in time: %s'
, device.id
, err.message
)
})
}
flippedTracker.on(device.id, deviceListener)
privateTracker.on('change', changeListener)
privateTracker.on('remove', removeListener)
}))
tracker.on('change', filterDevice(function(device) {
flippedTracker.emit(device.id, 'change', device)
}))
tracker.on('remove', filterDevice(function(device) {
flippedTracker.emit(device.id, 'remove', device)
}))
sub.on('message', wirerouter()
.on(wire.DeviceRegisteredMessage, function(channel, message) {
flippedTracker.emit(message.serial, 'register')
})
.handler())
lifecycle.share('Tracker', tracker)
})
// This keeps the devices "present" in the database. It relies on the
// provider channel changing on every run so that we never match old
// devices.
;(function heartbeat() {
push.send([
wireutil.heartbeat
, wireutil.envelope(new wire.ProviderHeartbeatMessage(
solo
))
])
setTimeout(heartbeat, options.heartbeatInterval)
})()
lifecycle.observe(function() {
clearTimeout(totalsTimer)
try {
push.close()
sub.close()
}
catch (err) {}
return Promise.all(Object.keys(workers).map(function(serial) {
return workers[serial].cancel()
}))
})
}

58
lib/units/reaper/index.js Normal file
View File

@@ -0,0 +1,58 @@
var Promise = require('bluebird')
var zmq = require('zmq')
var logger = require('../../util/logger')
var wire = require('../../wire')
var wireutil = require('../../wire/util')
var dbapi = require('../../db/api')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
var log = logger.createLogger('reaper')
, timer
if (options.name) {
logger.setGlobalIdentifier(options.name)
}
// Output
var push = zmq.socket('push')
options.endpoints.push.forEach(function(endpoint) {
log.info('Sending output to %s', endpoint)
push.connect(endpoint)
})
function reap() {
dbapi.getDeadDevices(options.heartbeatTimeout)
.then(function(cursor) {
return Promise.promisify(cursor.toArray, cursor)()
.then(function(list) {
list.forEach(function(device) {
log.info('Reaping device "%s"', device.serial)
push.send([
wireutil.global
, wireutil.envelope(new wire.DeviceAbsentMessage(
device.serial
))
])
})
})
})
.catch(function(err) {
log.error('Failed to load device list: ', err.message, err.stack)
lifecycle.fatal()
})
}
timer = setInterval(reap, options.reapInterval)
log.info('Reaping devices with no heartbeat')
lifecycle.observe(function() {
clearTimeout(timer)
try {
push.close()
}
catch (err) {}
})
}

View File

@@ -0,0 +1,54 @@
var http = require('http')
var url = require('url')
var express = require('express')
var httpProxy = require('http-proxy')
var logger = require('../../../../util/logger')
var download = require('../../../../util/download')
var manifest = require('./task/manifest')
module.exports = function(options) {
var log = logger.createLogger('storage:plugins:apk')
, app = express()
, server = http.createServer(app)
, proxy = httpProxy.createProxyServer()
proxy.on('error', function(err) {
log.error('Proxy had an error', err.stack)
})
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.set('trust proxy', true)
app.get('/api/v1/s/apk/:id/*/manifest', function(req, res) {
download(url.resolve(options.storageUrl, req.url), {
dir: options.cacheDir
})
.then(manifest)
.then(function(data) {
res.status(200)
.json({
success: true
, manifest: data
})
})
.catch(function(err) {
log.error('Unable to read manifest of "%s"', req.params.id, err.stack)
res.status(500)
.json({
success: false
})
})
})
app.get('/api/v1/s/apk/:id/*', function(req, res) {
proxy.web(req, res, {
target: options.storageUrl
})
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -0,0 +1,19 @@
var Promise = require('bluebird')
var ApkReader = require('adbkit-apkreader')
module.exports = function(file) {
var resolver = Promise.defer()
process.nextTick(function() {
try {
var reader = ApkReader.readFile(file.path)
var manifest = reader.readManifestSync()
resolver.resolve(manifest)
}
catch (err) {
resolver.reject(err)
}
})
return resolver.promise
}

View File

@@ -0,0 +1,52 @@
var http = require('http')
var express = require('express')
var logger = require('../../../../util/logger')
var requtil = require('../../../../util/requtil')
var parseCrop = require('./param/crop')
var parseGravity = require('./param/gravity')
var get = require('./task/get')
var transform = require('./task/transform')
module.exports = function(options) {
var log = logger.createLogger('storage:plugins:image')
, app = express()
, server = http.createServer(app)
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.set('trust proxy', true)
app.get(
'/api/v1/s/image/:id/*'
, requtil.limit(options.concurrency, function(req, res) {
return get(req.url, options)
.then(function(stream) {
return transform(stream, {
crop: parseCrop(req.query.crop)
, gravity: parseGravity(req.query.gravity)
})
})
.then(function(out) {
res.status(200)
out.pipe(res)
})
.catch(function(err) {
log.error(
'Unable to transform resource "%s"'
, req.params.id
, err.stack
)
res.status(500)
.json({
success: false
})
})
})
)
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -0,0 +1,14 @@
var RE_CROP = /^([0-9]*)x([0-9]*)$/
module.exports = function(raw) {
var parsed
if (raw && (parsed = RE_CROP.exec(raw))) {
return {
width: +parsed[1] || 0
, height: +parsed[2] || 0
}
}
return null
}

View File

@@ -0,0 +1,21 @@
var GRAVITY = {
northwest: 'NorthWest'
, north: 'North'
, northeast: 'NorthEast'
, west: 'West'
, center: 'Center'
, east: 'East'
, southwest: 'SouthWest'
, south: 'South'
, southeast: 'SouthEast'
}
module.exports = function(raw) {
var parsed
if (raw && (parsed = GRAVITY[raw])) {
return parsed
}
return null
}

View File

@@ -0,0 +1,20 @@
var util = require('util')
var http = require('http')
var url = require('url')
var Promise = require('bluebird')
module.exports = function(path, options) {
return new Promise(function(resolve, reject) {
http.get(url.resolve(options.storageUrl, path))
.on('response', function(res) {
if (res.statusCode !== 200) {
reject(new Error(util.format('HTTP %d', res.statusCode)))
}
else {
resolve(res)
}
})
.on('error', reject)
})
}

View File

@@ -0,0 +1,26 @@
var gm = require('gm')
var Promise = require('bluebird')
module.exports = function(stream, options) {
return new Promise(function(resolve, reject) {
var transform = gm(stream)
if (options.gravity) {
transform.gravity(options.gravity)
}
if (options.crop) {
transform.geometry(options.crop.width, options.crop.height, '^')
transform.crop(options.crop.width, options.crop.height, 0, 0)
}
transform.stream(function(err, stdout) {
if (err) {
reject(err)
}
else {
resolve(stdout)
}
})
})
}

149
lib/units/storage/temp.js Normal file
View File

@@ -0,0 +1,149 @@
var http = require('http')
var util = require('util')
var path = require('path')
var express = require('express')
var validator = require('express-validator')
var bodyParser = require('body-parser')
var formidable = require('formidable')
var Promise = require('bluebird')
var logger = require('../../util/logger')
var Storage = require('../../util/storage')
var requtil = require('../../util/requtil')
var download = require('../../util/download')
module.exports = function(options) {
var log = logger.createLogger('storage:temp')
, app = express()
, server = http.createServer(app)
, storage = new Storage()
app.set('strict routing', true)
app.set('case sensitive routing', true)
app.set('trust proxy', true)
app.use(bodyParser.json())
app.use(validator())
storage.on('timeout', function(id) {
log.info('Cleaning up inactive resource "%s"', id)
})
app.post('/api/v1/s/:type/download', function(req, res) {
requtil.validate(req, function() {
req.checkBody('url').notEmpty()
})
.then(function() {
return download(req.body.url, {
dir: options.cacheDir
})
})
.then(function(file) {
return {
id: storage.store(file)
, name: file.name
}
})
.then(function(file) {
res.status(201)
.json({
success: true
, resource: {
date: new Date()
, type: req.params.type
, id: file.id
, name: file.name
, href: util.format(
'/api/v1/s/%s/%s%s'
, req.params.type
, file.id
, file.name
? util.format('/%s', path.basename(file.name))
: ''
)
}
})
})
.catch(requtil.ValidationError, function(err) {
res.status(400)
.json({
success: false
, error: 'ValidationError'
, validationErrors: err.errors
})
})
.catch(function(err) {
log.error('Error storing resource', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.post('/api/v1/s/:type', function(req, res) {
var form = new formidable.IncomingForm()
Promise.promisify(form.parse, form)(req)
.spread(function(fields, files) {
return Object.keys(files).map(function(field) {
var file = files[field]
log.info('Uploaded "%s" to "%s"', file.name, file.path)
return {
field: field
, id: storage.store(file)
, name: file.name
}
})
})
.then(function(storedFiles) {
res.status(201)
.json({
success: true
, resources: (function() {
var mapped = Object.create(null)
storedFiles.forEach(function(file) {
mapped[file.field] = {
date: new Date()
, type: req.params.type
, id: file.id
, name: file.name
, href: util.format(
'/api/v1/s/%s/%s%s'
, req.params.type
, file.id
, file.name
? util.format('/%s', path.basename(file.name))
: ''
)
}
})
return mapped
})()
})
})
.catch(function(err) {
log.error('Error storing resource', err.stack)
res.status(500)
.json({
success: false
, error: 'ServerError'
})
})
})
app.get('/api/v1/s/:type/:id/*', function(req, res) {
var file = storage.retrieve(req.params.id)
if (file) {
res.set('Content-Type', file.type)
res.sendFile(file.path)
}
else {
res.send(404)
}
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -0,0 +1,44 @@
var zmq = require('zmq')
var logger = require('../../util/logger')
var lifecycle = require('../../util/lifecycle')
module.exports = function(options) {
var log = logger.createLogger('triproxy')
if (options.name) {
logger.setGlobalIdentifier(options.name)
}
function proxy(to) {
return function() {
to.send([].slice.call(arguments))
}
}
// App/device output
var pub = zmq.socket('pub')
pub.bindSync(options.endpoints.pub)
log.info('PUB socket bound on', options.endpoints.pub)
// Coordinator input/output
var dealer = zmq.socket('dealer')
dealer.bindSync(options.endpoints.dealer)
dealer.on('message', proxy(pub))
log.info('DEALER socket bound on', options.endpoints.dealer)
// App/device input
var pull = zmq.socket('pull')
pull.bindSync(options.endpoints.pull)
pull.on('message', proxy(dealer))
log.info('PULL socket bound on', options.endpoints.pull)
lifecycle.observe(function() {
try {
pub.close()
dealer.close()
pull.close()
}
catch (err) {}
})
}

View File

@@ -0,0 +1,680 @@
var http = require('http')
var events = require('events')
var util = require('util')
var socketio = require('socket.io')
var zmq = require('zmq')
var Promise = require('bluebird')
var _ = require('lodash')
var request = Promise.promisifyAll(require('request'))
var logger = require('../../util/logger')
var wire = require('../../wire')
var wireutil = require('../../wire/util')
var wirerouter = require('../../wire/router')
var dbapi = require('../../db/api')
var datautil = require('../../util/datautil')
var cookieSession = require('./middleware/cookie-session')
var ip = require('./middleware/remote-ip')
var auth = require('./middleware/auth')
module.exports = function(options) {
var log = logger.createLogger('websocket')
, server = http.createServer()
, io = socketio.listen(server, {
serveClient: false
, transports: ['websocket']
})
, channelRouter = new events.EventEmitter()
// Output
var push = zmq.socket('push')
options.endpoints.push.forEach(function(endpoint) {
log.info('Sending output to %s', endpoint)
push.connect(endpoint)
})
// Input
var sub = zmq.socket('sub')
options.endpoints.sub.forEach(function(endpoint) {
log.info('Receiving input from %s', endpoint)
sub.connect(endpoint)
})
// Establish always-on channels
;[wireutil.global].forEach(function(channel) {
log.info('Subscribing to permanent channel "%s"', channel)
sub.subscribe(channel)
})
sub.on('message', function(channel, data) {
channelRouter.emit(channel.toString(), channel, data)
})
io.use(cookieSession({
name: options.ssid
, keys: [options.secret]
}))
io.use(ip({
trust: function() {
return true
}
}))
io.use(auth)
io.on('connection', function(socket) {
var channels = []
, user = socket.request.user
socket.emit('socket.ip', socket.request.ip)
function joinChannel(channel) {
channels.push(channel)
channelRouter.on(channel, messageListener)
sub.subscribe(channel)
}
function leaveChannel(channel) {
_.pull(channels, channel)
channelRouter.removeListener(channel, messageListener)
sub.unsubscribe(channel)
}
function createTouchHandler(Klass) {
return function(channel, data) {
push.send([
channel
, wireutil.envelope(new Klass(
data.seq
, data.x
, data.y
))
])
}
}
function createKeyHandler(Klass) {
return function(channel, data) {
push.send([
channel
, wireutil.envelope(new Klass(
data.key
))
])
}
}
var messageListener = wirerouter()
.on(wire.DeviceLogMessage, function(channel, message) {
socket.emit('device.log', message)
})
.on(wire.DevicePresentMessage, function(channel, message) {
socket.emit('device.add', {
important: true
, data: {
serial: message.serial
, provider: message.provider
, present: true
, owner: null
}
})
})
.on(wire.DeviceAbsentMessage, function(channel, message) {
socket.emit('device.remove', {
important: true
, data: {
serial: message.serial
, present: false
, ready: false
, lastHeartbeatAt: null
, using: false
, likelyLeaveReason: 'device_absent'
}
})
})
.on(wire.JoinGroupMessage, function(channel, message) {
socket.emit('device.change', {
important: true
, data: datautil.applyOwner({
serial: message.serial
, owner: message.owner
, likelyLeaveReason: 'owner_change'
}
, user
)
})
})
.on(wire.LeaveGroupMessage, function(channel, message) {
socket.emit('device.change', {
important: true
, data: datautil.applyOwner({
serial: message.serial
, owner: null
, likelyLeaveReason: message.reason
}
, user
)
})
})
.on(wire.DeviceStatusMessage, function(channel, message) {
message.likelyLeaveReason = 'status_change'
socket.emit('device.change', {
important: true
, data: message
})
})
.on(wire.DeviceIdentityMessage, function(channel, message) {
datautil.applyData(message)
message.ready = true
socket.emit('device.change', {
important: true
, data: message
})
})
.on(wire.TransactionProgressMessage, function(channel, message) {
socket.emit('tx.progress', channel.toString(), message)
})
.on(wire.TransactionDoneMessage, function(channel, message) {
socket.emit('tx.done', channel.toString(), message)
})
.on(wire.DeviceLogcatEntryMessage, function(channel, message) {
socket.emit('logcat.entry', message)
})
.on(wire.AirplaneModeEvent, function(channel, message) {
socket.emit('device.change', {
important: true
, data: {
serial: message.serial
, airplaneMode: message.enabled
}
})
})
.on(wire.BatteryEvent, function(channel, message) {
var serial = message.serial
delete message.serial
socket.emit('device.change', {
important: false
, data: {
serial: serial
, battery: message
}
})
})
.on(wire.DeviceBrowserMessage, function(channel, message) {
var serial = message.serial
delete message.serial
socket.emit('device.change', {
important: true
, data: datautil.applyBrowsers({
serial: serial
, browser: message
})
})
})
.on(wire.ConnectivityEvent, function(channel, message) {
var serial = message.serial
delete message.serial
socket.emit('device.change', {
important: false
, data: {
serial: serial
, network: message
}
})
})
.on(wire.PhoneStateEvent, function(channel, message) {
var serial = message.serial
delete message.serial
socket.emit('device.change', {
important: false
, data: {
serial: serial
, network: message
}
})
})
.on(wire.RotationEvent, function(channel, message) {
socket.emit('device.change', {
important: false
, data: {
serial: message.serial
, display: {
rotation: message.rotation
}
}
})
})
.handler()
// Global messages
//
// @todo Use socket.io to push global events to all clients instead
// of listening on every connection, otherwise we're very likely to
// hit EventEmitter's leak complaints (plus it's more work)
channelRouter.on(wireutil.global, messageListener)
// User's private group
joinChannel(user.group)
new Promise(function(resolve) {
socket.on('disconnect', resolve)
// Settings
.on('user.settings.update', function(data) {
dbapi.updateUserSettings(user.email, data)
})
.on('user.settings.reset', function() {
dbapi.resetUserSettings(user.email)
})
// Touch events
.on('input.touchDown', createTouchHandler(wire.TouchDownMessage))
.on('input.touchMove', createTouchHandler(wire.TouchMoveMessage))
.on('input.touchUp', createTouchHandler(wire.TouchUpMessage))
.on('input.tap', createTouchHandler(wire.TapMessage))
// Key events
.on('input.keyDown', createKeyHandler(wire.KeyDownMessage))
.on('input.keyUp', createKeyHandler(wire.KeyUpMessage))
.on('input.keyPress', createKeyHandler(wire.KeyPressMessage))
.on('input.type', function(channel, data) {
push.send([
channel
, wireutil.envelope(new wire.TypeMessage(
data.text
))
])
})
.on('display.rotate', function(channel, data) {
push.send([
channel
, wireutil.envelope(new wire.RotateMessage(
data.rotation
))
])
})
// Transactions
.on('clipboard.paste', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.PasteMessage(data.text)
)
])
})
.on('clipboard.copy', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.CopyMessage()
)
])
})
.on('device.identify', function(channel, responseChannel) {
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.PhysicalIdentifyMessage()
)
])
})
.on('device.reboot', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.RebootMessage()
)
])
})
.on('account.check', function(channel, responseChannel, data){
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.AccountCheckMessage(data)
)
])
})
.on('account.remove', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.AccountRemoveMessage(data)
)
])
})
.on('account.addmenu', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.AccountAddMenuMessage()
)
])
})
.on('account.add', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.AccountAddMessage(data.user, data.password)
)
])
})
.on('account.get', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.AccountGetMessage(data)
)
])
})
.on('sd.status', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.SdStatusMessage()
)
])
})
.on('ringer.set', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.RingerSetMessage(data.mode)
)
])
})
.on('ringer.get', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.RingerGetMessage()
)
])
})
.on('wifi.set', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.WifiSetEnabledMessage(data.enabled)
)
])
})
.on('wifi.get', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.WifiGetStatusMessage()
)
])
})
.on('group.invite', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.GroupMessage(
new wire.OwnerMessage(
user.email
, user.name
, user.group
)
, data.timeout || null
, wireutil.toDeviceRequirements(data.requirements)
)
)
])
})
.on('group.kick', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.UngroupMessage(
wireutil.toDeviceRequirements(data.requirements)
)
)
])
})
.on('tx.cleanup', function(channel) {
leaveChannel(channel)
})
.on('tx.punch', function(channel) {
joinChannel(channel)
socket.emit('tx.punch', channel)
})
.on('shell.command', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ShellCommandMessage(data)
)
])
})
.on('shell.keepalive', function(channel, data) {
push.send([
channel
, wireutil.envelope(new wire.ShellKeepAliveMessage(data))
])
})
.on('device.install', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.InstallMessage(
data.href
, data.launch === true
, JSON.stringify(data.manifest)
)
)
])
})
.on('device.uninstall', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.UninstallMessage(data)
)
])
})
.on('storage.upload', function(channel, responseChannel, data) {
joinChannel(responseChannel)
request.postAsync({
url: util.format(
'%sapi/v1/resources?channel=%s'
, options.storageUrl
, responseChannel
)
, json: true
, body: {
url: data.url
}
})
.catch(function(err) {
log.error('Storage upload had an error', err.stack)
leaveChannel(responseChannel)
socket.emit('tx.cancel', responseChannel, {
success: false
, data: 'fail_upload'
})
})
})
.on('forward.test', function(channel, responseChannel, data) {
joinChannel(responseChannel)
if (!data.targetHost || data.targetHost === 'localhost') {
data.targetHost = socket.request.ip
}
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ForwardTestMessage(data)
)
])
})
.on('forward.create', function(channel, responseChannel, data) {
if (!data.targetHost || data.targetHost === 'localhost') {
data.targetHost = socket.request.ip
}
dbapi.addUserForward(user.email, data)
.then(function() {
socket.emit('forward.create', data)
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ForwardCreateMessage(data)
)
])
})
})
.on('forward.remove', function(channel, responseChannel, data) {
dbapi.removeUserForward(user.email, data.devicePort)
.then(function() {
socket.emit('forward.remove', data)
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ForwardRemoveMessage(data)
)
])
})
})
.on('logcat.start', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.LogcatStartMessage(data)
)
])
})
.on('logcat.stop', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.LogcatStopMessage()
)
])
})
.on('connect.start', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ConnectStartMessage()
)
])
})
.on('connect.stop', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ConnectStopMessage()
)
])
})
.on('browser.open', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.BrowserOpenMessage(data)
)
])
})
.on('browser.clear', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.BrowserClearMessage(data)
)
])
})
.on('store.open', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.StoreOpenMessage()
)
])
})
.on('screen.capture', function(channel, responseChannel) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.ScreenCaptureMessage()
)
])
})
})
.finally(function() {
// Clean up all listeners and subscriptions
channelRouter.removeListener(wireutil.global, messageListener)
channels.forEach(function(channel) {
channelRouter.removeListener(channel, messageListener)
sub.unsubscribe(channel)
})
})
.catch(function(err) {
// Cannot guarantee integrity of client
log.error(
'Client had an error, disconnecting due to probable loss of integrity'
, err.stack
)
socket.disconnect(true)
})
})
server.listen(options.port)
log.info('Listening on port %d', options.port)
}

View File

@@ -0,0 +1,22 @@
var dbapi = require('../../../db/api')
module.exports = function(socket, next) {
var req = socket.request
, token = req.session.jwt
if (token) {
return dbapi.loadUser(token.email)
.then(function(user) {
if (user) {
req.user = user
next()
}
else {
next(new Error('Invalid user'))
}
})
.catch(next)
}
else {
next(new Error('Missing authorization token'))
}
}

View File

@@ -0,0 +1,10 @@
var cookieSession = require('cookie-session')
module.exports = function(options) {
var session = cookieSession(options)
return function(socket, next) {
var req = socket.request
, res = Object.create(null)
session(req, res, next)
}
}

View File

@@ -0,0 +1,9 @@
var proxyaddr = require('proxy-addr')
module.exports = function(options) {
return function(socket, next) {
var req = socket.request
req.ip = proxyaddr(req, options.trust)
next()
}
}