module.exports.command = 'local [serial..]' module.exports.describe = 'Start a complete local development environment.' module.exports.builder = function(yargs) { var os = require('os') return yargs .env('STF_LOCAL') .strict() .option('adb-host', { describe: 'The ADB server host.' , type: 'string' , default: '127.0.0.1' }) .option('adb-port', { describe: 'The ADB server port.' , type: 'number' , default: 5037 }) .option('allow-remote', { alias: 'R' , describe: 'Whether to allow remote devices in STF. Highly ' + 'unrecommended due to almost unbelievable slowness on the ADB side ' + 'and duplicate device issues when used locally while having a ' + 'cable connected at the same time.' , type: 'boolean' }) .option('api-port', { describe: 'The port the api unit should run at.' , type: 'number' , default: 7106 }) .option('app-port', { describe: 'The port the app unit should run at.' , type: 'number' , default: 7105 }) .option('auth-options', { describe: 'JSON array of options to pass to the auth unit.' , type: 'string' , default: '[]' }) .option('auth-port', { describe: 'The port the auth unit should run at.' , type: 'number' , default: 7120 }) .option('auth-secret', { describe: 'The secret to use for auth JSON Web Tokens. Anyone who ' + 'knows this token can freely enter the system if they want, so keep ' + 'it safe.' , type: 'string' , default: 'kute kittykat' }) .option('auth-type', { describe: 'The type of auth unit to start.' , type: 'string' , choices: ['mock', 'ldap', 'oauth2', 'saml2', 'openid'] , default: 'mock' }) .option('auth-url', { alias: 'a' , describe: 'URL to the auth unit.' , type: 'string' }) .option('bind-app-dealer', { describe: 'The address to bind the app-side ZeroMQ DEALER endpoint to.' , type: 'string' , default: 'tcp://127.0.0.1:7112' }) .option('bind-app-pub', { describe: 'The address to bind the app-side ZeroMQ PUB endpoint to.' , type: 'string' , default: 'tcp://127.0.0.1:7111' }) .option('bind-app-pull', { describe: 'The address to bind the app-side ZeroMQ PULL endpoint to.' , type: 'string' , default: 'tcp://127.0.0.1:7113' }) .option('bind-dev-dealer', { describe: 'The address to bind the device-side ZeroMQ DEALER endpoint to.' , type: 'string' , default: 'tcp://127.0.0.1:7115' }) .option('bind-dev-pub', { describe: 'The address to bind the device-side ZeroMQ PUB endpoint to.' , type: 'string' , default: 'tcp://127.0.0.1:7114' }) .option('bind-dev-pull', { describe: 'The address to bind the device-side ZeroMQ PULL endpoint to.' , type: 'string' , default: 'tcp://127.0.0.1:7116' }) .option('cleanup', { describe: 'Attempt to reset the device between uses by uninstalling' + 'apps, resetting accounts and clearing caches. Does not do a perfect ' + 'job currently. Negate with --no-cleanup.' , type: 'boolean' , default: true }) .option('group-timeout', { alias: 't' , describe: 'Timeout in seconds for automatic release of inactive devices.' , type: 'number' , default: 900 }) .option('lock-rotation', { describe: 'Whether to lock rotation when devices are being used. ' + 'Otherwise changing device orientation may not always work due to ' + 'sensitive sensors quickly or immediately reverting it back to the ' + 'physical orientation.' , type: 'boolean' }) .option('mute-master', { describe: 'Whether to mute master volume.' , choices: ['always', 'inuse', 'never'] , default: 'never' , coerce: val => { if (val === true) { return 'inuse' // For backwards compatibility. } if (val === false) { return 'never' // For backwards compatibility. } return val } }) .option('port', { alias: ['p', 'poorxy-port'] , describe: 'The port STF should run at.' , type: 'number' , default: 7100 }) .option('provider', { describe: 'An easily identifiable name for the UI and/or log output.' , type: 'string' , default: os.hostname() }) .option('provider-max-port', { describe: 'Highest port number for device workers to use.' , type: 'number' , default: 7700 }) .option('provider-min-port', { describe: 'Lowest port number for device workers to use.' , type: 'number' , default: 7400 }) .option('public-ip', { describe: 'The IP or hostname to use in URLs.' , type: 'string' , default: 'localhost' }) .option('serial', { describe: 'Only use devices with these serial numbers.' , type: 'array' }) .option('storage-options', { describe: 'JSON array of options to pass to the storage unit.' , type: 'string' , default: '[]' }) .option('storage-plugin-apk-port', { describe: 'The port the storage-plugin-apk unit should run at.' , type: 'number' , default: 7104 }) .option('storage-plugin-image-port', { describe: 'The port the storage-plugin-image unit should run at.' , type: 'number' , default: 7103 }) .option('storage-port', { describe: 'The port the storage unit should run at.' , type: 'number' , default: 7102 }) .option('storage-type', { describe: 'The type of storage unit to start.' , type: 'string' , choices: ['temp', 's3'] , default: 'temp' }) .option('user-profile-url', { describe: 'URL to external user profile page' , type: 'string' }) .option('vnc-initial-size', { describe: 'The initial size to use for the experimental VNC server.' , type: 'string' , default: '600x800' , coerce: function(val) { return val.split('x').map(Number) } }) .option('websocket-port', { describe: 'The port the websocket unit should run at.' , type: 'number' , default: 7110 }) .epilog('Each option can be be overwritten with an environment variable ' + 'by converting the option to uppercase, replacing dashes with ' + 'underscores and prefixing it with `STF_LOCAL_` (e.g. ' + '`STF_LOCAL_ALLOW_REMOTE`).') } module.exports.handler = function(argv) { var util = require('util') var path = require('path') var Promise = require('bluebird') var logger = require('../../util/logger') var log = logger.createLogger('cli:local') var procutil = require('../../util/procutil') // Each forked process waits for signals to stop, and so we run over the // default limit of 10. So, it's not a leak, but a refactor wouldn't hurt. process.setMaxListeners(20) function run() { var procs = [ // app triproxy procutil.fork(path.resolve(__dirname, '..'), [ 'triproxy', 'app001' , '--bind-pub', argv.bindAppPub , '--bind-dealer', argv.bindAppDealer , '--bind-pull', argv.bindAppPull ]) // device triproxy , procutil.fork(path.resolve(__dirname, '..'), [ 'triproxy', 'dev001' , '--bind-pub', argv.bindDevPub , '--bind-dealer', argv.bindDevDealer , '--bind-pull', argv.bindDevPull ]) // processor one , procutil.fork(path.resolve(__dirname, '..'), [ 'processor', 'proc001' , '--connect-app-dealer', argv.bindAppDealer , '--connect-dev-dealer', argv.bindDevDealer ]) // processor two , procutil.fork(path.resolve(__dirname, '..'), [ 'processor', 'proc002' , '--connect-app-dealer', argv.bindAppDealer , '--connect-dev-dealer', argv.bindDevDealer ]) // reaper one , procutil.fork(path.resolve(__dirname, '..'), [ 'reaper', 'reaper001' , '--connect-push', argv.bindDevPull , '--connect-sub', argv.bindAppPub ]) // provider , procutil.fork(path.resolve(__dirname, '..'), [ 'provider' , '--name', argv.provider , '--min-port', argv.providerMinPort , '--max-port', argv.providerMaxPort , '--connect-sub', argv.bindDevPub , '--connect-push', argv.bindDevPull , '--group-timeout', argv.groupTimeout , '--public-ip', argv.publicIp , '--storage-url' , util.format('http://localhost:%d/', argv.port) , '--adb-host', argv.adbHost , '--adb-port', argv.adbPort , '--vnc-initial-size', argv.vncInitialSize.join('x') , '--mute-master', argv.muteMaster ] .concat(argv.allowRemote ? ['--allow-remote'] : []) .concat(argv.lockRotation ? ['--lock-rotation'] : []) .concat(!argv.cleanup ? ['--no-cleanup'] : []) .concat(argv.serial)) // auth , procutil.fork(path.resolve(__dirname, '..'), [ util.format('auth-%s', argv.authType) , '--port', argv.authPort , '--secret', argv.authSecret , '--app-url', util.format( 'http://%s:%d/' , argv.publicIp , argv.port ) ].concat(JSON.parse(argv.authOptions))) // app , procutil.fork(path.resolve(__dirname, '..'), [ 'app' , '--port', argv.appPort , '--secret', argv.authSecret , '--auth-url', argv.authUrl || util.format( 'http://%s:%d/auth/%s/' , argv.publicIp , argv.port , { oauth2: 'oauth' , saml2: 'saml' }[argv.authType] || argv.authType ) , '--websocket-url', util.format( 'http://%s:%d/' , argv.publicIp , argv.websocketPort ) ].concat((function() { var extra = [] if (argv.userProfileUrl) { extra.push('--user-profile-url', argv.userProfileUrl) } return extra })())) // api , procutil.fork(path.resolve(__dirname, '..'), [ 'api' , '--port', argv.apiPort , '--secret', argv.authSecret , '--connect-push', argv.bindAppPull , '--connect-sub', argv.bindAppPub ]) // websocket , procutil.fork(path.resolve(__dirname, '..'), [ 'websocket' , '--port', argv.websocketPort , '--secret', argv.authSecret , '--storage-url' , util.format('http://localhost:%d/', argv.port) , '--connect-sub', argv.bindAppPub , '--connect-push', argv.bindAppPull ]) // storage , procutil.fork(path.resolve(__dirname, '..'), [ util.format('storage-%s', argv.storageType) , '--port', argv.storagePort ].concat(JSON.parse(argv.storageOptions))) // image processor , procutil.fork(path.resolve(__dirname, '..'), [ 'storage-plugin-image' , '--port', argv.storagePluginImagePort , '--storage-url' , util.format('http://localhost:%d/', argv.port) ]) // apk processor , procutil.fork(path.resolve(__dirname, '..'), [ 'storage-plugin-apk' , '--port', argv.storagePluginApkPort , '--storage-url' , util.format('http://localhost:%d/', argv.port) ]) // poorxy , procutil.fork(path.resolve(__dirname, '..'), [ 'poorxy' , '--port', argv.port , '--app-url' , util.format('http://localhost:%d/', argv.appPort) , '--auth-url' , util.format('http://localhost:%d/', argv.authPort) , '--api-url' , util.format('http://localhost:%d/', argv.apiPort) , '--websocket-url' , util.format('http://localhost:%d/', argv.websocketPort) , '--storage-url' , util.format('http://localhost:%d/', argv.storagePort) , '--storage-plugin-image-url' , util.format('http://localhost:%d/', argv.storagePluginImagePort) , '--storage-plugin-apk-url' , util.format('http://localhost:%d/', argv.storagePluginApkPort) ]) ] function shutdown() { log.info('Shutting down all child processes') procs.forEach(function(proc) { proc.cancel() }) return Promise.settle(procs) } process.on('SIGINT', function() { log.info('Received SIGINT, waiting for processes to terminate') }) process.on('SIGTERM', function() { log.info('Received SIGTERM, waiting for processes to terminate') }) return Promise.all(procs) .then(function() { process.exit(0) }) .catch(function(err) { log.fatal('Child process had an error', err.stack) return shutdown() .then(function() { process.exit(1) }) }) } return procutil.fork(path.resolve(__dirname, '..'), ['migrate']) .done(run) }