From 5b740e44795e308f97a4fd0809522b9f23d71b59 Mon Sep 17 00:00:00 2001 From: Student007 Date: Fri, 31 Jul 2015 10:54:13 +0200 Subject: [PATCH 01/43] Travis containers added http://docs.travis-ci.com/user/migrating-from-legacy/?utm_source=legacy-notice&utm_medium=banner&utm_campaign=legacy-upgrade --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e57a8742..2e70c741 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,4 @@ before_install: - sudo apt-get install pkg-config build-essential libusb-1.0-0-dev libudev-dev install: npm install script: grunt +sudo: false From ec33032f774c5c0d15d6f639d2e0468061f0cc67 Mon Sep 17 00:00:00 2001 From: Student007 Date: Fri, 31 Jul 2015 11:00:12 +0200 Subject: [PATCH 02/43] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2e70c741..57e1a01e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ before_install: - sudo apt-get install pkg-config build-essential libusb-1.0-0-dev libudev-dev install: npm install script: grunt -sudo: false + From e6a8b584ca8cd7b74eb82e1e5cfa2c8a70935552 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:04:53 +0200 Subject: [PATCH 03/43] Travis-CI containers http://docs.travis-ci.com/user/migrating-from-legacy/?utm_source=legacy- notice&utm_medium=banner&utm_campaign=legacy-upgrade --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57e1a01e..9d34d24e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ before_install: - sudo apt-get install pkg-config build-essential libusb-1.0-0-dev libudev-dev install: npm install script: grunt - +sudo: false \ No newline at end of file From fe72e82e8ebe8ca1e30e9888a9d723d4fba09e25 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:09:13 +0200 Subject: [PATCH 04/43] apt-get fix for Travis-CI like described here: http://docs.travis-ci.com/user/migrating-from-legacy/?utm_source=legacy- notice&utm_medium=banner&utm_campaign=legacy-upgrade --- .travis.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9d34d24e..77935daf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,13 @@ node_js: - "0.12.0" before_install: - npm install -g grunt-cli - - sudo apt-get update -qq - - sudo apt-get install pkg-config build-essential libusb-1.0-0-dev libudev-dev +addons: + apt: + packages: + - pkg-config + - build-essential + - libusb-1.0-0-dev + - libudev-dev install: npm install script: grunt sudo: false \ No newline at end of file From 32611c0be9bc742a18d4fa8c4a02ea0513b8121c Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:13:46 +0200 Subject: [PATCH 05/43] docs confuse me --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 77935daf..875695cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,3 @@ addons: - libudev-dev install: npm install script: grunt -sudo: false \ No newline at end of file From 7aaf6adef807ce40ef4a5bd87f66b899bd5065a8 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:14:49 +0200 Subject: [PATCH 06/43] ... --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 875695cf..77935daf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,3 +12,4 @@ addons: - libudev-dev install: npm install script: grunt +sudo: false \ No newline at end of file From 4d82d6e1c286c1c6191cfbcb6a2268958e71c469 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:24:25 +0200 Subject: [PATCH 07/43] .. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 77935daf..230bef5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ addons: - libudev-dev install: npm install script: grunt -sudo: false \ No newline at end of file +sudo: false \ No newline at end of file From c592d9c3c58ac13bc42fc31116f6c775bbbb09fa Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:25:15 +0200 Subject: [PATCH 08/43] ,, --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 230bef5a..76285d18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: node_js node_js: - "0.12.0" @@ -11,5 +12,4 @@ addons: - libusb-1.0-0-dev - libudev-dev install: npm install -script: grunt -sudo: false \ No newline at end of file +script: grunt \ No newline at end of file From 61b104285d05d19371dbfb670a8b83e8c4e785cb Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:25:47 +0200 Subject: [PATCH 09/43] m --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 76285d18..436a729b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: false language: node_js node_js: - "0.12.0" From 70c80029707f3f79105cbfe6fbc26719cd61cc09 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:27:53 +0200 Subject: [PATCH 10/43] a --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 436a729b..925b7c0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,7 @@ node_js: - "0.12.0" before_install: - npm install -g grunt-cli -addons: - apt: - packages: +addons.apt.packages: - pkg-config - build-essential - libusb-1.0-0-dev From 4391bdd7aae13bbb3f739157494224fc54119b4a Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:29:54 +0200 Subject: [PATCH 11/43] a --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 925b7c0b..dd6f34d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ node_js: before_install: - npm install -g grunt-cli addons.apt.packages: - - pkg-config - build-essential - libusb-1.0-0-dev - libudev-dev From f262fc850ed0cf33aee9bb52b7543a2cef4e4269 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:31:28 +0200 Subject: [PATCH 12/43] s --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index dd6f34d4..925b7c0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ node_js: before_install: - npm install -g grunt-cli addons.apt.packages: + - pkg-config - build-essential - libusb-1.0-0-dev - libudev-dev From ef36e92d152bed323a477ba6c53028d0bebbfbb6 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:37:42 +0200 Subject: [PATCH 13/43] wrappy --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ef1763cb..e817f8d7 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "ssh2": "^0.4.2", "tessel": "git+https://github.com/tessel/cli#jon-export-v2", "usb": "^1.0.5", - "usb-daemon-parser": "0.0.1" + "usb-daemon-parser": "0.0.1", + "wrappy":">=1.0.1" }, "preferGlobal": true } From 9c1e15d660763fda84ba6c601441d02a1462c22e Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 11:44:15 +0200 Subject: [PATCH 14/43] s --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 925b7c0b..bccb0237 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: node_js node_js: - "0.12.0" From 2c0c9829015ea4efaad3d6583215b22cff8120d7 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 12:14:16 +0200 Subject: [PATCH 15/43] q --- .travis.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index bccb0237..81be87c3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,12 @@ sudo: false -language: node_js -node_js: - - "0.12.0" -before_install: - - npm install -g grunt-cli addons.apt.packages: - pkg-config - build-essential - libusb-1.0-0-dev - - libudev-dev + - libudev-devlanguage: node_js +node_js: + - "0.12.0" +before_install: + - npm install -g grunt-cli install: npm install script: grunt \ No newline at end of file From 95a71da6f14bb35d7d2dedf27fbeaa4d18807a6c Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 12:15:17 +0200 Subject: [PATCH 16/43] require sudo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 81be87c3..03f3bb36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -sudo: false +sudo: required addons.apt.packages: - pkg-config - build-essential From e680cbcca88415f6644b227326e56a22a36224ac Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 12:18:37 +0200 Subject: [PATCH 17/43] newline --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 03f3bb36..28893ab0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ addons.apt.packages: - pkg-config - build-essential - libusb-1.0-0-dev - - libudev-devlanguage: node_js + - libudev-dev +language: node_js node_js: - "0.12.0" before_install: From e8dc023009db7c74dd751163f67f65f799b3392e Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 12:26:30 +0200 Subject: [PATCH 18/43] reset to initial version --- .travis.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28893ab0..e57a8742 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,9 @@ -sudo: required -addons.apt.packages: - - pkg-config - - build-essential - - libusb-1.0-0-dev - - libudev-dev language: node_js node_js: - "0.12.0" before_install: - npm install -g grunt-cli + - sudo apt-get update -qq + - sudo apt-get install pkg-config build-essential libusb-1.0-0-dev libudev-dev install: npm install -script: grunt \ No newline at end of file +script: grunt From c76a849dd0a17d2df276ec655c6248f043452101 Mon Sep 17 00:00:00 2001 From: Student007 Date: Fri, 31 Jul 2015 12:30:50 +0200 Subject: [PATCH 19/43] Travis-CI Image to Student007 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 660f259a..62a079e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Join the [conversation on Slack](https://tessel-slack.herokuapp.com/), our proje [![Slack](http://tessel-slack.herokuapp.com/badge.svg)](https://tessel-slack.herokuapp.com/) -[![Build Status](https://travis-ci.org/tessel/t2-cli.svg?branch=master)](https://travis-ci.org/tessel/t2-cli) +[![Build Status](https://travis-ci.org/Student007/t2-cli.svg?branch=master)](https://travis-ci.org/student007/t2-cli) ## Installation Prerequisites for installation: [Node.js](https://nodejs.org/) and [Git](https://git-scm.com/downloads). From 9860c3d441a4849bacb5fdb6aa1b32203122b7b0 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 12:43:26 +0200 Subject: [PATCH 20/43] Revert "Travis-CI Image to Student007" This reverts commit c76a849dd0a17d2df276ec655c6248f043452101. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62a079e2..660f259a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Join the [conversation on Slack](https://tessel-slack.herokuapp.com/), our proje [![Slack](http://tessel-slack.herokuapp.com/badge.svg)](https://tessel-slack.herokuapp.com/) -[![Build Status](https://travis-ci.org/Student007/t2-cli.svg?branch=master)](https://travis-ci.org/student007/t2-cli) +[![Build Status](https://travis-ci.org/tessel/t2-cli.svg?branch=master)](https://travis-ci.org/tessel/t2-cli) ## Installation Prerequisites for installation: [Node.js](https://nodejs.org/) and [Git](https://git-scm.com/downloads). From 71a282093454c81915755a031efe0a0439d5a408 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 18:28:53 +0200 Subject: [PATCH 21/43] first clean version of "t2 root" but without tests and only tested on MacOSX. --- bin/tessel-2.js | 30 +++++++++ lib/controller.js | 159 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 964c7ad8..e5b39116 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -197,6 +197,36 @@ parser.command('rename') .help('Change the name of a Tessel to something new.'); +// accessing the root shell of your tessels +// Fixes issue https://github.com/tessel/t2-cli/issues/80 +/** +$ t2 root --help +> Usage: tessel root [-i ] [--help] +> -i : provide a path to the desired ssh key +$ + +$ t2 root +> Accessing root... +root@192.168.128.124 # +*/ +var default_id_rsa = '~/.tessel/id_rsa'; +var functional_msg = '\nGain SSH root access to one of your authorized tessels (menu listing if multiple targets)'; +parser.command('root') + .usage(functional_msg+"\n\nUsage: t2 root [-i PATH] [--help]\n\n-i PATH: Optional targeting a different Private Key \n\n(Note: default target created by \"t2 key generate\" is "+default_id_rsa+")\n") + .option('path',{ + abbr: 'i', + full: 'path', + metavar: 'PATH', + default: default_id_rsa, + help: 'Private Key (Note: created by "t2 key generate")' + }) + .callback(function(opts){ + controller.root(opts) + .then(closeSuccessfulCommand,closeFailedCommand); + }) + .help(functional_msg); + + module.exports = function(args) { parser.parse(args); }; diff --git a/lib/controller.js b/lib/controller.js index 3a7f2a21..356dd381 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -7,6 +7,7 @@ var sprintf = require('sprintf-js').sprintf; var cp = require('child_process'); var async = require('async'); var controller = {}; +var Menu = require('terminal-menu'); Tessel.list = function(opts) { @@ -171,6 +172,164 @@ Tessel.get = function(opts) { }); }; +Tessel.seekSelected = function(opts){ + return new Promise(function(resolve, reject) { + + if (opts.timeout && typeof opts.timeout === 'number') { + // Stop the search after that duration + setTimeout(stopSeeker, opts.timeout * 1000); + } else { + // default to 3 seconds searching + setTimeout(stopSeeker, 3 * 1000); + } + console.log("Searching accessable Tessels ...") + + var seeker = new discover.TesselSeeker().start(); + var tessels = []; + + // When we find Tessels + seeker.on('tessel', function(tessel) { + controller.closeTesselConnections(tessel) + .then(function() { + tessels.push(tessel); + }); + }); + + function stopSeeker(){ + try{ + seeker.stop(); + seeker = null; // preventing memory leaks + return resolve(tessels); + } catch(e){ + throw e; + } + } + }); +} + +controller.root = function(opts){ + // ~ conversion to home because spawn isn't able to handle this right + if(opts.path.substring(0,1) === '~'){ + var home = process.env.HOME; + opts.path = opts.path.replace('~', home); + } + return new Promise(function(resolve, reject) { + Tessel.seekSelected(opts) + .then(function(tessels){ + + if(tessels.length >= 2){ + tessels.push({ + connection: { + connectionType: "exit" + } + }); + // TODO: Tessel cooperate identity conform menu required (color, ascii logo) + var rtm = Menu({ + width: 50, + x: 1, + y: 2, + bg: 'red' + }); + rtm.reset(); + rtm.write(' \n'); + tessels.forEach(makeMenu); + // Menu navigation + process.stdin.pipe(rtm.createStream()).pipe(process.stdout); + process.stdin.setRawMode(true); + rtm.on('close', function () { + process.stdin.setRawMode(false); + process.stdin.end(); + }); + rtm.on('select', function (label) { + rtm.close(); + + if(label[0] !== "E"){ + runSSH(label[0]); + } else { + // selected exit! + process.stdout.write("\u001b[2J\u001b[0;0H"); + process.exit(); + } + + }); + } else if(tessels.length === 1) { + if(tessels[0].connection.authorized){ + runSSH(0); + } else { + console.log('Sorry, you are not authorized!\n\n') + console.log('"t2 key generate" might help :-)'); + process.exit(); + } + + } else { + console.log('Sorry no nearby Tessels found!'); + process.exit(); + } + function runSSH(id){ + // clear console + process.stdout.write("\u001b[2J\u001b[0;0H"); + var ch = require('child_process') + .spawn('ssh', + ['-i', + opts.path, + 'root@'+tessels[id].connections[0].host], + { + stdio: 'inherit' + }); + console.log('Connect to '+tessels[id].connections[0].host+'...'); + + ch.on('error', function(e){ + if(e.includes("Permission denied")){ + console.log('Sorry, you are not authorized!\n\n'+e+'\n'); + console.log('"t2 key generate" might help :-)'); + + process.kill(); + + }else{ + throw e; + + process.kill(); + } + + }); + ch.on('exit',function(e,signal){ + if(e === 0){ + // clear console + process.stdout.write("\u001b[2J\u001b[0;0H"); + console.log('Connection to '+tessels[id].connections[0].host+' closed!\n'); + } + if(e === 255){ + console.log('Connection to '+tessels[id].connections[0].host+' closed!\n'); + } + + process.exit(); + }); + }; + + function makeMenu(tessel, index, array){ + if(tessel.connection.connectionType === 'LAN'){ + + if(!tessel.connection.authorized){ + rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (not authorized) \n'); + } else { + rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (authorized) \n'); + } + + } else if(tessel.connection.connectionType === 'USB'){ + // TODO: Because of no real Tessel 2 present - not implemented now + if(!tessel.connection.authorized){ + rtm.add(index+') '+tessel.name+': [USB] (not authorized) \n'); + } else { + rtm.add(index+') '+tessel.name+': [USB] (authorized) \n'); + } + } else { + rtm.add('EXIT'); + } + } + }); + }); +} + /* Takes a list of Tessels with connections that may or may not be open and closes them diff --git a/package.json b/package.json index e817f8d7..cc342b25 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "tessel": "git+https://github.com/tessel/cli#jon-export-v2", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1", - "wrappy":">=1.0.1" + "wrappy":">=1.0.1", + "terminal-menu": "^2.1.1" }, "preferGlobal": true } From 24d312045c6799aa5963dfbb57b829b7362adc6d Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 18:35:27 +0200 Subject: [PATCH 22/43] stupid mindless mistake fixed --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc342b25..68b2af2f 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "tessel": "git+https://github.com/tessel/cli#jon-export-v2", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1", - "wrappy":">=1.0.1", + "wrappy":"1.0.1", "terminal-menu": "^2.1.1" }, "preferGlobal": true From f69d51a0eb3df3ef912dbf40fd4c30362fcf6629 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 18:40:52 +0200 Subject: [PATCH 23/43] travis test fails for linting --- bin/tessel-2.js | 30 --------- lib/controller.js | 159 ---------------------------------------------- package.json | 4 ++ 3 files changed, 4 insertions(+), 189 deletions(-) diff --git a/bin/tessel-2.js b/bin/tessel-2.js index e5b39116..964c7ad8 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -197,36 +197,6 @@ parser.command('rename') .help('Change the name of a Tessel to something new.'); -// accessing the root shell of your tessels -// Fixes issue https://github.com/tessel/t2-cli/issues/80 -/** -$ t2 root --help -> Usage: tessel root [-i ] [--help] -> -i : provide a path to the desired ssh key -$ - -$ t2 root -> Accessing root... -root@192.168.128.124 # -*/ -var default_id_rsa = '~/.tessel/id_rsa'; -var functional_msg = '\nGain SSH root access to one of your authorized tessels (menu listing if multiple targets)'; -parser.command('root') - .usage(functional_msg+"\n\nUsage: t2 root [-i PATH] [--help]\n\n-i PATH: Optional targeting a different Private Key \n\n(Note: default target created by \"t2 key generate\" is "+default_id_rsa+")\n") - .option('path',{ - abbr: 'i', - full: 'path', - metavar: 'PATH', - default: default_id_rsa, - help: 'Private Key (Note: created by "t2 key generate")' - }) - .callback(function(opts){ - controller.root(opts) - .then(closeSuccessfulCommand,closeFailedCommand); - }) - .help(functional_msg); - - module.exports = function(args) { parser.parse(args); }; diff --git a/lib/controller.js b/lib/controller.js index 356dd381..3a7f2a21 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -7,7 +7,6 @@ var sprintf = require('sprintf-js').sprintf; var cp = require('child_process'); var async = require('async'); var controller = {}; -var Menu = require('terminal-menu'); Tessel.list = function(opts) { @@ -172,164 +171,6 @@ Tessel.get = function(opts) { }); }; -Tessel.seekSelected = function(opts){ - return new Promise(function(resolve, reject) { - - if (opts.timeout && typeof opts.timeout === 'number') { - // Stop the search after that duration - setTimeout(stopSeeker, opts.timeout * 1000); - } else { - // default to 3 seconds searching - setTimeout(stopSeeker, 3 * 1000); - } - console.log("Searching accessable Tessels ...") - - var seeker = new discover.TesselSeeker().start(); - var tessels = []; - - // When we find Tessels - seeker.on('tessel', function(tessel) { - controller.closeTesselConnections(tessel) - .then(function() { - tessels.push(tessel); - }); - }); - - function stopSeeker(){ - try{ - seeker.stop(); - seeker = null; // preventing memory leaks - return resolve(tessels); - } catch(e){ - throw e; - } - } - }); -} - -controller.root = function(opts){ - // ~ conversion to home because spawn isn't able to handle this right - if(opts.path.substring(0,1) === '~'){ - var home = process.env.HOME; - opts.path = opts.path.replace('~', home); - } - return new Promise(function(resolve, reject) { - Tessel.seekSelected(opts) - .then(function(tessels){ - - if(tessels.length >= 2){ - tessels.push({ - connection: { - connectionType: "exit" - } - }); - // TODO: Tessel cooperate identity conform menu required (color, ascii logo) - var rtm = Menu({ - width: 50, - x: 1, - y: 2, - bg: 'red' - }); - rtm.reset(); - rtm.write(' \n'); - tessels.forEach(makeMenu); - // Menu navigation - process.stdin.pipe(rtm.createStream()).pipe(process.stdout); - process.stdin.setRawMode(true); - rtm.on('close', function () { - process.stdin.setRawMode(false); - process.stdin.end(); - }); - rtm.on('select', function (label) { - rtm.close(); - - if(label[0] !== "E"){ - runSSH(label[0]); - } else { - // selected exit! - process.stdout.write("\u001b[2J\u001b[0;0H"); - process.exit(); - } - - }); - } else if(tessels.length === 1) { - if(tessels[0].connection.authorized){ - runSSH(0); - } else { - console.log('Sorry, you are not authorized!\n\n') - console.log('"t2 key generate" might help :-)'); - process.exit(); - } - - } else { - console.log('Sorry no nearby Tessels found!'); - process.exit(); - } - function runSSH(id){ - // clear console - process.stdout.write("\u001b[2J\u001b[0;0H"); - var ch = require('child_process') - .spawn('ssh', - ['-i', - opts.path, - 'root@'+tessels[id].connections[0].host], - { - stdio: 'inherit' - }); - console.log('Connect to '+tessels[id].connections[0].host+'...'); - - ch.on('error', function(e){ - if(e.includes("Permission denied")){ - console.log('Sorry, you are not authorized!\n\n'+e+'\n'); - console.log('"t2 key generate" might help :-)'); - - process.kill(); - - }else{ - throw e; - - process.kill(); - } - - }); - ch.on('exit',function(e,signal){ - if(e === 0){ - // clear console - process.stdout.write("\u001b[2J\u001b[0;0H"); - console.log('Connection to '+tessels[id].connections[0].host+' closed!\n'); - } - if(e === 255){ - console.log('Connection to '+tessels[id].connections[0].host+' closed!\n'); - } - - process.exit(); - }); - }; - - function makeMenu(tessel, index, array){ - if(tessel.connection.connectionType === 'LAN'){ - - if(!tessel.connection.authorized){ - rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (not authorized) \n'); - } else { - rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (authorized) \n'); - } - - } else if(tessel.connection.connectionType === 'USB'){ - // TODO: Because of no real Tessel 2 present - not implemented now - if(!tessel.connection.authorized){ - rtm.add(index+') '+tessel.name+': [USB] (not authorized) \n'); - } else { - rtm.add(index+') '+tessel.name+': [USB] (authorized) \n'); - } - } else { - rtm.add('EXIT'); - } - } - }); - }); -} - /* Takes a list of Tessels with connections that may or may not be open and closes them diff --git a/package.json b/package.json index 68b2af2f..e4fea84a 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,12 @@ "tessel": "git+https://github.com/tessel/cli#jon-export-v2", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1", +<<<<<<< HEAD "wrappy":"1.0.1", "terminal-menu": "^2.1.1" +======= + "wrappy":">=1.0.1" +>>>>>>> parent of 71a2820... first clean version of "t2 root" }, "preferGlobal": true } From c3da2853506ac0e3f1bc1ebf5455b9847173bd53 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 31 Jul 2015 18:42:06 +0200 Subject: [PATCH 24/43] wrapper exact version --- package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package.json b/package.json index e4fea84a..68b2af2f 100644 --- a/package.json +++ b/package.json @@ -62,12 +62,8 @@ "tessel": "git+https://github.com/tessel/cli#jon-export-v2", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1", -<<<<<<< HEAD "wrappy":"1.0.1", "terminal-menu": "^2.1.1" -======= - "wrappy":">=1.0.1" ->>>>>>> parent of 71a2820... first clean version of "t2 root" }, "preferGlobal": true } From 370e1f7b4966bb87273c9295f9287dd969ee8b70 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sat, 1 Aug 2015 16:27:38 +0200 Subject: [PATCH 25/43] linting done --- bin/tessel-2.js | 30 +++++++++ lib/controller.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 964c7ad8..e5b39116 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -197,6 +197,36 @@ parser.command('rename') .help('Change the name of a Tessel to something new.'); +// accessing the root shell of your tessels +// Fixes issue https://github.com/tessel/t2-cli/issues/80 +/** +$ t2 root --help +> Usage: tessel root [-i ] [--help] +> -i : provide a path to the desired ssh key +$ + +$ t2 root +> Accessing root... +root@192.168.128.124 # +*/ +var default_id_rsa = '~/.tessel/id_rsa'; +var functional_msg = '\nGain SSH root access to one of your authorized tessels (menu listing if multiple targets)'; +parser.command('root') + .usage(functional_msg+"\n\nUsage: t2 root [-i PATH] [--help]\n\n-i PATH: Optional targeting a different Private Key \n\n(Note: default target created by \"t2 key generate\" is "+default_id_rsa+")\n") + .option('path',{ + abbr: 'i', + full: 'path', + metavar: 'PATH', + default: default_id_rsa, + help: 'Private Key (Note: created by "t2 key generate")' + }) + .callback(function(opts){ + controller.root(opts) + .then(closeSuccessfulCommand,closeFailedCommand); + }) + .help(functional_msg); + + module.exports = function(args) { parser.parse(args); }; diff --git a/lib/controller.js b/lib/controller.js index 3a7f2a21..fe68a080 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -7,6 +7,7 @@ var sprintf = require('sprintf-js').sprintf; var cp = require('child_process'); var async = require('async'); var controller = {}; +var Menu = require('terminal-menu'); Tessel.list = function(opts) { @@ -171,6 +172,167 @@ Tessel.get = function(opts) { }); }; +Tessel.seekTessels = function(opts){ + return new Promise(function(resolve, reject) { + + if (opts.timeout && typeof opts.timeout === 'number') { + // Stop the search after that duration + setTimeout(stopSeeker, opts.timeout * 1000); + } else { + // default to 3 seconds searching + setTimeout(stopSeeker, 3 * 1000); + } + logs.info("Searching accessable Tessels ..."); + + var seeker = new discover.TesselSeeker().start(); + var tessels = []; + + // When we find Tessels + seeker.on('tessel', function(tessel) { + controller.closeTesselConnections(tessel) + .then(function() { + tessels.push(tessel); + }); + }); + + function stopSeeker(){ + try{ + // If there were no Tessels found + if (tessels.length === 0) { + // Report the sadness + reject('No Tessels Found'); + } + seeker.stop(); + seeker = null; // preventing memory leaks + return resolve(tessels); + } catch(e){ + throw e; + } + } + }); +}; + +controller.root = function(opts){ + // ~ conversion to home because spawn isn't able to handle this right + if(opts.path.substring(0,1) === '~'){ + var home = process.env.HOME; + opts.path = opts.path.replace('~', home); + } + return new Promise(function(resolve, reject) { + Tessel.seekTessels(opts) + .then(function(tessels){ + + if(tessels.length >= 2){ + tessels.push({ + connection: { + connectionType: "exit" + } + }); + // TODO: Tessel cooperate identity conform menu required (color, ascii logo) + var rtm = Menu({ + width: 50, + x: 1, + y: 2, + bg: 'red' + }); + rtm.reset(); + rtm.write(' \n'); + tessels.forEach(makeMenu); + // Menu navigation + process.stdin.pipe(rtm.createStream()).pipe(process.stdout); + process.stdin.setRawMode(true); + rtm.on('close', function () { + process.stdin.setRawMode(false); + process.stdin.end(); + }); + rtm.on('select', function (label) { + rtm.close(); + + if(label[0] !== "E"){ + runSSH(label[0]); + } else { + // selected exit! + process.stdout.write("\u001b[2J\u001b[0;0H"); + resolve(); + process.exit(); + } + + }); + } else if(tessels.length === 1) { + if(tessels[0].connection.authorized){ + runSSH(0); + } else { + reject('Sorry, you are not authorized!\n\n"t2 key generate" might help :-)'); + } + + } else { + reject('Sorry no nearby Tessels found!'); + process.exit(); + } + function runSSH(id){ + // clear console + process.stdout.write("\u001b[2J\u001b[0;0H"); + var ch = require('child_process') + .spawn('ssh', + ['-i', + opts.path, + 'root@'+tessels[id].connections[0].host], + { + stdio: 'inherit' + }); + logs.info('Connect to '+tessels[id].connections[0].host+'...'); + + ch.on('error', function(e){ + if(e.includes("Permission denied")){ + reject('Sorry, you are not authorized!\n\n'+e+'\n"t2 key generate" might help :-)'); + + process.kill(); + + }else{ + throw e; + } + + }); + ch.on('exit',function(e){ + if(e === 0){ + // clear console + process.stdout.write("\u001b[2J\u001b[0;0H"); + resolve('Connection to '+tessels[id].connections[0].host+' closed!\n'); + } + if(e === 255){ + reject('Connection to '+tessels[id].connections[0].host+' closed!\n'); + } + + process.exit(); + }); + } + + function makeMenu(tessel, index){ + if(tessel.connection.connectionType === 'LAN'){ + + if(!tessel.connection.authorized){ + rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (not authorized) \n'); + } else { + rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (authorized) \n'); + } + + } else if(tessel.connection.connectionType === 'USB'){ + // TODO: Because of no real Tessel 2 present - not implemented now + if(!tessel.connection.authorized){ + rtm.add(index+') '+tessel.name+': [USB] (not authorized) \n'); + } else { + rtm.add(index+') '+tessel.name+': [USB] (authorized) \n'); + } + } else { + rtm.add('EXIT'); + } + } + }).catch(function(e) { + reject(e); + }); + }); +}; + /* Takes a list of Tessels with connections that may or may not be open and closes them From eb7346e432dbdf7be5e7a368f9e663ba5bf601a6 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sat, 1 Aug 2015 17:23:12 +0200 Subject: [PATCH 26/43] I hate the NEWLINE rule of JSCS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JavaScript Clean Styling … is a good thing. But for getting an individual cli help we need newlines \n … what isn’t allowed by the JSCS … --- bin/tessel-2.js | 4 +- lib/controller.js | 232 +++++++++++++++++++++++----------------------- 2 files changed, 118 insertions(+), 118 deletions(-) diff --git a/bin/tessel-2.js b/bin/tessel-2.js index e5b39116..9612a420 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -212,8 +212,8 @@ root@192.168.128.124 # var default_id_rsa = '~/.tessel/id_rsa'; var functional_msg = '\nGain SSH root access to one of your authorized tessels (menu listing if multiple targets)'; parser.command('root') - .usage(functional_msg+"\n\nUsage: t2 root [-i PATH] [--help]\n\n-i PATH: Optional targeting a different Private Key \n\n(Note: default target created by \"t2 key generate\" is "+default_id_rsa+")\n") - .option('path',{ + .usage(functional_msg + '\n\nUsage: t2 root [-i PATH] [--help]\n\n-i PATH: Optional targeting a different Private Key \n\n(Note: default target created by "t2 key generate" is ' + default_id_rsa + ')\n') + .option('path', { abbr: 'i', full: 'path', metavar: 'PATH', diff --git a/lib/controller.js b/lib/controller.js index fe68a080..2114c897 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -172,7 +172,7 @@ Tessel.get = function(opts) { }); }; -Tessel.seekTessels = function(opts){ +Tessel.seekTessels = function(opts) { return new Promise(function(resolve, reject) { if (opts.timeout && typeof opts.timeout === 'number') { @@ -180,23 +180,23 @@ Tessel.seekTessels = function(opts){ setTimeout(stopSeeker, opts.timeout * 1000); } else { // default to 3 seconds searching - setTimeout(stopSeeker, 3 * 1000); + setTimeout(stopSeeker, 3000); } - logs.info("Searching accessable Tessels ..."); + logs.info('Searching accessable Tessels ...'); var seeker = new discover.TesselSeeker().start(); var tessels = []; - + // When we find Tessels - seeker.on('tessel', function(tessel) { - controller.closeTesselConnections(tessel) - .then(function() { - tessels.push(tessel); - }); - }); + seeker.on('tessel', function(tessel) { + controller.closeTesselConnections(tessel) + .then(function() { + tessels.push(tessel); + }); + }); - function stopSeeker(){ - try{ + function stopSeeker() { + try { // If there were no Tessels found if (tessels.length === 0) { // Report the sadness @@ -205,132 +205,132 @@ Tessel.seekTessels = function(opts){ seeker.stop(); seeker = null; // preventing memory leaks return resolve(tessels); - } catch(e){ + } catch (e) { throw e; - } + } } }); }; -controller.root = function(opts){ +controller.root = function(opts) { // ~ conversion to home because spawn isn't able to handle this right - if(opts.path.substring(0,1) === '~'){ + if (opts.path.substring(0, 1) === '~') { var home = process.env.HOME; opts.path = opts.path.replace('~', home); - } - return new Promise(function(resolve, reject) { + } + return new Promise(function(resolve, reject) { Tessel.seekTessels(opts) - .then(function(tessels){ - - if(tessels.length >= 2){ - tessels.push({ - connection: { - connectionType: "exit" - } - }); - // TODO: Tessel cooperate identity conform menu required (color, ascii logo) - var rtm = Menu({ + .then(function(tessels) { + + if (tessels.length >= 2) { + tessels.push({ + connection: { + connectionType: 'exit' + } + }); + // TODO: Tessel cooperate identity conform menu required (color, ascii logo) + var rtm = Menu({ width: 50, - x: 1, - y: 2, - bg: 'red' + x: 1, + y: 2, + bg: 'red' }); - rtm.reset(); - rtm.write(' \n'); - tessels.forEach(makeMenu); - // Menu navigation - process.stdin.pipe(rtm.createStream()).pipe(process.stdout); - process.stdin.setRawMode(true); - rtm.on('close', function () { - process.stdin.setRawMode(false); - process.stdin.end(); - }); - rtm.on('select', function (label) { - rtm.close(); - - if(label[0] !== "E"){ - runSSH(label[0]); + rtm.reset(); + rtm.write(' \n'); + tessels.forEach(makeMenu); + // Menu navigation + process.stdin.pipe(rtm.createStream()).pipe(process.stdout); + process.stdin.setRawMode(true); + rtm.on('close', function() { + process.stdin.setRawMode(false); + process.stdin.end(); + }); + rtm.on('select', function(label) { + rtm.close(); + + if (label[0] !== 'E') { + runSSH(label[0]); + } else { + // selected exit! + process.stdout.write('\u001b[2J\u001b[0;0H'); + resolve(); + process.exit(); + } + + }); + } else if (tessels.length === 1) { + if (tessels[0].connection.authorized) { + runSSH(0); } else { - // selected exit! - process.stdout.write("\u001b[2J\u001b[0;0H"); - resolve(); - process.exit(); + reject('Sorry, you are not authorized!\n\n"t2 key generate" might help :-)'); } - - }); - } else if(tessels.length === 1) { - if(tessels[0].connection.authorized){ - runSSH(0); - } else { - reject('Sorry, you are not authorized!\n\n"t2 key generate" might help :-)'); - } - - } else { - reject('Sorry no nearby Tessels found!'); - process.exit(); - } - function runSSH(id){ - // clear console - process.stdout.write("\u001b[2J\u001b[0;0H"); - var ch = require('child_process') - .spawn('ssh', - ['-i', - opts.path, - 'root@'+tessels[id].connections[0].host], - { - stdio: 'inherit' - }); - logs.info('Connect to '+tessels[id].connections[0].host+'...'); - - ch.on('error', function(e){ - if(e.includes("Permission denied")){ - reject('Sorry, you are not authorized!\n\n'+e+'\n"t2 key generate" might help :-)'); - - process.kill(); - - }else{ - throw e; - } - - }); - ch.on('exit',function(e){ - if(e === 0){ - // clear console - process.stdout.write("\u001b[2J\u001b[0;0H"); - resolve('Connection to '+tessels[id].connections[0].host+' closed!\n'); - } - if(e === 255){ - reject('Connection to '+tessels[id].connections[0].host+' closed!\n'); - } - + + } else { + reject('Sorry no nearby Tessels found!'); process.exit(); - }); - } + } + + function runSSH(id) { + // clear console + process.stdout.write('\u001b[2J\u001b[0;0H'); + var ch = require('child_process') + .spawn('ssh', ['-i', + opts.path, + 'root@' + tessels[id].connections[0].host + ], { + stdio: 'inherit' + }); + logs.info('Connect to ' + tessels[id].connections[0].host + '...'); + + ch.on('error', function(e) { + if (e.includes('Permission denied')) { + reject('Sorry, you are not authorized!\n\n' + e + '\n"t2 key generate" might help :-)'); + + process.kill(); - function makeMenu(tessel, index){ - if(tessel.connection.connectionType === 'LAN'){ - - if(!tessel.connection.authorized){ - rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (not authorized) \n'); } else { - rtm.add(index+') '+tessel.name+': '+tessel.connection.ip+' (authorized) \n'); + throw e; } - } else if(tessel.connection.connectionType === 'USB'){ + }); + ch.on('exit', function(e) { + if (e === 0) { + // clear console + process.stdout.write('\u001b[2J\u001b[0;0H'); + resolve('Connection to ' + tessels[id].connections[0].host + ' closed!\n'); + } + if (e === 255) { + reject('Connection to ' + tessels[id].connections[0].host + ' closed!\n'); + } + + process.exit(); + }); + } + + function makeMenu(tessel, index) { + if (tessel.connection.connectionType === 'LAN') { + + if (!tessel.connection.authorized) { + rtm.add(index + ') ' + tessel.name + ': ' + tessel.connection.ip + ' (not authorized) \n'); + } else { + rtm.add(index + ') ' + tessel.name + ': ' + tessel.connection.ip + ' (authorized) \n'); + } + + } else if (tessel.connection.connectionType === 'USB') { // TODO: Because of no real Tessel 2 present - not implemented now - if(!tessel.connection.authorized){ - rtm.add(index+') '+tessel.name+': [USB] (not authorized) \n'); + if (!tessel.connection.authorized) { + rtm.add(index + ') ' + tessel.name + ': [USB] (not authorized) \n'); } else { - rtm.add(index+') '+tessel.name+': [USB] (authorized) \n'); + rtm.add(index + ') ' + tessel.name + ': [USB] (authorized) \n'); } - } else { - rtm.add('EXIT'); + } else { + rtm.add('EXIT'); + } } - } - }).catch(function(e) { - reject(e); - }); - }); + }).catch(function(e) { + reject(e); + }); + }); }; /* From a6d5551c62dd7badbc6cc3b39a50431365d7635f Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sat, 1 Aug 2015 17:25:50 +0200 Subject: [PATCH 27/43] Travis-CI is more restrictive ? --- bin/tessel-2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 9612a420..0719c06f 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -220,7 +220,7 @@ parser.command('root') default: default_id_rsa, help: 'Private Key (Note: created by "t2 key generate")' }) - .callback(function(opts){ + .callback(function(opts) { controller.root(opts) .then(closeSuccessfulCommand,closeFailedCommand); }) From 9424ce0fa0a528bf840d3f4a25298bb5e01ea34f Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sun, 2 Aug 2015 11:46:49 +0200 Subject: [PATCH 28/43] grunt test --force changes this each run for some reason --- bin/tessel-2.js | 2 +- test/unit/provision.js | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bin/tessel-2.js b/bin/tessel-2.js index 0719c06f..aa6e6aa2 100755 --- a/bin/tessel-2.js +++ b/bin/tessel-2.js @@ -222,7 +222,7 @@ parser.command('root') }) .callback(function(opts) { controller.root(opts) - .then(closeSuccessfulCommand,closeFailedCommand); + .then(closeSuccessfulCommand, closeFailedCommand); }) .help(functional_msg); diff --git a/test/unit/provision.js b/test/unit/provision.js index 794d6374..49e25101 100644 --- a/test/unit/provision.js +++ b/test/unit/provision.js @@ -121,18 +121,18 @@ exports['controller.provisionTessel'] = { this.isProvisioned.onSecondCall().returns(false); this.provisionTessel({ - force: true - }) - .then(function() { - test.equal(this.exec.callCount, 1); - test.equal(this.exec.lastCall.args[0], 'rm -r ' + Tessel.TESSEL_AUTH_PATH); - test.equal(this.provisionSpy.callCount, 1); - rimraf(path.join(process.cwd(), Tessel.TESSEL_AUTH_PATH), function(err) { - test.ifError(err); - Tessel.TESSEL_AUTH_PATH = tesselAuthPath; - test.done(); - }); - }.bind(this)); + force: true + }) + .then(function() { + test.equal(this.exec.callCount, 1); + test.equal(this.exec.lastCall.args[0], 'rm -r ' + Tessel.TESSEL_AUTH_PATH); + test.equal(this.provisionSpy.callCount, 1); + rimraf(path.join(process.cwd(), Tessel.TESSEL_AUTH_PATH), function(err) { + test.ifError(err); + Tessel.TESSEL_AUTH_PATH = tesselAuthPath; + test.done(); + }); + }.bind(this)); }, completeUnprovisioned: function(test) { From 4c3ed3b410674c488c26fc8c04bd1ce899faf6e6 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sun, 2 Aug 2015 12:25:07 +0200 Subject: [PATCH 29/43] clean up dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 68b2af2f..39141171 100644 --- a/package.json +++ b/package.json @@ -59,11 +59,11 @@ "sprintf-js": "^1.0.2", "ssh-keygen": "^0.2.1", "ssh2": "^0.4.2", + "terminal-menu": "^2.1.1", "tessel": "git+https://github.com/tessel/cli#jon-export-v2", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1", - "wrappy":"1.0.1", - "terminal-menu": "^2.1.1" + "wrapper": "^0.1.0" }, "preferGlobal": true } From 804d4939e84ab7a541169dfae9ed42adec77a3cf Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 12 Aug 2015 12:55:16 +0200 Subject: [PATCH 30/43] Tessel.seekTessels unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attention: Please notice comments within tests ! There are many TODOs… --- test/unit/root.js | 266 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 test/unit/root.js diff --git a/test/unit/root.js b/test/unit/root.js new file mode 100644 index 00000000..bd7cb317 --- /dev/null +++ b/test/unit/root.js @@ -0,0 +1,266 @@ +// unit test t2 root command +var sinon = require('sinon'); +//var _ = require('lodash'); +var controller = require('../../lib/controller'); +var Tessel = require('../../lib/tessel/tessel'); +var Seeker = require('../../lib/discover.js'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var logs = require('../../lib/logs'); + +// template for creating fake tessels +function createFakeTessel(options) { + var tessel = new Tessel({ + connectionType: options.type || 'LAN', + authorized: options.authorized !== undefined ? options.authorized : true, + end: function() { + // returns a value, promize or thenable + return Promise.resolve(); + // https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve + } + + }); + + function serialNumber(options) { + if (options.type === 'USB') { + return options.serialNumber; + } else { + return false; + } + } + tessel.serialNumber = serialNumber(options); + tessel.name = options.name || 'a'; + + options.sandbox.stub(tessel, 'close', function() { + return Promise.resolve(); + }); + // return the fake tessel + return tessel; +} + + +exports['Tessel.seekTessels'] = { + + setUp: function(done) { + var self = this; + this.sandbox = sinon.sandbox.create(); + this.processOn = this.sandbox.stub(process, 'on'); + this.activeSeeker = undefined; + this.seeker = this.sandbox.stub(Seeker, 'TesselSeeker', function Seeker() { + this.start = function() { + self.activeSeeker = this; + return this; + }; + this.stop = function() { + return this; + }; + }); + // copy the seeker events into the EventEmitter + + util.inherits(this.seeker, EventEmitter); + + // setting up fake system for logging + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + this.logsBasic = this.sandbox.stub(logs, 'basic', function() {}); + + + this.closeTesselConnections = this.sandbox.spy(controller, 'closeTesselConnections'); + done(); + }, + + tearDown: function(done) { + this.sandbox.restore(); + done(); + }, + + // Test is based on structure of controller.js Tessel.list tests (but documented!) + oneAuthorizedLANTessel: function(test) { + test.expect(3); + var a = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + type: 'LAN' + }); + Tessel.seekTessels({ + timeout: 0.01 + }).then(function() { + // it is a authorized tessel, so the test connection while seeking is closes once + test.equal(this.closeTesselConnections.callCount, 1); + // TODO: if tessels have multiple connections at once, this doesn't work anymore! + // test whether the Tessel is unauthorized + test.equal(a.connections[0].authorized, true); + // because we do not runHeuristics callCount is expected to be 0 + test.equal(a.close.callCount, 0); + a = null; + test.done(); + }.bind(this)); + + // simulate a LAN-Tessel is discovered + this.activeSeeker.emit('tessel', a); + }, + oneNotAuthorizedLANTessel: function(test) { + test.expect(3); + var a = createFakeTessel({ + sandbox: this.sandbox, + authorized: false, + type: 'LAN' + }); + Tessel.seekTessels({ + timeout: 0.01 + }).then(function() { + // a not authorized Tessel also close connections (connection is not a ssh login!) + test.equal(this.closeTesselConnections.callCount, 1); + // TODO: if tessels have multiple connections at once, this doesn't work anymore! + // test whether the Tessel is unauthorized + test.equal(a.connections[0].authorized, false); + // because we do not runHeuristics callCount is expected to be 0 + test.equal(a.close.callCount, 0); + test.done(); + }.bind(this)); + + // simulate a LAN-Tessel is discovered + this.activeSeeker.emit('tessel', a); + }, + oneTesselTwoConnections: function(test) { + test.expect(5); + + var a = createFakeTessel({ + sandbox: this.sandbox, + type: 'USB', + name: 'sameTessel', + serialNumber: '080027F00626' + }); + + var lan = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + type: 'LAN', + name: 'sameTessel' + }); + + + Tessel.seekTessels({ + timeout: 0.01 + }) + .then(function() { + + // discover.js handles both connection types by tessel.connection.open + // lan_connection.js is trying to do a ssh connection and sets authorized to false or true + // usb_connection.js register this connection with daemon to keeps track of active remote processes + // and adds a serialNumber what is simulated by the fakeTessel if USB + + // the second is the lan connection what is authorized + test.equal(a.connections[1].authorized, true); + // the two instead one connection causes in two seeked interfaces of the same tessel + test.equal(this.closeTesselConnections.callCount, 2); + + // one tessel with two connections has to have two connection objects + test.equal(a.connections.length, 2); + // because we do not runHeuristics callCount is expected to be 0 + test.equal(a.close.callCount, 0); + // because we do not runHeuristics callCount is expected to be 0 + test.equal(lan.close.callCount, 0); + + // TODO: Because of the connections object of sameTessel isn't conjunct already this test has to be rewritten later + test.done(); + }.bind(this)); + + this.activeSeeker.emit('tessel', a); + this.activeSeeker.emit('tessel', lan); + + // TODO: here it needs to be one object with multiple connections !! + a.connections.push(lan.connections[0]); + + }, + multipeDifferentTessels: function(test) { + test.expect(6); + + var usb = createFakeTessel({ + sandbox: this.sandbox, + type: 'USB', + name: 'dannyLanded', + serialNumber: '080027F00626' + }); + + var lan = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + type: 'LAN', + name: 'dannyFlying' + }); + + + Tessel.seekTessels({ + timeout: 0.01 + }) + .then(function() { + test.equal(lan.connections[0].authorized, true); + test.equal(usb.serialNumber, '080027F00626'); + test.equal(this.closeTesselConnections.callCount, 2); + + test.equal(usb.connections.length, 1); + // because we do not runHeuristics callCount is expected to be 0 + test.equal(usb.close.callCount, 0); + // because we do not runHeuristics callCount is expected to be 0 + test.equal(lan.close.callCount, 0); + + // TODO: Because of the connections object of sameTessel isn't conjunct already this test has to be rewritten later + test.done(); + }.bind(this)); + + this.activeSeeker.emit('tessel', usb); + this.activeSeeker.emit('tessel', lan); + + // TODO: here it needs to be one object with multiple connections !! + + }, +}; + +exports['controller.root'] = { + + setUp: function(done) { + //var self = this; + // creating fake methods + this.sandbox = sinon.sandbox.create(); + + //this. + done(); + }, + tearDown: function(done) { + this.sandbox.restore(); + done(); + }, + test1: function(test) { + test.expect(1); + test.ok(true, 'This shouldn\'t fail'); + test.done(); + }, + test2: function(test) { + test.expect(1); + test.ok(1 === 1, 'This shouldn\'t fail'); + // test.ok(false, 'This should fail'); + test.done(); + } +}; +// t2 root --help + +// t2 root # only one tessel (authorized) + +// t2 root # only one tessel (not authorized) + +// t2 root # no tessel found + +// t2 root # two tessels (authorized) + +// t2 root # two tessels (not authorized) + +// t2 root # invalid id_rsa for authorized tessel ! (froud prevention) + +// t2 root # Accessing root... + +// t2 root --help # Usage: tessel root [-i ] [--help] + +// TODO: adding USB tests ! + +// TODO: running tests on Windows 7 and try catch ssh exists From 67ff6710659050041e88e0328e022067f05b2ba5 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 12 Aug 2015 13:01:04 +0200 Subject: [PATCH 31/43] removed copy paste comments --- test/unit/root.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/root.js b/test/unit/root.js index bd7cb317..8d4b3169 100644 --- a/test/unit/root.js +++ b/test/unit/root.js @@ -205,14 +205,12 @@ exports['Tessel.seekTessels'] = { // because we do not runHeuristics callCount is expected to be 0 test.equal(lan.close.callCount, 0); - // TODO: Because of the connections object of sameTessel isn't conjunct already this test has to be rewritten later test.done(); }.bind(this)); this.activeSeeker.emit('tessel', usb); this.activeSeeker.emit('tessel', lan); - // TODO: here it needs to be one object with multiple connections !! }, }; From 2e2334f753a147c129c704e338a17f67d2840f91 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Fri, 14 Aug 2015 22:43:50 +0200 Subject: [PATCH 32/43] need workaround for VM tests --- lib/usb_connection.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/usb_connection.js b/lib/usb_connection.js index 8247d8ae..b0f672fd 100644 --- a/lib/usb_connection.js +++ b/lib/usb_connection.js @@ -228,10 +228,13 @@ util.inherits(USB.Scanner, EventEmitter); USB.Scanner.prototype.start = function() { var self = this; + if (haveusb) { + usb.getDeviceList().forEach(deviceInspector); + usb.on('attach', deviceInspector); + } else { + console.warn('WARNING: No usb controller found on this system.'); - usb.getDeviceList().forEach(deviceInspector); - - usb.on('attach', deviceInspector); + } function deviceInspector(device) { if ((device.deviceDescriptor.idVendor === TESSEL_VID) && (device.deviceDescriptor.idProduct === TESSEL_PID)) { From 3fbe20cd4c3f4474494eeac061ac6a7f5a2ff5f3 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sat, 15 Aug 2015 08:17:13 +0200 Subject: [PATCH 33/43] VM without USB relevant handling! --- lib/usb_connection.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/usb_connection.js b/lib/usb_connection.js index b0f672fd..287d5d0e 100644 --- a/lib/usb_connection.js +++ b/lib/usb_connection.js @@ -246,7 +246,9 @@ USB.Scanner.prototype.start = function() { }; USB.Scanner.prototype.stop = function() { - usb.removeAllListeners('attach'); + if (haveusb) { + usb.removeAllListeners('attach'); + } }; module.exports.startScan = startScan; From 99f3c438fd7ed45c8961c6242b1f1acdb98c660e Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Sat, 15 Aug 2015 08:54:32 +0200 Subject: [PATCH 34/43] Cleaning up user messages style --- lib/usb_connection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/usb_connection.js b/lib/usb_connection.js index 287d5d0e..226996ed 100644 --- a/lib/usb_connection.js +++ b/lib/usb_connection.js @@ -10,7 +10,7 @@ try { var usb = require('usb'); } catch (e) { haveusb = false; - console.error('WARNING: No usb controller found on this system.'); + logs.warn('No USB controller found on this system.'); } var Daemon = require('./usb/usb_daemon'); @@ -105,7 +105,7 @@ USB.Connection.prototype.open = function() { self.device.open(); } catch (e) { if (e.message === 'LIBUSB_ERROR_ACCESS' && process.platform === 'linux') { - console.error('Please run `sudo tessel install-drivers` to fix device permissions.\n(Error: could not open USB device.)'); + logs.error('Please run `sudo tessel install-drivers` to fix device permissions.\n(Error: could not open USB device.)'); } // Reject if error return reject(e, self); @@ -232,7 +232,7 @@ USB.Scanner.prototype.start = function() { usb.getDeviceList().forEach(deviceInspector); usb.on('attach', deviceInspector); } else { - console.warn('WARNING: No usb controller found on this system.'); + logs.warn('Skipping USB discovering! (No USB controller)'); } From 0fa8ba8f36e48a0f8f0b907556f0b8aa9239cfdd Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Thu, 3 Sep 2015 17:54:22 +0200 Subject: [PATCH 35/43] controller.root test no 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testing menu entries. Note: It isn’t possible to test the selected item because the inherit of menu events isn’t working well … events are bound to the callback of menu.add --- test/unit/root.js | 55 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/test/unit/root.js b/test/unit/root.js index 8d4b3169..286d8988 100644 --- a/test/unit/root.js +++ b/test/unit/root.js @@ -7,6 +7,7 @@ var Seeker = require('../../lib/discover.js'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var logs = require('../../lib/logs'); +var Menu = require('terminal-menu'); // template for creating fake tessels function createFakeTessel(options) { @@ -43,8 +44,8 @@ exports['Tessel.seekTessels'] = { setUp: function(done) { var self = this; - this.sandbox = sinon.sandbox.create(); - this.processOn = this.sandbox.stub(process, 'on'); + this.sandbox = sinon.sandbox.create(); // http://sinonjs.org/docs/#sinon-sandbox + this.processOn = this.sandbox.stub(process, 'on'); // set property 'process' to 'on' this.activeSeeker = undefined; this.seeker = this.sandbox.stub(Seeker, 'TesselSeeker', function Seeker() { this.start = function() { @@ -70,7 +71,7 @@ exports['Tessel.seekTessels'] = { }, tearDown: function(done) { - this.sandbox.restore(); + this.sandbox.restore(); // restores all fakes but also stubs because they are added to the internal collections of fakes in sandbox mode! done(); }, @@ -218,9 +219,32 @@ exports['Tessel.seekTessels'] = { exports['controller.root'] = { setUp: function(done) { - //var self = this; - // creating fake methods + var self = this; this.sandbox = sinon.sandbox.create(); + this.processOn = this.sandbox.stub(process, 'on'); + self.activeMenu = undefined; + self.menuLines = undefined; + // no display output necessary so overriding write + this.rtm = new Menu({ + width: 50, + x: 1, + y: 2, + bg: 'red' + }); + this.sandbox.stub(this.rtm, 'write', function Menu() { + this.write = function(msg) { + self.menuLines = msg.split('\n'); + return this; + }; + }); + + // copy the Menu events 'select' and 'close' into the EventEmitter + util.inherits(this.rtm, EventEmitter); + + // setting up fake system for logging + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + this.logsBasic = this.sandbox.stub(logs, 'basic', function() {}); //this. done(); @@ -229,17 +253,20 @@ exports['controller.root'] = { this.sandbox.restore(); done(); }, - test1: function(test) { - test.expect(1); - test.ok(true, 'This shouldn\'t fail'); - test.done(); - }, - test2: function(test) { - test.expect(1); - test.ok(1 === 1, 'This shouldn\'t fail'); - // test.ok(false, 'This should fail'); + addingMenuLine: function(test) { + // console.log('this: menu: ', this.rtm.items); + test.expect(3); + + this.rtm.add('MenulineOne\n'); + this.rtm.write(); + + test.equal(this.rtm.items.length, 1); + test.equal(this.rtm.x, 3); + test.equal(this.rtm.y, 4); + test.done(); } + }; // t2 root --help From 3056a6c0c736c5246842a20c244b09bd072f0b7f Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 12:19:37 +0200 Subject: [PATCH 36/43] modulize controller.ssh --- lib/controller.js | 192 ++++++++---------- lib/root_controller.js | 174 ++++++++++++++++ lib/tessel/root.js | 3 + lib/update-fetch.js | 129 ++++++++++++ lib/usb_connection.js | 2 +- package.json | 1 + test/common/tessel-classic.js | 2 +- test/unit/deploy.js | 2 +- test/unit/menu.js | 272 ++++++++++++++++++++++++++ test/unit/{root.js => seekTessels.js} | 110 +++-------- 10 files changed, 688 insertions(+), 199 deletions(-) create mode 100644 lib/root_controller.js create mode 100644 lib/tessel/root.js create mode 100644 lib/update-fetch.js create mode 100644 test/unit/menu.js rename test/unit/{root.js => seekTessels.js} (75%) diff --git a/lib/controller.js b/lib/controller.js index 2114c897..f5fdbc92 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -7,7 +7,10 @@ var sprintf = require('sprintf-js').sprintf; var cp = require('child_process'); var async = require('async'); var controller = {}; +controller.ssh = require('./root_controller'); var Menu = require('terminal-menu'); +var networkInterfaces = require('os').networkInterfaces(); +var debug = require('debug')('controller'); Tessel.list = function(opts) { @@ -23,21 +26,22 @@ Tessel.list = function(opts) { // When a Tessel is found seeker.on('tessel', function displayResults(tessel) { var note = ''; - // Add it to our array foundTessels.push(tessel); - // Add a note if the user isn't authorized to use it yet if (tessel.connection.connectionType === 'LAN' && !tessel.connection.authorized) { note = '(USB connect and run `tessel provision` to authorize)'; } - // Print out details... logs.basic(sprintf('\t%s\t%s\t%s', tessel.name, tessel.connection.connectionType, note)); }); // Called after CTRL+C or timeout function stopSearch() { + if (foundTessels.length > 1) { + debug('heuristics ? => found: ' + foundTessels.length + ' === 1 || (' + foundTessels.length + ' === 2 && ' + foundTessels[0].name + ' === ' + foundTessels[1].name + ' && ' + foundTessels[0].connections[0].connectionType + ' !== ' + foundTessels[1].connections[0].connectionType + ')'); + + } // If the seeker exists (it should) if (seeker !== undefined) { // Stop looking for more Tessels @@ -55,8 +59,10 @@ Tessel.list = function(opts) { } // If we have only one Tessel or two Tessels with the same name (just USB and LAN) else if (foundTessels.length === 1 || - (foundTessels.length === 2 && foundTessels[0].name === foundTessels[1].name)) { + (foundTessels.length === 2 && foundTessels[0].name === foundTessels[1].name && foundTessels[0].connections[0].connectionType !== foundTessels[1].connections[0].connectionType)) { // Close all opened connections and resolve + console.log(foundTessels[0].connections.connectionType); + console.log(foundTessels[1].connections.connectionType); controller.closeTesselConnections(foundTessels) .then(resolve); } @@ -171,7 +177,17 @@ Tessel.get = function(opts) { } }); }; +/* +Because of sync problem with master trunk and parallel development on Tessel.list +it was necessary to go my own way... +The seekTessels method never uses heuristics because all possible login variants are required. +There is a new feature in: In the case your network topology causes a Tessels IP is found twice, the +menu will only list it once. + +Due to my own stupid failing with mixing up a tessel is accessable via gateway and a Tessel is in the +same Netmask, I've added a notice for the case someone run into same issue. +*/ Tessel.seekTessels = function(opts) { return new Promise(function(resolve, reject) { @@ -191,142 +207,99 @@ Tessel.seekTessels = function(opts) { seeker.on('tessel', function(tessel) { controller.closeTesselConnections(tessel) .then(function() { - tessels.push(tessel); + var known = false; + if (tessels.length >= 1) { + for (var i in tessels) { + if (tessel.connections[0].ip === tessels[i].connections[0].ip && tessel.connections[0].connectionType === 'LAN') { + // Your network topology causes a single tessel is found twice + debug('known = true'); + known = true; + } + } + + } + if (known) { + debug('Due to your Network-Topology ' + tessel.name + ' is found twice! (' + tessel.connections[0].ip + ')'); + } else { + // Add it to our array + tessels.push(tessel); + } }); }); + seeker.on('error', function(e) { + reject(e); + }); + function stopSeeker() { try { // If there were no Tessels found - if (tessels.length === 0) { + if (!tessels || tessels.length === 0) { // Report the sadness - reject('No Tessels Found'); + debug('We are searching the following networks:\n', networkInterfaces); + debug('\n(Important: Check your Netmask - we do not follow gateways for discovering!)'); + logs.warn('No Tessels found (DEBUG=controller t2 root # might help!)'); + resolve(); } seeker.stop(); seeker = null; // preventing memory leaks return resolve(tessels); } catch (e) { - throw e; + reject(e); } } }); }; +/* + The T2 root command is used to login into the firmware and gaining superuser access. + The security is provided by RSA - your Tessel get the keys while provisioning via USB. + + If you have only one Tessel, the T2 root command will login directly else there is + a ncurses like menu you can select the Tessel you like to gain root access... + + The structure of code and maybe unusual parts are paid due being testable. + The lightweight terminal-menu isn't written testable what needs a little bit hacking. + Finally some parts are causing from my personnal learning curve about writing unit tests using sinon! +*/ controller.root = function(opts) { // ~ conversion to home because spawn isn't able to handle this right - if (opts.path.substring(0, 1) === '~') { + if (opts.path && opts.path.substring(0, 1) === '~') { var home = process.env.HOME; opts.path = opts.path.replace('~', home); } + var rtm; + if (!opts.menu) { + // TODO: Tessel cooperate identity conform menu required (color, ascii logo) + rtm = Menu({ + width: 50, + x: 1, + y: 2, + bg: 'red' + }); + } else { + // used while testing to override methods by stubs (testing doesn't like user interactions) + rtm = opts.menu; + } return new Promise(function(resolve, reject) { - Tessel.seekTessels(opts) + controller.ssh.seek(opts) .then(function(tessels) { - if (tessels.length >= 2) { - tessels.push({ - connection: { - connectionType: 'exit' - } - }); - // TODO: Tessel cooperate identity conform menu required (color, ascii logo) - var rtm = Menu({ - width: 50, - x: 1, - y: 2, - bg: 'red' - }); - rtm.reset(); - rtm.write(' \n'); - tessels.forEach(makeMenu); - // Menu navigation - process.stdin.pipe(rtm.createStream()).pipe(process.stdout); - process.stdin.setRawMode(true); - rtm.on('close', function() { - process.stdin.setRawMode(false); - process.stdin.end(); - }); - rtm.on('select', function(label) { - rtm.close(); - - if (label[0] !== 'E') { - runSSH(label[0]); - } else { - // selected exit! - process.stdout.write('\u001b[2J\u001b[0;0H'); - resolve(); - process.exit(); - } - - }); - } else if (tessels.length === 1) { + if (tessels && tessels.length >= 2) { + controller.ssh.multipleTessels(opts, tessels, rtm, resolve, reject); + } else if (tessels && tessels.length === 1) { if (tessels[0].connection.authorized) { - runSSH(0); + controller.ssh.runSSH(0, opts, tessels, resolve, reject); } else { - reject('Sorry, you are not authorized!\n\n"t2 key generate" might help :-)'); + logs.warn('Sorry, you are not authorized!'); + logs.info('"t2 key generate" might help :-)'); + reject(); } - } else { - reject('Sorry no nearby Tessels found!'); - process.exit(); - } - - function runSSH(id) { - // clear console - process.stdout.write('\u001b[2J\u001b[0;0H'); - var ch = require('child_process') - .spawn('ssh', ['-i', - opts.path, - 'root@' + tessels[id].connections[0].host - ], { - stdio: 'inherit' - }); - logs.info('Connect to ' + tessels[id].connections[0].host + '...'); - - ch.on('error', function(e) { - if (e.includes('Permission denied')) { - reject('Sorry, you are not authorized!\n\n' + e + '\n"t2 key generate" might help :-)'); - - process.kill(); - - } else { - throw e; - } - - }); - ch.on('exit', function(e) { - if (e === 0) { - // clear console - process.stdout.write('\u001b[2J\u001b[0;0H'); - resolve('Connection to ' + tessels[id].connections[0].host + ' closed!\n'); - } - if (e === 255) { - reject('Connection to ' + tessels[id].connections[0].host + ' closed!\n'); - } - - process.exit(); - }); + // everything works fine, but no tessels found + resolve(); } - function makeMenu(tessel, index) { - if (tessel.connection.connectionType === 'LAN') { - - if (!tessel.connection.authorized) { - rtm.add(index + ') ' + tessel.name + ': ' + tessel.connection.ip + ' (not authorized) \n'); - } else { - rtm.add(index + ') ' + tessel.name + ': ' + tessel.connection.ip + ' (authorized) \n'); - } - - } else if (tessel.connection.connectionType === 'USB') { - // TODO: Because of no real Tessel 2 present - not implemented now - if (!tessel.connection.authorized) { - rtm.add(index + ') ' + tessel.name + ': [USB] (not authorized) \n'); - } else { - rtm.add(index + ') ' + tessel.name + ': [USB] (authorized) \n'); - } - } else { - rtm.add('EXIT'); - } - } }).catch(function(e) { reject(e); }); @@ -613,7 +586,6 @@ controller.connectToNetwork = function(opts) { }); }); }; - module.exports = controller; // Shared exports diff --git a/lib/root_controller.js b/lib/root_controller.js new file mode 100644 index 00000000..875bd552 --- /dev/null +++ b/lib/root_controller.js @@ -0,0 +1,174 @@ +var Tessel = require('./tessel/tessel'); +var logs = require('./logs'); +var debug = require('debug')('controller'); + +// create an object for t2 root steps for beeing more handy about unit testing of the T2 root command itself. +var controller = {}; +controller.ssh = { + makeMenu: function(tessel, index, rtm) { + if (tessel.connection.connectionType === 'LAN') { + if (!tessel.connection.authorized) { + rtm.add(index + ') ' + tessel.name + ': ' + tessel.connection.ip + ' (not authorized) \n'); + } else { + rtm.add(index + ') ' + tessel.name + ': ' + tessel.connection.ip + ' (authorized) \n'); + } + + } else if (tessel.connection.connectionType === 'USB') { + if (!tessel.connection.authorized) { + rtm.add(index + ') ' + tessel.name + ': [USB] (not authorized) \n'); + } else { + rtm.add(index + ') ' + tessel.name + ': [USB] (authorized) \n'); + } + } else { + rtm.add('EXIT'); + } + // FIXME: Workaround for trouble with dynamic added menu elements + rtm._draw(); + }, + runSSH: function(id, opts, tessels, resolve, reject) { + // clear console + debug('runSSH...\n root@' + tessels[id].connections[0].host); + if (opts.menu) { + // because the test needs to resolve the promise ... + resolve({ + opts: opts, + tessels: tessels + }); + } else { + // if this is no test, starting the menu as child process + process.stdout.write('\u001b[2J\u001b[0;0H'); + var ch = require('child_process') + .spawn('ssh', ['-i', + opts.path, + 'root@' + tessels[id].connections[0].host + ], { + stdio: 'inherit' + }); + logs.info('Connect to ' + tessels[id].connections[0].host + '...'); + + // FIXME: There is no handler for poweroff Tessel ! Needs to be fixed within the firmware... + // (Terminal freezes imediatelly after poweroff the Tessel) + + ch.once('error', function(e) { + if (e === 255) { + process.stdout.write('\u001b[2J\u001b[0;0H'); + logs.warn('Sorry, you are not authorized!'); + logs.info('"t2 key generate" might help :-)'); + reject(); + } else { + logs.warn('Error while connected to ' + tessels[id].connections[0].host + ':', e); + reject(e); + } + + }); + ch.once('exit', function(e) { + + if (e === 255) { + process.stdout.write('\u001b[2J\u001b[0;0H'); + logs.warn('Sorry, you are not authorized!'); + logs.info('"t2 key generate" might help :-)'); + reject(); + } else if (e === 127) { // exit by user + // clear console + process.stdout.write('\u001b[2J\u001b[0;0H'); + logs.warn('Connection to ' + tessels[id].connections[0].host + ' closed!\n'); + resolve(); + } else if (e === 0) { + // everything works fine ... now lets clear the terminal + process.stdout.write('\u001b[2J\u001b[0;0H'); + logs.info('Connection to ' + tessels[id].connections[0].host + ' closed!\n'); + resolve(); + } else { + logs.warn('Connection to ' + tessels[id].connections[0].host + ' closed due to reason:\n', e); + reject(e); + } + }); + + ch.once('close', function(e) { + // tessel powers off + if (e === 0) { + // everything works fine... + resolve(); + } else if (e === 255) { + //reject(); + process.exit(); + } else if (e === 127) { + reject(); + } else { + logs.warn('Connection to ' + tessels[id].connections[0].host + ' closed due to reason:\n', e); + reject(); + } + }); + } + }, + pro: function(rtm, resolve, reject) { + // Menu navigation + + // raw mode for gain ability to navigate up and down within the menu + process.stdin.setRawMode(true); + + process.stdin.pipe(rtm.createStream()).pipe(process.stdout); + + rtm.on('close', function() { + process.stdin.setRawMode(false); + process.stdin.end(); + }); + rtm.on('error', function(e) { + process.stdin.setRawMode(false); + reject(e); + }); + }, + clear: function(resolve) { + // selected exit! + process.stdout.write('\u001b[2J\u001b[0;0H'); + controller.ssh.exit(resolve); + }, + exit: function(resolve) { + resolve(); + //process.exit(); + }, + multipleTessels: function(opts, tessels, rtm, resolve, reject) { + // Hack for creating usebility (exit menu) + tessels.push({ + connection: { + connectionType: 'exit' + } + }); + + rtm.reset(); + rtm.write(' \n'); + // create menu entries + for (var i in tessels) { + controller.ssh.makeMenu(tessels[i], i, rtm); + } + if (!opts.menu) { + // if this is no test, starting the menu as child process + controller.ssh.pro(rtm, resolve, reject); + } else { + // because the test needs to resolve the promise ... + resolve({ + opts: opts, + tessels: tessels + }); + } + + rtm.once('select', function(label) { + rtm.close(); + + // Identify the Exit command by first letter (all other entries start with numbers/index) + if (label[0] !== 'E') { + controller.ssh.runSSH(label[0], opts, tessels, resolve, reject); + } else { + // going to clear screen and calling exit to resolve promise + controller.ssh.clear(resolve); + } + + }); + }, + seek: function(opts) { + // to be able to replace the seeker in testing mode + return Tessel.seekTessels(opts); + } + +}; +module.exports = controller.ssh; diff --git a/lib/tessel/root.js b/lib/tessel/root.js new file mode 100644 index 00000000..7dd26efa --- /dev/null +++ b/lib/tessel/root.js @@ -0,0 +1,3 @@ +/*var Tessel = require('./tessel'), + logs = require('../logs'); +*/ diff --git a/lib/update-fetch.js b/lib/update-fetch.js new file mode 100644 index 00000000..48b03881 --- /dev/null +++ b/lib/update-fetch.js @@ -0,0 +1,129 @@ +var path = require('path'); +var urljoin = require('url-join'); +var request = require('request'); +var gunzip = require('zlib').createGunzip(); +var extract = require('tar-stream').extract(); +var logs = require('./logs'); +var streamToBuffer = require('stream-to-buffer'); + + +const BUILD_SERVER_ROOT = 'https://builds.tessel.io/t2'; +const FIRMWARE_PATH = urljoin(BUILD_SERVER_ROOT, 'firmware'); +const BUILDS_JSON_FILE = urljoin(FIRMWARE_PATH, 'builds.json'); +const OPENWRT_BINARY_FILE = 'openwrt.bin'; +const FIRMWARE_BINARY_FILE = 'firmware.bin'; + +/* + Requests a list of available builds from the + build server. Returns list of build names in + a Promise. +*/ +function requestBuildList() { + return new Promise(function(resolve, reject) { + // Fetch the list of available builds + request.get(BUILDS_JSON_FILE, function(err, response, body) { + if (err) { + return reject(err); + } + + var outcome = reviewResponse(response); + + // If there wasn't an issue with the request + if (outcome.success) { + // Resolve with the parsed data + try { + resolve(JSON.parse(body)); + } + // If the parse failed, reject + catch (err) { + reject(err); + } + } else { + reject(outcome.reason); + } + }); + }); +} + +/* + Accepts a build name and attempts to fetch + the build images from the server. Returns build contents + in a Promise +*/ +function fetchBuild(build) { + return new Promise(function(resolve, reject) { + logs.info('Beginning update download. This could take a couple minutes..'); + + var binaries = { + firmware: undefined, + openwrt: undefined, + }; + + // Fetch the list of available builds + extract.on('entry', function(header, stream, callback) { + // The buffer to save incoming data to + // The filename of this entry + var filename = path.basename(header.name); + // This entry is the openwrt binary + if (filename === OPENWRT_BINARY_FILE) { + // Save incoming data to the openwrt buffer + streamToBuffer(stream, function(err, buffer) { + binaries.openwrt = buffer; + callback(); + }); + } + // This entry is the firmware binary + else if (filename === FIRMWARE_BINARY_FILE) { + // Save incoming data to the firmware buffer + streamToBuffer(stream, function(err, buffer) { + binaries.firmware = buffer; + callback(); + }); + } else { + callback(); + } + }); + + extract.once('finish', function() { + if (!binaries.firmware.length || !binaries.openwrt.length) { + return reject(new Error('Fetched binary wasn\'t formatted properly.')); + } else { + logs.info('Download complete!'); + return resolve(binaries); + } + }); + + // Make a request to our build server for this particular sha + var req = request.get(urljoin(FIRMWARE_PATH, build.sha + '.tar.gz')); + // unzip and extract the binary tarball + req.pipe(gunzip).pipe(extract); + }); +} + +function reviewResponse(response) { + var outcome = { + success: true, + }; + + // If there was an issue with the server endpoint, reject + if (response.statusCode !== 200) { + outcome.success = false; + outcome.reason = 'Invalid status code on build server request: ' + response.statusCode; + } + + return outcome; +} + + + +function findBuild(builds, property, value) { + return builds.filter(function(build) { + return build[property] === value; + })[0]; +} + +module.exports.requestBuildList = requestBuildList; +module.exports.fetchBuild = fetchBuild; +module.exports.findBuild = findBuild; +module.exports.OPENWRT_BINARY_FILE = OPENWRT_BINARY_FILE; +module.exports.FIRMWARE_BINARY_FILE = FIRMWARE_BINARY_FILE; diff --git a/lib/usb_connection.js b/lib/usb_connection.js index 226996ed..478e54f0 100644 --- a/lib/usb_connection.js +++ b/lib/usb_connection.js @@ -248,7 +248,7 @@ USB.Scanner.prototype.start = function() { USB.Scanner.prototype.stop = function() { if (haveusb) { usb.removeAllListeners('attach'); - } + } }; module.exports.startScan = startScan; diff --git a/package.json b/package.json index 39141171..f41606d8 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "ssh2": "^0.4.2", "terminal-menu": "^2.1.1", "tessel": "git+https://github.com/tessel/cli#jon-export-v2", + "url-join": "0.0.1", "usb": "^1.0.5", "usb-daemon-parser": "0.0.1", "wrapper": "^0.1.0" diff --git a/test/common/tessel-classic.js b/test/common/tessel-classic.js index 4a45f2e2..06ec8c27 100644 --- a/test/common/tessel-classic.js +++ b/test/common/tessel-classic.js @@ -13,7 +13,7 @@ module.exports = { if (dir === 'fail') { callback(new Error()); } else { - callback(null, new Buffer('console.log("testing deploy");')); + callback(null, new Buffer('console.log(\'testing deploy\');')); } } }; diff --git a/test/unit/deploy.js b/test/unit/deploy.js index 6d4744cf..76e9e37f 100644 --- a/test/unit/deploy.js +++ b/test/unit/deploy.js @@ -9,7 +9,7 @@ var mkdirp = require('mkdirp'); var path = require('path'); var deployFolder = path.join(__dirname, 'tmp'); var deployFile = path.join(deployFolder, 'app.js'); -var codeContents = 'console.log("testing deploy");'; +var codeContents = 'console.log(\'testing deploy\');'; var rimraf = require('rimraf'); exports['Tessel.prototype.deployScript'] = { diff --git a/test/unit/menu.js b/test/unit/menu.js new file mode 100644 index 00000000..8ecf7855 --- /dev/null +++ b/test/unit/menu.js @@ -0,0 +1,272 @@ +// unit test t2 root menu object +var sinon = require('sinon'); +var controller = require('../../lib/controller'); +var Tessel = require('../../lib/tessel/tessel'); +var Seeker = require('../../lib/discover.js'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var logs = require('../../lib/logs'); +var Menu = require('terminal-menu'); + +// template for creating fake tessels +function createFakeTessel(options) { + var tessel = new Tessel({ + connectionType: options.type || 'LAN', + authorized: options.authorized !== undefined ? options.authorized : true, + end: function() { + return Promise.resolve(); + } + + }); + + function serialNumber(options) { + if (options.type === 'USB') { + return options.serialNumber; + } else { + return false; + } + } + tessel.serialNumber = serialNumber(options); + tessel.name = options.name || 'a'; + + options.sandbox.stub(tessel, 'close', function() { + return Promise.resolve(); + }); + // return the fake tessel + return tessel; +} + +exports['controller.menu'] = { + + setUp: function(done) { + + var self = this; + + this.sandbox = sinon.sandbox.create(); + this.processOn = this.sandbox.stub(process, 'on'); + // no display output necessary so overriding write + this.rtm = new Menu({ + width: 50, + x: 1, + y: 2, + bg: 'red' + }); + // add sandbox-stub to this.rtm and override method 'write' + this.sandbox.stub(this.rtm, 'add', function add() { + this.add = function(label) { + this.items.push({ + x: this.x, + y: this.y, + label: label + }); + this._fillLine(this.y); + this.y++; + }; + }); + this.sandbox.stub(this.rtm, 'write', function write() { + this.write = function() { + return this; + }; + }); + this.sandbox.stub(this.rtm, '_draw', function _draw() { + this._draw = function() { + return this; + }; + }); + // copy the Menu events 'select' and 'close' into the EventEmitter + // FIXME: This doesn't work due to the untestable way terminal-menu adds listener + // see: index.js Menu.prototype.add + + // util.inherits(this.rtm, EventEmitter); + + this.activeSeeker = undefined; + this.seeker = this.sandbox.stub(Seeker, 'TesselSeeker', function Seeker() { + this.start = function() { + self.activeSeeker = this; + return this; + }; + this.stop = function() { + return this; + }; + }); + // copy the seeker events into the EventEmitter + util.inherits(this.seeker, EventEmitter); + this.sandbox.stub(controller.ssh, 'pro', function() { + this.pro = function() { + return this; + }; + }); + + this.sandbox.stub(controller.ssh, 'clear', function() { + this.clear = function() { + return Promise.resolve(); + }; + }); + + this.sandbox.stub(controller.ssh, 'exit', function() { + this.exit = function(resolve) { + resolve(); + }; + }); + // setting up fake system for logging, so there is no console output while testing + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + this.logsBasic = this.sandbox.stub(logs, 'basic', function() {}); + + done(); + }, + tearDown: function(done) { + this.sandbox.restore(); + done(); + }, + addingMenuLine: function(test) { + test.expect(6); + + // wakeup the stubid ... stub + this.rtm.add(); + // add first item + this.rtm.add('MenulineOne\n'); + + test.equal(this.rtm.items.length, 1); + test.equal(this.rtm.x, 3); + test.equal(this.rtm.y, 4); + // validate logs + test.equal(this.logsInfo.callCount, 0); + test.equal(this.logsWarn.callCount, 0); + test.equal(this.logsBasic.callCount, 0); + + test.done(); + }, + addMenuLineBySeekingTessels: function(test) { + // due to the Memory-Leaks warning from node.js (11+ callbacks) + // we create 12 Menu-Entries + var UsbTessel1 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel1', + type: 'USB' + }); + var UsbTessel2 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel2', + type: 'USB' + }); + var UsbTessel3 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel3', + type: 'USB' + }); + var UsbTessel4 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel4', + type: 'USB' + }); + var UsbTessel5 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel5', + type: 'USB' + }); + var UsbTessel6 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel6', + type: 'USB' + }); + var UsbTessel7 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel7', + type: 'USB' + }); + var UsbTessel8 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel8', + type: 'USB' + }); + var UsbTessel9 = createFakeTessel({ + sandbox: this.sandbox, + authorized: false, + name: 'UsbTessel9', + type: 'USB' + }); + var UsbTessel10 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel10', + type: 'USB' + }); + var UsbTessel11 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel11', + type: 'USB' + }); + var UsbTessel12 = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'UsbTessel12', + type: 'USB' + }); + + test.expect(9); + + // adding menu + controller.root({ + timeout: 0.1, + path: '~/.tessel/id_rsa', + menu: this.rtm + }).then(function(obj) { + + // because 'EXIT' is added automatically length +1 + test.equal(this.rtm.items.length, 13); + // test.equal(1, 1); + test.equal(this.rtm.items[8].label, '9) UsbTessel9: [USB] (not authorized) \n'); + test.equal(this.rtm.items[10].label, '11) UsbTessel11: [USB] (authorized) \n'); + test.equal(this.rtm.items[12].label, 'EXIT'); + + // test if users home is converted + test.equal(obj.opts.path[0] !== '~', true); + // validate we are not testing nothing... + test.equal(obj.opts.menu, this.rtm); + // validate logs + test.equal(this.logsInfo.callCount, 1); + test.equal(this.logsWarn.callCount, 0); + test.equal(this.logsBasic.callCount, 0); + + UsbTessel1 = null; + UsbTessel2 = null; + UsbTessel3 = null; + UsbTessel4 = null; + UsbTessel5 = null; + UsbTessel6 = null; + UsbTessel7 = null; + UsbTessel8 = null; + UsbTessel9 = null; + UsbTessel10 = null; + UsbTessel11 = null; + UsbTessel12 = null; + test.done(); + }.bind(this)); + // first emit is for waking up the stubid seeker ... + this.activeSeeker.emit('tessel', UsbTessel1); + + // simulate a USB-Tessel is discovered + this.activeSeeker.emit('tessel', UsbTessel1); + this.activeSeeker.emit('tessel', UsbTessel2); + this.activeSeeker.emit('tessel', UsbTessel3); + this.activeSeeker.emit('tessel', UsbTessel4); + this.activeSeeker.emit('tessel', UsbTessel5); + this.activeSeeker.emit('tessel', UsbTessel6); + this.activeSeeker.emit('tessel', UsbTessel7); + this.activeSeeker.emit('tessel', UsbTessel8); + this.activeSeeker.emit('tessel', UsbTessel9); + this.activeSeeker.emit('tessel', UsbTessel10); + this.activeSeeker.emit('tessel', UsbTessel11); + this.activeSeeker.emit('tessel', UsbTessel12); + } +}; diff --git a/test/unit/root.js b/test/unit/seekTessels.js similarity index 75% rename from test/unit/root.js rename to test/unit/seekTessels.js index 286d8988..0c06edb2 100644 --- a/test/unit/root.js +++ b/test/unit/seekTessels.js @@ -1,13 +1,11 @@ -// unit test t2 root command +// unit test t2 root - Tessel.seekTessels var sinon = require('sinon'); -//var _ = require('lodash'); var controller = require('../../lib/controller'); var Tessel = require('../../lib/tessel/tessel'); var Seeker = require('../../lib/discover.js'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var logs = require('../../lib/logs'); -var Menu = require('terminal-menu'); // template for creating fake tessels function createFakeTessel(options) { @@ -15,9 +13,7 @@ function createFakeTessel(options) { connectionType: options.type || 'LAN', authorized: options.authorized !== undefined ? options.authorized : true, end: function() { - // returns a value, promize or thenable return Promise.resolve(); - // https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve } }); @@ -31,7 +27,7 @@ function createFakeTessel(options) { } tessel.serialNumber = serialNumber(options); tessel.name = options.name || 'a'; - + options.sandbox.stub(tessel, 'close', function() { return Promise.resolve(); }); @@ -59,7 +55,6 @@ exports['Tessel.seekTessels'] = { // copy the seeker events into the EventEmitter util.inherits(this.seeker, EventEmitter); - // setting up fake system for logging this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); @@ -77,7 +72,7 @@ exports['Tessel.seekTessels'] = { // Test is based on structure of controller.js Tessel.list tests (but documented!) oneAuthorizedLANTessel: function(test) { - test.expect(3); + test.expect(6); var a = createFakeTessel({ sandbox: this.sandbox, authorized: true, @@ -93,6 +88,11 @@ exports['Tessel.seekTessels'] = { test.equal(a.connections[0].authorized, true); // because we do not runHeuristics callCount is expected to be 0 test.equal(a.close.callCount, 0); + // validate logs + test.equal(this.logsInfo.callCount, 1); + test.equal(this.logsWarn.callCount, 0); + test.equal(this.logsBasic.callCount, 0); + a = null; test.done(); }.bind(this)); @@ -101,7 +101,7 @@ exports['Tessel.seekTessels'] = { this.activeSeeker.emit('tessel', a); }, oneNotAuthorizedLANTessel: function(test) { - test.expect(3); + test.expect(6); var a = createFakeTessel({ sandbox: this.sandbox, authorized: false, @@ -117,6 +117,11 @@ exports['Tessel.seekTessels'] = { test.equal(a.connections[0].authorized, false); // because we do not runHeuristics callCount is expected to be 0 test.equal(a.close.callCount, 0); + // validate logs + test.equal(this.logsInfo.callCount, 1); + test.equal(this.logsWarn.callCount, 0); + test.equal(this.logsBasic.callCount, 0); + test.done(); }.bind(this)); @@ -124,7 +129,7 @@ exports['Tessel.seekTessels'] = { this.activeSeeker.emit('tessel', a); }, oneTesselTwoConnections: function(test) { - test.expect(5); + test.expect(8); var a = createFakeTessel({ sandbox: this.sandbox, @@ -162,6 +167,10 @@ exports['Tessel.seekTessels'] = { test.equal(a.close.callCount, 0); // because we do not runHeuristics callCount is expected to be 0 test.equal(lan.close.callCount, 0); + // validate logs + test.equal(this.logsInfo.callCount, 1); + test.equal(this.logsWarn.callCount, 0); + test.equal(this.logsBasic.callCount, 0); // TODO: Because of the connections object of sameTessel isn't conjunct already this test has to be rewritten later test.done(); @@ -175,7 +184,7 @@ exports['Tessel.seekTessels'] = { }, multipeDifferentTessels: function(test) { - test.expect(6); + test.expect(9); var usb = createFakeTessel({ sandbox: this.sandbox, @@ -205,6 +214,10 @@ exports['Tessel.seekTessels'] = { test.equal(usb.close.callCount, 0); // because we do not runHeuristics callCount is expected to be 0 test.equal(lan.close.callCount, 0); + // validate logs + test.equal(this.logsInfo.callCount, 1); + test.equal(this.logsWarn.callCount, 0); + test.equal(this.logsBasic.callCount, 0); test.done(); }.bind(this)); @@ -212,80 +225,5 @@ exports['Tessel.seekTessels'] = { this.activeSeeker.emit('tessel', usb); this.activeSeeker.emit('tessel', lan); - - }, -}; - -exports['controller.root'] = { - - setUp: function(done) { - var self = this; - this.sandbox = sinon.sandbox.create(); - this.processOn = this.sandbox.stub(process, 'on'); - self.activeMenu = undefined; - self.menuLines = undefined; - // no display output necessary so overriding write - this.rtm = new Menu({ - width: 50, - x: 1, - y: 2, - bg: 'red' - }); - this.sandbox.stub(this.rtm, 'write', function Menu() { - this.write = function(msg) { - self.menuLines = msg.split('\n'); - return this; - }; - }); - - // copy the Menu events 'select' and 'close' into the EventEmitter - util.inherits(this.rtm, EventEmitter); - - // setting up fake system for logging - this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); - this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); - this.logsBasic = this.sandbox.stub(logs, 'basic', function() {}); - - //this. - done(); }, - tearDown: function(done) { - this.sandbox.restore(); - done(); - }, - addingMenuLine: function(test) { - // console.log('this: menu: ', this.rtm.items); - test.expect(3); - - this.rtm.add('MenulineOne\n'); - this.rtm.write(); - - test.equal(this.rtm.items.length, 1); - test.equal(this.rtm.x, 3); - test.equal(this.rtm.y, 4); - - test.done(); - } - }; -// t2 root --help - -// t2 root # only one tessel (authorized) - -// t2 root # only one tessel (not authorized) - -// t2 root # no tessel found - -// t2 root # two tessels (authorized) - -// t2 root # two tessels (not authorized) - -// t2 root # invalid id_rsa for authorized tessel ! (froud prevention) - -// t2 root # Accessing root... - -// t2 root --help # Usage: tessel root [-i ] [--help] - -// TODO: adding USB tests ! - -// TODO: running tests on Windows 7 and try catch ssh exists From a879c6700d69f3877f31bd7d0ec9030a3ee396c6 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 12:21:11 +0200 Subject: [PATCH 37/43] accidentally pushed the commit button... sorry for no comments --- test/unit/root.js | 156 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 test/unit/root.js diff --git a/test/unit/root.js b/test/unit/root.js new file mode 100644 index 00000000..7d632f44 --- /dev/null +++ b/test/unit/root.js @@ -0,0 +1,156 @@ +// unit test t2 root command +var sinon = require('sinon'); +var ctrl = require('../../lib/controller'); +var Tessel = require('../../lib/tessel/tessel'); +var Seeker = require('../../lib/discover.js'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +// due to the sinon stub objects on Menu we need to incrase the default max of 10 event listeners +EventEmitter.defaultMaxListeners = 100; + +var logs = require('../../lib/logs'); +var M = require('terminal-menu'); + +// template for creating fake tessels +function createFakeTessel(options) { + var tessel = new Tessel({ + connectionType: options.type || 'LAN', + authorized: options.authorized !== undefined ? options.authorized : true, + end: function() { + return Promise.resolve(); + } + + }); + + function serialNumber(options) { + if (options.type === 'USB') { + return options.serialNumber; + } else { + return false; + } + } + tessel.serialNumber = serialNumber(options); + tessel.name = options.name || 'a'; + + options.sandbox.stub(tessel, 'close', function() { + return Promise.resolve(); + }); + // return the fake tessel + return tessel; +} + +exports['controller.root'] = { + + setUp: function(done) { + + var self = this; + + this.sandbox = sinon.sandbox.create(); + this.processOn = this.sandbox.stub(process, 'on'); + // no display output necessary so overriding write + this.menu = new M({ + width: 50, + x: 1, + y: 2, + bg: 'red' + }); + // add sandbox-stub to this.rtm and override method 'write' + this.sandbox.stub(this.menu, 'add', function add() { + this.add = function(label) { + this.items.push({ + x: this.x, + y: this.y, + label: label + }); + this._fillLine(this.y); + this.y++; + }; + }); + this.sandbox.stub(this.menu, 'write', function write() { + this.write = function() { + return this; + }; + }); + this.sandbox.stub(this.menu, '_draw', function _draw() { + this._draw = function() { + return this; + }; + }); + // copy the Menu events 'select' and 'close' into the EventEmitter + // FIXME: This doesn't work due to the untestable way terminal-menu adds listener + // see: index.js Menu.prototype.add + + // util.inherits(this.rtm, EventEmitter); + + this.activeSeeker = undefined; + this.seeker = this.sandbox.stub(Seeker, 'TesselSeeker', function Seeker() { + this.start = function() { + self.activeSeeker = this; + return this; + }; + this.stop = function() { + return this; + }; + }); + // copy the seeker events into the EventEmitter + util.inherits(this.seeker, EventEmitter); + this.sandbox.stub(ctrl.ssh, 'pro', function() { + this.pro = function() { + return this; + }; + }); + + this.sandbox.stub(ctrl.ssh, 'clear', function() { + this.clear = function() { + return Promise.resolve(); + }; + }); + + this.sandbox.stub(ctrl.ssh, 'exit', function() { + this.exit = function(resolve) { + resolve(); + }; + }); + // setting up fake system for logging, so there is no console output while testing + this.logsWarn = this.sandbox.stub(logs, 'warn', function() {}); + this.logsInfo = this.sandbox.stub(logs, 'info', function() {}); + this.logsBasic = this.sandbox.stub(logs, 'basic', function() {}); + + done(); + }, + tearDown: function(done) { + this.sandbox.restore(); + done(); + }, + singleTesselNoMenu: function(test) { + var LanTessel = createFakeTessel({ + sandbox: this.sandbox, + authorized: true, + name: 'LanTessel', + type: 'LAN' + }); + test.expect(7); + ctrl.root({ + timeout: 0.3, + path: '~/.tessel/id_rsa', + menu: this.menu + }).then(function(obj) { + // wakeup the stubid ... stub + this.menu.add(); + // add first item + test.equal(this.menu.items.length, 0); + test.equal(this.menu.x, 3); + test.equal(this.menu.y, 3); + // + this.menu = null; + delete this.menu; + + test.equal(obj.opts.timeout, 0.3); + test.equal(obj.tessels.length, 1); + test.equal(LanTessel.connections[0].authorized, true); + test.equal(LanTessel, obj.tessels[0]); + test.done(); + }.bind(this)); + this.activeSeeker.emit('tessel', LanTessel); + } +}; From 2cb829b2e8093a8afade332aa1dfdb3ba508fe99 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 13:20:47 +0200 Subject: [PATCH 38/43] mixed up reject and resolve usage ;-) --- lib/controller.js | 2 +- test/unit/root.js | 2 +- test/unit/seekTessels.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/controller.js b/lib/controller.js index f5fdbc92..4d6a584a 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -293,7 +293,7 @@ controller.root = function(opts) { } else { logs.warn('Sorry, you are not authorized!'); logs.info('"t2 key generate" might help :-)'); - reject(); + resolve(); } } else { // everything works fine, but no tessels found diff --git a/test/unit/root.js b/test/unit/root.js index 7d632f44..b8773572 100644 --- a/test/unit/root.js +++ b/test/unit/root.js @@ -31,7 +31,7 @@ function createFakeTessel(options) { } tessel.serialNumber = serialNumber(options); tessel.name = options.name || 'a'; - + options.sandbox.stub(tessel, 'close', function() { return Promise.resolve(); }); diff --git a/test/unit/seekTessels.js b/test/unit/seekTessels.js index 0c06edb2..cb5ec316 100644 --- a/test/unit/seekTessels.js +++ b/test/unit/seekTessels.js @@ -27,7 +27,7 @@ function createFakeTessel(options) { } tessel.serialNumber = serialNumber(options); tessel.name = options.name || 'a'; - + options.sandbox.stub(tessel, 'close', function() { return Promise.resolve(); }); From 6f6dbffdb6dcc6065335d9866f192f9d7f8bcd47 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 13:44:37 +0200 Subject: [PATCH 39/43] Solving Windows related hostname resolution bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host / ip translation on windows 7 didn’t work with the node ssh … so we need to use the IP directly (at least within sub process is connecting) --- lib/root_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/root_controller.js b/lib/root_controller.js index 875bd552..e3f0f3cc 100644 --- a/lib/root_controller.js +++ b/lib/root_controller.js @@ -40,7 +40,7 @@ controller.ssh = { var ch = require('child_process') .spawn('ssh', ['-i', opts.path, - 'root@' + tessels[id].connections[0].host + 'root@' + tessels[id].connections[0].ip ], { stdio: 'inherit' }); From 17e0914f882ddd57957bb142c1ce7f03e624bbd5 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 14:15:45 +0200 Subject: [PATCH 40/43] not necessary --- lib/tessel/root.js | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 lib/tessel/root.js diff --git a/lib/tessel/root.js b/lib/tessel/root.js deleted file mode 100644 index 7dd26efa..00000000 --- a/lib/tessel/root.js +++ /dev/null @@ -1,3 +0,0 @@ -/*var Tessel = require('./tessel'), - logs = require('../logs'); -*/ From b882da5313c78c08f7a265cfdc55a0d54ef19b11 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 14:16:42 +0200 Subject: [PATCH 41/43] not my work (copied for testing) --- lib/update-fetch.js | 129 -------------------------------------------- 1 file changed, 129 deletions(-) delete mode 100644 lib/update-fetch.js diff --git a/lib/update-fetch.js b/lib/update-fetch.js deleted file mode 100644 index 48b03881..00000000 --- a/lib/update-fetch.js +++ /dev/null @@ -1,129 +0,0 @@ -var path = require('path'); -var urljoin = require('url-join'); -var request = require('request'); -var gunzip = require('zlib').createGunzip(); -var extract = require('tar-stream').extract(); -var logs = require('./logs'); -var streamToBuffer = require('stream-to-buffer'); - - -const BUILD_SERVER_ROOT = 'https://builds.tessel.io/t2'; -const FIRMWARE_PATH = urljoin(BUILD_SERVER_ROOT, 'firmware'); -const BUILDS_JSON_FILE = urljoin(FIRMWARE_PATH, 'builds.json'); -const OPENWRT_BINARY_FILE = 'openwrt.bin'; -const FIRMWARE_BINARY_FILE = 'firmware.bin'; - -/* - Requests a list of available builds from the - build server. Returns list of build names in - a Promise. -*/ -function requestBuildList() { - return new Promise(function(resolve, reject) { - // Fetch the list of available builds - request.get(BUILDS_JSON_FILE, function(err, response, body) { - if (err) { - return reject(err); - } - - var outcome = reviewResponse(response); - - // If there wasn't an issue with the request - if (outcome.success) { - // Resolve with the parsed data - try { - resolve(JSON.parse(body)); - } - // If the parse failed, reject - catch (err) { - reject(err); - } - } else { - reject(outcome.reason); - } - }); - }); -} - -/* - Accepts a build name and attempts to fetch - the build images from the server. Returns build contents - in a Promise -*/ -function fetchBuild(build) { - return new Promise(function(resolve, reject) { - logs.info('Beginning update download. This could take a couple minutes..'); - - var binaries = { - firmware: undefined, - openwrt: undefined, - }; - - // Fetch the list of available builds - extract.on('entry', function(header, stream, callback) { - // The buffer to save incoming data to - // The filename of this entry - var filename = path.basename(header.name); - // This entry is the openwrt binary - if (filename === OPENWRT_BINARY_FILE) { - // Save incoming data to the openwrt buffer - streamToBuffer(stream, function(err, buffer) { - binaries.openwrt = buffer; - callback(); - }); - } - // This entry is the firmware binary - else if (filename === FIRMWARE_BINARY_FILE) { - // Save incoming data to the firmware buffer - streamToBuffer(stream, function(err, buffer) { - binaries.firmware = buffer; - callback(); - }); - } else { - callback(); - } - }); - - extract.once('finish', function() { - if (!binaries.firmware.length || !binaries.openwrt.length) { - return reject(new Error('Fetched binary wasn\'t formatted properly.')); - } else { - logs.info('Download complete!'); - return resolve(binaries); - } - }); - - // Make a request to our build server for this particular sha - var req = request.get(urljoin(FIRMWARE_PATH, build.sha + '.tar.gz')); - // unzip and extract the binary tarball - req.pipe(gunzip).pipe(extract); - }); -} - -function reviewResponse(response) { - var outcome = { - success: true, - }; - - // If there was an issue with the server endpoint, reject - if (response.statusCode !== 200) { - outcome.success = false; - outcome.reason = 'Invalid status code on build server request: ' + response.statusCode; - } - - return outcome; -} - - - -function findBuild(builds, property, value) { - return builds.filter(function(build) { - return build[property] === value; - })[0]; -} - -module.exports.requestBuildList = requestBuildList; -module.exports.fetchBuild = fetchBuild; -module.exports.findBuild = findBuild; -module.exports.OPENWRT_BINARY_FILE = OPENWRT_BINARY_FILE; -module.exports.FIRMWARE_BINARY_FILE = FIRMWARE_BINARY_FILE; From eb50c0f01a8a509f8e88845e1b07d1f107c5b248 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 14:19:17 +0200 Subject: [PATCH 42/43] logs.error isn't known so changed to logs.warn --- lib/usb_connection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/usb_connection.js b/lib/usb_connection.js index 478e54f0..0ab8fe81 100644 --- a/lib/usb_connection.js +++ b/lib/usb_connection.js @@ -90,7 +90,7 @@ USB.Connection.prototype._receiveMessages = function() { return; } else { // Otherwise print the error - logs.err('Error reading USB message endpoint:', e); + logs.warn('Error reading USB message endpoint:', e); // Return a non-zero return code process.exit(-5); } @@ -105,7 +105,7 @@ USB.Connection.prototype.open = function() { self.device.open(); } catch (e) { if (e.message === 'LIBUSB_ERROR_ACCESS' && process.platform === 'linux') { - logs.error('Please run `sudo tessel install-drivers` to fix device permissions.\n(Error: could not open USB device.)'); + logs.warn('Please run `sudo tessel install-drivers` to fix device permissions.\n(Error: could not open USB device.)'); } // Reject if error return reject(e, self); From cdd15b238ec0e32b97aea6afbfce87e545b8d589 Mon Sep 17 00:00:00 2001 From: Daniel Bunzendahl Date: Wed, 9 Sep 2015 14:52:51 +0200 Subject: [PATCH 43/43] due to editing usb_connection tests need to change --- test/unit/usb_connection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/usb_connection.js b/test/unit/usb_connection.js index 2c0f543c..64275e8a 100644 --- a/test/unit/usb_connection.js +++ b/test/unit/usb_connection.js @@ -100,7 +100,7 @@ exports['USB.Connection.prototype._write'] = { exports['USB.Connection.prototype._receiveMessages'] = { setUp: function(done) { this.sandbox = sinon.sandbox.create(); - this.err = this.sandbox.stub(logs, 'err'); + this.warn = this.sandbox.stub(logs, 'warn'); this.processExit = this.sandbox.stub(process, 'exit'); this.usbConnection = new USB.Connection({}); this.usbConnection.epIn = new Emitter(); @@ -141,7 +141,7 @@ exports['USB.Connection.prototype._receiveMessages'] = { this.usbConnection.epIn.emit('error', 'oh no!'); - test.equal(this.err.callCount, 1); + test.equal(this.warn.callCount, 1); test.equal(this.processExit.callCount, 1); test.done(); }, @@ -155,7 +155,7 @@ exports['USB.Connection.prototype._receiveMessages'] = { // The immediate return prevents these from being // called from in the error handler - test.equal(this.err.callCount, 0); + test.equal(this.warn.callCount, 0); test.equal(this.processExit.callCount, 0); test.done(); },