diff --git a/.eslintrc b/.eslintrc index f4d1e3c8a..05cf6a80f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,7 @@ "node": true }, "rules": { - "no-var": 2 + "no-var": 2, + "no-console": 0 } } diff --git a/CHANGELOG.md b/CHANGELOG.md index c71fa572b..b51a7c242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.21.0 - 23 March 2017 + +## Features + +* [#353](https://github.com/spark/particle-cli/pull/353) Wi-Fi switching on Windows +* [#351](https://github.com/spark/particle-cli/pull/351) Library publish without a name + ## 1.20.1 - 1 March 2017 * Include binaries for firmware 0.6.1 diff --git a/README.md b/README.md index ce858fbae..aa07427ec 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,23 @@ [![npm](https://img.shields.io/npm/v/particle-cli.svg?style=flat-square)](https://www.npmjs.com/package/particle-cli)[![Build Status](https://img.shields.io/travis/spark/particle-cli.svg?style=flat-square)](https://travis-ci.org/spark/particle-cli)[![Code Coverage](https://img.shields.io/coveralls/spark/particle-cli.svg?style=flat-square)](https://coveralls.io/github/spark/particle-cli)[![License](https://img.shields.io/badge/license-LGPL-blue.svg?style=flat-square)](https://github.com/spark/particle-cli/blob/master/LICENSE) +Particle's full-stack Internet of Things (IoT) device platform +gives you everything you need to securely and reliably connect +your IoT devices to the web. For more details please visit [www.particle.io](http:/www.particle.io). + # Particle CLI -The Particle CLI is a powerful tool for interacting with your devices and the Particle Cloud. The CLI uses [node.js](http://nodejs.org/) and can run on Windows, Mac OS X, and Linux fairly easily. It's also [open source](https://github.com/spark/particle-cli) so you can edit and change it, and even send in your changes as [pull requests](https://help.github.com/articles/using-pull-requests) if you want to share! +The Particle CLI is a powerful tool for interacting with your IoT devices and the Particle Cloud. The CLI uses [node.js](http://nodejs.org/) and can run on Windows, Mac OS X, and Linux. It's also [open source](https://github.com/spark/particle-cli) so you can edit and change it, and even send in your changes as [pull requests](https://help.github.com/articles/using-pull-requests) if you want to share! ## Known Issues * The Wireless Photon Setup Wizard will only automatically switch networks on OS X. Users of other operating systems will need to manually connect their computer to the Photon's Wi-Fi. You will be prompted during the wizard when this is required. ## Installing -#### If you've already installed ```spark-cli```, please uninstall it before continuing. +#### If you've previously installed the old version of this package,```spark-cli```, please uninstall it before continuing. #### Simply type: ```npm uninstall -g spark-cli``` into the command line. - First, make sure you have [node.js](http://nodejs.org/) installed! - - Next, open a command prompt or terminal, and install by typing: - -```sh -$ npm install -g particle-cli -$ particle cloud login -``` - - *Note!* If you have problems running this, make sure you using Terminal / the Command Prompt as an Administator, or try using ```sudo``` - -```sh -$ sudo npm install -g particle-cli -``` - - -## Install (advanced) - -To use the local flash and key features you'll need to install [DFU-util](http://DFU-util.sourceforge.net/) and [openssl](http://www.openssl.org/). They are freely available and open-source, and there are installers and binaries for most major platforms as well. - -Here are some great tutorials on the community for full installs: - -[Installing on Ubuntu](https://community.particle.io/t/how-to-install-spark-cli-on-ubuntu-12-04/3474) - -[Installing on Windows](https://community.particle.io/t/tutorial-spark-cli-on-windows-06-may-2014/3112) - -### Installing on Mac OS X: -Rather than installing these packages from source, and instead of using MacPorts, it is relatively straightforward to use [Homebrew](http://brew.sh) to install ```dfu-util``` and ```openssl```. Once you have installed `brew` the basic command is ```brew install dfu-util openssl```. +For the most up-to-date installation instructions, including Windows installer, see [CLI - Installation](https://docs.particle.io/guide/tools-and-features/cli/photon/#installing) on our documentation site. -## Upgrading - -To upgrade Particle-CLI, enter the following command: - -```sh -$ npm update -g particle-cli -``` ## Running from source (advanced) @@ -58,7 +28,7 @@ To grab the CLI source and play with it locally git clone git@github.com:spark/particle-cli.git cd particle-cli npm install -node app.js help +node bin/particle help ``` @@ -151,514 +121,6 @@ If you wish to easily update the system firmware running on your device to a lat 1. Connect your device via USB, and put it into [DFU mode](https://docs.particle.io/guide/getting-started/modes/#dfu-mode-device-firmware-upgrade-). 1. Run `particle update`. -### Core - -#### Apply the CC3000 patch - -The easiest way to apply the CC3000 patch is to flash the known "cc3000" firmware followed by the "tinker" firmware over USB. - -1. Make sure you have [DFU-util](http://dfu-util.sourceforge.net/) installed -1. Connect your Core via USB, and place it into DFU mode by holding both buttons, and releasing reset, keep holding mode until your Core flashes yellow. -1. Run `particle flash --usb cc3000`. This will run a special firmware program that will update the firmware running inside the CC3000 WiFi module. -When it's done running, your Core will be blinking yellow in DFU-mode, you'll need to flash regular firmware like Tinker -to get connected and developing again. -1. Run `particle flash --usb tinker`. This will flash a new version of Tinker to your Core and return to a blinking blue "listening" state, where -you can: -1. Run `particle setup` or `particle setup wifi` to provide your network credentials to get connected again. - -#### Performing a "Deep update" - -Any Core shipped before Summer 2014 would benefit from having this update applied at least once. It improves the Core's performance on very busy networks, and helps fix other minor issues. This update now ships with the CLI so you can apply it to Cores that are unable to get online otherwise. - -1. Make sure you have [DFU-util](http://dfu-util.sourceforge.net/) installed -1. Connect your Core via usb, and place it into DFU mode by holding both buttons, and releasing RESET, keep holding MODE until your Core flashes yellow. -1. Run ```particle flash --usb deep_update_2014_06``` -1. Your Core should reboot and try to connect to any previously saved wifi networks, and then update itself again. - -## Command Reference - -### particle setup wifi - -Helpful shortcut for adding another wifi network to a device connected over USB. Make sure your device is connected via a USB cable, and is slow blinking blue [listening mode](http://docs.particle.io/guide/getting-started/modes/#listening-mode) - -```sh -$ particle setup wifi -``` - -### particle login - -Login and save an access token for interacting with your account on the Particle Cloud. - -```sh -$ particle login -``` - -### particle logout - -Logout and optionally revoke the access token for your CLI session. - -```sh -$ particle logout -``` - -### particle list - -Generates a list of what devices you own, and displays information about their status, including what variables and functions are available - -```sh -$ particle list - -Checking with the cloud... -Retrieving devices... (this might take a few seconds) -my_device_name (0123456789abcdef01234567) 0 variables, and 4 functions - Functions: - int digitalwrite(string) - int digitalread(string) - int analogwrite(string) - int analogread(string) - -``` - -### particle device add - -Adds a new device to your account - -```sh -$ particle device add 0123456789abcdef01234567 -Claiming device 0123456789abcdef01234567 -Successfully claimed device 0123456789abcdef01234567 -``` - -### particle device rename - -Assigns a new name to a device you've claimed - -```sh -$ particle device rename 0123456789abcdef01234567 "pirate frosting" -``` - -### particle device remove - -Removes a device from your account so someone else can claim it. - -```sh -$ particle device remove 0123456789abcdef01234567 -Are you sure? Please Type yes to continue: yes -releasing device 0123456789abcdef01234567 -server said { ok: true } -Okay! -``` - -### particle flash - -Sends a firmware binary, a source file, or a directory of source files, or a known app to your device. - -Note! When sending source code, the cloud compiles ```.ino``` and ```.cpp``` files differently. For ```.ino``` files, the cloud will apply a pre-processor. It will add missing function declarations, and it will inject an ```#include " - application.h"``` line at the top of your files if it is missing. - -If you want to build a library that can be used for both Arduino and Particle, here's a useful code snippet: - -```cpp -#if defined(ARDUINO) && ARDUINO >= 100 -#include "Arduino.h" -#elif defined(SPARK) -#include "application.h" -#endif -``` - -#### Flashing a directory - -You can setup a directory of source files and libraries for your project, and the CLI will use those when compiling remotely. You can also create ```particle.include``` and / or a ```particle.ignore``` file in that directory that will tell the CLI specifically which files to use or ignore. - -```sh -$ particle flash deviceName my_project -``` - -#### Flashing one or more source files - -You can include any number of individual source files after the device Name, and the CLI will include them while flashing your app. - - -```sh -$ particle flash deviceName app.ino library1.cpp library1.h -``` - -#### Flashing a known app - -You can easily reset a device back to a previous existing app with a quick command. Three app names are reserved right now: "tinker", "voodoo", and "cc3000". Tinker is the original firmware that ships with the device, and cc3000 will patch the wifi module on your Core. Voodoo is a build of [VoodooSpark](http://voodoospark.me/) to allow local wireless firmata control of a device. - -```sh -$ particle flash deviceName tinker -$ particle flash deviceName cc3000 -$ particle flash deviceName voodoo - -``` - -You can also update the factory reset version using the `--factory` flag, over USB with `--usb`, or over serial using `--serial`. - -```sh -$ particle flash --factory tinker -$ particle flash --usb tinker -$ particle flash --serial tinker -``` - -#### Compiling remotely and Flashing locally - -To work locally, but use the cloud compiler, simply use the compile command, and then the local flash command after. Make sure you connect your device via USB and place it into [DFU mode](https://docs.particle.io/guide/getting-started/modes/#dfu-mode-device-firmware-upgrade-). - -```sh -$ particle compile device_type my_project_folder --saveTo firmware.bin -OR -$ particle compile device_type app.ino library1.cpp library1.h --saveTo firmware.bin - -$ particle flash --usb firmware.bin -OR -$ particle flash --serial firmware.bin -``` - - -### particle compile - -Compiles one or more source file, or a directory of source files, and downloads a firmware binary. This is device specific and must be passed as an argument during compilation. - -The devices available are: - -- photon (alias is 'p') -- core (alias is 'c') -- electron (alias is 'e') -- duo (alias is 'd') -- oak (alias is 'o') -- bluz (alias is 'b') -- bluz-gateway (alias is 'bg') -- bluz-beacon (alias is 'bb') - -eg. `particle compile photon xxx` OR `particle compile p xxxx` both targets the photon - -Note! The cloud compiles ```.ino``` and ```.cpp``` files differently. For ```.ino``` files, the cloud will apply a pre-processor. It will add missing function declarations, and it will inject an ```#include " -application.h"``` line at the top of your files if it is missing. - -If you want to build a library that can be used for both Arduino and Particle, here's a useful code snippet: - -```cpp -#if defined(ARDUINO) && ARDUINO >= 100 -#include "Arduino.h" -#elif defined(SPARK) -#include "application.h" -#endif -``` - -#### compiling against a particular system firmware target - -You can specify a `--target` when compiling or flashing to target a particular system target. - -- `particle compile electron myapp.ino --target 0.5.1` would compile myapp.ino for an Electron running system firmware 0.5.1. -- `particle flash myapp.ino --target 0.5.1` would compile and flash myapp.ino for device against system firmware 0.5.1. - -#### compiling a directory - -You can setup a directory of source files and libraries for your project, and the CLI will use those when compiling remotely. You can also create ```particle.include``` and / or a ```particle.ignore``` file in that directory that will tell the CLI specifically which files to use or ignore. Those files are just plain text with one line per filename - -```sh -$ particle compile device_type my_project_folder -``` - -#### example particle.include -```text -application.cpp -library1.h -library1.cpp -``` - -#### example particle.ignore -```text -.ds_store -logo.png -old_version.cpp -``` - -#### Compiling one or more source files - -You can include any number of individual source files after the device id, and the CLI will include them while compiling your app. - - -```sh -$ particle compile device_type app.ino library1.cpp library1.h -``` - -#### Compiling in a directory containing project files - -This will push all the files in a directory that the command line is currently 'cd' in for compilation. - -```sh -$ particle compile device_type . -``` - -### particle call - -Calls a function on one of your devices, use ```particle list``` to see which devices are online, and what functions are available. - -```sh -$ particle call deviceName digitalwrite "D7,HIGH" -1 -``` - -### particle get - -Retrieves a variable value from one of your devices, use ```particle list``` to see which devices are online, and what variables are available. - -```sh -$ particle get deviceName temperature -72.1 -``` - -### particle monitor - -Pulls the value of a variable at a set interval, and optionally display a timestamp - -* Minimum delay for now is 500 (there is a check anyway if you keyed anything less) -* hitting ```CTRL + C``` in the console will exit the monitoring - -```sh -$ particle monitor deviceName temperature 5000 -$ particle monitor deviceName temperature 5000 --time -$ particle monitor all temperature 5000 -$ particle monitor all temperature 5000 --time -$ particle monitor all temperature 5000 --time > my_temperatures.csv -``` - -### particle identify - -Retrieves your device id when the device is connected via USB and in listening mode (flashing blue). - -```sh -$ particle identify -$ particle identify 1 -$ particle identify COM3 -$ particle identify /dev/cu.usbmodem12345 - -$ particle identify -0123456789abcdef01234567 -``` - -### particle subscribe - -Subscribes to published events on the cloud, and pipes them to the console. Special device name "mine" will subscribe to events from just your devices. - -```sh -$ particle subscribe -$ particle subscribe mine -$ particle subscribe eventName -$ particle subscribe eventName mine -$ particle subscribe eventName deviceName -$ particle subscribe eventName 0123456789abcdef01234567 - -# special case to subscribe to all events for a particular device -$ particle subscribe mine deviceName -$ particle subscribe mine 0123456789abcdef01234567 -``` - -### particle publish - -Allows a message to be published via the CLI without using a physical Particle device. This is particularly useful when you are testing your firmware against an actual `published` event. - -There is a `--private` flag that allows you to `publish` events to devices subscribing to events with the `MY_DEVICES` option. - -```sh -$ particle publish eventName -$ particle publish eventName --private -$ particle publish eventName someData -$ particle publish eventName someData --private -``` - -### particle serial list - -Shows currently connected devices acting as serial devices over USB. - -```sh -$ particle serial list -``` - - -### particle serial monitor - -Starts listening to the specified serial device, and echoes to the terminal. - -```sh -$ particle serial monitor -$ particle serial monitor 1 -$ particle serial monitor COM3 -$ particle serial monitor /dev/cu.usbmodem12345 -``` - -### particle serial flash - -Flash a firmware binary over serial using the YMODEM protocol. - -```sh -$ particle serial flash firmware.bin -``` - -### particle keys doctor - -Helps you update your keys, or recover your device when the keys on the server are out of sync with the keys on your device. The ```particle keys``` tools requires both DFU-util, and openssl to be installed. - -Connect your device in [DFU mode](https://docs.particle.io/guide/getting-started/modes/#dfu-mode-device-firmware-upgrade-), and run this command to replace the unique cryptographic keys on your device. Automatically attempts to send the new public key to the cloud as well. - -```sh -$ particle keys doctor your_device_id -``` - -There have been reports of the new public key not being sent to the cloud, in which case ```particle keys send``` will need to be run manually. - -### particle keys new - -Generates a new public / private keypair that can be used on a device. - -```sh -$ particle keys new -running openssl genrsa -out device.pem 1024 -running openssl rsa -in device.pem -pubout -out device.pub.pem -running openssl rsa -in device.pem -outform DER -out device.der -New Key Created! - -$ particle keys new mykey -running openssl genrsa -out mykey.pem 1024 -running openssl rsa -in mykey.pem -pubout -out mykey.pub.pem -running openssl rsa -in mykey.pem -outform DER -out mykey.der -New Key Created! -``` - -### particle keys load - -Copies a ```.DER``` formatted private key onto your device's external flash. Make sure your device is connected and in [DFU mode](https://docs.particle.io/guide/getting-started/modes/#dfu-mode-device-firmware-upgrade-). The `particle keys` tools requires both DFU-util, and openssl to be installed. Make sure any key you load is sent to the cloud with `particle keys send device.pub.pem` - -```sh -$ particle keys load device.der -... -Saved! -``` - -### particle keys save - -Copies a ```.DER``` formatted private key from your device's external flash to your computer. Make sure your device is connected and in [DFU mode](https://docs.particle.io/guide/getting-started/modes/#dfu-mode-device-firmware-upgrade-). The ```particle keys``` tools requires both DFU-util, and openssl to be installed. - -```sh -$ particle keys save name_of_file -... -Saved! -``` - -### particle keys send - -Sends a device's public key to the cloud for use in opening an encrypted session with your device. Please make sure your device has the corresponding private key loaded using the ```particle keys load``` command. - -```sh -$ particle keys send 0123456789abcdef01234567 device.pub.pem -submitting public key succeeded! -``` - -### particle keys server - -Switches the server public key stored on the device's external flash. This command is important when changing which server your device is connecting to, and the server public key helps protect your connection. Your device will stay in DFU mode after this command, so that you can load new firmware to connect to your server. By default this will only change the server key associated with the default protocol for a device. If you wish to change a specific protocol, add `--protocol tcp` or `--protocol udp` to the end of your command. - - -```sh -$ particle keys server my_server.der -$ particle keys server my_server.der --protocol udp -``` - -#### Encoding a server address and port - -When using the local cloud you can ask the CLI to encode the IP or dns address into your key to control where your device will connect. You may also specify a port number to be included. - -```sh -$ particle keys server my_server.pub.pem 192.168.1.10 -$ particle keys server my_server.der 192.168.1.10 9000 -$ particle keys server my_server.der 192.168.1.10 9000 --protocol udp -``` - -### particle keys address - -Reads and displays the server address, port, and protocol from a device. - -```sh -$ particle keys address - -tcp://device.spark.io:5683 -``` - -### particle keys protocol - -Changes the transport protocol used to communicate with the cloud. Available options are `tcp` and `udp` for Electrons (if you are running at least firmware version 0.4.8). - -```sh -$ particle keys protocol tcp -$ particle keys protocol udp -``` - -### particle config - -The config command lets you create groups of settings and quickly switch to a profile by calling `particle config profile-name`. This is especially useful for switching to your local server or between other environments. - -Calling `particle config particle` will switch **Particle-Cli** back to the Particle Cloud API server. - -```sh -$ particle config profile-name -$ particle config particle -$ particle config local apiUrl http://localhost:8080 //creates a new profile with name "local" and saves the IP-address parameter -$ particle config useSudoForDfu true -``` - -Calling `particle config identify` will output your current config settings. - -```sh -$ particle config identify -Current profile: particle -Using API: https://api.particle.io -Access token: 01234567890abcdef01234567890abcdef012345 -``` - -### particle binary inspect file.bin - -Describe binary generated by compile. - -```sh -$ particle binary inspect file.bin -file.bin - CRC is ok (06276dc6) - Compiled for photon - This is a system module number 2 at version 6 - It depends on a system module number 1 at version 6 -``` - -### particle webhook - -Registers your webhook with the Particle Cloud. Creates a postback to the given url when your event is sent. - -```sh -$ particle webhook create example.json #run this command in the directory containing example.json -$ particle webhook GET http:// http://", handlerFunction, MY_DEVICES); -} - -void handlerFunction(const char *name, const char *data) { - // Important note! -- Right now the response comes in 512 byte chunks. - // This code assumes we're getting the response in large chunks, and this - // assumption breaks down if a line happens to be split across response chunks - - process the data received here.... -} -``` -More examples and information about **webhooks** can be found here: https://docs.particle.io/guide/tools-and-features/webhooks/ # Development diff --git a/accept/features/fixtures/webhook/hook.JSON b/accept/features/fixtures/webhook/hook.JSON new file mode 100644 index 000000000..101b8a15a --- /dev/null +++ b/accept/features/fixtures/webhook/hook.JSON @@ -0,0 +1,4 @@ +{ + "event": "my-event", + "url": "https://my-website.com/fancy_things.php" +} \ No newline at end of file diff --git a/accept/features/library_upload.feature b/accept/features/library_upload.feature index 7571cffb4..b3774f632 100644 --- a/accept/features/library_upload.feature +++ b/accept/features/library_upload.feature @@ -56,6 +56,16 @@ Feature: library upload And the output should contain "test-library-publish" And the output should not contain "[private]" + Scenario: can publish and upload a library in one step from the library directory + Given The particle library "test-library-publish" is removed + And I use the fixture named "library/upload/valid/0.0.2" + When I run particle "library publish" + # seems that the spinner output is not captured + # Then the output should contain "Uploading library test-library-publish" + Then the output should not contain "Library test-library-publish was successfully uploaded" + And the output should contain "Library test-library-publish was successfully published" + And the exit status should be 0 + Scenario: cleanup Given The particle library "test-library-publish" is removed When I run particle "library search test-library-publish" diff --git a/accept/features/webhook.feature b/accept/features/webhook.feature new file mode 100644 index 000000000..79499742b --- /dev/null +++ b/accept/features/webhook.feature @@ -0,0 +1,18 @@ +Feature: webhooks + + # https://github.com/spark/particle-cli/issues/282 + Scenario: the filename can end with uppercase JSON + Given I use the fixture named "webhook" + When I run particle "webhook create hook.JSON" + Then the output should not contain "Please specify a url" + And the output should contain "created" + And the exit status should be 0 + + Scenario: delete all + When I run particle "webhook delete all" interactively + And I respond to the prompt "delete ALL" with "y" + And I close the stdin stream + Then the output should contain "Found 1 hooks registered" + And the exit status should be 0 + + diff --git a/bin/testWiFi.js b/bin/testWiFi.js new file mode 100644 index 000000000..8238b5303 --- /dev/null +++ b/bin/testWiFi.js @@ -0,0 +1,4 @@ +var TestWiFi = require('../commands/WirelessCommand/test'); + +var test = new TestWiFi(); +test.run(); \ No newline at end of file diff --git a/commands/AccessTokenCommands.js b/commands/AccessTokenCommands.js index ea1a68669..08ce20f05 100644 --- a/commands/AccessTokenCommands.js +++ b/commands/AccessTokenCommands.js @@ -178,6 +178,11 @@ AccessTokenCommands.prototype = extend(BaseCommand.prototype, { }); }, + /** + * Creates an access token using the given client name. + * @param clientName The client name to use + * @returns {Promise} Will print the access token to the console, along with the expiration date. + */ createAccessToken: function (clientName) { if (!clientName) { diff --git a/commands/CloudCommands.js b/commands/CloudCommands.js index 67e1f7d05..2880da79f 100644 --- a/commands/CloudCommands.js +++ b/commands/CloudCommands.js @@ -128,9 +128,13 @@ CloudCommand.prototype = extend(BaseCommand.prototype, { this.options.local = idx>=0; } if (!this.options.verbose) { - var idx = utilities.indexOf(args, '--verbose') + var idx = utilities.indexOf(args, '--verbose'); this.options.verbose = idx>=0; } + if (!this.options.noconfirm) { + var idx = utilities.indexOf(args, '--yes'); + this.options.noconfirm = idx>=0; + } }, claimDevice: function (deviceid) { @@ -279,7 +283,9 @@ CloudCommand.prototype = extend(BaseCommand.prototype, { return pipeline([ function () { - return self._handleMultiFileArgs(args); + var fileMapping = self._handleMultiFileArgs(args); + api._populateFileMapping(fileMapping); + return fileMapping; }, function (fileMapping) { if (Object.keys(fileMapping.map).length == 0) { @@ -374,7 +380,10 @@ CloudCommand.prototype = extend(BaseCommand.prototype, { if (!isCellular) { return fileMapping; } - + else if (self.options.noconfirm) { + console.log('! Skipping Bandwidth Prompt !'); + return fileMapping; + } return self._promptForOta(api, attrs, fileMapping, targetVersion); }, function flashyFlash(flashFiles) { @@ -419,7 +428,7 @@ CloudCommand.prototype = extend(BaseCommand.prototype, { var spec = _.find(specs, { productId: attrs.product_id }); if (spec) { if (spec.knownApps[filePath]) { - return { list: [spec.knownApps[filePath]] }; + return api._populateFileMapping( { list: [spec.knownApps[filePath]] } ); } if (spec.productName) { @@ -448,7 +457,7 @@ CloudCommand.prototype = extend(BaseCommand.prototype, { return reject(); } - resolve({ list: [binary] }); + resolve({ map: {binary: binary} }); }); }); }, @@ -748,56 +757,67 @@ CloudCommand.prototype = extend(BaseCommand.prototype, { }); }, - logout: function () { + + doLogout: function(keep, password) { + var allDone = when.defer(); var api = new ApiClient(); + + pipeline([ + function () { + if (!keep) { + return api.removeAccessToken(settings.username, password, settings.access_token); + } else { + console.log(arrow, 'Leaving your token intact.'); + } + }, + function () { + console.log( + arrow, + util.format('You have been logged out from %s.', + chalk.bold.cyan(settings.username)) + ); + settings.override(null, 'username', null); + settings.override(null, 'access_token', null); + } + ]).then(function () { + allDone.resolve(); + }, function (err) { + console.error('There was an error revoking the token', err); + allDone.reject(err); + }); + return allDone.promise; + }, + + logout: function (noPrompt) { if (!settings.access_token) { console.log('You were already logged out.'); return when.resolve(); } + var self = this; + if (noPrompt) { + return self.doLogout(true); + } var allDone = when.defer(); inquirer.prompt([ { type: 'confirm', - name: 'wipe', - message: 'Would you like to revoke the current authentication token?', - default: false + name: 'keep', + message: 'Would you like to keep the current authentication token?', + default: true }, { type: 'password', name: 'password', message: 'Please enter your password', when: function(ans) { - return ans.wipe; + return !ans.keep; } } - ], function(answers) { - pipeline([ - function() { - if (answers.wipe) { - return api.removeAccessToken(settings.username, answers.password, settings.access_token); - } else { - console.log(arrow, 'Leaving your token intact.'); - } - }, - function() { - console.log( - arrow, - util.format('You have been logged out from %s.', - chalk.bold.cyan(settings.username)) - ); - settings.override(null, 'username', null); - settings.override(null, 'access_token', null); - } - ]).then(function() { - allDone.resolve(); - }, function(err) { - console.error('There was an error revoking the token', err); - allDone.reject(err); - }); + ], function doit(ans) { + return allDone.resolve(self.doLogout(ans.keep, ans.password)); }); - return allDone.promise; }, diff --git a/commands/HelpCommand.js b/commands/HelpCommand.js index d5f9611e7..fd23cc808 100644 --- a/commands/HelpCommand.js +++ b/commands/HelpCommand.js @@ -154,8 +154,10 @@ HelpCommand.prototype = extend(BaseCommand.prototype, { lines = lines.concat(cmds.map(function (subcmdname) { var subcmdObj = command[subcmdname]; - var line = ' particle ' + name + ' ' + subcmdname; - return utilities.padRight(line, ' ', 25) + ' - ' + subcmdObj.does; + if (subcmdObj.does) { + var line = ' particle ' + name + ' ' + subcmdname; + return utilities.padRight(line, ' ', 25) + ' - ' + subcmdObj.does; + } })); } else if (command.optionsByName) { lines.push(''); diff --git a/commands/KeyCommands.js b/commands/KeyCommands.js index 3b4905224..9f880df97 100644 --- a/commands/KeyCommands.js +++ b/commands/KeyCommands.js @@ -210,7 +210,7 @@ KeyCommands.prototype = extend(BaseCommand.prototype, { return this._makeNewKey(filename); }, - keyAlgorithmForProtocol: function(protocol) { + keyAlgorithmForProtocol: function (protocol) { return protocol === 'udp' ? 'ec' : 'rsa'; }, diff --git a/commands/SerialCommand.js b/commands/SerialCommand.js index c7c62803f..ec1cb9fb1 100644 --- a/commands/SerialCommand.js +++ b/commands/SerialCommand.js @@ -31,6 +31,7 @@ var _ = require('lodash'); var fs = require('fs'); var prompt = require('inquirer').prompt; +var path = require('path'); var when = require('when'); var sequence = require('when/sequence'); var extend = require('xtend'); @@ -39,6 +40,8 @@ var inquirer = require('inquirer'); var chalk = require('chalk'); var wifiScan = require('node-wifiscanner2').scan; var specs = require('../oldlib/deviceSpecs'); +var ApiClient = require('../oldlib/ApiClient2'); +var OldApiClient = require('../oldlib/ApiClient'); var log = require('../oldlib/log'); var settings = require('../settings'); var DescribeParser = require('binary-version-reader').HalDescribeParser; @@ -49,7 +52,19 @@ var utilities = require('../oldlib/utilities.js'); var SerialBoredParser = require('../oldlib/SerialBoredParser.js'); var SerialTrigger = require('../oldlib/SerialTrigger'); +// TODO: DRY this up somehow +// The categories of output will be handled via the log class, and similar for protip. +var cmd = path.basename(process.argv[1]); var arrow = chalk.green('>'); +var alert = chalk.yellow('!'); +var protip = function() { + var args = Array.prototype.slice.call(arguments); + args.unshift(chalk.cyan('!'), chalk.bold.white('PROTIP:')); + console.log.apply(null, args); +}; + + +var timeoutError = 'Serial timed out'; var SerialCommand = function (cli, options) { SerialCommand.super_.call(this, cli, options); @@ -64,7 +79,7 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { options: null, name: 'serial', description: 'simple serial interface to your devices', - + timeoutError: timeoutError, init: function () { this.addOption('list', this.listDevices.bind(this), 'Show devices connected via serial to your computer'); this.addOption('monitor', this.monitorSwitch.bind(this), 'Connect and display messages from a device'); @@ -73,7 +88,7 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { this.addOption('mac', this.deviceMac.bind(this), 'Ask for and display MAC address via serial'); this.addOption('inspect', this.inspectDevice.bind(this), 'Ask for and display device module information via serial'); this.addOption('flash', this.flashDevice.bind(this), 'Flash firmware over serial using YMODEM protocol'); - + this.addOption('claim', this.claimDevice.bind(this), 'Claim a device with the given claim code'); //this.addOption(null, this.helpCommand.bind(this)); }, @@ -105,10 +120,8 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { var pnpMatches = !!(port.pnpId && (port.pnpId.indexOf('VID_' + vid.toUpperCase()) >= 0) && (port.pnpId.indexOf('PID_' + pid.toUpperCase()) >= 0)); var serialNumberMatches = port.serialNumber && port.serialNumber.indexOf(serialNumber) >= 0; - if (usbMatches || pnpMatches || serialNumberMatches) { - return true; - } - return false; + return !!(usbMatches || pnpMatches || serialNumberMatches); + }); if (serialDeviceSpec) { device = { @@ -221,7 +234,7 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { }) } } - } + }; // Called only when the port opens successfully var handleOpen = function () { @@ -271,7 +284,7 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { setTimeout(function () { openPort(selectedDevice); }, 5); - } + }; process.on('SIGINT', handleInterrupt); process.on('SIGQUIT', handleInterrupt); @@ -509,8 +522,10 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { return self.error('Unable to scan for Wi-Fi networks. Do you have permission to do that on this system?'); } + // todo - if the prompt is auto answering, then only auto answer once, to prevent + // never ending loops if (networkList.length === 0) { - inquirer.prompt([{ + self.prompt([{ type: 'confirm', name: 'rescan', message: 'Uh oh, no networks found. Try again?', @@ -548,6 +563,10 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { // TODO remove once we have verbose flag settings.verboseOutput = true; + function parameterMissing(param) { + return 'The "'+param+'" parameter was missing. Please specify a filename of a valid JSON object, ie {"network":"myNetwork","security":"WPA_AES","channel":2,"password":"mySecret!"}'; + } + var wifi = when.defer(); this.checkArguments(arguments); @@ -591,8 +610,6 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { // Directly var obj = JSON.parse(fs.readFileSync(json, "utf-8")); - - if (!obj.hasOwnProperty('network') || obj.network.length < 2){ _jsonErr(parameterMissing('network')); } else { @@ -612,29 +629,33 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { // Configure it self.serialWifiConfig(device, ssid, security, password).then(wifi.resolve, wifi.reject); //.then(self.wifiInfo.resolve, self.wifiInfo.reject); } else { - inquirer.prompt([ - { - type: 'confirm', - name: 'scan', - message: chalk.bold.white('Should I scan for nearby Wi-Fi networks?'), - default: true - } - ], function (ans) { - if (ans.scan) { - return self._scanNetworks(function (networks) { - self._getWifiInformation(device, networks).then(wifi.resolve, wifi.reject); - }); - } else { - self._getWifiInformation(device).then(wifi.resolve, wifi.reject); - } - }); - + self._promptWifiScan(wifi, device); } }); return wifi.promise; }, + _promptWifiScan(wifi, device) { + var self = this; + self.prompt([ + { + type: 'confirm', + name: 'scan', + message: chalk.bold.white('Should I scan for nearby Wi-Fi networks?'), + default: true + } + ], function (ans) { + if (ans.scan) { + return self._scanNetworks(function (networks) { + self._getWifiInformation(device, networks).then(wifi.resolve, wifi.reject); + }); + } else { + self._getWifiInformation(device).then(wifi.resolve, wifi.reject); + } + }); + }, + _jsonErr: function(err) { return console.log(chalk.red('!'), 'An error occurred:', err); }, @@ -659,7 +680,7 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { var ssids = _.pluck(networks, 'ssid'); ssids = this._removePhotonNetworks(ssids); - inquirer.prompt([ + self.prompt([ { type: 'list', name: 'ap', @@ -792,6 +813,332 @@ SerialCommand.prototype = extend(BaseCommand.prototype, { return dfd.promise; }, + supportsClaimCode: function(device) { + if (!device) { + return when.reject('No serial port available'); + } + return this._issueSerialCommand(device, 'c', 500).then(function (data) { + var matches = data.match(/Device claimed: (\w+)/); + return !!matches; + }).catch(function(err) { + if (err!==timeoutError) { + throw err; + } + return false; + }); + }, + + /** + * Performs device setup via serial. The device should already be in listening mode. + * Setup comprises these steps: + * - fetching the claim code from the API + * - setting the claim code on the device + * - configuring + * @param device + */ + setup: function(device) { + var self = this; + var _deviceID = ''; + var api = new ApiClient(); + + // todo - factor this out from here and also the WiFiCommand + function getClaim() { + self.newSpin('Obtaining magical secure claim code from the cloud...').start(); + api.getClaimCode(undefined, afterClaim); + } + function afterClaim(err, dat) { + self.stopSpin(); + if (err) { + // TODO: Graceful recovery here + // How about retrying the claim code again + // console.log(arrow, arrow, err); + if (err.code === 'ENOTFOUND') { + protip("Your computer couldn't find the cloud..."); + } else { + protip('There was a network error while connecting to the cloud...'); + } + protip('We need an active internet connection to successfully complete setup.'); + protip('Are you currently connected to the internet? Please double-check and try again.'); + return; + } + + console.log(arrow, 'Obtained magical secure claim code.'); + console.log(); + return self.sendClaimCode(device, dat.claim_code) + .then(function() { + console.log('Claim code set. Now setting up Wi-Fi'); + // todo - add additional commands over USB to have the device scan for Wi-Fi + var wifi = when.defer(); + self._promptWifiScan(wifi, device); + return wifi.promise; + }) + .then(revived); + } + + function revived() { + // if (err) { + // manualReconnectPrompt(); + // return; + // } + + self.stopSpin(); + self.newSpin("Attempting to verify the Photon's connection to the cloud...").start(); + + setTimeout(function () { + api.listDevices(checkDevices); + }, 2000); + } + + function updateWarning() { + + } + + function checkDevices(err, dat) { + self.stopSpin(); + if (err) { + if (err.code === 'ENOTFOUND') { + // todo - limit the number of retries here. + console.log(alert, 'Network not ready yet, retrying...'); + console.log(); + return revived(null); + } + console.log(alert, 'Unable to verify your Photon\'s connection.'); + console.log(alert, "Please make sure you're connected to the internet."); + console.log(alert, 'Then try', chalk.bold.cyan(cmd + ' list'), "to verify it's connected."); + updateWarning(); + self.exit(); + } + + // self.__deviceID -> _deviceID + var onlinePhoton = _.find(dat, function (device) { + return (device.id.toUpperCase() === _deviceID.toUpperCase()) && device.connected === true; + }); + + if (onlinePhoton) { + console.log(arrow, 'It looks like your Photon has made it happily to the cloud!'); + console.log(); + updateWarning(); + namePhoton(onlinePhoton.id); + return; + } + + console.log(alert, "It doesn't look like your Photon has made it to the cloud yet."); + console.log(); + self.prompt([{ + + type: 'list', + name: 'recheck', + message: 'What would you like to do?', + choices: [ + { name: 'Check again to see if the Photon has connected', value: 'recheck' }, + { name: 'Reconfigure the Wi-Fi settings of the Photon', value: 'reconfigure' } + ] + + }], recheck); + + function recheck(ans) { + if (ans.recheck === 'recheck') { + api.listDevices(checkDevices); + } else { + self._promptForListeningMode() + self.setup(device); + } + } + } + + function namePhoton(deviceId) { + var __oldapi = new OldApiClient(); + + self.prompt([ + { + type: 'input', + name: 'deviceName', + message: 'What would you like to call your photon?' + } + ], function(ans) { + // todo - retrieve existing name of the device? + var deviceName = ans.deviceName; + if (deviceName) { + __oldapi.renameDevice(deviceId, deviceName).then(function () { + console.log(); + console.log(arrow, 'Your Photon has been given the name', chalk.bold.cyan(deviceName)); + console.log(arrow, "Congratulations! You've just won the internet!"); + console.log(); + self.exit(); + }, function (err) { + console.error(alert, 'Error naming your photon: ', err); + namePhoton(deviceId); + }); + } else { + console.log('Skipping device naming.'); + self.exit(); + } + }); + } + + return self.askForDeviceID(device). + then(function (deviceID) { + _deviceID = deviceID; + console.log('setting up device', deviceID); + return getClaim(); + }) + .catch(function (err) { + self.stopSpin(); + console.log(err); + throw err; + }); + }, + + setDeviceClaimCode: function (device, claimCode) { + var self = this; + return this.supportsClaimCode(device).then(function (supported) { + if (!supported) { + return when.reject('device does not support claiming over USB'); + } + + return self.sendClaimCode(device, claimCode); + }); + }, + + claimDevice: function (comPort, claimCode) { + var self = this; + if (!claimCode) { + // todo - why do we need to duplicate exceptions? + log.error('claimCode required'); + return when.reject('claimCode required'); + } + + return this.whatSerialPortDidYouMean(comPort, true, function (device) { + return self.sendClaimCode(device, claimCode) + .then(function () { + console.log('Claim code set.'); + return true; + }); + }); + }, + + sendClaimCode: function (device, claimCode, withLogging) { + var prompt = 'Enter 63-digit claim code: '; + var confirmation = 'Claim code set to: '+claimCode; + return this.doSerialInteraction(device, 'C', [ + [ prompt, 2000, function(promise, next) { + next(claimCode+'\n'); + }], + [ confirmation, 2000, function(promise, next) { + next(); + promise.resolve(); + }] + ], !withLogging); + }, + + /** + * + * @param {Device} device The device to interact with + * @param {String} command The initial command to send to the device + * @param {Array} interactions an array of interactions. Each interaction is + * an array, with these elements: + * [0] - the prompt text to interact with + * [1] - the timeout to wait for this prompt + * [2] - the callback when the prompt has been received. The callback takes + * these arguments: + * promise: the deferred result (call resolve/reject) + * next: the response callback, should be called with (response) to send a response. + * Response can be undefined + * $param {Boolean} nologging when truthy, logging is disabled. + * @returns {Promise} + */ + doSerialInteraction: function(device, command, interactions, noLogging) { + if (!device) { + return when.reject('No serial port available'); + } + + if (!interactions.length) { + return when.resolve(); + } + + var serialPort = this.serialPort || new SerialPort(device.port, { + baudrate: 9600, + parser: SerialBoredParser.makeParser(250), + autoOpen: false + }); + + var done = when.defer(); + serialPort.on('error', function (err) { + done.reject(err); + }); + function serialClosedEarly() { + done.reject('Serial port closed early'); + } + serialPort.on('close', serialClosedEarly); + + var self = this; + function startTimeout(to) { + self._serialTimeout = setTimeout(function () { + done.reject('Serial timed out'); + }, to); + } + function resetTimeout() { + clearTimeout(self._serialTimeout); + self._serialTimeout = null; + } + + var st = new SerialTrigger(serialPort); + + var addTrigger = function (prompt, timeout, callback) { + st.addTrigger(prompt, function (cb) { + resetTimeout(); + function next(response) { + cb(response, timeout ? startTimeout.bind(self, timeout) : undefined); + } + callback(done, next); + }); + }; + + var prompt = interactions[0][0]; + var callback = interactions[0][2]; + for (var i=1; i'), 'Your device should now restart automatically.'); - console.log(chalk.cyan('>'), 'You may need to re-flash your application to the device.'); console.log(); }; diff --git a/commands/WebhookCommands.js b/commands/WebhookCommands.js index 52257c7fb..abb538c9a 100644 --- a/commands/WebhookCommands.js +++ b/commands/WebhookCommands.js @@ -123,7 +123,7 @@ WebhookCommand.prototype = extend(BaseCommand.prototype, { if (eventName && !url && !deviceID) { var filename = eventName; - if (utilities.getFilenameExt(filename) === '.json') { + if (utilities.getFilenameExt(filename).toLowerCase() === '.json') { if (!fs.existsSync(filename)) { console.log(filename + ' is not found.'); return -1; diff --git a/commands/WirelessCommand/WiFiManager.js b/commands/WirelessCommand/WiFiManager.js index 7b5aa7123..93e722af0 100644 --- a/commands/WirelessCommand/WiFiManager.js +++ b/commands/WirelessCommand/WiFiManager.js @@ -4,7 +4,8 @@ var _ = require('lodash'); var os = require('os'); var connect = { 'darwin': require('./connect/darwin'), - 'linux': require('./connect/linux') + 'linux': require('./connect/linux'), + 'win32': require('./connect/windows') }; function WiFiManager(opts) { @@ -15,17 +16,19 @@ function WiFiManager(opts) { this.platform = os.platform(); this.osConnect = connect[this.platform]; + // todo - allow the connector to actively check for preconditions, specific OS version support etc this.supported = { getCurrentNetwork: !!(this.osConnect && this.osConnect.getCurrentNetwork), connect: !!(this.osConnect && this.osConnect.connect) }; this.__cache = undefined; -}; +} WiFiManager.prototype.getCurrentNetwork = function(cb) { if (!this.supported.getCurrentNetwork) { // default to nothing + // todo - why not raise an error? return cb(); } this.osConnect.getCurrentNetwork(cb); @@ -39,7 +42,6 @@ WiFiManager.prototype.scan = function scan(opts, cb) { return cb(err); } if (dat.length) { - self.__cache = dat; return cb(null, dat); } @@ -80,7 +82,7 @@ WiFiManager.prototype.connect = function(opts, cb) { } opts.ssid = ap.ssid; return self.__connect(opts, cb); - }; + } }; diff --git a/commands/WirelessCommand/connect/darwin.js b/commands/WirelessCommand/connect/darwin.js index bd4b545ba..74afc23dc 100644 --- a/commands/WirelessCommand/connect/darwin.js +++ b/commands/WirelessCommand/connect/darwin.js @@ -1,36 +1,9 @@ -var spawn = require('child_process').spawn; - -function runCommand(cmd, args, cb) { - var argArray = args.split(' '); - - var s = spawn(cmd, argArray, { - stdio: ['ignore', 'pipe', 'pipe'] - }); - - var stdout = ''; - s.stdout.on('data', function (data) { - stdout += data; - }); - - var stderr = ''; - s.stderr.on('data', function (data) { - stderr += data; - }); - - s.on('error', function(err) { - cb(err, stdout, stderr); - }); - - s.on('close', function (code) { - cb(code, stdout, stderr); - }); -} - +var runCommand = require('./executor').runCommand; function getFirstWifiPort(cb) { - runCommand('networksetup', '-listnetworkserviceorder', function (err, stdout, stderr) { - if (err || stderr) { - return cb(err || stderr); + runCommand('networksetup', '-listnetworkserviceorder', function (err, code, stdout, stderr) { + if (err || stderr || code) { + return cb(err || stderr || code); } var device; var useNextDevice = false; @@ -71,9 +44,9 @@ function getCurrentNetwork(cb) { return cb(new Error('Unable to find a Wi-Fi network interface')); } - runCommand('networksetup', '-getairportnetwork ' + device, function (err, stdout, stderr) { - if (err || stderr) { - return cb(err || stderr); + runCommand('networksetup', '-getairportnetwork ' + device, function (err, code, stdout, stderr) { + if (err || stderr || code) { + return cb(err || stderr || code); } var lines = stdout.split('\n'); @@ -102,15 +75,15 @@ function connect(opts, cb) { if (opts.password) { params += ' ' + opts.password; } // TODO: something with opts & interfaces? - runCommand('networksetup', params, function results(err, stdout, stderr) { - if (err || stderr) { + runCommand('networksetup', params, function results(err, code, stdout, stderr) { + if (err || stderr || code) { // TODO: more research into failure modes of this command - return cb(err || stderr); + return cb(err || stderr || code); } cb(null, opts); }); }); -}; +} module.exports = { connect: connect, diff --git a/commands/WirelessCommand/connect/executor.js b/commands/WirelessCommand/connect/executor.js new file mode 100644 index 000000000..1c91a2432 --- /dev/null +++ b/commands/WirelessCommand/connect/executor.js @@ -0,0 +1,42 @@ +var spawn = require('child_process').spawn; +var extend = require('xtend'); + +/*** + * Executes a command, collecting the output from stdout and stderr. + * @param cmd + * @param args + * @param cb callback that receives (error, exitCode, stdout, stderr) + */ +function runCommand(cmd, args, cb) { + + // set locale so we can be sure of consistency of command execution + var env = extend(process.env, { LANG: "en", LC_ALL: "en", LC_MESSAGES: "en"}); + + var argArray = Array.isArray(args) ? args : args.split(' '); + + var s = spawn(cmd, argArray, { + stdio: ['ignore', 'pipe', 'pipe'] + }); + + var stdout = ''; + s.stdout.on('data', function (data) { + stdout += data; + }); + + var stderr = ''; + s.stderr.on('data', function (data) { + stderr += data; + }); + + s.on('error', function (error) { + cb(error, null, stdout, stderr); + }); + + s.on('close', function (code) { + cb(null, code, stdout, stderr); + }); +} + +module.exports = { + runCommand: runCommand +}; \ No newline at end of file diff --git a/commands/WirelessCommand/connect/linux.js b/commands/WirelessCommand/connect/linux.js index 322d081df..ecec8d3c1 100644 --- a/commands/WirelessCommand/connect/linux.js +++ b/commands/WirelessCommand/connect/linux.js @@ -1,38 +1,13 @@ -var spawn = require('child_process').spawn; var wifiCli = '/usr/bin/nmcli'; -function runCommand(cmd, args, cb) { - var argArray = args.split(' '); - - var s = spawn(cmd, argArray, { - stdio: ['ignore', 'pipe', 'pipe'] - }); - - var stdout = ''; - s.stdout.on('data', function (data) { - stdout += data; - }); - - var stderr = ''; - s.stderr.on('data', function (data) { - stderr += data; - }); - - s.on('error', function (error) { - cb(error, stdout, stderr); - }); - - s.on('close', function (code) { - cb(code, stdout, stderr); - }); -} +var runCommand = require('./executor').runCommand; function getCurrentNetwork(cb) { var currentNetworkParams = "--terse --fields NAME,TYPE connection show --active"; - runCommand(wifiCli, currentNetworkParams, function (err, stdout, stderr) { - if(err || stderr) { - return cb(err || stderr); + runCommand(wifiCli, currentNetworkParams, function (err, code, stdout, stderr) { + if(err || stderr || code) { + return cb(err || stderr || code); } var wifiType = "802-11-wireless"; @@ -54,8 +29,8 @@ function connect(opts, cb) { function reconnect() { var connectionDoesNotExistError = 10; var reconnectParams = 'connection up id ' + opts.ssid; - runCommand(wifiCli, reconnectParams, function (err, stdout, stderr) { - if(err == connectionDoesNotExistError) { + runCommand(wifiCli, reconnectParams, function (err, code, stdout, stderr) { + if(code == connectionDoesNotExistError) { return newConnect(); } else if(err || stderr) { return cb(err || stderr); @@ -71,9 +46,9 @@ function connect(opts, cb) { newConnectParams += ' password ' + opts.password; } - runCommand(wifiCli, newConnectParams, function (err, stdout, stderr) { - if(err || stderr) { - return cb(err || stderr); + runCommand(wifiCli, newConnectParams, function (err, code, stdout, stderr) { + if(err || stderr || code) { + return cb(err || stderr || code); } cb(null, opts); @@ -81,7 +56,7 @@ function connect(opts, cb) { } reconnect(); -}; +} module.exports = { connect: connect, diff --git a/commands/WirelessCommand/connect/windows.js b/commands/WirelessCommand/connect/windows.js new file mode 100644 index 000000000..ac030b1db --- /dev/null +++ b/commands/WirelessCommand/connect/windows.js @@ -0,0 +1,347 @@ + +var extend = require('xtend'); +var runCommand = require('./executor').runCommand; +var when = require('when'); +var pipeline = require('when/pipeline'); +var fs = require('fs'); + +function systemExecutor(cmdArgs) { + + var dfd = when.defer(); + + runCommand(cmdArgs[0], cmdArgs.splice(1), function handler(err, code, stdout, stderr) { + var fail = err || stderr || code; + if (fail) { + dfd.reject({err:err, stderr:stderr, stdout:stdout, code:code}); + } + else { + dfd.resolve(stdout); + } + }); + + return dfd.promise; +} + +/** + * @param commandExecutor A function that returns a promise to execute a given command. + * @constructor + */ +function Connect(commandExecutor) { + this.commandExecutor = commandExecutor || systemExecutor; +} + +Connect.prototype = extend(Object.prototype, { + + _execWiFiCommand: function(cmdArgs) { + return this._exec(['netsh', 'wlan'].concat(cmdArgs)); + }, + + _exec: function(cmdArgs) { + return this.commandExecutor(cmdArgs); + }, + + /** + * Retrieves the profile name of the currently connected network. + * @returns {Promise.} The profile name of the currently connected network, or undefined if no + + connection. + */ + current: function() { + return this.currentInterface() + .then(function(iface) { + return iface ? iface.profile : undefined; + }); + }, + + /** + * Determine the current network interface. + * @return {Promise.} the current network interface object + */ + currentInterface: function() { + var self = this; + return this._execWiFiCommand(['show', 'interfaces']) + .then(function(output) { + var lines = self._stringToLines(output); + var iface = self._currentFromInterfaces(lines); + if (iface && !iface['profile']) { + iface = null; + } + return iface; + }); + }, + + /** + * Connect the wifi interface to the given access point with the named profile. + * If the profile already exists, it is used. Otherwise a new profile for an open AP is created. + */ + connect: function(profile) { + var self = this; + var interfaceName; + return pipeline([ + this.currentInterface.bind(this), // find the current interface + this._checkHasInterface.bind(this), // fail if no interfaces + function (ifaceName) { // save the interface name + interfaceName = ifaceName; + return ifaceName; + }, + this.listProfiles.bind(this), // fetch the profiles for the interface + function (profiles) { + return self._createProfileIfNeeded(profile, interfaceName, profiles); + }, + function () { + return self._connectProfile(profile, interfaceName); + } + ]); + }, + + _connectProfile(profile, interfaceName) { + var self = this; + var args = ['connect', 'name='+profile, 'interface='+interfaceName]; + return this._execWiFiCommand(args) + .then(function() { + return self.waitForConnected(profile, interfaceName, 20, 500); + }) + .then(function() { + return { ssid: profile }; + }); + }, + + waitForConnected(profile, interfaceName, count, retryPeriod) { + var self = this; + return this.current() + .then(function(ssid) { + if (ssid!==profile) { + var dfd = when.defer(); + if (--count <= 0) { + return dfd.reject(new Error('timeout waiting for network to connect')); + } + + setTimeout(retry, retryPeriod); + function retry() { + dfd.resolve(self.waitForConnected(profile, interfaceName, + + count, retryPeriod)); + } + return dfd.promise; + } + }); + }, + + /** + * Create the profile if it doesn't already exist. + * @param profile The name of the profile to create (and the SSID of the open network to connect to.) + * @param interfaceName The interface to create the profile on. + * @param profiles The current list of profiles. + * @return the profile name or a promise to create the profile, resolving to the profile name + * @private + */ + _createProfileIfNeeded(profile, interfaceName, profiles) { + if (!this._profileExists(profile, profiles)) { + return this._createProfile(profile, interfaceName); + } + return profile; + }, + + _profileExists(profile, profiles) { + return profiles.indexOf(profile)>=0; + }, + + /** + * Creates a open AP profile so that the AP can be subsequently connected to. + * @param profile The name of the profile and the SSID to connect to + * @param interfaceName The wifi interface to register the profile with + * @param _fs + * @returns {*} + * @private + */ + _createProfile(profile, interfaceName, fs) { + if (!fs) { + fs = require('fs'); + } + var filename = '_wifi_profile.xml'; + var content = this._buildProfile(profile); + var self = this; + fs.writeFileSync(filename, content); + var args = ['add', 'profile', 'filename='+filename+'']; + if (interfaceName) { + args.push('interface='+interfaceName); + } + return pipeline([function() { + return self._execWiFiCommand(args) + }]) + .finally(function() { + fs.unlinkSync(filename); + }); + }, + + /** + * Validates that the given interface object is properly defined. + * @param {object} iface The object to validate + * @throws Error if the interface is not valid + * @private + */ + _checkHasInterface: function(iface) { + // todo - make this a programmatically identifiable error + if (!iface || !iface.name) { + throw Error('no Wi-Fi interface detected'); + } + return iface.name; + }, + + /** + * Lists all the profiles registered, either for all interfaces or for a specific interface. + * @param {string} ifaceName The name of the interface to list profiles for. + * @returns {Promise.>} An array of profile names + */ + listProfiles: function(ifaceName) { + var self = this; + var cmd = ['show', 'profiles']; + if (ifaceName) { + cmd.push('interface='+ifaceName); + } + return this._execWiFiCommand(cmd) + .then(function(output) { + var lines = self._stringToLines(output); + return self._parseProfiles(lines); + }); + }, + + /** + * Parses the output of the "show profiles" command. Profiles are "type : name"-style key-value. + * @param lines + * @returns {Array} + * @private + */ + _parseProfiles: function(lines) { + var profiles = []; + for (var i=0; i} lines The lines from the command output. + * @private + */ + _currentFromInterfaces: function(lines) { + var idx = 0; + var iface; + while (idx < lines.length && (!iface || !iface['profile'])) { + var data = this._extractInterface(lines, idx); + iface = data.iface; + idx = data.range.end; + } + return iface; + }, + + /** + * Reads all the lines of info up until the end, or the next 'name', collecting the property keys and values into + * an object keyed by 'iface'. a `range` property provides `start` and `end` for the indices of the range. + * The end index is exclusive. + * @param lines + * @param index + * @private + */ + _extractInterface: function(lines, index) { + index = index || 0; + var result = { iface: {}, range: {} }; + var name = 'name'; + var kv; + for (;index0) { + var key = line.slice(0, colonIndex).trim().toLowerCase(); + var value = line.slice(colonIndex+1).trim(); + result = { key: key, value: value }; + } + return result; + }, + + _stringToLines: function(s) { + return s.match(/[^\r\n]+/g) || []; + }, + + /** + * Creates a new open profile using the given ssid. + * @param {string} ssid The ssid of the AP to connect to. It is also the name of the profile. + * @returns {string} The XML descriptor of the profile. + * @private + */ + _buildProfile(ssid) { + // todo - xml encode profile name + var result = ' ' + ssid + ' ' + ssid + ' '; + result += ' ESS manual open none false '; + result += " "; + return result; + } +}); + +function asCallback(promise, cb) { + return promise.then(function success(arg) { + cb(null, arg); + }).catch(function fail(error) { + cb(error); + }); +} + +function getCurrentNetwork(cb) { + asCallback(new Connect().current(), cb); +} + +/** + * + * @param opts + * - ssid property is the SSID of the network to connect to. + * - profileName is the name of the network profile to connect to. Defaults to ssid if not defined. + * @param cb + */ +function connect(opts, cb) { + asCallback(new Connect().connect(opts.ssid), cb); +} + +module.exports = { + connect: connect, + getCurrentNetwork: getCurrentNetwork, + asCallback: asCallback, + Connector: Connect +}; diff --git a/commands/WirelessCommand/index.js b/commands/WirelessCommand/index.js index 01d41fe25..34526d3b2 100644 --- a/commands/WirelessCommand/index.js +++ b/commands/WirelessCommand/index.js @@ -42,7 +42,6 @@ var SAP = require('softap-setup'); var path = require('path'); var strings = { - 'monitorPrompt': 'Would you like to wait and monitor for Photons entering setup mode?', 'scanError': 'Unable to scan for Wi-Fi networks. Do you have permission to do that on this computer?', 'credentialsNeeded': 'You will need to know the password for your Wi-Fi network (if any) to proceed.', @@ -105,36 +104,42 @@ WirelessCommand.prototype.init = function init() { this.addOption('monitor', this.monitor.bind(this), 'Begin monitoring nearby Wi-Fi networks for Photons in setup mode.'); }; -WirelessCommand.prototype.list = function list(macAddress) { +WirelessCommand.prototype.prompt = prompt; +WirelessCommand.prototype.list = function list(macAddress, manual) { + if (manual) { + this.__manual = true; + } // if we get passed a MAC address from setup if (macAddress && macAddress.length === 17) { - this.__macAddressFilter = macAddress; - } else { this.__macAddressFilter = null; } - console.log(); - protip('Wireless setup of Photons works like a', chalk.cyan('wizard!')); - protip( - 'We will', - chalk.cyan('automagically'), - 'change the', - chalk.cyan('Wi-Fi'), - 'network to which your computer is connected.' - ); - protip('You will lose your connection to the internet periodically.'); console.log(); - this.newSpin('%s ' + chalk.bold.white('Scanning Wi-Fi for nearby Photons in setup mode...')).start(); - scan(this.__networks.bind(this)); + if (manual) { + return this.setup(null, manualDone); + } else { + protip('Wireless setup of Photons works like a', chalk.cyan('wizard!')); + protip( + 'We will', + chalk.cyan('automagically'), + 'change the', + chalk.cyan('Wi-Fi'), + 'network to which your computer is connected.' + ); + protip('You will lose your connection to the internet periodically.'); + console.log(); + this.newSpin('%s ' + chalk.bold.white('Scanning Wi-Fi for nearby Photons in setup mode...')).start(); + scan(this.__networks.bind(this)); + } }; -function manualAsk(cb) { - return prompt([{ +WirelessCommand.prototype.manualAsk = function manualAsk(cb) { + return this.prompt([{ type: 'confirm', name: 'manual', @@ -142,8 +147,25 @@ function manualAsk(cb) { default: true }], cb); +}; + +function manualDone(err, dat) { + if (err) { + return console.log(chalk.read('!'), 'An error occurred:', err); + } + if (dat && dat.id) { + return console.log(arrow, 'We successfully configured your Photon! Great work. We make a good team!', chalk.magenta('<3')); + } } +/** + * Callback from scanning for wifi networks. + * @param err Any error that happened during scanning + * @param dat A list of APs that matched with these properties: + * mac: MAC address for the AP + * ssid: the SSID of the AP + * @private + */ WirelessCommand.prototype.__networks = function networks(err, dat) { var self = this; @@ -164,16 +186,13 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { console.log(alert, chalk.bold.white('OOPS:'), 'I was unable to scan for nearby Wi-Fi networks', chalk.magenta('(-___-)')); console.log(); - return manualAsk(manualChoice); + return this.manualAsk(manualChoice); } detectedDevices = dat; if (this.__macAddressFilter) { - var macDevices = detectedDevices.filter(function (ap) { - - return ap.mac.toLowerCase() === self.__macAddressFilter; - + return ap.mac && (ap.mac.toLowerCase() === self.__macAddressFilter); }); if (macDevices && macDevices.length === 1) { @@ -187,7 +206,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { if (detectedDevices.length > 1) { // Multiple Photons detected - prompt([{ + this.prompt([{ type: 'confirm', name: 'setup', @@ -198,7 +217,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { } else if (detectedDevices.length === 1) { // Perform wireless setup? - prompt([{ + this.prompt([{ type: 'confirm', name: 'setupSingle', @@ -218,7 +237,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { ); // Monitor for new Photons? - prompt([{ + this.prompt([{ type: 'confirm', name: 'monitor', @@ -247,7 +266,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { console.log(alert, 'Try running', chalk.cyan(cmd + ' setup'), 'with Administrator privileges.'); console.log(alert, 'If the problem persists, please let us know:', chalk.cyan('https://community.particle.io/')); console.log(); - }; + } function multipleChoice(ans) { @@ -256,7 +275,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { self.__batch = false; // Select any/all Photons to setup - return prompt([{ + return self.prompt([{ type: 'list', name: 'selected', @@ -266,7 +285,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { }], multipleAnswer); } self.exit(); - }; + } function multipleAnswer(ans) { @@ -275,16 +294,15 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { return self.setup(ans.selected); } self.exit(); - }; + } function singleChoice(ans) { if (ans.setupSingle) { self.setup(detectedDevices[0]); } else { - // Monitor for new Photons? - prompt([{ + self.prompt([{ type: 'confirm', name: 'monitor', @@ -293,7 +311,7 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { }], monitorChoice); } - }; + } function monitorChoice(ans) { @@ -304,22 +322,9 @@ WirelessCommand.prototype.__networks = function networks(err, dat) { } else { self.exit(); } - }; - - function manualDone(err, dat) { - - if (err) { - - return console.log(chalk.read('!'), 'An error occurred:', err); - } - if (dat && dat.id) { - - return console.log(arrow, 'We successfully configured your Photon! Great work. We make a good team!', chalk.magenta('<3')); - }; - }; + } }; - WirelessCommand.prototype.monitor = function(args) { var self = this; @@ -377,7 +382,7 @@ WirelessCommand.prototype.setup = function setup(photon, cb) { chalk.bold.white('You are still connected to your Photon\'s Wi-Fi network. Please reconnect to a Wi-Fi network with internet access.') ); console.log(); - prompt([{ + self.prompt([{ type: 'confirm', message: 'Have you reconnected to the internet?', default: true, @@ -398,22 +403,20 @@ WirelessCommand.prototype.setup = function setup(photon, cb) { function getClaim() { self.newSpin('Obtaining magical secure claim code from the cloud...').start(); - api.getClaimCode(undefined, next); + api.getClaimCode(undefined, afterClaim); } - function next(err, dat) { + + function afterClaim(err, dat) { self.stopSpin(); - console.log(arrow, 'Obtained magical secure claim code.'); - console.log(); if (err) { // TODO: Graceful recovery here + // How about retrying the claim code again // console.log(arrow, arrow, err); if (err.code === 'ENOTFOUND') { - protip("Your computer couldn't find the cloud..."); } else { - protip('There was a network error while connecting to the cloud...'); } protip('We need an active internet connection to successfully complete setup.'); @@ -421,13 +424,16 @@ WirelessCommand.prototype.setup = function setup(photon, cb) { return; } + console.log(arrow, 'Obtained magical secure claim code.'); + console.log(); self.__claimCode = dat.claim_code; + // todo - prompt for manual connection before getting the claim code since this exits the setup process if (!self.__manual && !mgr.supported.connect) { console.log(); console.log(alert, 'I am unable to automatically connect to Wi-Fi networks', chalk.magenta('(-___-)')); console.log(); - return manualAsk(function (ans) { + return self.manualAsk(function (ans) { if (ans.manual) { self.__manual = true; return manualConnect(); @@ -444,7 +450,7 @@ WirelessCommand.prototype.setup = function setup(photon, cb) { } function manualConnect() { - return prompt([{ + return self.prompt([{ type: 'input', name: 'connect', @@ -452,14 +458,15 @@ WirelessCommand.prototype.setup = function setup(photon, cb) { }], manualReady); } - }; + } function manualReady() { self.__configure(null, manualConfigure); - }; + } + function manualConfigure(err, dat) { cb(err, dat); - }; + } function connected(err, opts) { @@ -478,12 +485,14 @@ WirelessCommand.prototype.setup = function setup(photon, cb) { chalk.bold.cyan(opts.ssid) ); self.__configure(opts.ssid); - }; + } }; WirelessCommand.prototype.__configure = function __configure(ssid, cb) { console.log(); + + // todo - distinguish Photon/P1 console.log(arrow, 'Now to configure our precious', chalk.cyan(ssid ? ssid : 'Photon')); console.log(); @@ -499,23 +508,23 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { var security; protip('If you want to skip scanning, or your network is configured as a'); - protip(chalk.cyan('non-broadcast'), 'network, please enter manual mode to proceed...'); + protip(chalk.cyan('non-broadcast'), 'network, please choose No to the next prompt to enter manual mode.'); console.log(); - prompt([{ + self.prompt([{ type: 'confirm', - name: 'manual', - message: 'Would you like to manually enter your Wi-Fi network configuration?', - default: false + name: 'auto', + message: 'Shall I have the Photon scan for available Wi-Fi networks?', + default: true }], scanChoice); function scanChoice(ans) { - if (ans.manual) { + if (!ans.auto) { - return prompt([{ + return self.prompt([{ type: 'input', name: 'network', @@ -559,19 +568,19 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { self.newSpin('Asking the Photon to scan for nearby Wi-Fi networks...').start(); retry = setTimeout(start, 1000); - }; + } function manualChoices(ans) { if (!ans.network) { console.log(alert, "We can't setup your Photon without a Wi-Fi network! Let's try again..."); - return scanChoice({ manual: true }); + return scanChoice({ auto: false }); } if (!ans.password && ans.security !== 'None') { console.log(alert, "You chose a security type that requires a password! Let's try again..."); - return scanChoice({ manual: true }); + return scanChoice({ auto: false }); } networkChoices({ @@ -580,36 +589,41 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { security: ans.security.toLowerCase().replace(' ', '_') }); - }; + } function start() { clearTimeout(retry); if (retries >= 9) { // scan has failed 10 times already + self.stopSpin(); + console.log( arrow, 'Your Photon failed to scan for nearby Wi-Fi networks.' ); - prompt([{ - + retries = 0; + self.prompt([{ type: 'confirm', name: 'manual', message: 'Would you like to manually enter your Wi-Fi network configuration?', default: true - }], scanChoice); + }], function manualAuto(ans) { + return scanChoice({auto:!ans.manual}); + }); return; } sap.scan(results); - }; + } function results(err, dat) { clearTimeout(retry); self.stopSpin(); if (err) { + console.error(err); console.log( arrow, 'Your Photon encountered an error while trying to scan for nearby Wi-Fi networks. Retrying...' @@ -618,10 +632,9 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { retries++; return; } + var networks = dat; - var networks = dat.scans; - - dat.scans.forEach(function save(ap) { + dat.forEach(function save(ap) { list[ap.ssid] = ap; }); @@ -636,13 +649,11 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { } networks.unshift(new inquirer.Separator()); - prompt([{ - + self.prompt([{ type: 'list', name: 'network', message: 'Please select the network to which your Photon should connect:', choices: networks - }], __networkChoice); function __networkChoice(ans) { @@ -655,7 +666,7 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { } if (ans.network === strings.manualEntryLabel) { - scanChoice({ manual: true }); + scanChoice({ auto: false }); return; } @@ -665,7 +676,7 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { return networkChoices({ network: network }); } - prompt([{ + self.prompt([{ type: 'input', name: 'password', @@ -674,10 +685,9 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { }], __passwordChoice); } function __passwordChoice(ans) { - networkChoices({ network: network, password: ans.password }); } - }; + } function networkChoices(ans) { @@ -703,19 +713,17 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { console.log(arrow, 'Password:', chalk.bold.cyan(password || '[none]')); console.log(); - prompt([{ - + self.prompt([{ type: 'confirm', name: 'continue', - message: 'Would you like to continue with the information shown above?' - + message: 'Would you like to continue with the information shown above?', + default: true, }], continueChoice); - }; + } function continueChoice(ans) { if (!ans.continue) { - console.log(arrow, "Let's try again..."); console.log(); return self.__configure(ssid, cb); @@ -724,7 +732,7 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { self.__password = password; info(); - }; + } function info() { @@ -732,6 +740,10 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { console.log(); console.log(arrow, 'Obtaining device information...'); + + + // todo - this is the first attempt to connect to the photon + // if the network hasn't switched then the connection process may hang sap.deviceInfo(pubKey); } @@ -748,7 +760,7 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { clearTimeout(retry); console.log(arrow, 'Requesting public key from the device...'); sap.publicKey(code); - }; + } function code(err) { if (err) { @@ -759,7 +771,7 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { clearTimeout(retry); console.log(arrow, 'Setting the magical cloud claim code...'); sap.setClaimCode(self.__claimCode, configure); - }; + } function configure(err) { if (err) { @@ -768,17 +780,15 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { } var conf = { - ssid: network, security: security, password: password - }; clearTimeout(retry); console.log(arrow, 'Telling the Photon to apply your Wi-Fi configuration...'); sap.configure(conf, connect); - }; + } function connect(err) { if (err) { @@ -790,7 +800,7 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { console.log(); clearTimeout(retry); sap.connect(done); - }; + } function done(err) { if (err) { @@ -802,52 +812,36 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { clearTimeout(retry); self.stopSpin(); - console.log(arrow, chalk.bold.white('Configuration complete! You\'ve just won the internet!')); + //console.log(arrow, chalk.bold.white('Configuration complete! You\'ve just won the internet!')); if (!self.__manual) { - - prompt([{ - - name: 'revive', - type: 'confirm', - message: 'Would you like to return this computer to the wireless network you just configured?', - default: true - - }], function(ans) { - if (!ans.revive) { - manualReconnectPrompt(); - return; - } - reconnect(false); - }); + reconnect(false); } else { manualReconnectPrompt(); } - }; + } function manualReconnectPrompt() { - prompt([{ - + self.prompt([{ name: 'reconnect', type: 'input', message: 'Please re-connect your computer to your Wi-Fi network now. Press enter when ready.' - }], manualPrompt); } function manualPrompt() { reconnect(true); - }; - function reconnect(manual) { + } + function reconnect(manual) { if (!manual) { - self.newSpin('Reconnecting your computer to your Wi-Fi network...').start(); mgr.connect({ ssid: self.__network, password: self.__password }, revived); } else { revived(); } - }; + } + function revived(err) { if (err) { manualReconnectPrompt(); @@ -863,14 +857,13 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { }, 2000); - }; + } function checkDevices(err, dat) { self.stopSpin(); if (err) { - if (err.code === 'ENOTFOUND') { - + // todo - limit the number of retries here. console.log(alert, 'Network not ready yet, retrying...'); console.log(); return revived(null); @@ -897,47 +890,52 @@ WirelessCommand.prototype.__configure = function __configure(ssid, cb) { console.log(alert, "It doesn't look like your Photon has made it to the cloud yet."); console.log(); - prompt([{ + self.prompt([{ type: 'list', name: 'recheck', message: 'What would you like to do?', choices: [ - { name: 'Reconfigure the Wi-Fi settings of the Photon', value: 'reconfigure' }, - { name: 'Check again to see if the Photon has connected', value: 'recheck' } + { name: 'Check again to see if the Photon has connected', value: 'recheck' }, + { name: 'Reconfigure the Wi-Fi settings of the Photon', value: 'reconfigure' } ] }], recheck); function recheck(ans) { if (ans.recheck === 'recheck') { - api.listDevices(checkDevices); } else { - self.setup(self.__ssid); } } - }; + } + function namePhoton(deviceId) { - prompt([ + self.prompt([ { type: 'input', name: 'deviceName', message: 'What would you like to call your photon?' } ], function(ans) { + // todo - retrieve existing name of the device? var deviceName = ans.deviceName; - self.__oldapi.renameDevice(deviceId, deviceName).then(function () { - console.log(); - console.log(arrow, 'Your Photon has been bestowed with the name', chalk.bold.cyan(deviceName)); - console.log(arrow, "Congratulations! You've just won the internet!"); - console.log(); + if (deviceName) { + self.__oldapi.renameDevice(deviceId, deviceName).then(function () { + console.log(); + console.log(arrow, 'Your Photon has been given the name', chalk.bold.cyan(deviceName)); + console.log(arrow, "Congratulations! You've just won the internet!"); + console.log(); + self.exit(); + }, function (err) { + console.error(alert, 'Error naming your photon: ', err); + namePhoton(deviceId); + }); + } else { + console.log('Skipping device naming.'); self.exit(); - }, function(err) { - console.error(alert, 'Error naming your photon: ', err); - namePhoton(deviceId); - }); + } }); } }; @@ -950,7 +948,6 @@ WirelessCommand.prototype.exit = function() { chalk.bold.magenta('<3')) ); process.exit(0); - }; function filter(list, pattern, inverse) { @@ -963,14 +960,14 @@ function filter(list, pattern, inverse) { // return false return inverse ? !ap.ssid.match(pattern) : ap.ssid.match(pattern); }); -}; +} function ssids(list) { return clean(list).map(function map(ap) { return ap.ssid; }); -}; +} function removePhotonNetworks(ssids) { return ssids.filter(function (ap) { @@ -979,7 +976,7 @@ function removePhotonNetworks(ssids) { } return true; }); -}; +} function clean(list) { diff --git a/commands/WirelessCommand/test.js b/commands/WirelessCommand/test.js new file mode 100644 index 000000000..b85bd8ad5 --- /dev/null +++ b/commands/WirelessCommand/test.js @@ -0,0 +1,151 @@ + +var WiFiManager = require('./WiFiManager'); +var prompt = require('inquirer').prompt; +var os = require('os'); +var scan = require('node-wifiscanner2').scan; + + +function TestWiFi() { + this.mgr = new WiFiManager(); + this._next = null; + this.wirelessSetupFilter = /^Photon-.*$/; +} + +TestWiFi.prototype.setNext = function(fn) { + this._next = fn; +}; + +TestWiFi.prototype.next = function() { + var fn = this._next; + this._next = null; + if (fn) { + fn(); + } +}; + +TestWiFi.prototype.run = function() { + if (this.mgr.osConnect) { + console.log('Using Wi-Fi connector for the current platform '+os.platform()); + this.setNext(this.selectNetwork.bind(this)); + this.mgr.getCurrentNetwork(this.handleCurrentNetwork.bind(this)); + } + else { + console.error('No Wi-Fi connector for the current platform '+os.platform()); + } +}; + +TestWiFi.prototype.handleCurrentNetwork = function(err, network) { + var self = this; + if (err) { + console.err('Unable to get current network:', err); + } else { + console.log('Current network detected as', network); + prompt([{ + type: 'confirm', + message: 'Is that correct?', + default: true, + name: 'correct' + }], function (ans) { + if (ans.correct) { + self.originalNetwork = network; + self.next(); + return; + } + console.error('Incorrect network was detected.'); + }); + } +}; + +TestWiFi.prototype.selectNetwork = function() { + console.log('Scanning for nearby networks matching', this.wirelessSetupFilter); + var self = this; + scan(function(err, aps) { + if (err) { + console.log('unable to scan for wifi networks:', err); + return; + } + + console.log('Found', aps.length, 'networks.'); + + var photons = ssids(filter(aps, self.wirelessSetupFilter)); + console.log('Found', photons.length, 'photons.'); + if (photons.length) { + console.log(photons); + return prompt([{ + type: 'list', + name: 'selected', + message: 'Please select which Photon network you would like to switch to:', + choices: photons + }], function(ans) { + if (ans.selected) { + self.setNext(function() { + console.log('Restoring to original network', self.originalNetwork); + self.connect(self.originalNetwork); + }); + self.connect(ans.selected); + } + }); + } + }); +}; + +/** + * Connect to the given network + * @param ssid + */ +TestWiFi.prototype.connect = function(ssid) { + var self = this; + console.log('Connecting to network', ssid); + self.mgr.connect({ssid:ssid}, function(err, opts) { + if (err) { + console.error('Unable to connect to network', ssid, ':', err); + return; + } + + console.log('connected to network ', opts.ssid); + self.mgr.getCurrentNetwork(function(err, current) { + if (err) { + console.error('Unable to detect current network:', err); + return; + } + console.log('current network detected as', current); + if (current!==ssid) { + console.error('Current network should have been', ssid); + return; + } + + prompt([{ + type: 'confirm', + message: 'Is that correct? (Please verify your computer is connected to this network.)', + default: true, + name: 'correct' + }], function (ans) { + if (ans.correct) { + self.next(); + return; + } + console.error('Incorrect network was detected.'); + }); + }); + }); +}; + +function filter(list, pattern) { + // var returnedOne = false; + return list.filter(function filter(ap) { + // if(!returnedOne && ap.ssid.match(pattern)) { + // returnedOne = true + // return true + // } + // return false + return ap.ssid.match(pattern); + }); +} + +function ssids(list) { + return list.map(function map(ap) { + return ap.ssid; + }); +} + +module.exports = TestWiFi; \ No newline at end of file diff --git a/oldlib/ApiClient.js b/oldlib/ApiClient.js index fb9b5d78a..a6c4a8a64 100644 --- a/oldlib/ApiClient.js +++ b/oldlib/ApiClient.js @@ -55,6 +55,11 @@ var chalk = require('chalk'); /** * Provides a framework for interacting with and testing the API + * - apiUrl and access_token can be set, otherwise default to those in global settings + * - accessors/mutators for access token + * - returns promises + * - most functions generate console output on error, but not for success + * - tests for specific known errors such as invalid access token. * */ @@ -89,8 +94,8 @@ ApiClient.prototype = { this._access_token = token; }, - - createUser: function (user, pass) { + // doesn't appear to be used (renamed) + _createUser: function (user, pass) { var dfd = when.defer(); //todo; if !user, make random? @@ -136,7 +141,10 @@ ApiClient.prototype = { return dfd.promise; }, - //GET /oauth/token + /** + * Login and update the access token on this instance. Doesn't update the global settings. + * Outputs failure to the console. + */ login: function (client_id, user, pass) { var that = this; @@ -151,7 +159,13 @@ ApiClient.prototype = { }); }, - //GET /oauth/token + /** + * Creates an access token but doesn't change the CLI state/global token etc.. + * @param client_id The OAuth client ID to identify the client + * @param username The username + * @param password The password + * @returns {Promise} to create the token + */ createAccessToken: function (client_id, username, password) { var that = this; return when.promise(function (resolve, reject) { @@ -180,6 +194,10 @@ ApiClient.prototype = { }, //DELETE /v1/access_tokens/{ACCESS_TOKEN} + /** + * Removes the given access token, outputting any errors to the console. + * @returns {Promise} To retrieve the API response body + */ removeAccessToken: function (username, password, access_token) { var dfd = when.defer(); this.request({ @@ -536,18 +554,22 @@ ApiClient.prototype = { return result; }, - _addFilesToCompile: function (r, fileMapping, targetVersion, platform_id) { - var form = r.form(); + _populateFileMapping(fileMapping) { if (!fileMapping.map) { - fileMapping.map = {} + fileMapping.map = {}; if (fileMapping.list) { for (var i = 0; i < fileMapping.list.length; i++) { - var item = fileMapping.list[i] + var item = fileMapping.list[i]; fileMapping.map[item] = item; } } } + return fileMapping; + }, + _addFilesToCompile: function (r, fileMapping, targetVersion, platform_id) { + var form = r.form(); + this._populateFileMapping(fileMapping); var list = Object.keys(fileMapping.map); for (var i = 0, n = list.length; i < n; i++) { var relativeFilename = list[i]; @@ -839,7 +861,8 @@ ApiClient.prototype = { return dfd.promise; }, - createWebhook: function (event, url, deviceId, requestType, headers, json, query, auth, mydevices, rejectUnauthorized) { + // not used + _createWebhook: function (event, url, deviceId, requestType, headers, json, query, auth, mydevices, rejectUnauthorized) { var that = this; var dfd = when.defer(); diff --git a/oldlib/ApiClient2.js b/oldlib/ApiClient2.js index 64bdf5271..03302f179 100644 --- a/oldlib/ApiClient2.js +++ b/oldlib/ApiClient2.js @@ -4,6 +4,12 @@ var request = require('request'); var utilities = require('./utilities'); var settings = require('../settings'); +/* + * This variant of APIClient uses callbacks rather than promises, to satisfy the needs of the setup command, which + * also uses callbacks. Login alters global state, setting the access token on the settings instance, but otheriwse + * the commands do not introduce side effects (e.g. no console output.) + */ + function APIClient2(baseUrl, token) { this.__token = token || settings.access_token; this.request = request.defaults({ @@ -20,6 +26,9 @@ APIClient2.prototype.clearToken = function() { this.__token = null; }; +/** + * Used in setup command. + */ APIClient2.prototype.login = function(clientId, user, pass, cb) { this.createAccessToken(clientId, user, pass, function tokenResponse(err, dat) { if (err) { @@ -30,6 +39,7 @@ APIClient2.prototype.login = function(clientId, user, pass, cb) { }); }; +// used in setup process to create a new account APIClient2.prototype.createUser = function(user, pass, cb) { if (!user || (user === '') || (!utilities.contains(user, '@')) @@ -58,6 +68,11 @@ APIClient2.prototype.createUser = function(user, pass, cb) { }); }; +/** + * Creates an access token, updates the global settings with the new token, and the token in this instance. + * Used only by login above. + * todo - updating the token should probably be moved to login() + */ APIClient2.prototype.createAccessToken = function(clientId, user, pass, cb) { var self = this; @@ -83,7 +98,7 @@ APIClient2.prototype.createAccessToken = function(clientId, user, pass, cb) { cb(new Error(err || body.error)); } else { - + // todo factor this out creating and updating should be separate // update the token if (body.access_token) { diff --git a/oldlib/SerialBoredParser.js b/oldlib/SerialBoredParser.js index f9cdb991a..b734f5e13 100644 --- a/oldlib/SerialBoredParser.js +++ b/oldlib/SerialBoredParser.js @@ -1,7 +1,30 @@ 'use strict'; +var original = { + setTimeout: global.setTimeout, + clearTimeout: global.clearTimeout +}; + +var setTimeoutLocal = function() { + original.setTimeout.apply(this, arguments); +}; + +var clearTimeoutLocal = function() { + original.clearTimeout.apply(this, arguments); +}; + + +/** + * A parser that waits for a given length of time for a response, or for a given terminator. + * @type {{makeParser: module.exports.makeParser}} + */ module.exports = { - makeParser: function (boredDelay) { + setTimeoutFunctions(setTimeout, clearTimeout) { + setTimeoutLocal = setTimeout; + clearTimeoutLocal = clearTimeout; + }, + + makeParser: function (boredDelay, terminator) { var boredTimer, chunks = []; @@ -11,16 +34,24 @@ module.exports = { }; var updateTimer = function (emitter) { - clearTimeout(boredTimer); - boredTimer = setTimeout(function () { + clearTimeoutLocal(boredTimer); + boredTimer = setTimeoutLocal(function () { whenBored(emitter); }, boredDelay); }; + var result = function (emitter, buffer) { + var s = buffer.toString(); + chunks.push(s); - return function (emitter, buffer) { - chunks.push(buffer.toString()); - updateTimer(emitter); + if (terminator!==undefined && s.indexOf(terminator)>=0) { + whenBored(emitter); + clearTimeoutLocal(boredTimer); + boredTimer = undefined; + } else { + updateTimer(emitter); + } }; + return result; } }; diff --git a/oldlib/SerialTrigger.js b/oldlib/SerialTrigger.js index e22897a69..47b9e1a8b 100644 --- a/oldlib/SerialTrigger.js +++ b/oldlib/SerialTrigger.js @@ -17,31 +17,69 @@ SerialTrigger.prototype.addTrigger = function(prompt, next) { throw new Error('prompt must be specified'); } this.triggers[prompt] = next; + this.data = ''; }; -SerialTrigger.prototype.start = function() { +/** + * There is no guarantee that 'data' events contain all of the data emitted by the device + * in one block, so we have to match on substrings. + * @param noLogs + */ +SerialTrigger.prototype.start = function(noLogs) { + var serialDataCallback = function (data) { - data = data.toString(); var self = this; - var triggerFn = _.find(this.triggers, function (fn, prompt) { - return data.indexOf(prompt) >= 0; - }); - - if (triggerFn) { - triggerFn(function (response, cb) { - if (response) { - self.port.flush(function () { - self.port.write(response, function() { - self.port.drain(function () { - log.serialInput(response); - if (cb) { - cb(); - } + this.data += data.toString(); + var substring = this.data; + var substringMatch = ''; + var matchPrompt = ''; + + while (!matchPrompt && substring) { + (function(substring) { + _.forOwn(self.triggers, function (fn, prompt) { + if (substring.length > prompt.length) { + if (substring.startsWith(prompt)) { + matchPrompt = prompt; + return false; // quit iteration + } + } else { + if (prompt.startsWith(substring)) { + matchPrompt = prompt; + return false; + } + } + }); + })(substring); + + if (!matchPrompt) { + substring = substring.substring(1); + } + } + + this.data = substring; + + if (matchPrompt && substring.length >= matchPrompt.length) { + this.data = substring.substring(matchPrompt.length); + + var triggerFn = this.triggers[matchPrompt]; + if (triggerFn) { + triggerFn(function (response, cb) { + if (response) { + self.port.flush(function () { + self.port.write(response, function () { + self.port.drain(function () { + if (!noLogs) { + log.serialInput(response); + } + if (cb) { + cb(); + } + }); }); }); - }); - } - }); + } + }); + } } }; this.dataCallback = serialDataCallback.bind(this); diff --git a/oldlib/interpreter.js b/oldlib/interpreter.js index c1026f01b..93041fb27 100644 --- a/oldlib/interpreter.js +++ b/oldlib/interpreter.js @@ -118,15 +118,12 @@ Interpreter.prototype = { getCommandModule: function (name) { var commands = this._commands; for (var i = 0; i < commands.length; i++) { - try { - var c = commands[i]; - if (c.name === name) { - return c; - } - } catch (ex) { - console.error('Error loading command ' + ex); + var c = commands[i]; + if (c.name === name) { + return c; } } + throw Error('no command called '+name); }, diff --git a/package.json b/package.json index 327ead23e..84ed98251 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "particle-cli", "description": "Simple Node commandline application for working with your Particle devices and using the Particle Cloud", - "version": "1.20.1", + "version": "1.21.0", "author": "David Middlecamp", "bin": { "particle": "./bin/particle.js" @@ -69,18 +69,18 @@ "moment": "^2.9.0", "node-wifiscanner2": "^1.2.0", "particle-api-js": "^6.4.1", - "particle-commands": "^0.2.9", - "particle-library-manager": "0.1.10", + "particle-commands": "^0.2.11", + "particle-library-manager": "0.1.11", "request": "https://github.com/spark/request/releases/download/v2.75.1-relativepath.1/request-2.75.1-relativepath.1.tgz", "semver": "^5.1.0", "serialport": "^4.0.7", - "softap-setup": "^1.1.4", + "softap-setup": "^4.0.3", "temp": "^0.8.3", "when": "^3.7.2", "xtend": "^4.0.0", "yargs": "^5.0.0", - "yeoman-environment": "^1.6.4", - "yeoman-generator": "^0.24.1" + "yeoman-environment": "^1.6.6", + "yeoman-generator": "^1.1.1" }, "devDependencies": { "babel-cli": "^6.10.1", @@ -94,12 +94,12 @@ "doctoc": "^0.15.0", "dotenv": "^4.0.0", "eslint": "^3.15.0", - "eslint-config-particle": "1.0.5", + "eslint-config-particle": "^1.0.5", "fs-extra": "^0.30.0", "github-api": "^3.0.0", "istanbul": "^0.3.22", "mocha": "^3.0.2", - "mock-fs": "^3.11.0", + "mock-fs": "^4.2.0", "proxyquire": "^1.6.0", "rimraf-promise": "^2.0.0", "should": "^7.0.2", @@ -138,6 +138,7 @@ "coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec --compilers js:babel-register test/ && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", "doctoc": "doctoc --title '## Table of Contents' README.md", "lint": "eslint -f unix src/**/*.js", + "lint:fix": "eslint --fix -f unix src/**/*.js", "test": "mocha test/ test/oldcmd/ test/app test/cli test/integration --compilers js:babel-register", "compile": "babel src -d dist", "prepublish": "npm run compile", diff --git a/settings.js b/settings.js index 89edeb74e..d01b7d29d 100644 --- a/settings.js +++ b/settings.js @@ -100,7 +100,10 @@ var settings = { systemFirmwareThree: 'system-part1-0.6.1-electron.bin' } }, - commandMappings: path.join(__dirname, 'mappings.json') + commandMappings: path.join(__dirname, 'mappings.json'), + nativeModules: [ + 'serialport' + ] }; function envValue(varName, defaultValue) { @@ -191,10 +194,13 @@ settings.readProfileData = function() { var particleDir = settings.ensureFolder(); var proFile = path.join(particleDir, 'profile.json'); //proFile, get it? if (fs.existsSync(proFile)) { - var data = JSON.parse(fs.readFileSync(proFile)); - - settings.profile = (data) ? data.name : 'particle'; - settings.profile_json = data; + try { + var data = JSON.parse(fs.readFileSync(proFile)); + settings.profile = (data) ? data.name : 'particle'; + settings.profile_json = data; + } catch (err) { + throw new Error('Error parsing file '+proFile+': '+err); + } } else { settings.profile = 'particle'; settings.profile_json = {}; diff --git a/src/app/app.js b/src/app/app.js index 31a65bf60..dc40a0f10 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -7,6 +7,7 @@ import * as cliargs from './nested-yargs'; import commands from '../cli'; import * as settings from '../../settings'; import when from 'when'; +import chalk from 'chalk'; export class CLI { @@ -70,7 +71,7 @@ export class CLI { * @param {CLIRootCommandCategory} root The root command category to setup. */ setup(yargs, root) { - commands({root, factory: cliargs, app}); + commands({ root, factory: cliargs, app }); if (includeOldCommands) { app.addOldCommands(yargs); } @@ -203,17 +204,46 @@ export class CLI { cli.handle(args, true); } + hasArg(name, args) { + const index = args.indexOf(name); + if (index >= 0) { + args.splice(index, 1); + return true; + } + return false; + } + + loadNativeModules(modules) { + let errors = []; + for (let module of modules) { + try { + require(module); + } catch (err) { + errors.push(`Error loading module '${module}': ${err.message}`); + } + } + return errors; + } + run(args) { settings.transitionSparkProfiles(); settings.whichProfile(); settings.loadOverrides(); - const index = args.indexOf('--no-update-check'); - if (index >= 0) { - args.splice(index, 1); - settings.disableUpdateCheck = true; + const nativeErrors = this.loadNativeModules(settings.nativeModules); + if (nativeErrors.length) { + const log = require('./log'); + for (let error of nativeErrors) { + log.error(error); + } + log.fatal(`Please reinstall the CLI again using ${chalk.bold('npm install -g particle-cli')}`); + return; } - updateCheck(settings.disableUpdateCheck).then(() => { + + settings.disableUpdateCheck = this.hasArg('--no-update-check', args); + const force = this.hasArg('--force-update-check', args); + + updateCheck(settings.disableUpdateCheck, force).then(() => { const cmdargs = args.slice(2); // remove executable and script let promise; if (this.isNewCommand(cmdargs)) { diff --git a/src/app/nested-yargs.js b/src/app/nested-yargs.js index 7fd53408a..8d96a0af8 100644 --- a/src/app/nested-yargs.js +++ b/src/app/nested-yargs.js @@ -100,7 +100,7 @@ class CLICommandItem { * @param {function} version A function to retrieve the version * @param {string} epilogue Printed at the end of the command block. */ - configure(yargs, {options, setup, examples, version, epilogue}=this.buildOptions()) { + configure(yargs, { options, setup, examples, version, epilogue }=this.buildOptions()) { if (options) { this.fetchAliases(options); yargs.options(options); @@ -292,7 +292,7 @@ class CLICommandCategory extends CLICommandItem { // add the subcommands of this category _.forEach(this.commands, (command) => { const builder = (yargs) => { - return { argv: command.parse(args, yargs)}; + return { argv: command.parse(args, yargs) }; }; // const handler = (yargs) => { diff --git a/src/app/ui.js b/src/app/ui.js index f99aaaf6a..bc9de7454 100644 --- a/src/app/ui.js +++ b/src/app/ui.js @@ -55,10 +55,10 @@ function retry(fToRetry, times, handler, finalHandler) { handler(err); } return fToRetry.apply(fContext, args).catch(fAttempt); - }; + } return fAttempt(); }; -}; +} function render(templateName, data, supportingData) { if (global.outputJson) { diff --git a/src/app/update-check.js b/src/app/update-check.js index 3a821d954..db89c4206 100644 --- a/src/app/update-check.js +++ b/src/app/update-check.js @@ -10,7 +10,7 @@ function spin() { import info from '../../package'; import settings from '../../settings'; -function check(skip) { +function check(skip, force) { return when.promise((resolve) => { if (skip) { return resolve(); @@ -18,7 +18,7 @@ function check(skip) { const now = Date.now(); const lastCheck = settings.profile_json.last_version_check || 0; - if (now - lastCheck >= settings.updateCheckInterval) { + if ((now - lastCheck >= settings.updateCheckInterval) || force) { settings.profile_json.last_version_check = now; checkVersion().then(() => { settings.saveProfileData(); @@ -28,7 +28,6 @@ function check(skip) { return; } - start(); resolve(); }); } diff --git a/src/cli/echo.js b/src/cli/echo.js index 229c3a55b..082fc6006 100644 --- a/src/cli/echo.js +++ b/src/cli/echo.js @@ -2,7 +2,7 @@ * A simple command to echo the passed in options and parameters. */ -export default ({root, factory}) => { +export default ({ root, factory }) => { factory.createCommand(root, 'echo', false, { options: { f: { diff --git a/src/cli/help.js b/src/cli/help.js index 86fe0c1cc..916a0ee0b 100644 --- a/src/cli/help.js +++ b/src/cli/help.js @@ -1,5 +1,5 @@ -export default ({root, factory, app}) => { +export default ({ root, factory, app }) => { factory.createCommand(root, 'help', 'Provides extra details and options for a given command', { options: {}, params: '[command] [subcommand...]', diff --git a/src/cli/library.js b/src/cli/library.js index 81855016b..5c06aeefe 100644 --- a/src/cli/library.js +++ b/src/cli/library.js @@ -12,7 +12,7 @@ function api() { return api._instance; } -export default ({root, factory}) => { +export default ({ root, factory }) => { const lib = factory.createCategory(root, 'library', 'Manages firmware libraries', { alias: 'libraries' }); factory.createCommand(lib, 'add', 'Adds a library to the current project.', { diff --git a/src/cli/library_add.js b/src/cli/library_add.js index 188671b9a..3b191294e 100644 --- a/src/cli/library_add.js +++ b/src/cli/library_add.js @@ -1,8 +1,8 @@ -import {LibraryAddCommand, LibraryAddCommandSite} from '../cmd'; +import { LibraryAddCommand, LibraryAddCommandSite } from '../cmd'; import chalk from 'chalk'; import log from '../app/log'; -import {spin} from '../app/ui'; -import {buildAPIClient} from './apiclient'; +import { spin } from '../app/ui'; +import { buildAPIClient } from './apiclient'; class CLILibraryAddCommandSite extends LibraryAddCommandSite { constructor(argv, apiClient) { diff --git a/src/cli/library_delete.js b/src/cli/library_delete.js index 3d46266aa..9313d8f23 100644 --- a/src/cli/library_delete.js +++ b/src/cli/library_delete.js @@ -1,8 +1,8 @@ -import {LibraryDeleteCommandSite, LibraryDeleteCommand} from '../cmd'; -import {spin} from '../app/ui'; +import { LibraryDeleteCommandSite, LibraryDeleteCommand } from '../cmd'; +import { spin } from '../app/ui'; import log from '../app/log'; import chalk from 'chalk'; -import {buildAPIClient} from './apiclient'; +import { buildAPIClient } from './apiclient'; export class CLILibraryDeleteCommandSite extends LibraryDeleteCommandSite { diff --git a/src/cli/library_init.js b/src/cli/library_init.js index 0cfb3c7f5..24d1f75b7 100644 --- a/src/cli/library_init.js +++ b/src/cli/library_init.js @@ -1,4 +1,4 @@ -import {LibraryInitCommandSite, LibraryInitCommand} from '../cmd'; +import { LibraryInitCommandSite, LibraryInitCommand } from '../cmd'; const TerminalAdapter = require('yeoman-environment/lib/adapter.js'); diff --git a/src/cli/library_install.js b/src/cli/library_install.js index 6e540c772..bff1066dd 100644 --- a/src/cli/library_install.js +++ b/src/cli/library_install.js @@ -1,7 +1,7 @@ -import {LibraryInstallCommand, LibraryInstallCommandSite} from '../cmd'; -import {convertApiError} from '../cmd/api'; +import { LibraryInstallCommand, LibraryInstallCommandSite } from '../cmd'; +import { convertApiError } from '../cmd/api'; import chalk from 'chalk'; -import {buildAPIClient} from './apiclient'; +import { buildAPIClient } from './apiclient'; export class CLILibraryInstallCommandSite extends LibraryInstallCommandSite { diff --git a/src/cli/library_list.js b/src/cli/library_list.js index 705516a55..6f4e294ad 100644 --- a/src/cli/library_list.js +++ b/src/cli/library_list.js @@ -1,9 +1,9 @@ -import {LibraryListCommand, LibraryListCommandSite} from '../cmd'; -import {convertApiError} from '../cmd/api'; -import {spin} from '../app/ui'; -import {buildAPIClient} from './apiclient'; +import { LibraryListCommand, LibraryListCommandSite } from '../cmd'; +import { convertApiError } from '../cmd/api'; +import { spin } from '../app/ui'; +import { buildAPIClient } from './apiclient'; import chalk from 'chalk'; -import {formatLibrary} from './library_ui.js'; +import { formatLibrary } from './library_ui.js'; import prompt from '../../oldlib/prompts'; export class CLILibraryListCommandSite extends LibraryListCommandSite { @@ -73,10 +73,10 @@ export class CLILibraryListCommandSite extends LibraryListCommandSite { const result = {}; const sections = this.sectionNames(); for (let section of sections) { - result[section] = {page:1}; + result[section] = { page:1 }; } if (result.mine) { - result.mine.excludeBadges = {mine:true}; + result.mine.excludeBadges = { mine:true }; } return result; } diff --git a/src/cli/library_migrate.js b/src/cli/library_migrate.js index 9ab66cd8e..30ce0b1da 100644 --- a/src/cli/library_migrate.js +++ b/src/cli/library_migrate.js @@ -1,4 +1,4 @@ -import {LibraryMigrateCommandSite, LibraryMigrateTestCommand, LibraryMigrateCommand} from '../cmd'; +import { LibraryMigrateCommandSite, LibraryMigrateTestCommand, LibraryMigrateCommand } from '../cmd'; export class CLIBaseLibraryMigrateCommandSite extends LibraryMigrateCommandSite { constructor(argv, defaultDir) { @@ -21,7 +21,7 @@ export class CLIBaseLibraryMigrateCommandSite extends LibraryMigrateCommandSite } notifyEnd(lib, data, err) { - this.result = {lib, data, err}; + this.result = { lib, data, err }; } handleError(lib, err) { diff --git a/src/cli/library_publish.js b/src/cli/library_publish.js index 3a670cb79..bd2654ab7 100644 --- a/src/cli/library_publish.js +++ b/src/cli/library_publish.js @@ -1,8 +1,11 @@ -import {LibraryPublishCommand, LibraryPublishCommandSite} from '../cmd'; +import { LibraryPublishCommand, LibraryPublishCommandSite } from '../cmd'; +import { LibraryContributeCommand } from '../cmd'; + import chalk from 'chalk'; import log from '../app/log'; -import {spin} from '../app/ui'; -import {buildAPIClient} from './apiclient'; +import { spin } from '../app/ui'; +import { buildAPIClient } from './apiclient'; +import { CLILibraryContributeCommandSite } from './library_upload'; export class CLILibraryPublishCommandSite extends LibraryPublishCommandSite { @@ -38,8 +41,32 @@ export class CLILibraryPublishCommandSite extends LibraryPublishCommandSite { } } +class CLILibraryPublishContributeCommandSite extends CLILibraryContributeCommandSite { + + /** + * Saves the constributed library and doesn't output a contributed success message since + * the publish steps comes immediately afterwards - only want to print success when all steps + * are complete. + * @param {Library} library The library that was contributed. + */ + contributeComplete(library) { + this.contributedLibrary = library; + } +} + + export function command(apiJS, argv) { const site = new CLILibraryPublishCommandSite(argv, buildAPIClient(apiJS)); const cmd = new LibraryPublishCommand(); - return site.run(cmd); + let promise = Promise.resolve(); + if (!site.libraryIdent()) { + // no library name given - try publishing the current library + const contributeSite = new CLILibraryPublishContributeCommandSite(argv, process.cwd(), buildAPIClient(apiJS)); + // todo - set more stringent validation on the contribute command since this is pre-publish + const contribute = new LibraryContributeCommand(); + promise = contributeSite.run(contribute).then(() => { + site.ident = contributeSite.contributedLibrary.name; + }); + } + return promise.then(() => site.run(cmd)); } diff --git a/src/cli/library_search.js b/src/cli/library_search.js index 967faed44..69df4c7d8 100644 --- a/src/cli/library_search.js +++ b/src/cli/library_search.js @@ -1,9 +1,9 @@ -import {LibrarySearchCommandSite, LibrarySearchCommand} from '../cmd'; -import {spin} from '../app/ui'; +import { LibrarySearchCommandSite, LibrarySearchCommand } from '../cmd'; +import { spin } from '../app/ui'; import log from '../app/log'; import chalk from 'chalk'; -import {buildAPIClient} from './apiclient'; -import {formatLibrary} from './library_ui.js'; +import { buildAPIClient } from './apiclient'; +import { formatLibrary } from './library_ui.js'; export class CLILibrarySearchCommandSite extends LibrarySearchCommandSite { diff --git a/src/cli/library_upload.js b/src/cli/library_upload.js index 59b8d4e32..1efc9c840 100644 --- a/src/cli/library_upload.js +++ b/src/cli/library_upload.js @@ -1,9 +1,9 @@ -import {LibraryContributeCommand, LibraryContributeCommandSite} from '../cmd'; -import {convertApiError} from '../cmd/api'; +import { LibraryContributeCommand, LibraryContributeCommandSite } from '../cmd'; +import { convertApiError } from '../cmd/api'; import chalk from 'chalk'; import log from '../app/log'; -import {spin} from '../app/ui'; -import {buildAPIClient} from './apiclient'; +import { spin } from '../app/ui'; +import { buildAPIClient } from './apiclient'; export class CLILibraryContributeCommandSite extends LibraryContributeCommandSite { diff --git a/src/cli/library_view.js b/src/cli/library_view.js index a4bb1f10c..3d5e630f6 100644 --- a/src/cli/library_view.js +++ b/src/cli/library_view.js @@ -1,6 +1,6 @@ -import {CLILibraryInstallCommandSite} from './library_install'; -import {LibraryInstallCommand} from '../cmd'; -import {buildAPIClient} from './apiclient'; +import { CLILibraryInstallCommandSite } from './library_install'; +import { LibraryInstallCommand } from '../cmd'; +import { buildAPIClient } from './apiclient'; import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; @@ -64,7 +64,7 @@ class CLILibraryViewCommandSite extends CLILibraryInstallCommandSite { try { return fs.readFileSync(full, 'utf-8'); } catch (error) { - + return undefined; } } } diff --git a/src/cli/project.js b/src/cli/project.js index e63090c40..7deec1492 100644 --- a/src/cli/project.js +++ b/src/cli/project.js @@ -1,6 +1,6 @@ -export default ({root, factory}) => { +export default ({ root, factory }) => { const project = factory.createCategory(root, 'project', 'Manages application projects'); factory.createCommand(project, 'create', 'Create a new project in the current or specified directory.', { diff --git a/src/cli/project_init.js b/src/cli/project_init.js index a59975408..ecb0b1572 100644 --- a/src/cli/project_init.js +++ b/src/cli/project_init.js @@ -1,5 +1,5 @@ -import {ProjectInitCommand, ProjectInitCommandSite, Projects} from '../cmd'; -import {validateField} from 'particle-library-manager'; +import { ProjectInitCommand, ProjectInitCommandSite, Projects } from '../cmd'; +import { validateField } from 'particle-library-manager'; import path from 'path'; import log from '../app/log'; import chalk from 'chalk'; @@ -32,7 +32,7 @@ function yesNoValidator() { class CLIProjectInitCommandSite extends ProjectInitCommandSite { - constructor({name, directory}) { + constructor({ name, directory }) { super(); this._name = name; this._dir = directory; diff --git a/src/cmd/api.js b/src/cmd/api.js index 7e964c22f..c8e4f50d8 100644 --- a/src/cmd/api.js +++ b/src/cmd/api.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import url from 'url'; import chalk from 'chalk'; -export {convertApiError} from 'particle-commands'; +export { convertApiError } from 'particle-commands'; class UnauthorizedError extends Error { constructor(message) { diff --git a/test/cli/library_init.spec.js b/test/cli/library_init.spec.js index ed4bc418d..ed80b751e 100644 --- a/test/cli/library_init.spec.js +++ b/test/cli/library_init.spec.js @@ -25,6 +25,9 @@ import { LibraryInitCommand } from '../../src/cmd'; describe('library init command', () => { + require('yeoman-environment'); // ensure these dynamically loaded modules are loaded before the mock-fs is installed + require('yeoman-generator'); + describe('site', () => { const sut = new CLILibraryInitCommandSite({}); diff --git a/test/mocks/Serial.mock.js b/test/mocks/Serial.mock.js new file mode 100644 index 000000000..e673f67b2 --- /dev/null +++ b/test/mocks/Serial.mock.js @@ -0,0 +1,45 @@ +var extend = require('xtend'); + + +function MockSerial() { + var self = this; + self.open = false; + self.listeners = {}; + return extend(this, { + drain: function (next) { + next(); + }, + flush: function (next) { + next(); + }, + on: function(type, cb) { + self.listeners[type] = cb; + }, + respond: function (data) { + if (self.listeners.data) { + self.listeners.data(data); + } + }, + open: function (cb) { + self.open = true; + cb(); + }, + removeAllListeners(type) { + this.removeListener(type); + }, + removeListener(type) { + delete self.listeners[type]; + }, + isOpen: function() { + return self.open; + }, + close: function(cb) { + self.open = false; + if (cb) { + cb(); + } + } + }); +} + +module.exports = MockSerial; \ No newline at end of file diff --git a/test/oldcmd/KeyCommand.spec.js b/test/oldcmd/KeyCommand.spec.js index 5d57fdf1a..764ea7938 100644 --- a/test/oldcmd/KeyCommand.spec.js +++ b/test/oldcmd/KeyCommand.spec.js @@ -258,5 +258,14 @@ describe('Key Command', function() { }); }); + describe('keyAlgorithmForProtocol', function() { + it('returns rsa for TCP protocol', function() { + expect(key.keyAlgorithmForProtocol('tcp')).eql('rsa'); + }); + + it('returns ec for UDP protocol', function() { + expect(key.keyAlgorithmForProtocol('udp')).eql('ec'); + }); + }); }); diff --git a/test/oldcmd/SerialCommand.spec.js b/test/oldcmd/SerialCommand.spec.js index 81e7bfb70..f16103d48 100644 --- a/test/oldcmd/SerialCommand.spec.js +++ b/test/oldcmd/SerialCommand.spec.js @@ -1,8 +1,18 @@ 'use strict'; var proxyquire = require('proxyquire'); +var MockSerial = require('../mocks/Serial.mock') require('should'); +var sinon = require('sinon'); +var chai = require('chai'); +var sinonChai = require('sinon-chai'); +var chaiAsPromised = require('chai-as-promised'); +chai.use(chaiAsPromised); +chai.use(sinonChai); +var expect = chai.expect; + + var Interpreter = require('../../oldlib/interpreter'); var SerialCommand = proxyquire('../../commands/SerialCommand.js', { @@ -11,39 +21,85 @@ var SerialCommand = proxyquire('../../commands/SerialCommand.js', { describe('Serial Command', function() { - var serial; - + var cli, serial; before(function() { - var cli = new Interpreter(); + cli = new Interpreter(); cli.startup(); + }); + beforeEach(function () { serial = new SerialCommand(cli, { }); - }); it('Can list devices', function() { - serial.optionsByName['list'].should.be.an.instanceOf(Function); }); it('Can monitor a device', function() { - serial.optionsByName['monitor'].should.be.an.instanceOf(Function); }); it('Can identify a device', function() { - serial.optionsByName['identify'].should.be.an.instanceOf(Function); }); it('Can setup Wi-Fi over serial', function() { - serial.optionsByName['wifi'].should.be.an.instanceOf(Function); }); it('can retrieve mac address', function() { - serial.optionsByName['mac'].should.be.an.instanceOf(Function); }); + + describe('supportsClaimCode', function () { + it('can check if a device supports claiming', function () { + var device = { port: 'vintage' }; + var mockSerial = new MockSerial(); + mockSerial.write = function (data, cb) { + if (data==='c') { + mockSerial.respond('Device claimed: no'); + } + cb(); + }; + serial.serialPort = mockSerial; + return expect(serial.supportsClaimCode(device)).to.eventually.equal(true); + }); + + it('supports a device that does not recognise the claim command', function () { + var device = { port: 'vintage' }; + var mockSerial = new MockSerial(); + mockSerial.write = function (data, cb) { + cb(); + }; + serial.serialPort = mockSerial; + return expect(serial.supportsClaimCode(device)).to.eventually.equal(false); + }); + }); + + describe('sendClaimCode', function() { + it('can claim a device', function() { + var device = { port: 'shanghai' }; + var mockSerial = new MockSerial(); + var code = '1234'; + mockSerial.write = function (data, cb) { + if (data==='C') { + mockSerial.expectingClaimCode = true; + mockSerial.respond('Enter 63-digit claim code: '); + } + else if (this.expectingClaimCode) { + mockSerial.expectingClaimCode = false; + mockSerial.claimCodeSet = data.split('\n')[0]; + mockSerial.respond('Claim code set to: '+data); + } + cb(); + }; + serial.serialPort = mockSerial; + return serial.sendClaimCode(device, code, false). + then(function () { + expect(mockSerial.claimCodeSet).to.be.eql(code); + }); + }); + }); + }); diff --git a/test/oldcmd/WirelessCommand/connect/windows.spec.js b/test/oldcmd/WirelessCommand/connect/windows.spec.js new file mode 100644 index 000000000..6a537f0ec --- /dev/null +++ b/test/oldcmd/WirelessCommand/connect/windows.spec.js @@ -0,0 +1,477 @@ +'use strict'; + +const sinon = require('sinon'); +const chai = require('chai'); +const sinonChai = require('sinon-chai'); +const chaiAsPromised = require('chai-as-promised'); +const when = require('when'); + +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; +require('sinon-as-promised'); + +const connector = require('../../../../commands/WirelessCommand/connect/windows.js'); +var Connector = connector.Connector; + +describe('Windows wifi', function() { + var sut; + + beforeEach(function() { + // default sut has no executor + sut = new Connector(sinon.stub().throws('don\'t push me')); + }); + + describe('asCallback', function() { + it('handles success', function() { + var value = 123; + + function handler(err, data) { + expect(data).to.be.eql(value); + expect(err).to.be.not.ok; + } + return connector.asCallback(Promise.resolve(value), handler); + }); + + it('handles rejection', function() { + var rejection = 'My hat is too big'; + + function handler(err, data) { + expect(err).to.be.eql(rejection); + expect(data).to.be.undefined; + } + return connector.asCallback(Promise.reject(rejection), handler); + }); + }); + + describe('_exec', function() { + it('invokes the command executor', function() { + var executor = sinon.stub().returns(Promise.resolve(123)); + var sut = new Connector(executor); + var args = ['a', 'b', 'c']; + return sut._exec(args).then(function() { + expect(executor).to.have.been.calledWith(args); + }); + }); + }); + + describe('_execWiFiCommand', function() { + it('invokes the command executor with a "netsh wlan" prefix', function() { + var executor = sinon.stub().returns(Promise.resolve(123)); + var sut = new Connector(executor); + var args = ['a', 'b', 'c']; + return sut._execWiFiCommand(args).then(function() { + expect(executor).to.have.been.calledWith(['netsh', 'wlan', 'a', 'b', 'c']); + }); + }); + }); + + describe('parsing', function () { + describe('_stringToLines', function() { + it('converts all kinds of line endings', function() { + var s = `one\ntwo\r\nthree\r\n\n\n\n`; + var lines = sut._stringToLines(s); + expect(lines).to.be.eql(['one', 'two', 'three']); + }); + + it('returns the empty array when there are no lines', function() { + expect(sut._stringToLines('')).to.be.eql([]); + }); + + it('returns a single line', function() { + expect(sut._stringToLines('abcd\n')).to.be.eql(['abcd']); + }); + + }); + + describe('_keyValue', function() { + it('returns undefined if no colon', function() { + var result = sut._keyValue('key value'); + expect(result).to.be.eql(undefined); + }); + + it('splits at the first colon', function() { + var result = sut._keyValue('key space: value : value 2'); + expect(result).to.have.property('key').eql('key space'); + expect(result).to.have.property('value').eql('value : value 2'); + }); + + it('returns the key lowercased, and value in original case', function() { + var result = sut._keyValue('KeY : MY VALUE'); + expect(result).to.have.property('key').eql('key'); + expect(result).to.have.property('value').eql('MY VALUE'); + }); + + it('trims external whitespace', function() { + var result = sut._keyValue('KeY : MY VALUE '); + expect(result).to.have.property('key').eql('key'); + expect(result).to.have.property('value').eql('MY VALUE'); + }); + }); + + describe('_extractInterface', function() { + it('ignores properties up to the first name', function() { + var lines = ` + blah: blah + Name: bob + Favorite food: worms + `.split('\n'); + + var data = sut._extractInterface(lines); + expect(data).to.be.ok; + expect(data.range).to.eql({start:2, end:5}); + expect(data.iface).to.not.have.property('blah'); + expect(data.iface).to.have.property('name').eql('bob'); + expect(data.iface).to.have.property('favorite food').eql('worms'); + }); + + it('gathers properties up to the next name from the start', function() { + var lines = ` + blah: blah + Name: bob + Favorite food: worms + pet: dogs + + name: joe + height: 1234 + `.split('\n'); + + var data = sut._extractInterface(lines); + expect(data).to.be.ok; + expect(data.range).to.eql({start:2, end:6}); + expect(data.iface).to.not.have.property('blah'); + expect(data.iface).to.have.property('name').eql('bob'); + expect(data.iface).to.have.property('favorite food').eql('worms'); + expect(data.iface).to.have.property('pet').eql('dogs'); + expect(data.iface).to.not.have.property('height'); + }); + + + it('gathers properties up to the next name from the index given', function() { + var lines = ` + blah: blah + Name: bob + Favorite food: worms + pet: dogs + + name: joe + height: 1234 + + + `.split('\n'); + + var data = sut._extractInterface(lines, 6); + expect(data).to.be.ok; + expect(data.range).to.eql({start:6, end:11}); + expect(data.iface).to.not.have.property('blah'); + expect(data.iface).to.have.property('name').eql('joe'); + expect(data.iface).to.not.have.property('favorite food'); + expect(data.iface).to.not.have.property('pet'); + expect(data.iface).to.have.property('height').eql('1234'); + }); + }); + + describe('_currentFromInterfaces', function() { + it('retrieves the first interface with a profile', function() { + var lines = ` + blah: blah + Name: bob + + name: joe + Profile: beer palace + + name: kim + Profile: 1234 + pet: dog + `.split('\n'); + + var iface = sut._currentFromInterfaces(lines); + expect(iface).to.be.ok; + expect(iface).to.not.have.property('blah'); + expect(iface).to.not.have.property('pet'); + expect(iface).to.have.property('name').eql('joe'); + expect(iface).to.have.property('profile').eql('beer palace'); + }) + }); + }); + + describe('currentInterface', function() { + function assertCurrent(response, current) { + var cmd = 'netsh wlan show interfaces'.split(' '); + var executor = sinon.stub().returns(Promise.resolve(response)); + var sut = new Connector(executor); + return sut.currentInterface().then((result) => { + expect(result).to.eql(current); + expect(executor).to.have.been.calledWith(cmd); + }); + } + + it('returns null when there are no interfaces', function () { + var response = 'There is 0 interface on the system'; + return assertCurrent(response, null); + }); + + it('returns null when the interface is not connected', function () { + var response = `There is 1 interface on the system: + + Name : WiFi + Description : D-Link DWA-132 Wireless N USB Adapter(rev.B) + GUID : b023475e-7b92-4714-9cb2-0d15bc7c182b + Physical address : 78:54:2e:df:1b:01 + State : disconnected + Radio status : Hardware On + Software On + + Hosted network status : Not available`; + + return assertCurrent(response, null); + }); + + it('returns the 2nd interface when the first interface is disconnected', function () { + const response = ` + Name : no more Mr Wi-Fi + State : disconnected + Name : no more Mr Wi-Fi 2 + State : connected + Profile : profileName`; + return assertCurrent(response, {'name' : 'no more Mr Wi-Fi 2', 'state' : 'connected', 'profile' : 'profileName' }); + }); + + it('returns the profile name of the first interface when an interface is not specified', function () { + const response = ` + Name : no more Mr Wi-Fi + State : connected + Profile : profile name 1 + Name : no more Mr Wi-Fi 2 + State : connected + Profile : profile name 2`; + return assertCurrent(response, {'name' : 'no more Mr Wi-Fi', 'state' : 'connected', 'profile' : 'profile name 1' }); + }); + + // todo - allow the interface name to be specified + }); + + describe('current', function() { + it('returns the current profile when defined', function() { + sut.currentInterface = sinon.stub().resolves({name:'beer', profile:'Beer'}); + expect(sut.current()).to.eventually.eql('Beer'); + }); + + it('returns undefined when no current network interface', function() { + sut.currentInterface = sinon.stub().resolves({}); + expect(sut.current()).to.eventually.eql(undefined); + }); + }); + + describe('_buildProfile', function() { + it('builds a profile with the name and ssid equal', function() { + var expected = ` + + Photon-8QNP + + + Photon-8QNP + + + ESS + manual + + + + open + none + false + + + + `.replace(/\s+/g, ' '); + var name = 'Photon-8QNP'; + + var result = new Connector()._buildProfile(name); + expect(result).to.be.equal(expected); + }); + }); + + describe('connect', function() { + var interfaceName = 'blah'; + var profile = 'foo'; + + it('invokes a pipeline of functions', function() { + var profiles = [ 'a', 'b', profile ]; + sut.currentInterface = sinon.stub().resolves(interfaceName); + sut._checkHasInterface = sinon.stub().resolves(interfaceName); + sut.listProfiles = sinon.stub().resolves(profiles); + sut._createProfileIfNeeded = sinon.stub().resolves(profile); + sut._connectProfile = sinon.stub().resolves('ok'); + + return sut.connect(profile) + .then(function() { + expect(sut.currentInterface).to.have.been.calledOnce; + expect(sut._checkHasInterface).to.have.been.calledWith(interfaceName); + expect(sut.listProfiles).to.have.been.calledWith(interfaceName); + expect(sut._createProfileIfNeeded).to.have.been.calledWith(profile, interfaceName, profiles); + expect(sut._connectProfile).to.have.been.calledWith(profile, interfaceName); + }); + }); + + it('creates a new profile for the given interface if it does not exist', function() { + sut._execWiFiCommand = sinon.stub(); + var profiles = []; + sut.currentInterface = sinon.stub().resolves(interfaceName); + sut._checkHasInterface = sinon.stub().resolves(interfaceName); + sut.listProfiles = sinon.stub().resolves(profiles); + sut._createProfile = sinon.stub().resolves(); + sut._connectProfile = sinon.stub().resolves('ok'); + return sut.connect(profile) + .then(function() { + expect(sut._createProfile).to.have.been.calledWith(profile, interfaceName); + expect(sut._connectProfile).to.have.been.calledWith(profile, interfaceName); + }); + }); + + it('connects to the network when a profile already exists', function() { + sut._execWiFiCommand = sinon.stub(); + var profiles = [profile]; + sut.currentInterface = sinon.stub().resolves(interfaceName); + sut._checkHasInterface = sinon.stub().resolves(interfaceName); + sut.listProfiles = sinon.stub().resolves(profiles); + sut._createProfile = sinon.stub().resolves(); + sut._connectProfile = sinon.stub().resolves('ok'); + return sut.connect(profile) + .then(function() { + expect(sut._createProfile).to.not.have.been.called; + expect(sut._connectProfile).to.have.been.calledWith(profile, interfaceName); + }); + }); + }); + + describe('_connectProfile', function() { + it('runs netsh wlan connect', function() { + sut._execWiFiCommand = sinon.stub().resolves(''); + var profile = 'blah'; + var iface = 'may contain spaces'; + sut._connectProfile(profile, iface); + expect(sut._execWiFiCommand).to.be.calledWith(['connect', 'name=blah', 'interface=may contain spaces']); + }); + }); + + describe('_createProfileIfNeeded', function() { + it('skips creation when it already exists and returns the profile name', function() { + sut._createProfile = sinon.stub(); + var profile = 'blah', iface = 'foo', profiles = ['a', profile]; + var result = sut._createProfileIfNeeded(profile, iface, profiles); + expect(result).to.eql(profile); + expect(sut._createProfile).to.not.have.been.called; + }); + + it('creates the profile when it does not exist and returns the created profile', function() { + sut._createProfile = sinon.stub().returns(123); + var profile = 'blah', iface = 'foo', profiles = ['a']; + var result = sut._createProfileIfNeeded(profile, iface, profiles); + expect(result).to.eql(123); + expect(sut._createProfile).to.have.been.calledWith(profile, iface); + }); + }); + + describe('_profileExists', function() { + it('returns false when the profile does not exist', function() { + expect(sut._profileExists('abcd', ['blah', 'foo'])).to.be.eql(false); + }); + + it('returns true when the profile does exist', function() { + expect(sut._profileExists('abcd', ['blah', 'abcd', 'foo'])).to.be.eql(true); + }); + }); + + describe('_createProfile', function() { + var fs; + var profile = 'myprofile'; + var profileContent = 'blah'; + var filename = "_wifi_profile.xml"; + var response = "Profile blah is added on interface Some Interface"; + beforeEach(function() { + fs = { + writeFileSync: sinon.stub(), + unlinkSync: sinon.stub() + }; + }); + + it('writes the profile to disk and runs metsh wlan add profile', function() { + sut._execWiFiCommand = sinon.stub().resolves(response); + sut._buildProfile = sinon.stub().returns(profileContent); + return sut._createProfile(profile, undefined, fs).then(function() { + expect(sut._buildProfile).to.have.been.calledWith(profile); + expect(fs.writeFileSync).to.have.been.calledWith(filename, profileContent); + expect(sut._execWiFiCommand).to.have.been.calledWith(['add', 'profile', 'filename=_wifi_profile.xml']); + expect(fs.unlinkSync).to.have.been.calledWith(filename); + }); + }); + + it('propagates errors from the wifi command', function() { + sut._execWiFiCommand = sinon.stub().rejects(1); + return expect(sut._createProfile(profile, undefined, fs)).to.eventually.be.rejected; + }); + + it('unlinks the file when an error occurs', function() { + var error = Error('it is tuesday'); + var errorRaised = false; + sut._execWiFiCommand = sinon.stub().rejects(error); + sut._buildProfile = sinon.stub().returns(profileContent); + return sut._createProfile(profile, undefined, fs) + .catch(function(err) { + expect(err).to.eql(error); + errorRaised = true; + }) + .then(function() { + expect(sut._buildProfile).to.have.been.calledWith(profile); + expect(fs.writeFileSync).to.have.been.calledWith(filename, profileContent); + expect(sut._execWiFiCommand).to.have.been.calledWith(['add', 'profile', 'filename=_wifi_profile.xml']); + expect(fs.unlinkSync).to.have.been.calledWith(filename); + expect(errorRaised).to.be.eql(true); + }); + }); + + it('adds the interface to the command when specified', function() { + sut._execWiFiCommand = sinon.stub().resolves(); + sut._buildProfile = sinon.stub().returns(profileContent); + const ifaceName = 'myface'; + return sut._createProfile(profile, ifaceName, fs).then(function() { + expect(sut._buildProfile).to.have.been.calledWith(profile); + expect(fs.writeFileSync).to.have.been.calledWith(filename, profileContent); + expect(sut._execWiFiCommand).to.have.been.calledWith(['add', 'profile', 'filename=_wifi_profile.xml', 'interface='+ifaceName]); + expect(fs.unlinkSync).to.have.been.calledWith(filename); + }); + }); + }); + + describe('listProfiles', function() { + var list = 'profiles for interface:\nuser profile: profile 1\nuser profile: profile 2'; + var noProfiles = 'no profiles for interface'; + + it('calls show profiles interface=ifaceName when an interface is specified', function() { + sut._execWiFiCommand = sinon.stub().resolves(''); + return sut.listProfiles("abcd"). + then(function () { + expect(sut._execWiFiCommand).to.have.been.calledWith(['show', 'profiles', 'interface=abcd']); + }); + }); + + it('calls show profiles when no interface is specified', function() { + sut._execWiFiCommand = sinon.stub().resolves(''); + return sut.listProfiles(). + then(function () { + expect(sut._execWiFiCommand).to.have.been.calledWith(['show', 'profiles']); + }); + }); + + it('it parses the profiles', function() { + sut._execWiFiCommand = sinon.stub().resolves(list); + return sut.listProfiles(). + then(function (profiles) { + expect(profiles).to.eql(['profile 1', 'profile 2']); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/oldlib/SerialBoredParser.spec.js b/test/oldlib/SerialBoredParser.spec.js new file mode 100644 index 000000000..1973245d3 --- /dev/null +++ b/test/oldlib/SerialBoredParser.spec.js @@ -0,0 +1,80 @@ + +var MockSerial = require('../mocks/Serial.mock'); +var proxyquire = require('proxyquire'); +var sinon = require('sinon'); +var chai = require('chai'); +var sinonChai = require('sinon-chai'); +var chaiAsPromised = require('chai-as-promised'); +chai.use(chaiAsPromised); +chai.use(sinonChai); +var expect = chai.expect; + +var serialBoredParser = require('../../oldlib/SerialBoredParser'); + +describe('SerialBoredParser', function() { + var setTimeout, clearTimeout; + + beforeEach(function () { + setTimeout = sinon.stub(); + clearTimeout = sinon.stub(); + serialBoredParser.setTimeoutFunctions(setTimeout, clearTimeout); + }); + + describe('terminator', function() { + it('returns before the timeout when the terminator is seen.', function() { + var terminator = 'arnie'; + var timeout = 5000; + var sut = serialBoredParser.makeParser(timeout, terminator); + var timer = 'timer'; + var emitter = { emit: sinon.stub() }; + + setTimeout.returns(timer); + sut(emitter, 'abc'); + expect(clearTimeout).to.have.been.calledWith(undefined); + + expect(setTimeout).has.been.calledWith(sinon.match.func, timeout); + expect(emitter.emit).to.have.not.been.called; + + setTimeout.reset(); + clearTimeout.reset(); + + sut(emitter, 'def'+terminator); + expect(emitter.emit).has.been.calledWith('data', 'abcdef'+terminator); + expect(setTimeout).to.have.not.been.called; + expect(clearTimeout).to.have.been.calledWith(timer); + }); + + it('waits for the timeout when the terminator is not seen.', function() { + var timeout = 50; + var sut = serialBoredParser.makeParser(timeout); + var terminator = 'arnie'; + var timer = 'timer'; + var emitter = { emit: sinon.stub() }; + + setTimeout.returns(timer); + sut(emitter, 'abc'); + expect(clearTimeout).to.have.been.calledWith(undefined); + expect(setTimeout).has.been.calledWith(sinon.match.func, timeout); + expect(emitter.emit).to.have.not.been.called; + + setTimeout.reset(); + clearTimeout.reset(); + + sut(emitter, 'def'); + expect(emitter.emit).to.have.not.been.called; + expect(clearTimeout).to.have.been.calledWith(timer); + expect(setTimeout).has.been.calledWith(sinon.match.func, timeout); + + // now emulate the passage of time by calling the function passed to setTimeout + var timeoutFunction = setTimeout.args[0][0]; + setTimeout.reset(); + clearTimeout.reset(); + + timeoutFunction(); + expect(emitter.emit).has.been.calledWith('data', 'abcdef'); + expect(clearTimeout).to.have.not.been.called; + expect(setTimeout).has.not.been.called; + }); + + }); +}); \ No newline at end of file