diff --git a/.eslintrc b/.eslintrc index d120e453..30365a07 100644 --- a/.eslintrc +++ b/.eslintrc @@ -78,7 +78,7 @@ "no-shadow-restricted-names": 2, "no-shadow": 0, // TODO: 1 may be ok "no-undefined": 1, - "no-unused-vars": 1, + "no-unused-vars": [1, {"varsIgnorePattern": "^_"}], "no-use-before-define": 1, // TODO: 0 or 2 may be ok, sometimes there are ciclic dependencies // Style diff --git a/CHANGELOG.md b/CHANGELOG.md index 9019c9c3..8c26ac87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Enhancements - You can now set screen JPEG quality with the `SCREEN_JPEG_QUALITY` environment variable at launch time. Can be useful for slow networks. +- Switched to [yargs](http://yargs.js.org) for option parsing to make it easier to modify the CLI. +- Almost all command line options can now be specified with environment variables. +- Internal commands are now hidden from help output but can still be used. +- Running the `stf` binary without a command now errors and shows help output (previously there was no output whatsoever). +- Improved help messages for various options. ### Fixes @@ -14,6 +19,12 @@ - We now use [please-update-dependencies](https://github.com/sorccu/please-update-dependencies) to check for outdated dependencies when running from source. It's a super quick local check that compares `package.json` with installed dependencies. Should help avoid unnecessary issues caused by forgetting to run `npm install` after `git pull`. +### Breaking changes + +- The `-C` shortcut for the `--no-cleanup` option has been removed due to the switch to [yargs](http://yargs.js.org). Please use the full `--no-cleanup` option instead. +- Although likely not used by anyone, it was possible to give multiple ZeroMQ endpoints to options such as `--connect-push` by separating them with commas. This is still possible but now works in a different way due to the switch to [yargs](http://yargs.js.org). Comma-separated hosts in a single value are no longer accepted. If you need to specify multiple hosts, simply use the option as many times as you like. This change is unlikely to have any impact whatsoever on most users. +- The `--devices` option of `stf doctor` has been removed due to unnecessary complexity. + ## 2.3.0 (2016-11-09) Minor release addressing the following: diff --git a/bin/stf b/bin/stf index 074e9771..cd11240d 100755 --- a/bin/stf +++ b/bin/stf @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../lib/cli.please') +require('../lib/cli/please') diff --git a/lib/cli.js b/lib/cli.js deleted file mode 100644 index 69d53ccd..00000000 --- a/lib/cli.js +++ /dev/null @@ -1,1466 +0,0 @@ -var util = require('util') -var os = require('os') - -var program = require('commander') -var Promise = require('bluebird') -var ip = require('my-local-ip') - -var pkg = require('../package') -var cliutil = require('./util/cliutil') -var logger = require('./util/logger') - -Promise.longStackTraces() - -program - .version(pkg.version) - -program - .command('provider [serial...]') - .description('start provider') - .option('-s, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .option('-p, --connect-push ' - , 'push endpoint' - , cliutil.list) - .option('-n, --name ' - , 'name (or os.hostname())' - , String - , os.hostname()) - .option('--min-port ' - , 'minimum port number for worker use' - , Number - , 7400) - .option('--max-port ' - , 'maximum port number for worker use' - , Number - , 7700) - .option('--public-ip ' - , 'public ip for global access' - , String - , ip()) - .option('-t, --group-timeout ' - , 'group timeout' - , Number - , 900) - .option('-r, --storage-url ' - , 'URL to storage client' - , String) - .option('--heartbeat-interval ' - , 'heartbeat interval' - , Number - , 10000) - .option('--boot-complete-timeout ' - , 'how long to wait for boot to complete during device setup' - , Number - , 60000) - .option('--adb-host ' - , 'ADB host (defaults to 127.0.0.1)' - , String - , '127.0.0.1') - .option('--adb-port ' - , 'ADB port (defaults to 5037)' - , Number - , 5037) - .option('-R, --allow-remote' - , 'Whether to allow remote devices to be set up') - .option('--screen-ws-url-pattern ' - , 'screen WebSocket URL pattern' - , String - , 'ws://${publicIp}:${publicPort}') - .option('--screen-jpeg-quality ' - , 'screen JPEG quality' - , Number - , process.env.SCREEN_JPEG_QUALITY || 80) - .option('--connect-url-pattern ' - , 'adb connect URL pattern' - , String - , '${publicIp}:${publicPort}') - .option('--vnc-initial-size ' - , 'initial VNC size' - , cliutil.size - , [600, 800]) - .option('--mute-master' - , 'whether to mute master volume when devices are being used') - .option('--lock-rotation' - , 'whether to lock rotation when devices are being used') - .option('-C, --no-cleanup' - , 'do not attempt to clean up devices between uses') - .action(function(serials, options) { - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - if (!options.connectPush) { - this.missingArgument('--connect-push') - } - if (!options.storageUrl) { - this.missingArgument('--storage-url') - } - - require('./units/provider')({ - name: options.name - , killTimeout: 10000 - , ports: cliutil.range(options.minPort, options.maxPort) - , filter: function(device) { - return serials.length === 0 || serials.indexOf(device.id) !== -1 - } - , allowRemote: options.allowRemote - , fork: function(device, ports) { - var fork = require('child_process').fork - return fork(__filename, [ - 'device', device.id - , '--provider', options.name - , '--connect-sub', options.connectSub.join(',') - , '--connect-push', options.connectPush.join(',') - , '--screen-port', ports.shift() - , '--connect-port', ports.shift() - , '--vnc-port', ports.shift() - , '--public-ip', options.publicIp - , '--group-timeout', options.groupTimeout - , '--storage-url', options.storageUrl - , '--adb-host', options.adbHost - , '--adb-port', options.adbPort - , '--screen-ws-url-pattern', options.screenWsUrlPattern - , '--screen-jpeg-quality', options.screenJpegQuality - , '--connect-url-pattern', options.connectUrlPattern - , '--heartbeat-interval', options.heartbeatInterval - , '--boot-complete-timeout', options.bootCompleteTimeout - , '--vnc-initial-size', options.vncInitialSize.join('x') - ] - .concat(options.muteMaster ? ['--mute-master'] : []) - .concat(options.lockRotation ? ['--lock-rotation'] : []) - .concat(!options.cleanup ? ['--no-cleanup'] : [])) - } - , endpoints: { - sub: options.connectSub - , push: options.connectPush - } - , adbHost: options.adbHost - , adbPort: options.adbPort - }) - }) - -program - .command('device ') - .description('start device worker') - .option('-n, --provider ' - , 'provider name' - , String) - .option('-s, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .option('-p, --connect-push ' - , 'push endpoint' - , cliutil.list) - .option('--screen-port ' - , 'port allocated to the screen websocket' - , Number) - .option('--connect-port ' - , 'port allocated to adb connect' - , Number) - .option('--vnc-port ' - , 'port allocated to vnc' - , Number) - .option('--vnc-initial-size ' - , 'initial VNC size' - , cliutil.size - , [600, 800]) - .option('--connect-url-pattern ' - , 'adb connect URL pattern' - , String - , '${publicIp}:${publicPort}') - .option('--public-ip ' - , 'public ip for global access' - , String - , ip()) - .option('-t, --group-timeout ' - , 'group timeout' - , Number - , 900) - .option('-r, --storage-url ' - , 'URL to storage client' - , String) - .option('--adb-host ' - , 'ADB host (defaults to 127.0.0.1)' - , String - , '127.0.0.1') - .option('--adb-port ' - , 'ADB port (defaults to 5037)' - , Number - , 5037) - .option('--screen-ws-url-pattern ' - , 'screen WebSocket URL pattern' - , String - , 'ws://${publicIp}:${publicPort}') - .option('--screen-jpeg-quality ' - , 'screen JPEG quality' - , Number - , process.env.SCREEN_JPEG_QUALITY || 80) - .option('--heartbeat-interval ' - , 'heartbeat interval' - , Number - , 10000) - .option('--boot-complete-timeout ' - , 'how long to wait for boot to complete during device setup' - , Number - , 60000) - .option('--mute-master' - , 'whether to mute master volume when devices are being used') - .option('--lock-rotation' - , 'whether to lock rotation when devices are being used') - .option('-C, --no-cleanup' - , 'do not attempt to clean up devices between uses') - .action(function(serial, options) { - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - if (!options.connectPush) { - this.missingArgument('--connect-push') - } - if (!options.provider) { - this.missingArgument('--provider') - } - if (!options.screenPort) { - this.missingArgument('--screen-port') - } - if (!options.connectPort) { - this.missingArgument('--connect-port') - } - if (!options.vncPort) { - this.missingArgument('--vnc-port') - } - if (!options.storageUrl) { - this.missingArgument('--storage-url') - } - - require('./units/device')({ - serial: serial - , provider: options.provider - , publicIp: options.publicIp - , endpoints: { - sub: options.connectSub - , push: options.connectPush - } - , groupTimeout: options.groupTimeout * 1000 // change to ms - , storageUrl: options.storageUrl - , adbHost: options.adbHost - , adbPort: options.adbPort - , screenWsUrlPattern: options.screenWsUrlPattern - , screenJpegQuality: options.screenJpegQuality - , screenPort: options.screenPort - , connectUrlPattern: options.connectUrlPattern - , connectPort: options.connectPort - , vncPort: options.vncPort - , vncInitialSize: options.vncInitialSize - , heartbeatInterval: options.heartbeatInterval - , bootCompleteTimeout: options.bootCompleteTimeout - , muteMaster: options.muteMaster - , lockRotation: options.lockRotation - , cleanup: options.cleanup - }) - }) - -program - .command('processor ') - .description('start processor') - .option('-a, --connect-app-dealer ' - , 'app dealer endpoint' - , cliutil.list) - .option('-d, --connect-dev-dealer ' - , 'device dealer endpoint' - , cliutil.list) - .action(function(name, options) { - if (!options.connectAppDealer) { - this.missingArgument('--connect-app-dealer') - } - if (!options.connectDevDealer) { - this.missingArgument('--connect-dev-dealer') - } - - require('./units/processor')({ - name: name - , endpoints: { - appDealer: options.connectAppDealer - , devDealer: options.connectDevDealer - } - }) - }) - -program - .command('reaper ') - .description('start reaper') - .option('-p, --connect-push ' - , 'push endpoint' - , cliutil.list) - .option('-s, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .option('-t, --heartbeat-timeout ' - , 'consider devices with heartbeat older than this value dead' - , Number - , 30000) - .action(function(name, options) { - require('./units/reaper')({ - name: name - , heartbeatTimeout: options.heartbeatTimeout - , endpoints: { - push: options.connectPush - , sub: options.connectSub - } - }) - }) - -program - .command('triproxy ') - .description('start triproxy') - .option('-u, --bind-pub ' - , 'pub endpoint' - , String - , 'tcp://*:7111') - .option('-d, --bind-dealer ' - , 'dealer endpoint' - , String - , 'tcp://*:7112') - .option('-p, --bind-pull ' - , 'pull endpoint' - , String - , 'tcp://*:7113') - .action(function(name, options) { - require('./units/triproxy')({ - name: name - , endpoints: { - pub: options.bindPub - , dealer: options.bindDealer - , pull: options.bindPull - } - }) - }) - -program - .command('auth-ldap') - .description('start LDAP auth client') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7120) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-a, --app-url ' - , 'URL to app' - , String) - .option('-u, --ldap-url ' - , 'LDAP server URL (or $LDAP_URL)' - , String - , process.env.LDAP_URL) - .option('-t, --ldap-timeout ' - , 'LDAP timeout (or $LDAP_TIMEOUT)' - , Number - , process.env.LDAP_TIMEOUT || 1000) - .option('--ldap-bind-dn ' - , 'LDAP bind DN (or $LDAP_BIND_DN)' - , String - , process.env.LDAP_BIND_DN) - .option('--ldap-bind-credentials ' - , 'LDAP bind credentials (or $LDAP_BIND_CREDENTIALS)' - , String - , process.env.LDAP_BIND_CREDENTIALS) - .option('--ldap-search-dn ' - , 'LDAP search DN (or $LDAP_SEARCH_DN)' - , String - , process.env.LDAP_SEARCH_DN) - .option('--ldap-search-scope ' - , 'LDAP search scope (or $LDAP_SEARCH_SCOPE)' - , String - , process.env.LDAP_SEARCH_SCOPE || 'sub') - .option('--ldap-search-class ' - , 'LDAP search objectClass (or $LDAP_SEARCH_CLASS)' - , String - , process.env.LDAP_SEARCH_CLASS || 'top') - .option('--ldap-search-field ' - , 'LDAP search field (or $LDAP_SEARCH_FIELD)' - , String - , process.env.LDAP_SEARCH_FIELD) - .option('--ldap-username-field ' - , 'LDAP username field (or $LDAP_USERNAME_FIELD)' - , String - , process.env.LDAP_USERNAME_FIELD || 'cn') - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.appUrl) { - this.missingArgument('--app-url') - } - - require('./units/auth/ldap')({ - port: options.port - , secret: options.secret - , ssid: options.ssid - , appUrl: options.appUrl - , ldap: { - url: options.ldapUrl - , timeout: options.ldapTimeout - , bind: { - dn: options.ldapBindDn - , credentials: options.ldapBindCredentials - } - , search: { - dn: options.ldapSearchDn - , scope: options.ldapSearchScope - , objectClass: options.ldapSearchClass - , field: options.ldapSearchField - } - , username: { - field: options.ldapUsernameField - } - } - }) - }) - -program - .command('auth-oauth2') - .description('start OAuth 2.0 auth client') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7120) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-a, --app-url ' - , 'URL to app' - , String) - .option('--oauth-authorization-url ' - , 'OAuth 2.0 authorization URL (or $OAUTH_AUTHORIZATION_URL)' - , String - , process.env.OAUTH_AUTHORIZATION_URL) - .option('--oauth-token-url ' - , 'OAuth 2.0 token URL (or $OAUTH_TOKEN_URL)' - , String - , process.env.OAUTH_TOKEN_URL) - .option('--oauth-userinfo-url ' - , 'OAuth 2.0 token URL (or $OAUTH_USERINFO_URL)' - , String - , process.env.OAUTH_USERINFO_URL) - .option('--oauth-client-id ' - , 'OAuth 2.0 client ID (or $OAUTH_CLIENT_ID)' - , String - , process.env.OAUTH_CLIENT_ID) - .option('--oauth-client-secret ' - , 'OAuth 2.0 client secret (or $OAUTH_CLIENT_SECRET)' - , String - , process.env.OAUTH_CLIENT_SECRET) - .option('--oauth-callback-url ' - , 'OAuth 2.0 callback URL (or $OAUTH_CALLBACK_URL)' - , String - , process.env.OAUTH_CALLBACK_URL) - .option('--oauth-scope ' - , 'Space-separated OAuth 2.0 scope (or $OAUTH_SCOPE)' - , String - , process.env.OAUTH_SCOPE) - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.appUrl) { - this.missingArgument('--app-url') - } - if (!options.oauthAuthorizationUrl) { - this.missingArgument('--oauth-authorization-url') - } - if (!options.oauthTokenUrl) { - this.missingArgument('--oauth-token-url') - } - if (!options.oauthUserinfoUrl) { - this.missingArgument('--oauth-userinfo-url') - } - if (!options.oauthClientId) { - this.missingArgument('--oauth-client-id') - } - if (!options.oauthClientSecret) { - this.missingArgument('--oauth-client-secret') - } - if (!options.oauthCallbackUrl) { - this.missingArgument('--oauth-callback-url') - } - if (!options.oauthScope) { - this.missingArgument('--oauth-scope') - } - - require('./units/auth/oauth2')({ - port: options.port - , secret: options.secret - , ssid: options.ssid - , appUrl: options.appUrl - , oauth: { - authorizationURL: options.oauthAuthorizationUrl - , tokenURL: options.oauthTokenUrl - , userinfoURL: options.oauthUserinfoUrl - , clientID: options.oauthClientId - , clientSecret: options.oauthClientSecret - , callbackURL: options.oauthCallbackUrl - , scope: options.oauthScope.split(/\s+/) - } - }) - }) - - program - .command('auth-saml2') - .description('start SAML 2.0 auth client') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7120) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-a, --app-url ' - , 'URL to app' - , String) - .option('--saml-id-provider-entry-point-url ' - , 'SAML 2.0 identity provider URL (or $SAML_ID_PROVIDER_ENTRY_POINT_URL)' - , String - , process.env.SAML_ID_PROVIDER_ENTRY_POINT_URL) - .option('--saml-id-provider-issuer ' - , 'SAML 2.0 identity provider issuer (or $SAML_ID_PROVIDER_ISSUER)' - , String - , process.env.SAML_ID_PROVIDER_ISSUER) - .option('--saml-id-provider-cert-path ' - , 'SAML 2.0 identity provider certificate file path (or $SAML_ID_PROVIDER_CERT_PATH)' - , String - , process.env.SAML_ID_PROVIDER_CERT_PATH) - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.appUrl) { - this.missingArgument('--app-url') - } - if (!options.samlIdProviderEntryPointUrl) { - this.missingArgument('--saml-id-provider-entry-point-url') - } - if (!options.samlIdProviderIssuer) { - this.missingArgument('--saml-id-provider-issuer') - } - - require('./units/auth/saml2')({ - port: options.port - , secret: options.secret - , ssid: options.ssid - , appUrl: options.appUrl - , saml: { - entryPoint: options.samlIdProviderEntryPointUrl - , issuer: options.samlIdProviderIssuer - , certPath: options.samlIdProviderCertPath - } - }) - }) - -program - .command('auth-mock') - .description('start mock auth client') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7120) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-a, --app-url ' - , 'URL to app' - , String) - .option('--use-basic-auth' - , 'Whether to use basic authentication for login or not') - .option('--basic-auth-username ' - , 'Basic Auth Username (or $BASIC_AUTH_USERNAME)' - , String - , process.env.BASIC_AUTH_USERNAME || 'username') - .option('--basic-auth-password ' - , 'Basic Auth Password (or $BASIC_AUTH_PASSWORD)' - , String - , process.env.BASIC_AUTH_PASSWORD || 'password') - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.appUrl) { - this.missingArgument('--app-url') - } - - require('./units/auth/mock')({ - port: options.port - , secret: options.secret - , ssid: options.ssid - , appUrl: options.appUrl - , mock: { - useBasicAuth: options.useBasicAuth - , basicAuth: { - username: options.basicAuthUsername - , password: options.basicAuthPassword - } - } - }) - }) - -program - .command('auth-openid') - .description('start openid auth client') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7120) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-a, --app-url ' - , 'URL to app' - , String) - .option('--openid-identifier-url ' - , 'openidIdentifierUrl' - , String - , process.env.OPENID_IDENTIFIER_URL) - .action(function(options) { - if (!options.openidIdentifierUrl) { - this.missingArgument('--openid-identifier-url') - } - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.appUrl) { - this.missingArgument('--app-url') - } - - require('./units/auth/openid')({ - port: options.port - , secret: options.secret - , appUrl: options.appUrl - , openid: { - identifierUrl: options.openidIdentifierUrl - } - }) - }) - -program - .command('notify-hipchat') - .description('start HipChat notifier') - .option('-t, --token ' - , 'HipChat v2 API token (or $HIPCHAT_TOKEN)' - , String - , process.env.HIPCHAT_TOKEN) - .option('-r, --room ' - , 'HipChat room (or $HIPCHAT_ROOM)' - , String - , process.env.HIPCHAT_ROOM) - .option('-p, --priority ' - , 'minimum log level' - , Number - , logger.Level.IMPORTANT) - .option('-n, --notify-priority ' - , 'minimum log level to cause a notification' - , Number - , logger.Level.WARNING) - .option('-s, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .action(function(options) { - if (!options.token) { - this.missingArgument('--token') - } - if (!options.room) { - this.missingArgument('--room') - } - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - - require('./units/notify/hipchat')({ - token: options.token - , room: options.room - , priority: options.priority - , notifyPriority: options.notifyPriority - , endpoints: { - sub: options.connectSub - } - }) - }) - -program - .command('notify-slack') - .description('start Slack notifier') - .option('-t, --token ' - , 'Slack API token (or $SLACK_TOKEN)' - , String - , process.env.SLACK_TOKEN) - .option('-c, --channel #' - , 'Slack channel (or $SLACK_CHANNEL)' - , String - , process.env.SLACK_CHANNEL) - .option('-p, --priority ' - , 'minimum log level' - , Number - , logger.Level.IMPORTANT) - .option('-s, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .action(function(options) { - if (!options.token) { - this.missingArgument('--token') - } - if (!options.channel) { - this.missingArgument('--channel') - } - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - - require('./units/notify/slack')({ - token: options.token - , channel: options.channel - , priority: options.priority - , endpoints: { - sub: options.connectSub - } - }) - }) - -program - .command('log-rethinkdb') - .description('start a rethinkdb log recorder') - .option('-p, --priority ' - , 'minimum log level' - , Number - , logger.Level.DEBUG) - .option('-s, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .action(function(options) { - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - - require('./units/log/rethinkdb')({ - priority: options.priority - , endpoints: { - sub: options.connectSub - } - }) - }) - -program - .command('poorxy') - .description('start a poor reverse proxy for local development') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7100) - .option('-u, --app-url ' - , 'URL to app' - , String) - .option('-i, --api-url ' - , 'URL to api' - , String) - .option('-a, --auth-url ' - , 'URL to auth client' - , String) - .option('-w, --websocket-url ' - , 'URL to websocket client' - , String) - .option('-r, --storage-url ' - , 'URL to storage client' - , String) - .option('--storage-plugin-image-url ' - , 'URL to image storage plugin' - , String) - .option('--storage-plugin-apk-url ' - , 'URL to apk storage plugin' - , String) - .action(function(options) { - if (!options.appUrl) { - this.missingArgument('--app-url') - } - if (!options.authUrl) { - this.missingArgument('--auth-url') - } - if (!options.apiUrl) { - this.missingArgument('--api-url') - } - if (!options.websocketUrl) { - this.missingArgument('--websocket-url') - } - if (!options.storageUrl) { - this.missingArgument('--storage-url') - } - if (!options.storagePluginImageUrl) { - this.missingArgument('--storage-plugin-image-url') - } - if (!options.storagePluginApkUrl) { - this.missingArgument('--storage-plugin-apk-url') - } - - require('./units/poorxy')({ - port: options.port - , appUrl: options.appUrl - , apiUrl: options.apiUrl - , authUrl: options.authUrl - , websocketUrl: options.websocketUrl - , storageUrl: options.storageUrl - , storagePluginImageUrl: options.storagePluginImageUrl - , storagePluginApkUrl: options.storagePluginApkUrl - }) - }) - -program - .command('app') - .description('start app') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7105) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-a, --auth-url ' - , 'URL to auth client' - , String) - .option('-w, --websocket-url ' - , 'URL to websocket client' - , String) - .option('--user-profile-url ' - , 'URL to external user profile page' - , String) - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.authUrl) { - this.missingArgument('--auth-url') - } - if (!options.websocketUrl) { - this.missingArgument('--websocket-url') - } - - require('./units/app')({ - port: options.port - , secret: options.secret - , ssid: options.ssid - , authUrl: options.authUrl - , websocketUrl: options.websocketUrl - , userProfileUrl: options.userProfileUrl - }) - }) - - program - .command('api') - .description('start api') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7106) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-c, --connect-push ' - , 'push endpoint' - , cliutil.list) - .option('-u, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.connectPush) { - this.missingArgument('--connect-push') - } - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - - require('./units/api')({ - port: options.port - , ssid: options.ssid - , secret: options.secret - , endpoints: { - push: options.connectPush - , sub: options.connectSub - } - }) - }) - -program - .command('websocket') - .description('start websocket') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7110) - .option('-s, --secret ' - , 'secret (or $SECRET)' - , String - , process.env.SECRET) - .option('-i, --ssid ' - , 'session SSID (or $SSID)' - , String - , process.env.SSID || 'ssid') - .option('-r, --storage-url ' - , 'URL to storage client' - , String) - .option('-u, --connect-sub ' - , 'sub endpoint' - , cliutil.list) - .option('-c, --connect-push ' - , 'push endpoint' - , cliutil.list) - .action(function(options) { - if (!options.secret) { - this.missingArgument('--secret') - } - if (!options.storageUrl) { - this.missingArgument('--storage-url') - } - if (!options.connectSub) { - this.missingArgument('--connect-sub') - } - if (!options.connectPush) { - this.missingArgument('--connect-push') - } - - require('./units/websocket')({ - port: options.port - , secret: options.secret - , ssid: options.ssid - , storageUrl: options.storageUrl - , endpoints: { - sub: options.connectSub - , push: options.connectPush - } - }) - }) - -program - .command('storage-temp') - .description('start temp storage') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7100) - .option('--save-dir ' - , 'where to save files' - , String - , os.tmpdir()) - .action(function(options) { - require('./units/storage/temp')({ - port: options.port - , saveDir: options.saveDir - }) - }) - -program - .command('storage-s3') - .description('start s3 storage') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7100) - .option('--bucket ' - , 'your s3 bucket name' - , String) - .option('--profile ' - , 'your aws credentials profile name' - , String) - .option('--endpoint ' - , 'your buckets endpoint' - , String) - .action(function(options) { - if (!options.profile) { - this.missingArgument('--profile') - } - - if (!options.endpoint) { - this.missingArgument('--endpoint') - } - - require('./units/storage/s3')({ - port: options.port - , profile: options.profile - , bucket: options.bucket - , endpoint: options.endpoint - }) - }) - -program - .command('storage-plugin-image') - .description('start storage image plugin') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7100) - .option('-r, --storage-url ' - , 'URL to storage client' - , String) - .option('-c, --concurrency ' - , 'maximum number of simultaneous transformations' - , Number) - .option('--cache-dir ' - , 'where to cache images' - , String - , os.tmpdir()) - .action(function(options) { - if (!options.storageUrl) { - this.missingArgument('--storage-url') - } - - require('./units/storage/plugins/image')({ - port: options.port - , storageUrl: options.storageUrl - , cacheDir: options.cacheDir - , concurrency: options.concurrency || os.cpus().length - }) - }) - -program - .command('storage-plugin-apk') - .description('start storage apk plugin') - .option('-p, --port ' - , 'port (or $PORT)' - , Number - , process.env.PORT || 7100) - .option('-r, --storage-url ' - , 'URL to storage client' - , String) - .option('--cache-dir ' - , 'where to cache images' - , String - , os.tmpdir()) - .action(function(options) { - if (!options.storageUrl) { - this.missingArgument('--storage-url') - } - - require('./units/storage/plugins/apk')({ - port: options.port - , storageUrl: options.storageUrl - , cacheDir: options.cacheDir - }) - }) - -program - .command('migrate') - .description('migrates the database to the latest version') - .action(function() { - var log = logger.createLogger('cli:migrate') - var db = require('./db') - - db.setup() - .then(function() { - process.exit(0) - }) - .catch(function(err) { - log.fatal('Migration had an error:', err.stack) - process.exit(1) - }) - }) - -program - .command('generate-fake-device [model]') - .description('generates a fake device for testing') - .option('-n, --number ' - , 'how many devices to create (defaults to 1)' - , Number - , 1) - .action(function(model, options) { - var log = logger.createLogger('cli:generate-fake-device') - var fake = require('./util/fakedevice') - var n = options.number - - function nextDevice() { - return fake.generate(model) - .then(function(serial) { - log.info('Created fake device "%s"', serial) - - if (--n) { - return nextDevice() - } - }) - } - - nextDevice() - .then(function() { - process.exit(0) - }) - .catch(function(err) { - log.fatal('Fake device creation had an error:', err.stack) - process.exit(1) - }) - }) - -program - .command('local [serial...]') - .description('start everything locally') - .option('--bind-app-pub ' - , 'app pub endpoint' - , String - , 'tcp://127.0.0.1:7111') - .option('--bind-app-dealer ' - , 'app dealer endpoint' - , String - , 'tcp://127.0.0.1:7112') - .option('--bind-app-pull ' - , 'app pull endpoint' - , String - , 'tcp://127.0.0.1:7113') - .option('--bind-dev-pub ' - , 'device pub endpoint' - , String - , 'tcp://127.0.0.1:7114') - .option('--bind-dev-dealer ' - , 'device dealer endpoint' - , String - , 'tcp://127.0.0.1:7115') - .option('--bind-dev-pull ' - , 'device pull endpoint' - , String - , 'tcp://127.0.0.1:7116') - .option('--auth-type ' - , 'auth type' - , String - , 'mock') - .option('-a, --auth-url ' - , 'URL to auth client' - , String) - .option('--auth-port ' - , 'auth port' - , Number - , 7120) - .option('--auth-secret ' - , 'auth secret' - , String - , 'kute kittykat') - .option('--auth-options ' - , 'array of options to pass to the auth implementation' - , String - , '[]') - .option('--poorxy-port ' - , 'poorxy port' - , Number - , 7100) - .option('--app-port ' - , 'app port' - , Number - , 7105) - .option('--api-port ' - , 'api port' - , Number - , 7106) - .option('--websocket-port ' - , 'websocket port' - , Number - , 7110) - .option('--storage-type ' - , 'storage type' - , String - , 'temp') - .option('--storage-port ' - , 'storage port' - , Number - , 7102) - .option('--storage-options ' - , 'array of options to pass to the storage implementation' - , String - , '[]') - .option('--storage-plugin-image-port ' - , 'storage image plugin port' - , Number - , 7103) - .option('--storage-plugin-apk-port ' - , 'storage apk plugin port' - , Number - , 7104) - .option('--provider ' - , 'provider name (or os.hostname())' - , String - , os.hostname()) - .option('--provider-min-port ' - , 'minimum port number for worker use' - , Number - , 7400) - .option('--provider-max-port ' - , 'maximum port number for worker use' - , Number - , 7700) - .option('-t, --group-timeout ' - , 'group timeout' - , Number - , 900) - .option('--public-ip ' - , 'public ip for global access' - , String - , 'localhost') - .option('--adb-host ' - , 'ADB host (defaults to 127.0.0.1)' - , String - , '127.0.0.1') - .option('--adb-port ' - , 'ADB port (defaults to 5037)' - , Number - , 5037) - .option('-R, --allow-remote' - , 'Whether to allow remote devices to be set up') - .option('--user-profile-url ' - , 'URL to external user profile page' - , String) - .option('--vnc-initial-size ' - , 'initial VNC size' - , cliutil.size - , [600, 800]) - .option('--mute-master' - , 'whether to mute master volume when devices are being used') - .option('--lock-rotation' - , 'whether to lock rotation when devices are being used') - .option('-C, --no-cleanup' - , 'do not attempt to clean up devices between uses') - .action(function(serials, options) { - 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(__filename, [ - 'triproxy', 'app001' - , '--bind-pub', options.bindAppPub - , '--bind-dealer', options.bindAppDealer - , '--bind-pull', options.bindAppPull - ]) - - // device triproxy - , procutil.fork(__filename, [ - 'triproxy', 'dev001' - , '--bind-pub', options.bindDevPub - , '--bind-dealer', options.bindDevDealer - , '--bind-pull', options.bindDevPull - ]) - - // processor one - , procutil.fork(__filename, [ - 'processor', 'proc001' - , '--connect-app-dealer', options.bindAppDealer - , '--connect-dev-dealer', options.bindDevDealer - ]) - - // processor two - , procutil.fork(__filename, [ - 'processor', 'proc002' - , '--connect-app-dealer', options.bindAppDealer - , '--connect-dev-dealer', options.bindDevDealer - ]) - - // reaper one - , procutil.fork(__filename, [ - 'reaper', 'reaper001' - , '--connect-push', options.bindDevPull - , '--connect-sub', options.bindAppPub - ]) - - // provider - , procutil.fork(__filename, [ - 'provider' - , '--name', options.provider - , '--min-port', options.providerMinPort - , '--max-port', options.providerMaxPort - , '--connect-sub', options.bindDevPub - , '--connect-push', options.bindDevPull - , '--group-timeout', options.groupTimeout - , '--public-ip', options.publicIp - , '--storage-url' - , util.format('http://localhost:%d/', options.poorxyPort) - , '--adb-host', options.adbHost - , '--adb-port', options.adbPort - , '--vnc-initial-size', options.vncInitialSize.join('x') - ] - .concat(options.allowRemote ? ['--allow-remote'] : []) - .concat(options.muteMaster ? ['--mute-master'] : []) - .concat(options.lockRotation ? ['--lock-rotation'] : []) - .concat(!options.cleanup ? ['--no-cleanup'] : []) - .concat(serials)) - - // auth - , procutil.fork(__filename, [ - util.format('auth-%s', options.authType) - , '--port', options.authPort - , '--secret', options.authSecret - , '--app-url', util.format( - 'http://%s:%d/' - , options.publicIp - , options.poorxyPort - ) - ].concat(JSON.parse(options.authOptions))) - - // app - , procutil.fork(__filename, [ - 'app' - , '--port', options.appPort - , '--secret', options.authSecret - , '--auth-url', options.authUrl || util.format( - 'http://%s:%d/auth/%s/' - , options.publicIp - , options.poorxyPort - , { - oauth2: 'oauth' - , saml2: 'saml' - }[options.authType] || options.authType - ) - , '--websocket-url', util.format( - 'http://%s:%d/' - , options.publicIp - , options.websocketPort - ) - ].concat((function() { - var extra = [] - if (options.userProfileUrl) { - extra.push('--user-profile-url', options.userProfileUrl) - } - return extra - })())) - - // api - , procutil.fork(__filename, [ - 'api' - , '--port', options.apiPort - , '--secret', options.authSecret - , '--connect-push', options.bindAppPull - , '--connect-sub', options.bindAppPub - ]) - // websocket - , procutil.fork(__filename, [ - 'websocket' - , '--port', options.websocketPort - , '--secret', options.authSecret - , '--storage-url' - , util.format('http://localhost:%d/', options.poorxyPort) - , '--connect-sub', options.bindAppPub - , '--connect-push', options.bindAppPull - ]) - - // storage - , procutil.fork(__filename, [ - util.format('storage-%s', options.storageType) - , '--port', options.storagePort - ].concat(JSON.parse(options.storageOptions))) - - // image processor - , procutil.fork(__filename, [ - 'storage-plugin-image' - , '--port', options.storagePluginImagePort - , '--storage-url' - , util.format('http://localhost:%d/', options.poorxyPort) - ]) - - // apk processor - , procutil.fork(__filename, [ - 'storage-plugin-apk' - , '--port', options.storagePluginApkPort - , '--storage-url' - , util.format('http://localhost:%d/', options.poorxyPort) - ]) - - // poorxy - , procutil.fork(__filename, [ - 'poorxy' - , '--port', options.poorxyPort - , '--app-url' - , util.format('http://localhost:%d/', options.appPort) - , '--auth-url' - , util.format('http://localhost:%d/', options.authPort) - , '--api-url' - , util.format('http://localhost:%d/', options.apiPort) - , '--websocket-url' - , util.format('http://localhost:%d/', options.websocketPort) - , '--storage-url' - , util.format('http://localhost:%d/', options.storagePort) - , '--storage-plugin-image-url' - , util.format('http://localhost:%d/', options.storagePluginImagePort) - , '--storage-plugin-apk-url' - , util.format('http://localhost:%d/', options.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) - }) - }) - } - - procutil.fork(__filename, ['migrate']) - .done(run) - }) - -program - .command('doctor') - .description('diagnose issues before starting') - .option('--devices' - , 'diagnose devices connected to stf') - .action(function(options) { - require('./util/doctor').run(options) - }) - -program.parse(process.argv) diff --git a/lib/cli/api/index.js b/lib/cli/api/index.js new file mode 100644 index 00000000..6654f953 --- /dev/null +++ b/lib/cli/api/index.js @@ -0,0 +1,58 @@ +module.exports.command = 'api' + +module.exports.describe = 'Start an API unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_API') + .strict() + .option('connect-push', { + alias: 'c' + , describe: 'App-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub', { + alias: 'u' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7106 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .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_API_` (e.g. ' + + '`STF_API_PORT`).') +} + +module.exports.handler = function(argv) { + return require('../../units/api')({ + port: argv.port + , ssid: argv.ssid + , secret: argv.secret + , endpoints: { + push: argv.connectPush + , sub: argv.connectSub + } + }) +} diff --git a/lib/cli/app/index.js b/lib/cli/app/index.js new file mode 100644 index 00000000..962dd2e0 --- /dev/null +++ b/lib/cli/app/index.js @@ -0,0 +1,61 @@ +module.exports.command = 'app' + +module.exports.describe = 'Start an app unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_APP') + .strict() + .option('auth-url', { + alias: 'a' + , describe: 'URL to the auth unit.' + , type: 'string' + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7105 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .option('user-profile-url', { + describe: 'URL to an external user profile page.' + , type: 'string' + }) + .option('websocket-url', { + alias: 'w' + , describe: 'URL to the websocket unit.' + , type: 'string' + , demand: true + }) + .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_APP_` (e.g. ' + + '`STF_APP_AUTH_URL`).') +} + +module.exports.handler = function(argv) { + return require('../../units/app')({ + port: argv.port + , secret: argv.secret + , ssid: argv.ssid + , authUrl: argv.authUrl + , websocketUrl: argv.websocketUrl + , userProfileUrl: argv.userProfileUrl + }) +} diff --git a/lib/cli/auth-ldap/index.js b/lib/cli/auth-ldap/index.js new file mode 100644 index 00000000..4788862a --- /dev/null +++ b/lib/cli/auth-ldap/index.js @@ -0,0 +1,119 @@ +module.exports.command = 'auth-ldap' + +module.exports.describe = 'Start an LDAP auth unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_AUTH_LDAP') + .strict() + .option('app-url', { + alias: 'a' + , describe: 'URL to the app unit.' + , type: 'string' + , demand: true + }) + .option('ldap-bind-credentials', { + describe: 'LDAP bind credentials.' + , type: 'string' + , default: process.env.LDAP_BIND_CREDENTIALS + }) + .option('ldap-bind-dn', { + describe: 'LDAP bind DN.' + , type: 'string' + , default: process.env.LDAP_BIND_DN + }) + .option('ldap-search-class', { + describe: 'LDAP search objectClass.' + , type: 'string' + , default: process.env.LDAP_SEARCH_CLASS || 'top' + }) + .option('ldap-search-dn', { + describe: 'LDAP search DN.' + , type: 'string' + , default: process.env.LDAP_SEARCH_DN + , demand: true + }) + .option('ldap-search-field', { + describe: 'LDAP search field.' + , type: 'string' + , default: process.env.LDAP_SEARCH_FIELD + , demand: true + }) + .option('ldap-search-scope', { + describe: 'LDAP search scope.' + , type: 'string' + , default: process.env.LDAP_SEARCH_SCOPE || 'sub' + }) + .option('ldap-timeout', { + alias: 't' + , describe: 'LDAP timeout.' + , type: 'number' + , default: process.env.LDAP_TIMEOUT || 1000 + }) + .option('ldap-url', { + alias: 'u' + , describe: 'URL to the LDAP server (e.g. `ldap://127.0.0.1`).' + , type: 'string' + , default: process.env.LDAP_URL + , demand: true + }) + .option('ldap-username-field', { + describe: 'LDAP username field.' + , type: 'string' + , default: process.env.LDAP_USERNAME_FIELD || 'cn' + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7120 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .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_AUTH_LDAP_` (e.g. ' + + '`STF_AUTH_LDAP_SECRET`). Legacy environment variables like ' + + 'LDAP_USERNAME_FIELD are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/auth/ldap')({ + port: argv.port + , secret: argv.secret + , ssid: argv.ssid + , appUrl: argv.appUrl + , ldap: { + url: argv.ldapUrl + , timeout: argv.ldapTimeout + , bind: { + dn: argv.ldapBindDn + , credentials: argv.ldapBindCredentials + } + , search: { + dn: argv.ldapSearchDn + , scope: argv.ldapSearchScope + , objectClass: argv.ldapSearchClass + , field: argv.ldapSearchField + } + , username: { + field: argv.ldapUsernameField + } + } + }) +} diff --git a/lib/cli/auth-mock/index.js b/lib/cli/auth-mock/index.js new file mode 100644 index 00000000..8f294d09 --- /dev/null +++ b/lib/cli/auth-mock/index.js @@ -0,0 +1,72 @@ +module.exports.command = 'auth-mock' + +module.exports.describe = 'Start a mock auth unit that accepts any user.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_AUTH_MOCK') + .strict() + .option('app-url', { + alias: 'a' + , describe: 'URL to the app unit.' + , type: 'string' + , demand: true + }) + .option('basic-auth-password', { + describe: 'Basic auth password (if enabled).' + , type: 'string' + , default: process.env.BASIC_AUTH_PASSWORD + }) + .option('basic-auth-username', { + describe: 'Basic auth username (if enabled).' + , type: 'string' + , default: process.env.BASIC_AUTH_USERNAME + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7120 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .option('use-basic-auth', { + describe: 'Whether to "secure" the login page with basic authentication.' + , type: 'boolean' + }) + .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_AUTH_MOCK_` (e.g. ' + + '`STF_AUTH_MOCK_SECRET`). Legacy environment variables like ' + + 'BASIC_AUTH_USERNAME are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/auth/mock')({ + port: argv.port + , secret: argv.secret + , ssid: argv.ssid + , appUrl: argv.appUrl + , mock: { + useBasicAuth: argv.useBasicAuth + , basicAuth: { + username: argv.basicAuthUsername + , password: argv.basicAuthPassword + } + } + }) +} diff --git a/lib/cli/auth-oauth2/index.js b/lib/cli/auth-oauth2/index.js new file mode 100644 index 00000000..5898c66a --- /dev/null +++ b/lib/cli/auth-oauth2/index.js @@ -0,0 +1,102 @@ +module.exports.command = 'auth-oauth2' + +module.exports.describe = 'Start an OAuth 2.0 auth unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_AUTH_OAUTH2') + .strict() + .option('app-url', { + alias: 'a' + , describe: 'URL to the app unit.' + , type: 'string' + , demand: true + }) + .option('oauth-authorization-url', { + describe: 'OAuth 2.0 authorization URL.' + , type: 'string' + , default: process.env.OAUTH_AUTHORIZATION_URL + , demand: true + }) + .option('oauth-token-url', { + describe: 'OAuth 2.0 token URL.' + , type: 'string' + , default: process.env.OAUTH_TOKEN_URL + , demand: true + }) + .option('oauth-userinfo-url', { + describe: 'OAuth 2.0 user info URL.' + , type: 'string' + , default: process.env.OAUTH_USERINFO_URL + , demand: true + }) + .option('oauth-client-id', { + describe: 'OAuth 2.0 client ID.' + , type: 'string' + , default: process.env.OAUTH_CLIENT_ID + , demand: true + }) + .option('oauth-client-secret', { + describe: 'OAuth 2.0 client secret.' + , type: 'string' + , default: process.env.OAUTH_CLIENT_SECRET + , demand: true + }) + .option('oauth-callback-url', { + describe: 'OAuth 2.0 callback URL.' + , type: 'string' + , default: process.env.OAUTH_CALLBACK_URL + , demand: true + }) + .option('oauth-scope', { + describe: 'Space-separated OAuth 2.0 scope.' + , type: 'string' + , default: process.env.OAUTH_SCOPE + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7120 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .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_AUTH_OAUTH2_` (e.g. ' + + '`STF_AUTH_OAUTH2_SECRET`). Legacy environment variables like ' + + 'OAUTH_SCOPE are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/auth/oauth2')({ + port: argv.port + , secret: argv.secret + , ssid: argv.ssid + , appUrl: argv.appUrl + , oauth: { + authorizationURL: argv.oauthAuthorizationUrl + , tokenURL: argv.oauthTokenUrl + , userinfoURL: argv.oauthUserinfoUrl + , clientID: argv.oauthClientId + , clientSecret: argv.oauthClientSecret + , callbackURL: argv.oauthCallbackUrl + , scope: argv.oauthScope.split(/\s+/) + } + }) +} diff --git a/lib/cli/auth-openid/index.js b/lib/cli/auth-openid/index.js new file mode 100644 index 00000000..802a79d4 --- /dev/null +++ b/lib/cli/auth-openid/index.js @@ -0,0 +1,59 @@ +module.exports.command = 'auth-openid' + +module.exports.describe = 'Start an OpenID auth unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_AUTH_OPENID') + .strict() + .option('app-url', { + alias: 'a' + , describe: 'URL to the app unit.' + , type: 'string' + , demand: true + }) + .option('openid-identifier-url', { + describe: 'OpenID identifier URL.' + , type: 'string' + , default: process.env.OPENID_IDENTIFIER_URL + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7120 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .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_AUTH_OPENID_` (e.g. ' + + '`STF_AUTH_OPENID_SECRET`). Legacy environment variables like ' + + 'OPENID_IDENTIFIER_URL are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/auth/openid')({ + port: argv.port + , secret: argv.secret + , appUrl: argv.appUrl + , openid: { + identifierUrl: argv.openidIdentifierUrl + } + }) +} diff --git a/lib/cli/auth-saml2/index.js b/lib/cli/auth-saml2/index.js new file mode 100644 index 00000000..5a75e900 --- /dev/null +++ b/lib/cli/auth-saml2/index.js @@ -0,0 +1,73 @@ +module.exports.command = 'auth-saml2' + +module.exports.describe = 'Start a SAML 2.0 auth unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_AUTH_SAML2') + .strict() + .option('app-url', { + alias: 'a' + , describe: 'URL to the app unit.' + , type: 'string' + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7120 + }) + .option('saml-id-provider-entry-point-url', { + describe: 'SAML 2.0 identity provider URL.' + , type: 'string' + , default: process.env.SAML_ID_PROVIDER_ENTRY_POINT_URL + , demand: true + }) + .option('saml-id-provider-issuer', { + describe: 'SAML 2.0 identity provider issuer.' + , type: 'string' + , default: process.env.SAML_ID_PROVIDER_ISSUER + , demand: true + }) + .option('saml-id-provider-cert-path', { + describe: 'SAML 2.0 identity provider certificate file path.' + , type: 'string' + , default: process.env.SAML_ID_PROVIDER_CERT_PATH + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .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_AUTH_SAML2_` (e.g. ' + + '`STF_AUTH_SAML2_SECRET`). Legacy environment variables like ' + + 'SAML_ID_PROVIDER_ISSUER are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/auth/saml2')({ + port: argv.port + , secret: argv.secret + , ssid: argv.ssid + , appUrl: argv.appUrl + , saml: { + entryPoint: argv.samlIdProviderEntryPointUrl + , issuer: argv.samlIdProviderIssuer + , certPath: argv.samlIdProviderCertPath + } + }) +} diff --git a/lib/cli/device/index.js b/lib/cli/device/index.js new file mode 100644 index 00000000..e1158a83 --- /dev/null +++ b/lib/cli/device/index.js @@ -0,0 +1,145 @@ +module.exports.command = 'device ' + +module.exports.builder = function(yargs) { + return yargs + .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('boot-complete-timeout', { + describe: 'How long to wait for boot to complete during device setup.' + , type: 'number' + , default: 60000 + }) + .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('connect-port', { + describe: 'Port allocated to adb connections.' + , type: 'number' + , demand: true + }) + .option('connect-push', { + alias: 'p' + , describe: 'ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub', { + alias: 's' + , describe: 'ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-url-pattern', { + describe: 'The URL pattern to use for `adb connect`.' + , type: 'string' + , default: '${publicIp}:${publicPort}' + }) + .option('group-timeout', { + alias: 't' + , describe: 'Timeout in seconds for automatic release of inactive devices.' + , type: 'number' + , default: 900 + }) + .option('heartbeat-interval', { + describe: 'Send interval in milliseconds for heartbeat messages.' + , type: 'number' + , default: 10000 + }) + .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 when devices are being used.' + , type: 'boolean' + }) + .option('provider', { + alias: 'n' + , describe: 'Name of the provider.' + , type: 'string' + , demand: true + }) + .option('public-ip', { + describe: 'The IP or hostname to use in URLs.' + , type: 'string' + , demand: true + }) + .option('screen-jpeg-quality', { + describe: 'The JPG quality to use for the screen.' + , type: 'number' + , default: process.env.SCREEN_JPEG_QUALITY || 80 + }) + .option('screen-port', { + describe: 'Port allocated to the screen WebSocket.' + , type: 'number' + , demand: true + }) + .option('screen-ws-url-pattern', { + describe: 'The URL pattern to use for the screen WebSocket.' + , type: 'string' + , default: 'ws://${publicIp}:${publicPort}' + }) + .option('storage-url', { + alias: 'r' + , describe: 'The URL to the storage unit.' + , type: 'string' + , demand: true + }) + .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('vnc-port', { + describe: 'Port allocated to VNC connections.' + , type: 'number' + , demand: true + }) +} + +module.exports.handler = function(argv) { + return require('../../units/device')({ + serial: argv.serial + , provider: argv.provider + , publicIp: argv.publicIp + , endpoints: { + sub: argv.connectSub + , push: argv.connectPush + } + , groupTimeout: argv.groupTimeout * 1000 // change to ms + , storageUrl: argv.storageUrl + , adbHost: argv.adbHost + , adbPort: argv.adbPort + , screenWsUrlPattern: argv.screenWsUrlPattern + , screenJpegQuality: argv.screenJpegQuality + , screenPort: argv.screenPort + , connectUrlPattern: argv.connectUrlPattern + , connectPort: argv.connectPort + , vncPort: argv.vncPort + , vncInitialSize: argv.vncInitialSize + , heartbeatInterval: argv.heartbeatInterval + , bootCompleteTimeout: argv.bootCompleteTimeout + , muteMaster: argv.muteMaster + , lockRotation: argv.lockRotation + , cleanup: argv.cleanup + }) +} diff --git a/lib/cli/doctor/index.js b/lib/cli/doctor/index.js new file mode 100644 index 00000000..596dc5d6 --- /dev/null +++ b/lib/cli/doctor/index.js @@ -0,0 +1,11 @@ +module.exports.command = 'doctor' + +module.exports.describe = 'Diagnose potential issues with your installation.' + +module.exports.builder = function(yargs) { + return yargs +} + +module.exports.handler = function() { + return require('../../util/doctor').run() +} diff --git a/lib/cli/generate-fake-device/index.js b/lib/cli/generate-fake-device/index.js new file mode 100644 index 00000000..867c24c5 --- /dev/null +++ b/lib/cli/generate-fake-device/index.js @@ -0,0 +1,35 @@ +module.exports.command = 'generate-fake-device ' + +module.exports.builder = function(yargs) { + return yargs + .strict() + .option('n', { + alias: 'number' + , describe: 'How many devices to create.' + , type: 'number' + , default: 1 + }) +} + +module.exports.handler = function(argv) { + var logger = require('../../util/logger') + var log = logger.createLogger('cli:generate-fake-device') + var fake = require('../../util/fakedevice') + var n = argv.number + + function next() { + return fake.generate(argv.model).then(function(serial) { + log.info('Created fake device "%s"', serial) + return --n ? next() : null + }) + } + + return next() + .then(function() { + process.exit(0) + }) + .catch(function(err) { + log.fatal('Fake device creation had an error:', err.stack) + process.exit(1) + }) +} diff --git a/lib/cli/index.js b/lib/cli/index.js new file mode 100644 index 00000000..e6fc8556 --- /dev/null +++ b/lib/cli/index.js @@ -0,0 +1,40 @@ +var yargs = require('yargs') +var Promise = require('bluebird') + +Promise.longStackTraces() + +var _argv = yargs.usage('Usage: $0 [options]') + .strict() + .command(require('./api')) + .command(require('./app')) + .command(require('./auth-ldap')) + .command(require('./auth-mock')) + .command(require('./auth-oauth2')) + .command(require('./auth-openid')) + .command(require('./auth-saml2')) + .command(require('./device')) + .command(require('./doctor')) + .command(require('./generate-fake-device')) + .command(require('./local')) + .command(require('./log-rethinkdb')) + .command(require('./migrate')) + .command(require('./notify-hipchat')) + .command(require('./notify-slack')) + .command(require('./poorxy')) + .command(require('./processor')) + .command(require('./provider')) + .command(require('./reaper')) + .command(require('./storage-plugin-apk')) + .command(require('./storage-plugin-image')) + .command(require('./storage-s3')) + .command(require('./storage-temp')) + .command(require('./triproxy')) + .command(require('./websocket')) + .demand(1, 'Must provide a valid command.') + .help('h', 'Show help.') + .alias('h', 'help') + .version('V', 'Show version.', function() { + return require('../../package').version + }) + .alias('V', 'version') + .argv diff --git a/lib/cli/local/index.js b/lib/cli/local/index.js new file mode 100644 index 00000000..2d6dd646 --- /dev/null +++ b/lib/cli/local/index.js @@ -0,0 +1,408 @@ +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 when devices are being used.' + , type: 'boolean' + }) + .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') + ] + .concat(argv.allowRemote ? ['--allow-remote'] : []) + .concat(argv.muteMaster ? ['--mute-master'] : []) + .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(__filename, ['migrate']) + .done(run) +} diff --git a/lib/cli/log-rethinkdb/index.js b/lib/cli/log-rethinkdb/index.js new file mode 100644 index 00000000..1cf91f4e --- /dev/null +++ b/lib/cli/log-rethinkdb/index.js @@ -0,0 +1,36 @@ +module.exports.command = 'log-rethinkdb' + +module.exports.describe = 'Start a RethinkDB log unit.' + +module.exports.builder = function(yargs) { + var logger = require('../../util/logger') + + return yargs + .env('STF_LOG_RETHINKDB') + .strict() + .option('connect-sub', { + alias: 's' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('priority', { + alias: 'p' + , describe: 'Minimum log level.' + , type: 'number' + , default: logger.Level.IMPORTANT + }) + .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_LOG_RETHINKDB_` (e.g. ' + + '`STF_LOG_RETHINKDB_PRIORITY`).') +} + +module.exports.handler = function(argv) { + return require('../../units/log/rethinkdb')({ + priority: argv.priority + , endpoints: { + sub: argv.connectSub + } + }) +} diff --git a/lib/cli/migrate/index.js b/lib/cli/migrate/index.js new file mode 100644 index 00000000..f5954c0d --- /dev/null +++ b/lib/cli/migrate/index.js @@ -0,0 +1,22 @@ +module.exports.command = 'migrate' + +module.exports.describe = 'Migrates the database to the latest version.' + +module.exports.builder = function(yargs) { + return yargs +} + +module.exports.handler = function() { + var logger = require('../../util/logger') + var log = logger.createLogger('cli:migrate') + var db = require('../../db') + + return db.setup() + .then(function() { + process.exit(0) + }) + .catch(function(err) { + log.fatal('Migration had an error:', err.stack) + process.exit(1) + }) +} diff --git a/lib/cli/notify-hipchat/index.js b/lib/cli/notify-hipchat/index.js new file mode 100644 index 00000000..43cb10e5 --- /dev/null +++ b/lib/cli/notify-hipchat/index.js @@ -0,0 +1,61 @@ +module.exports.command = 'notify-hipchat' + +module.exports.describe = 'Start a HipChat notifier unit.' + +module.exports.builder = function(yargs) { + var logger = require('../../util/logger') + + return yargs + .env('STF_NOTIFY_HIPCHAT') + .strict() + .option('connect-sub', { + alias: 's' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , type: 'array' + , demand: true + }) + .option('notify-priority', { + alias: 'n' + , describe: 'Minimum log level to cause a notification.' + , type: 'number' + , default: logger.Level.WARNING + }) + .option('priority', { + alias: 'p' + , describe: 'Minimum log level.' + , type: 'number' + , default: logger.Level.IMPORTANT + }) + .option('room', { + alias: 'r' + , describe: 'HipChat room.' + , type: 'string' + , default: process.env.HIPCHAT_ROOM + , demand: true + }) + .option('token', { + alias: 't' + , describe: 'HipChat v2 API token.' + , type: 'string' + , default: process.env.HIPCHAT_TOKEN + , demand: true + }) + .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_NOTIFY_HIPCHAT_` (e.g. ' + + '`STF_NOTIFY_HIPCHAT_ROOM`). Legacy environment variables like ' + + 'HIPCHAT_TOKEN are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/notify/hipchat')({ + token: argv.token + , room: argv.room + , priority: argv.priority + , notifyPriority: argv.notifyPriority + , endpoints: { + sub: argv.connectSub + } + }) +} diff --git a/lib/cli/notify-slack/index.js b/lib/cli/notify-slack/index.js new file mode 100644 index 00000000..2f6fedf1 --- /dev/null +++ b/lib/cli/notify-slack/index.js @@ -0,0 +1,54 @@ +module.exports.command = 'notify-slack' + +module.exports.describe = 'Start a Slack notifier unit.' + +module.exports.builder = function(yargs) { + var logger = require('../../util/logger') + + return yargs + .env('STF_NOTIFY_SLACK') + .strict() + .option('channel', { + alias: 'c' + , describe: 'Slack channel.' + , type: 'string' + , default: process.env.SLACK_CHANNEL + , demand: true + }) + .option('connect-sub', { + alias: 's' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , type: 'array' + , demand: true + }) + .option('priority', { + alias: 'p' + , describe: 'Minimum log level.' + , type: 'number' + , default: logger.Level.IMPORTANT + }) + .option('token', { + alias: 't' + , describe: 'Slack API token.' + , type: 'string' + , default: process.env.SLACK_TOKEN + , demand: true + }) + .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_NOTIFY_SLACK_` (e.g. ' + + '`STF_NOTIFY_SLACK_CHANNEL`). Legacy environment variables like ' + + 'SLACK_TOKEN are still accepted, too, but consider them ' + + 'deprecated.') +} + +module.exports.handler = function(argv) { + return require('../../units/notify/slack')({ + token: argv.token + , channel: argv.channel + , priority: argv.priority + , endpoints: { + sub: argv.connectSub + } + }) +} diff --git a/lib/cli.please.js b/lib/cli/please.js similarity index 73% rename from lib/cli.please.js rename to lib/cli/please.js index a5a0c0e6..3f6cabcd 100644 --- a/lib/cli.please.js +++ b/lib/cli/please.js @@ -1,2 +1,2 @@ require('please-update-dependencies')(module) -require('./cli') +require('./') diff --git a/lib/cli/poorxy/index.js b/lib/cli/poorxy/index.js new file mode 100644 index 00000000..90f36c28 --- /dev/null +++ b/lib/cli/poorxy/index.js @@ -0,0 +1,70 @@ +module.exports.command = 'poorxy' + +module.exports.builder = function(yargs) { + return yargs + .strict() + .env('STF_POORXY') + .option('api-url', { + alias: 'i' + , describe: 'URL to the api unit.' + , type: 'string' + , demand: true + }) + .option('app-url', { + alias: 'u' + , describe: 'URL to the app unit.' + , type: 'string' + , demand: true + }) + .option('auth-url', { + alias: 'a' + , describe: 'URL to the auth unit.' + , type: 'string' + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to launch poorxy on.' + , type: 'number' + , default: process.env.PORT || 7100 + }) + .option('storage-plugin-apk-url', { + describe: 'URL to the APK storage plugin unit.' + , type: 'string' + , demand: true + }) + .option('storage-plugin-image-url', { + describe: 'URL to the image storage plugin unit.' + , type: 'string' + , demand: true + }) + .option('storage-url', { + alias: 'r' + , describe: 'URL to the storage unit.' + , type: 'string' + , demand: true + }) + .option('websocket-url', { + alias: 'w' + , describe: 'URL to the websocket unit.' + , type: 'string' + , demand: true + }) + .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_POORXY_` (e.g. ' + + '`STF_POORXY_PORT`).') +} + +module.exports.handler = function(argv) { + return require('../../units/poorxy')({ + port: argv.port + , appUrl: argv.appUrl + , apiUrl: argv.apiUrl + , authUrl: argv.authUrl + , websocketUrl: argv.websocketUrl + , storageUrl: argv.storageUrl + , storagePluginImageUrl: argv.storagePluginImageUrl + , storagePluginApkUrl: argv.storagePluginApkUrl + }) +} diff --git a/lib/cli/processor/index.js b/lib/cli/processor/index.js new file mode 100644 index 00000000..fa4e20ea --- /dev/null +++ b/lib/cli/processor/index.js @@ -0,0 +1,42 @@ +module.exports.command = 'processor [name]' + +module.exports.describe = 'Start a processor unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + + return yargs + .env('STF_PROCESSOR') + .strict() + .option('connect-app-dealer', { + alias: 'a' + , describe: 'App-side ZeroMQ DEALER endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-dev-dealer', { + alias: 'd' + , describe: 'Device-side ZeroMQ DEALER endpoint to connect to.' + , array: true + , demand: true + }) + .option('name', { + describe: 'An easily identifiable name for log output.' + , type: 'string' + , default: os.hostname() + }) + .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_PROCESSOR_` (e.g. ' + + '`STF_PROCESSOR_CONNECT_APP_DEALER`).') +} + +module.exports.handler = function(argv) { + return require('../../units/processor')({ + name: argv.name + , endpoints: { + appDealer: argv.connectAppDealer + , devDealer: argv.connectDevDealer + } + }) +} diff --git a/lib/cli/provider/index.js b/lib/cli/provider/index.js new file mode 100644 index 00000000..7e044b68 --- /dev/null +++ b/lib/cli/provider/index.js @@ -0,0 +1,192 @@ +module.exports.command = 'provider [serial..]' + +module.exports.describe = 'Start a provider unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + var ip = require('my-local-ip') + + return yargs + .strict() + .env('STF_PROVIDER') + .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('boot-complete-timeout', { + describe: 'How long to wait for boot to complete during device setup.' + , type: 'number' + , default: 60000 + }) + .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('connect-push', { + alias: 'p' + , describe: 'Device-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub', { + alias: 's' + , describe: 'Device-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-url-pattern', { + describe: 'The URL pattern to use for `adb connect`.' + , type: 'string' + , default: '${publicIp}:${publicPort}' + }) + .option('group-timeout', { + alias: 't' + , describe: 'Timeout in seconds for automatic release of inactive devices.' + , type: 'number' + , default: 900 + }) + .option('heartbeat-interval', { + describe: 'Send interval in milliseconds for heartbeat messages.' + , type: 'number' + , default: 10000 + }) + .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('max-port', { + describe: 'Highest port number for device workers to use.' + , type: 'number' + , default: 7700 + }) + .option('min-port', { + describe: 'Lowest port number for device workers to use.' + , type: 'number' + , default: 7400 + }) + .option('mute-master', { + describe: 'Whether to mute master volume when devices are being used.' + , type: 'boolean' + }) + .option('name', { + alias: 'n' + , describe: 'An easily identifiable name for the UI and/or log output.' + , type: 'string' + , default: os.hostname() + }) + .option('public-ip', { + describe: 'The IP or hostname to use in URLs.' + , type: 'string' + , default: ip() + }) + .option('screen-jpeg-quality', { + describe: 'The JPG quality to use for the screen.' + , type: 'number' + , default: process.env.SCREEN_JPEG_QUALITY || 80 + }) + .option('screen-ws-url-pattern', { + describe: 'The URL pattern to use for the screen WebSocket.' + , type: 'string' + , default: 'ws://${publicIp}:${publicPort}' + }) + .option('storage-url', { + alias: 'r' + , describe: 'The URL to the storage unit.' + , type: 'string' + , demand: true + }) + .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) + } + }) + .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_PROVIDER_` (e.g. ' + + '`STF_PROVIDER_NAME`).') +} + +module.exports.handler = function(argv) { + var path = require('path') + var cli = path.resolve(__dirname, '..') + + function range(from, to) { + var items = [] + for (var i = from; i <= to; ++i) { + items.push(i) + } + return items + } + + return require('../../units/provider')({ + name: argv.name + , killTimeout: 10000 + , ports: range(argv.minPort, argv.maxPort) + , filter: function(device) { + return argv.serial.length === 0 || argv.serial.indexOf(device.id) !== -1 + } + , allowRemote: argv.allowRemote + , fork: function(device, ports) { + var fork = require('child_process').fork + + var args = [ + 'device', device.id + , '--provider', argv.name + , '--screen-port', ports.shift() + , '--connect-port', ports.shift() + , '--vnc-port', ports.shift() + , '--public-ip', argv.publicIp + , '--group-timeout', argv.groupTimeout + , '--storage-url', argv.storageUrl + , '--adb-host', argv.adbHost + , '--adb-port', argv.adbPort + , '--screen-ws-url-pattern', argv.screenWsUrlPattern + , '--screen-jpeg-quality', argv.screenJpegQuality + , '--connect-url-pattern', argv.connectUrlPattern + , '--heartbeat-interval', argv.heartbeatInterval + , '--boot-complete-timeout', argv.bootCompleteTimeout + , '--vnc-initial-size', argv.vncInitialSize.join('x') + ] + .concat(argv.connectSub.reduce(function(all, val) { + return all.concat(['--connect-sub', val]) + }, [])) + .concat(argv.connectPush.reduce(function(all, val) { + return all.concat(['--connect-push', val]) + }, [])) + .concat(argv.muteMaster ? ['--mute-master'] : []) + .concat(argv.lockRotation ? ['--lock-rotation'] : []) + .concat(!argv.cleanup ? ['--no-cleanup'] : []) + + return fork(cli, args) + } + , endpoints: { + sub: argv.connectSub + , push: argv.connectPush + } + , adbHost: argv.adbHost + , adbPort: argv.adbPort + }) +} diff --git a/lib/cli/reaper/index.js b/lib/cli/reaper/index.js new file mode 100644 index 00000000..b19859a7 --- /dev/null +++ b/lib/cli/reaper/index.js @@ -0,0 +1,50 @@ +module.exports.command = 'reaper [name]' + +module.exports.describe = 'Start a reaper unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + + return yargs + .env('STF_REAPER') + .strict() + .option('connect-push', { + alias: 'p' + , describe: 'Device-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub', { + alias: 's' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('heartbeat-timeout', { + alias: 't' + , describe: 'Consider devices with heartbeat older than the timeout ' + + 'value dead. Given in milliseconds.' + , type: 'number' + , default: 30000 + }) + .option('name', { + describe: 'An easily identifiable name for log output.' + , type: 'string' + , default: os.hostname() + }) + .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_REAPER_` (e.g. ' + + '`STF_REAPER_CONNECT_PUSH`).') +} + +module.exports.handler = function(argv) { + return require('../../units/reaper')({ + name: argv.name + , heartbeatTimeout: argv.heartbeatTimeout + , endpoints: { + push: argv.connectPush + , sub: argv.connectSub + } + }) +} diff --git a/lib/cli/storage-plugin-apk/index.js b/lib/cli/storage-plugin-apk/index.js new file mode 100644 index 00000000..40833227 --- /dev/null +++ b/lib/cli/storage-plugin-apk/index.js @@ -0,0 +1,40 @@ +module.exports.command = 'storage-plugin-apk' + +module.exports.describe = 'Start an APK storage plugin unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + + return yargs + .env('STF_STORAGE_PLUGIN_APK') + .strict() + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7100 + }) + .option('storage-url', { + alias: 'r' + , describe: 'URL to the storage unit.' + , type: 'string' + , demand: true + }) + .option('cache-dir', { + describe: 'The location where to cache images.' + , type: 'string' + , default: os.tmpdir() + }) + .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_STORAGE_PLUGIN_APK_` (e.g. ' + + '`STF_STORAGE_PLUGIN_APK_CACHE_DIR`).') +} + +module.exports.handler = function(argv) { + return require('../../units/storage/plugins/apk')({ + port: argv.port + , storageUrl: argv.storageUrl + , cacheDir: argv.cacheDir + }) +} diff --git a/lib/cli/storage-plugin-image/index.js b/lib/cli/storage-plugin-image/index.js new file mode 100644 index 00000000..3b6f1d07 --- /dev/null +++ b/lib/cli/storage-plugin-image/index.js @@ -0,0 +1,48 @@ +module.exports.command = 'storage-plugin-image' + +module.exports.describe = 'Start an image storage plugin unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + + return yargs + .env('STF_STORAGE_PLUGIN_IMAGE') + .strict() + .option('concurrency', { + alias: 'c' + , describe: 'Maximum number of simultaneous transformations.' + , type: 'number' + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7100 + }) + .option('storage-url', { + alias: 'r' + , describe: 'URL to the storage unit.' + , type: 'string' + , demand: true + }) + .option('cache-dir', { + describe: 'The location where to cache images.' + , type: 'string' + , default: os.tmpdir() + }) + .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_STORAGE_PLUGIN_IMAGE_` (e.g. ' + + '`STF_STORAGE_PLUGIN_IMAGE_CONCURRENCY`).') +} + +module.exports.handler = function(argv) { + var os = require('os') + + return require('../../units/storage/plugins/image')({ + port: argv.port + , storageUrl: argv.storageUrl + , cacheDir: argv.cacheDir + , concurrency: argv.concurrency || os.cpus().length + }) +} diff --git a/lib/cli/storage-s3/index.js b/lib/cli/storage-s3/index.js new file mode 100644 index 00000000..13d86245 --- /dev/null +++ b/lib/cli/storage-s3/index.js @@ -0,0 +1,33 @@ +module.exports.command = 'storage-temp' + +module.exports.describe = 'Start a temp storage unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + + return yargs + .env('STF_STORAGE_TEMP') + .strict() + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7100 + }) + .option('save-dir', { + describe: 'The location where files are saved to.' + , type: 'string' + , default: os.tmpdir() + }) + .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_STORAGE_TEMP_` (e.g. ' + + '`STF_STORAGE_TEMP_SAVE_DIR`).') +} + +module.exports.handler = function(argv) { + return require('../../units/storage/temp')({ + port: argv.port + , saveDir: argv.saveDir + }) +} diff --git a/lib/cli/storage-temp/index.js b/lib/cli/storage-temp/index.js new file mode 100644 index 00000000..334a13f0 --- /dev/null +++ b/lib/cli/storage-temp/index.js @@ -0,0 +1,43 @@ +module.exports.command = 'storage-s3' + +module.exports.describe = 'Start an S3 storage unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_STORAGE_S3') + .strict() + .option('bucket', { + describe: 'S3 bucket name.' + , type: 'string' + , demand: true + }) + .option('endpoint', { + describe: 'S3 bucket endpoint.' + , type: 'string' + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7100 + }) + .option('profile', { + describe: 'AWS credentials profile name.' + , type: 'string' + , demand: true + }) + .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_STORAGE_S3_` (e.g. ' + + '`STF_STORAGE_S3_PROFILE`).') +} + +module.exports.handler = function(argv) { + return require('../../units/storage/s3')({ + port: argv.port + , profile: argv.profile + , bucket: argv.bucket + , endpoint: argv.endpoint + }) +} diff --git a/lib/cli/triproxy/index.js b/lib/cli/triproxy/index.js new file mode 100644 index 00000000..b0801d34 --- /dev/null +++ b/lib/cli/triproxy/index.js @@ -0,0 +1,49 @@ +module.exports.command = 'triproxy [name]' + +module.exports.describe = 'Start a triproxy unit.' + +module.exports.builder = function(yargs) { + var os = require('os') + + return yargs + .env('STF_TRIPROXY') + .strict() + .option('bind-dealer', { + alias: 'd' + , describe: 'The address to bind the ZeroMQ DEALER endpoint to.' + , type: 'string' + , default: 'tcp://*:7112' + }) + .option('bind-pub', { + alias: 'u' + , describe: 'The address to bind the ZeroMQ PUB endpoint to.' + , type: 'string' + , default: 'tcp://*:7111' + }) + .option('bind-pull', { + alias: 'p' + , describe: 'The address to bind the ZeroMQ PULL endpoint to.' + , type: 'string' + , default: 'tcp://*:7113' + }) + .option('name', { + describe: 'An easily identifiable name for log output.' + , type: 'string' + , default: os.hostname() + }) + .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_TRIPROXY_` (e.g. ' + + '`STF_TRIPROXY_BIND_PUB`).') +} + +module.exports.handler = function(argv) { + return require('../../units/triproxy')({ + name: argv.name + , endpoints: { + pub: argv.bindPub + , dealer: argv.bindDealer + , pull: argv.bindPull + } + }) +} diff --git a/lib/cli/websocket/index.js b/lib/cli/websocket/index.js new file mode 100644 index 00000000..f5d9ed13 --- /dev/null +++ b/lib/cli/websocket/index.js @@ -0,0 +1,65 @@ +module.exports.command = 'websocket' + +module.exports.describe = 'Start a websocket unit.' + +module.exports.builder = function(yargs) { + return yargs + .env('STF_WEBSOCKET') + .strict() + .option('connect-push', { + alias: 'c' + , describe: 'App-side ZeroMQ PULL endpoint to connect to.' + , array: true + , demand: true + }) + .option('connect-sub', { + alias: 'u' + , describe: 'App-side ZeroMQ PUB endpoint to connect to.' + , array: true + , demand: true + }) + .option('port', { + alias: 'p' + , describe: 'The port to bind to.' + , type: 'number' + , default: process.env.PORT || 7110 + }) + .option('secret', { + alias: 's' + , 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: process.env.SECRET + , demand: true + }) + .option('ssid', { + alias: 'i' + , describe: 'The name of the session ID cookie.' + , type: 'string' + , default: process.env.SSID || 'ssid' + }) + .option('storage-url', { + alias: 'r' + , describe: 'URL to the storage unit.' + , type: 'string' + , demand: true + }) + .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_WEBSOCKET_` (e.g. ' + + '`STF_WEBSOCKET_STORAGE_URL`).') +} + +module.exports.handler = function(argv) { + return require('../../units/websocket')({ + port: argv.port + , secret: argv.secret + , ssid: argv.ssid + , storageUrl: argv.storageUrl + , endpoints: { + sub: argv.connectSub + , push: argv.connectPush + } + }) +} diff --git a/lib/util/cliutil.js b/lib/util/cliutil.js index 5b619638..1d03909e 100644 --- a/lib/util/cliutil.js +++ b/lib/util/cliutil.js @@ -8,11 +8,3 @@ module.exports.size = function(val) { return [Number(match[1]), Number(match[2])] } } - -module.exports.range = function(from, to) { - var items = [] - for (var i = from; i <= to; ++i) { - items.push(i) - } - return items -} diff --git a/lib/util/doctor.js b/lib/util/doctor.js index 444addc5..476ff25d 100644 --- a/lib/util/doctor.js +++ b/lib/util/doctor.js @@ -145,64 +145,7 @@ doctor.checkADB = function() { ) } -doctor.checkDevices = function() { - // Show all connected USB devices, including hubs - if (os.platform() === 'darwin') { - childProcess.execFile('ioreg', ['-p', 'IOUSB', '-w0'], - function(error, stdout, stderr) { - log.info('USB devices connected including hubs:') - if (!execHasErrors(error, stderr)) { - var list = stdout.replace(/@.*|\+-o Root\s{2}.*\n|\+-o |^\s{2}/gm, '') - .split('\n') - list.forEach(function(device) { - log.info(device) - }) - } - } - ) - } - else if (os.platform() === 'linux') { - childProcess.execFile('lsusb', [], - function(error, stdout, stderr) { - log.info('USB devices connected including hubs:') - if (!execHasErrors(error, stderr)) { - var list = stdout.replace(/Bus \d+ Device \d+: ID \w+:\w+ /gm, '') - .split('\n') - list.forEach(function(device) { - log.info(device) - }) - } - } - ) - } - - // Show all the devices seen by adb - childProcess.execFile('adb', ['devices'], - function(error, stdout, stderr) { - log.info('Devices that ADB can see:') - if (!execHasErrors(error, stderr)) { - var s = stdout.replace(/List of devices attached \n|^\s*/gm, '') - if (s.length === 0) { - log.error('No devices') - } - else { - var list = s.split('\n') - list.forEach(function(device) { - log.info(device) - }) - } - } - } - ) -} - -doctor.run = function(options) { - // Check devices - if (options.devices) { - doctor.checkDevices() - return - } - +doctor.run = function() { // Check OS architecture doctor.checkOSArch() diff --git a/lib/util/srv.js b/lib/util/srv.js index 0a785952..caa4f2a9 100644 --- a/lib/util/srv.js +++ b/lib/util/srv.js @@ -81,10 +81,10 @@ srv.resolve = function(domain) { var parsedUrl = url.parse(domain) if (!parsedUrl.protocol) { - return Promise.reject(new Error( + return Promise.reject(new Error(util.format( 'Must include protocol in "%s"' , domain - )) + ))) } if (/^srv\+/.test(parsedUrl.protocol)) { diff --git a/package.json b/package.json index c2a23ad0..71984938 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "body-parser": "^1.13.3", "bufferutil": "^1.2.1", "chalk": "~1.1.1", - "commander": "^2.9.0", "compression": "^1.5.2", "cookie-session": "^2.0.0-alpha.1", "csurf": "^1.7.0", @@ -90,6 +89,7 @@ "utf-8-validate": "^1.2.1", "uuid": "^3.0.0", "ws": "^1.0.1", + "yargs": "^6.5.0", "zmq": "^2.14.0" }, "devDependencies": {