mirror of
https://github.com/DeviceFarmer/stf.git
synced 2026-04-23 14:25:17 +02:00
Rename "roles" to "units". Put units in their own folders.
This commit is contained in:
215
lib/units/app/index.js
Normal file
215
lib/units/app/index.js
Normal 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)
|
||||
}
|
||||
12
lib/units/app/middleware/appstore-icons.js
Normal file
12
lib/units/app/middleware/appstore-icons.js
Normal 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'
|
||||
}
|
||||
)
|
||||
}
|
||||
50
lib/units/app/middleware/auth.js
Normal file
50
lib/units/app/middleware/auth.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
lib/units/app/middleware/browser-icons.js
Normal file
12
lib/units/app/middleware/browser-icons.js
Normal 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'
|
||||
}
|
||||
)
|
||||
}
|
||||
12
lib/units/app/middleware/device-icons.js
Normal file
12
lib/units/app/middleware/device-icons.js
Normal 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'
|
||||
}
|
||||
)
|
||||
}
|
||||
109
lib/units/app/middleware/webpack.js
Normal file
109
lib/units/app/middleware/webpack.js
Normal 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
140
lib/units/auth/ldap.js
Normal 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
124
lib/units/auth/mock.js
Normal 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
54
lib/units/device/index.js
Normal 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()
|
||||
})
|
||||
}
|
||||
324
lib/units/device/plugins/account.js
Normal file
324
lib/units/device/plugins/account.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
122
lib/units/device/plugins/browser.js
Normal file
122
lib/units/device/plugins/browser.js
Normal 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()
|
||||
})
|
||||
51
lib/units/device/plugins/clipboard.js
Normal file
51
lib/units/device/plugins/clipboard.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
111
lib/units/device/plugins/connect.js
Normal file
111
lib/units/device/plugins/connect.js
Normal 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)
|
||||
})
|
||||
20
lib/units/device/plugins/data.js
Normal file
20
lib/units/device/plugins/data.js
Normal 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()
|
||||
})
|
||||
25
lib/units/device/plugins/display.js
Normal file
25
lib/units/device/plugins/display.js
Normal 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()
|
||||
})
|
||||
237
lib/units/device/plugins/forward.js
Normal file
237
lib/units/device/plugins/forward.js
Normal 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')
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
170
lib/units/device/plugins/group.js
Normal file
170
lib/units/device/plugins/group.js
Normal 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
|
||||
})
|
||||
143
lib/units/device/plugins/http.js
Normal file
143
lib/units/device/plugins/http.js
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
22
lib/units/device/plugins/identity.js
Normal file
22
lib/units/device/plugins/identity.js
Normal 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()
|
||||
})
|
||||
189
lib/units/device/plugins/install.js
Normal file
189
lib/units/device/plugins/install.js
Normal 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')
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
141
lib/units/device/plugins/logcat.js
Normal file
141
lib/units/device/plugins/logcat.js
Normal 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
|
||||
})
|
||||
27
lib/units/device/plugins/logger.js
Normal file
27
lib/units/device/plugins/logger.js
Normal 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
|
||||
})
|
||||
21
lib/units/device/plugins/phone.js
Normal file
21
lib/units/device/plugins/phone.js
Normal 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()
|
||||
})
|
||||
35
lib/units/device/plugins/reboot.js
Normal file
35
lib/units/device/plugins/reboot.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
57
lib/units/device/plugins/ringer.js
Normal file
57
lib/units/device/plugins/ringer.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
66
lib/units/device/plugins/screenshot.js
Normal file
66
lib/units/device/plugins/screenshot.js
Normal 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
|
||||
})
|
||||
33
lib/units/device/plugins/sd.js
Normal file
33
lib/units/device/plugins/sd.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
695
lib/units/device/plugins/service.js
Normal file
695
lib/units/device/plugins/service.js
Normal 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)
|
||||
})
|
||||
86
lib/units/device/plugins/shell.js
Normal file
86
lib/units/device/plugins/shell.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
50
lib/units/device/plugins/solo.js
Normal file
50
lib/units/device/plugins/solo.js
Normal 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
|
||||
))
|
||||
])
|
||||
}
|
||||
}
|
||||
})
|
||||
46
lib/units/device/plugins/stats.js
Normal file
46
lib/units/device/plugins/stats.js
Normal 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 {}
|
||||
})
|
||||
})
|
||||
40
lib/units/device/plugins/store.js
Normal file
40
lib/units/device/plugins/store.js
Normal 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()
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
142
lib/units/device/plugins/touch.js
Normal file
142
lib/units/device/plugins/touch.js
Normal 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)
|
||||
})
|
||||
53
lib/units/device/plugins/wifi.js
Normal file
53
lib/units/device/plugins/wifi.js
Normal 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)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
103
lib/units/device/resources/remote.js
Normal file
103
lib/units/device/resources/remote.js
Normal 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
|
||||
}
|
||||
})
|
||||
})
|
||||
104
lib/units/device/resources/service.js
Normal file
104
lib/units/device/resources/service.js
Normal 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
|
||||
})
|
||||
})
|
||||
30
lib/units/device/support/adb.js
Normal file
30
lib/units/device/support/adb.js
Normal 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)
|
||||
})
|
||||
14
lib/units/device/support/channels.js
Normal file
14
lib/units/device/support/channels.js
Normal 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
|
||||
})
|
||||
29
lib/units/device/support/http.js
Normal file
29
lib/units/device/support/http.js
Normal 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
|
||||
})
|
||||
17
lib/units/device/support/properties.js
Normal file
17
lib/units/device/support/properties.js
Normal 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()
|
||||
})
|
||||
19
lib/units/device/support/push.js
Normal file
19
lib/units/device/support/push.js
Normal 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
|
||||
})
|
||||
19
lib/units/device/support/router.js
Normal file
19
lib/units/device/support/router.js
Normal 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
|
||||
})
|
||||
53
lib/units/device/support/storage.js
Normal file
53
lib/units/device/support/storage.js
Normal 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
|
||||
})
|
||||
26
lib/units/device/support/sub.js
Normal file
26
lib/units/device/support/sub.js
Normal 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
|
||||
})
|
||||
77
lib/units/notify/hipchat.js
Normal file
77
lib/units/notify/hipchat.js
Normal 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()
|
||||
})
|
||||
}
|
||||
131
lib/units/processor/index.js
Normal file
131
lib/units/processor/index.js
Normal 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
409
lib/units/provider/index.js
Normal 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
58
lib/units/reaper/index.js
Normal 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) {}
|
||||
})
|
||||
}
|
||||
54
lib/units/storage/plugins/apk/index.js
Normal file
54
lib/units/storage/plugins/apk/index.js
Normal 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)
|
||||
}
|
||||
19
lib/units/storage/plugins/apk/task/manifest.js
Normal file
19
lib/units/storage/plugins/apk/task/manifest.js
Normal 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
|
||||
}
|
||||
52
lib/units/storage/plugins/image/index.js
Normal file
52
lib/units/storage/plugins/image/index.js
Normal 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)
|
||||
}
|
||||
14
lib/units/storage/plugins/image/param/crop.js
Normal file
14
lib/units/storage/plugins/image/param/crop.js
Normal 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
|
||||
}
|
||||
21
lib/units/storage/plugins/image/param/gravity.js
Normal file
21
lib/units/storage/plugins/image/param/gravity.js
Normal 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
|
||||
}
|
||||
20
lib/units/storage/plugins/image/task/get.js
Normal file
20
lib/units/storage/plugins/image/task/get.js
Normal 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)
|
||||
})
|
||||
}
|
||||
26
lib/units/storage/plugins/image/task/transform.js
Normal file
26
lib/units/storage/plugins/image/task/transform.js
Normal 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
149
lib/units/storage/temp.js
Normal 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)
|
||||
}
|
||||
44
lib/units/triproxy/index.js
Normal file
44
lib/units/triproxy/index.js
Normal 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) {}
|
||||
})
|
||||
}
|
||||
680
lib/units/websocket/index.js
Normal file
680
lib/units/websocket/index.js
Normal 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)
|
||||
}
|
||||
22
lib/units/websocket/middleware/auth.js
Normal file
22
lib/units/websocket/middleware/auth.js
Normal 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'))
|
||||
}
|
||||
}
|
||||
10
lib/units/websocket/middleware/cookie-session.js
Normal file
10
lib/units/websocket/middleware/cookie-session.js
Normal 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)
|
||||
}
|
||||
}
|
||||
9
lib/units/websocket/middleware/remote-ip.js
Normal file
9
lib/units/websocket/middleware/remote-ip.js
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user