diff --git a/bin/tessel-2.js b/bin/tessel-2.js index acf36ad6..5fc28ca3 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -11,7 +11,6 @@ var controller = require('../lib/controller'); var init = require('../lib/init'); var key = require('../lib/key'); var logs = require('../lib/logs'); -var Tessel = require('../lib/tessel/tessel'); var drivers = require('./tessel-install-drivers'); function makeCommand(commandName) { @@ -26,7 +25,6 @@ function makeCommand(commandName) { required: false, metavar: 'PRIVATEKEY', abbr: 'i', - default: Tessel.LOCAL_AUTH_KEY, help: 'SSH key for authorization with your Tessel' }) .option('name', { diff --git a/lib/controller.js b/lib/controller.js index 6a4bfaca..aa9e0bdb 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -24,273 +24,276 @@ var responses = { auth: 'No Tessels Found.' }; -Tessel.list = function(opts) { +// Wrapper function for Tessel.list to set SSH key path +controller.list = function(opts) { + return controller.keyHelper(opts, Tessel.list); +}; + +// Wrapper function for Tessel.get to set SSH key path +controller.get = function(opts) { + return controller.keyHelper(opts, Tessel.get); +}; + +controller.keyHelper = function(opts, next) { var keyPromise = Promise.resolve(); if (opts.key) { // Set the default SSH key path before we search keyPromise = provision.setDefaultKey(opts.key); } - return keyPromise.then(function() { + return keyPromise.then(() => next(opts)); +}; - return new Promise(function(resolve, reject) { +Tessel.list = function(opts) { - // Grab all attached Tessels - logs.info('Searching for nearby Tessels...'); + return new Promise(function(resolve, reject) { - // Keep a list of all the Tessels we discovered - var foundTessels = []; + // Grab all attached Tessels + logs.info('Searching for nearby Tessels...'); - // Options for Tessel discovery - var seekerOpts = { - timeout: opts.timeout * 1000, - usb: opts.usb, - lan: opts.lan, - authorized: undefined - }; + // Keep a list of all the Tessels we discovered + var foundTessels = []; - // Start looking for Tessels - var seeker = new discover.TesselSeeker().start(seekerOpts); - var noTessels = opts.authorized ? - responses.noAuth : - responses.auth; + // Options for Tessel discovery + var seekerOpts = { + timeout: opts.timeout * 1000, + usb: opts.usb, + lan: opts.lan, + authorized: undefined + }; - // When a Tessel is found - seeker.on('tessel', function displayResults(tessel) { + // Start looking for Tessels + var seeker = new discover.TesselSeeker().start(seekerOpts); + var noTessels = opts.authorized ? + responses.noAuth : + responses.auth; - var note = ''; + // When a Tessel is found + seeker.on('tessel', function displayResults(tessel) { - // Add it to our array - foundTessels.push(tessel); + var note = ''; - // Add a note if the user isn't authorized to use it yet - if (tessel.connection.connectionType === 'LAN' && !tessel.connection.authorized) { - note = '(USB connect and run `t2 provision` to authorize)'; - } + // Add it to our array + foundTessels.push(tessel); - // Print out details... - logs.basic(sprintf('\t%s\t%s\t%s', tessel.name, tessel.connection.connectionType, note)); - }); + // Add a note if the user isn't authorized to use it yet + if (tessel.connection.connectionType === 'LAN' && !tessel.connection.authorized) { + note = '(USB connect and run `t2 provision` to authorize)'; + } - // Called after CTRL+C or timeout - seeker.once('end', function stopSearch() { - // If there were no Tessels found - if (foundTessels.length === 0) { - // Report the sadness - return reject(noTessels); - } else if (foundTessels.length === 1) { - // Close all opened connections and resolve - controller.closeTesselConnections(foundTessels) - .then(resolve); - } - // If we have only one Tessel or two Tessels with the same name (just USB and LAN) - else if (foundTessels.length === 1 || - (foundTessels.length === 2 && foundTessels[0].name === foundTessels[1].name)) { - // Close all opened connections and resolve - controller.closeTesselConnections(foundTessels) - .then(resolve); - } - // Otherwise - else { - logs.info('Multiple Tessels found.'); - // Figure out which Tessel will be selected - return controller.runHeuristics(opts, foundTessels) - .then(function logSelected(tessel) { - // Report that selected Tessel to the user - logs.info('Will default to %s.', tessel.name); - }) - .catch(function(err) { - if (!(err instanceof controller.HeuristicAmbiguityError)) { - return controller.closeTesselConnections(foundTessels) - .then(reject.bind(this, err)); - } - }) - .then(function() { - // Helpful instructions on how to switch - logs.info('Set default Tessel with environment variable (e.g. "export TESSEL=bulbasaur") or use the --name flag.'); - // Close all opened connections and resolve - controller.closeTesselConnections(foundTessels) - .then(resolve); - }); - } - }); + // Print out details... + logs.basic(sprintf('\t%s\t%s\t%s', tessel.name, tessel.connection.connectionType, note)); + }); - // Stop the search if CTRL+C is hit - process.once('SIGINT', function() { - // If the seeker exists (it should) - if (seeker !== undefined) { - // Stop looking for more Tessels - seeker.stop(); - } - }); + // Called after CTRL+C or timeout + seeker.once('end', function stopSearch() { + // If there were no Tessels found + if (foundTessels.length === 0) { + // Report the sadness + return reject(noTessels); + } else if (foundTessels.length === 1) { + // Close all opened connections and resolve + controller.closeTesselConnections(foundTessels) + .then(resolve); + } + // If we have only one Tessel or two Tessels with the same name (just USB and LAN) + else if (foundTessels.length === 1 || + (foundTessels.length === 2 && foundTessels[0].name === foundTessels[1].name)) { + // Close all opened connections and resolve + controller.closeTesselConnections(foundTessels) + .then(resolve); + } + // Otherwise + else { + logs.info('Multiple Tessels found.'); + // Figure out which Tessel will be selected + return controller.runHeuristics(opts, foundTessels) + .then(function logSelected(tessel) { + // Report that selected Tessel to the user + logs.info('Will default to %s.', tessel.name); + }) + .catch(function(err) { + if (!(err instanceof controller.HeuristicAmbiguityError)) { + return controller.closeTesselConnections(foundTessels) + .then(reject.bind(this, err)); + } + }) + .then(function() { + // Helpful instructions on how to switch + logs.info('Set default Tessel with environment variable (e.g. "export TESSEL=bulbasaur") or use the --name flag.'); + // Close all opened connections and resolve + controller.closeTesselConnections(foundTessels) + .then(resolve); + }); + } + }); + + // Stop the search if CTRL+C is hit + process.once('SIGINT', function() { + // If the seeker exists (it should) + if (seeker !== undefined) { + // Stop looking for more Tessels + seeker.stop(); + } }); }); }; Tessel.get = function(opts) { + return new Promise(function(resolve, reject) { + logs.info('Looking for your Tessel...'); + // Collection variable as more Tessels are found + var tessels = []; + + // Store the amount of time to look for Tessel in seconds + var seekerOpts = { + timeout: (opts.timeout || 2) * 1000, + usb: opts.usb, + lan: opts.lan, + authorized: true + }; + + if (opts.authorized !== undefined) { + seekerOpts.authorized = opts.authorized; + } - var keyPromise = Promise.resolve(); - if (opts.key) { - // Set the default SSK key path before we search - keyPromise = provision.setDefaultKey(opts.key); - } - // Set the default SSK key path before we search - return keyPromise.then(function() { - return new Promise(function(resolve, reject) { - logs.info('Looking for your Tessel...'); - // Collection variable as more Tessels are found - var tessels = []; - - // Store the amount of time to look for Tessel in seconds - var seekerOpts = { - timeout: (opts.timeout || 2) * 1000, - usb: opts.usb, - lan: opts.lan, - authorized: true - }; - - if (opts.authorized !== undefined) { - seekerOpts.authorized = opts.authorized; + // Create a seeker object and start detecting any Tessels + var seeker = new discover.TesselSeeker().start(seekerOpts); + var noTessels = opts.authorized ? + responses.noAuth : + responses.auth; + + function searchComplete() { + // If we found no Tessels + if (tessels.length === 0) { + // Report it + return reject(noTessels); } - - // Create a seeker object and start detecting any Tessels - var seeker = new discover.TesselSeeker().start(seekerOpts); - var noTessels = opts.authorized ? - responses.noAuth : - responses.auth; - - function searchComplete() { - // If we found no Tessels - if (tessels.length === 0) { - // Report it - return reject(noTessels); - } - // The name match for a given Tessel happens upon discovery, not at - // the completion of discovery. So if we got to this point, no Tessel - // was found with that name - else if (opts.name !== undefined) { - return reject('No Tessel found by the name ' + opts.name); - } - // If there was only one Tessel - else if (tessels.length === 1) { - // Return it immediately - logAndFinish(tessels[0]); - } - // Otherwise - else { - // Combine the same Tessels into one object - return controller.reconcileTessels(tessels) - .then(function(reconciledTessels) { - tessels = reconciledTessels; - // Run the heuristics to pick which Tessel to use - return controller.runHeuristics(opts, tessels) - .then(function finalSection(tessel) { - return logAndFinish(tessel); - }) - .catch(function(err) { - if (err instanceof controller.HeuristicAmbiguityError) { - var map = {}; - - // Open up an interactive menu for the user to choose - return controller.menu({ - prefix: colors.grey('INFO '), - prompt: { - name: 'selected', - type: 'list', - message: 'Which Tessel do want to use?', - choices: tessels.map(function(tessel, i) { - var isLAN = !!tessel.lanConnection; - var isAuthorized = isLAN && tessel.lanConnection.authorized; - var authorization = isAuthorized ? '' : '(not authorized)'; - var display = sprintf( - '\t%s\t%s\t%s', - tessel.name, - tessel.connection.connectionType, - authorization - ); - - // Map displayed name to tessel index - map[display] = i; - - return display; - }) - }, - translate: function(answer) { - return tessels[map[answer.selected]]; - } - }).then(function(tessel) { - if (!tessel) { - return controller.closeTesselConnections(tessels) - .then(function() { - return reject('No Tessel selected, mission aborted!'); - }); - } else { - // Log we found it and return it to the caller - return logAndFinish(tessel); - } - }); - - } else { - controller.closeTesselConnections(tessels) - .then(reject.bind(this, err)); - } - }); - }); - } + // The name match for a given Tessel happens upon discovery, not at + // the completion of discovery. So if we got to this point, no Tessel + // was found with that name + else if (opts.name !== undefined) { + return reject('No Tessel found by the name ' + opts.name); } + // If there was only one Tessel + else if (tessels.length === 1) { + // Return it immediately + logAndFinish(tessels[0]); + } + // Otherwise + else { + // Combine the same Tessels into one object + return controller.reconcileTessels(tessels) + .then(function(reconciledTessels) { + tessels = reconciledTessels; + // Run the heuristics to pick which Tessel to use + return controller.runHeuristics(opts, tessels) + .then(function finalSection(tessel) { + return logAndFinish(tessel); + }) + .catch(function(err) { + if (err instanceof controller.HeuristicAmbiguityError) { + var map = {}; + + // Open up an interactive menu for the user to choose + return controller.menu({ + prefix: colors.grey('INFO '), + prompt: { + name: 'selected', + type: 'list', + message: 'Which Tessel do want to use?', + choices: tessels.map(function(tessel, i) { + var isLAN = !!tessel.lanConnection; + var isAuthorized = isLAN && tessel.lanConnection.authorized; + var authorization = isAuthorized ? '' : '(not authorized)'; + var display = sprintf( + '\t%s\t%s\t%s', + tessel.name, + tessel.connection.connectionType, + authorization + ); + + // Map displayed name to tessel index + map[display] = i; + + return display; + }) + }, + translate: function(answer) { + return tessels[map[answer.selected]]; + } + }).then(function(tessel) { + if (!tessel) { + return controller.closeTesselConnections(tessels) + .then(function() { + return reject('No Tessel selected, mission aborted!'); + }); + } else { + // Log we found it and return it to the caller + return logAndFinish(tessel); + } + }); - - function finishSearchEarly(tessel) { - // Remove this listener because we don't need to search for the Tessel - seeker.removeListener('end', searchComplete); - // Stop searching - seeker.stop(); - // Send this Tessel back to the caller - logAndFinish(tessel); + } else { + controller.closeTesselConnections(tessels) + .then(reject.bind(this, err)); + } + }); + }); } + } - // When we find Tessels - seeker.on('tessel', function(tessel) { - tessel.setLANConnectionPreference(opts.lanPrefer); - // Check if this name matches the provided option (if any) - // This speeds up development by immediately ending the search - if (opts.name && opts.name === tessel.name) { - finishSearchEarly(tessel); - } - // If we just found a USB connection and should prefer it - else if (!opts.name && tessel.usbConnection !== undefined && !opts.lanPrefer) { - // Finish early with this Tessel - finishSearchEarly(tessel); - } - // Otherwise - else { - // Store this Tessel with the others - tessels.push(tessel); - } - }); - seeker.once('end', searchComplete); - - // Accesses `tessels` in closure - function logAndFinish(tessel) { - // The Tessels that we won't be using should have their connections closed - var connectionsToClose = tessels; - if (tessel) { - logs.info(sprintf('Connected to %s.', tessel.name)); - connectionsToClose.splice(tessels.indexOf(tessel), 1); - controller.closeTesselConnections(connectionsToClose) - .then(function() { - return resolve(tessel); - }); - } else { - logs.info('Please specify a Tessel by name [--name ]'); - controller.closeTesselConnections(connectionsToClose) - .then(function() { - return reject('Multiple possible Tessel connections found.'); - }); - } + function finishSearchEarly(tessel) { + // Remove this listener because we don't need to search for the Tessel + seeker.removeListener('end', searchComplete); + // Stop searching + seeker.stop(); + // Send this Tessel back to the caller + logAndFinish(tessel); + } + + // When we find Tessels + seeker.on('tessel', function(tessel) { + tessel.setLANConnectionPreference(opts.lanPrefer); + // Check if this name matches the provided option (if any) + // This speeds up development by immediately ending the search + if (opts.name && opts.name === tessel.name) { + finishSearchEarly(tessel); + } + // If we just found a USB connection and should prefer it + else if (!opts.name && tessel.usbConnection !== undefined && !opts.lanPrefer) { + // Finish early with this Tessel + finishSearchEarly(tessel); + } + // Otherwise + else { + // Store this Tessel with the others + tessels.push(tessel); } }); + + seeker.once('end', searchComplete); + + // Accesses `tessels` in closure + function logAndFinish(tessel) { + // The Tessels that we won't be using should have their connections closed + var connectionsToClose = tessels; + if (tessel) { + logs.info(sprintf('Connected to %s.', tessel.name)); + connectionsToClose.splice(tessels.indexOf(tessel), 1); + controller.closeTesselConnections(connectionsToClose) + .then(function() { + return resolve(tessel); + }); + } else { + logs.info('Please specify a Tessel by name [--name ]'); + controller.closeTesselConnections(connectionsToClose) + .then(function() { + return reject('Multiple possible Tessel connections found.'); + }); + } + } }); }; @@ -304,7 +307,7 @@ Tessel.get = function(opts) { controller.standardTesselCommand = function(opts, command) { return new Promise(function(resolve, reject) { // Fetch a Tessel - return Tessel.get(opts) + return controller.get(opts) // Once we have it .then(function(tessel) { // Create a promise for a sigint @@ -941,5 +944,5 @@ controller.menu = function(setup) { module.exports = controller; // Shared exports -module.exports.listTessels = Tessel.list; -module.exports.getTessel = Tessel.get; +module.exports.listTessels = controller.list; +module.exports.getTessel = controller.get; diff --git a/test/unit/bin-tessel-2.js b/test/unit/bin-tessel-2.js index 53c3b39c..19f755dd 100644 --- a/test/unit/bin-tessel-2.js +++ b/test/unit/bin-tessel-2.js @@ -30,7 +30,6 @@ var defaults = { name: 'usb', string: '--usb', }, - key: Tessel.LOCAL_AUTH_KEY, lanPrefer: { flag: true, default: false, @@ -149,7 +148,6 @@ exports['Tessel (cli: update)'] = { version: 42, _: ['update'], timeout: 5, - key: Tessel.LOCAL_AUTH_KEY, lanPrefer: false }); @@ -481,6 +479,67 @@ exports['Tessel (cli: push)'] = { }, }; +exports['Tessel (cli: list)'] = { + setUp: function(done) { + this.sandbox = sinon.sandbox.create(); + this.warn = this.sandbox.stub(logs, 'warn'); + this.info = this.sandbox.stub(logs, 'info'); + this.controllerList = this.sandbox.spy(controller, 'listTessels'); + this.tesselList = this.sandbox.stub(Tessel, 'list').returns(Promise.resolve()); + this.setDefaultKey = this.sandbox.spy(provision, 'setDefaultKey'); + this.processExit = this.sandbox.stub(process, 'exit'); + this.closeSuccessful = this.sandbox.stub(cli, 'closeSuccessfulCommand'); + this.closeFailed = this.sandbox.stub(cli, 'closeFailedCommand'); + done(); + }, + + tearDown: function(done) { + this.sandbox.restore(); + done(); + }, + + listStandard: function(test) { + + test.expect(4); + + cli(['list', '--timeout', '0.001']); + + setImmediate(function() { + // Ensure controller list was called + test.ok(this.controllerList.calledOnce); + // Ensure it did not have a key option + test.ok(this.controllerList.lastCall.args[0].key === undefined); + // We should not try to set the keypath if not specifically requested + test.ok(!this.setDefaultKey.called); + // Tessel list should have been called afterwards + test.ok(this.tesselList.called); + test.done(); + }.bind(this)); + }, + + listKey: function(test) { + + test.expect(4); + + var keyPath = './FAKE_KEY'; + + cli(['list', '--timeout', '0.001', '-i', keyPath]); + + setImmediate(function() { + // Restore our func so other tests pass + // Ensure list was called + test.ok(this.controllerList.calledOnce); + // It was called with the keypath + test.ok(this.controllerList.lastCall.args[0].key === keyPath); + // We did try to set the key path + test.ok(this.setDefaultKey.called); + // It was called with the key path + test.ok(this.setDefaultKey.lastCall.args[0] === keyPath); + test.done(); + }.bind(this)); + } +}; + exports['closeFailedCommand'] = { setUp: function(done) { this.sandbox = sinon.sandbox.create();