forked from tessel/t2-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
controller.js
999 lines (893 loc) · 33.1 KB
/
controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
// System Objects
var cp = require('child_process');
var util = require('util');
// Third Party Dependencies
var async = require('async');
var colors = require('colors');
var inquirer = require('inquirer');
var sprintf = require('sprintf-js').sprintf;
var semver = require('semver');
// Internal
var discover = require('./discover');
var logs = require('./logs');
var updates = require('./update-fetch');
var provision = require('./tessel/provision');
var Tessel = require('./tessel/tessel');
var controller = {};
var responses = {
noAuth: 'No Authorized Tessels Found.',
auth: 'No Tessels Found.'
};
// Wrapper function for Tessel.list to set SSH key path
controller.list = function(opts) {
return controller.defaultHelpers(opts, Tessel.list);
};
// Wrapper function for Tessel.get to set SSH key path
controller.get = function(opts) {
return controller.defaultHelpers(opts, Tessel.get);
};
// Calls any helpers for the get and list codepaths
controller.defaultHelpers = function(opts, next) {
// First disable output if necessary
return controller.outputHelper(opts)
// Then make sure our SSH Key path is updated
.then(() => controller.keyHelper(opts))
// before continuing to the next function
.then(() => next(opts));
};
controller.keyHelper = function(opts) {
var keyPromise = Promise.resolve();
if (opts.key) {
// Set the default SSH key path before we search
keyPromise = provision.setDefaultKey(opts.key);
}
return keyPromise;
};
controller.outputHelper = function(opts) {
// If the user doesn't want output
if (opts.output === false) {
// Turn off the logs
logs.disable();
}
return Promise.resolve();
};
controller.setupLocal = function(opts) {
return provision.setupLocal(opts);
};
Tessel.list = function(opts) {
return new Promise(function(resolve, reject) {
// Grab all attached Tessels
logs.info('Searching for nearby Tessels...');
// Keep a list of all the Tessels we discovered
var foundTessels = [];
// Options for Tessel discovery
var seekerOpts = {
timeout: opts.timeout * 1000,
usb: opts.usb,
lan: opts.lan,
authorized: undefined
};
// Start looking for Tessels
var seeker = new discover.TesselSeeker().start(seekerOpts);
var noTessels = opts.authorized ?
responses.noAuth :
responses.auth;
// 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 `t2 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
seeker.once('end', function stopSearch() {
// If there were no Tessels found
if (foundTessels.length === 0) {
// Report the sadness
return reject(noTessels);
} else if (foundTessels.length === 1) {
// Close all opened connections and resolve
controller.closeTesselConnections(foundTessels)
.then(() => resolve(foundTessels));
}
// If we have only one Tessel or two Tessels with the same name (just USB and LAN)
else if (foundTessels.length === 1 ||
(foundTessels.length === 2 && foundTessels[0].name === foundTessels[1].name)) {
// Close all opened connections and resolve
controller.closeTesselConnections(foundTessels)
.then(() => resolve(foundTessels));
}
// Otherwise
else {
logs.info('Multiple Tessels found.');
// Figure out which Tessel will be selected
return controller.runHeuristics(opts, foundTessels)
.then(function logSelected(tessel) {
// Report that selected Tessel to the user
logs.info('Will default to %s.', tessel.name);
})
.catch(function(err) {
if (!(err instanceof controller.HeuristicAmbiguityError)) {
return controller.closeTesselConnections(foundTessels)
.then(() => reject(err));
}
})
.then(function() {
// Helpful instructions on how to switch
logs.info('Set default Tessel with environment variable (e.g. "export TESSEL=bulbasaur") or use the --name flag.');
// Close all opened connections and resolve
controller.closeTesselConnections(foundTessels).then(() => resolve(foundTessels));
});
}
});
// Stop the search if CTRL+C is hit
process.once('SIGINT', function() {
// If the seeker exists (it should)
if (seeker !== undefined) {
// Stop looking for more Tessels
seeker.stop();
}
});
});
};
Tessel.get = function(opts) {
return new Promise(function(resolve, reject) {
logs.info('Looking for your Tessel...');
// Collection variable as more Tessels are found
var tessels = [];
// Store the amount of time to look for Tessel in seconds
var seekerOpts = {
timeout: (opts.timeout || 2) * 1000,
usb: opts.usb,
lan: opts.lan,
authorized: true
};
if (opts.authorized !== undefined) {
seekerOpts.authorized = opts.authorized;
}
// Create a seeker object and start detecting any Tessels
var seeker = new discover.TesselSeeker().start(seekerOpts);
var noTessels = opts.authorized ?
responses.noAuth :
responses.auth;
function searchComplete() {
// If we found no Tessels
if (tessels.length === 0) {
// Report it
return reject(noTessels);
}
// The name match for a given Tessel happens upon discovery, not at
// the completion of discovery. So if we got to this point, no Tessel
// was found with that name
else if (opts.name !== undefined) {
return reject('No Tessel found by the name ' + opts.name);
}
// If there was only one Tessel
else if (tessels.length === 1) {
// Return it immediately
logAndFinish(tessels[0]);
}
// Otherwise
else {
// Combine the same Tessels into one object
return controller.reconcileTessels(tessels)
.then(function(reconciledTessels) {
tessels = reconciledTessels;
// Run the heuristics to pick which Tessel to use
return controller.runHeuristics(opts, tessels)
.then(function finalSection(tessel) {
return logAndFinish(tessel);
})
.catch(function(err) {
if (err instanceof controller.HeuristicAmbiguityError) {
var map = {};
// Open up an interactive menu for the user to choose
return controller.menu({
prefix: colors.grey('INFO '),
prompt: {
name: 'selected',
type: 'list',
message: 'Which Tessel do want to use?',
choices: tessels.map(function(tessel, i) {
var isLAN = !!tessel.lanConnection;
var isAuthorized = isLAN && tessel.lanConnection.authorized;
var authorization = isAuthorized ? '' : '(not authorized)';
var display = sprintf(
'\t%s\t%s\t%s',
tessel.name,
tessel.connection.connectionType,
authorization
);
// Map displayed name to tessel index
map[display] = i;
return display;
})
},
translate: function(answer) {
return tessels[map[answer.selected]];
}
}).then(function(tessel) {
if (!tessel) {
return controller.closeTesselConnections(tessels)
.then(function() {
return reject('No Tessel selected, mission aborted!');
});
} else {
// Log we found it and return it to the caller
return logAndFinish(tessel);
}
});
} else {
controller.closeTesselConnections(tessels)
.then(() => reject(err));
}
});
});
}
}
function finishSearchEarly(tessel) {
// Remove this listener because we don't need to search for the Tessel
seeker.removeListener('end', searchComplete);
// Stop searching
seeker.stop();
// Send this Tessel back to the caller
logAndFinish(tessel);
}
// When we find Tessels
seeker.on('tessel', function(tessel) {
tessel.setLANConnectionPreference(opts.lanPrefer);
// Check if this name matches the provided option (if any)
// This speeds up development by immediately ending the search
if (opts.name && opts.name === tessel.name) {
finishSearchEarly(tessel);
}
// If we just found a USB connection and should prefer it
else if (!opts.name && tessel.usbConnection !== undefined && !opts.lanPrefer) {
// Finish early with this Tessel
finishSearchEarly(tessel);
}
// Otherwise
else {
// Store this Tessel with the others
tessels.push(tessel);
}
});
seeker.once('end', searchComplete);
// Accesses `tessels` in closure
function logAndFinish(tessel) {
// The Tessels that we won't be using should have their connections closed
var connectionsToClose = tessels;
if (tessel) {
logs.info(sprintf('Connected to %s.', tessel.name));
connectionsToClose.splice(tessels.indexOf(tessel), 1);
controller.closeTesselConnections(connectionsToClose)
.then(function() {
return resolve(tessel);
});
} else {
logs.info('Please specify a Tessel by name [--name <tessel name>]');
controller.closeTesselConnections(connectionsToClose)
.then(function() {
return reject('Multiple possible Tessel connections found.');
});
}
}
});
};
/*
1. Fetches a Tessel
2. Runs a given function that returns a promise
3. Whenever either a SIGINT is received, the provided promise resolves, or an error was thrown
4. All the open Tessel connections are closed
5. The command returns from whence it came (so the process can be closed)
*/
controller.standardTesselCommand = function(opts, command) {
return new Promise(function(resolve, reject) {
// Fetch a Tessel
return controller.get(opts)
// Once we have it
.then(function(tessel) {
// Create a promise for a sigint
var sigintPromise = new Promise(function(resolve) {
process.once('SIGINT', resolve);
});
// It doesn't matter whether the sigint finishes first or the provided command
Promise.race([sigintPromise, command(tessel)])
// Once one completes
.then(function(optionalValue) {
// Close the open Tessel connection
return controller.closeTesselConnections([tessel])
// Then resolve with the optional value
.then(function closeComplete() {
return resolve(optionalValue);
});
})
// If something threw an error
.catch(function(err) {
// Still close the open connections
return controller.closeTesselConnections([tessel])
// Then reject with the error
.then(function closeComplete() {
return reject(err);
});
});
})
.catch(reject);
}).catch(function(error) {
return Promise.reject(error);
});
};
/*
Takes a list of Tessels with connections that
may or may not be open and closes them
*/
controller.closeTesselConnections = function(tessels) {
return new Promise(function(resolve, reject) {
async.each(tessels, function closeThem(tessel, done) {
// If not an unauthorized LAN Tessel, it's connected
if (!(tessel.connection.connectionType === 'LAN' &&
!tessel.connection.authorized)) {
// Close the connection
return tessel.close()
.then(done, done);
} else {
done();
}
},
function closed(err) {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
/*
Takes list of USB and LAN Tessels and merges
and Tessels that are the same origin with difference
connection methods.
Assumes tessel.getName has already been called for each.
*/
controller.reconcileTessels = function(tessels) {
return new Promise(function(resolve) {
// If there is only one, just return
if (tessels.length <= 1) {
return resolve(tessels);
}
var accounts = {};
var reconciled = tessels.reduce(function(accum, tessel) {
if (accounts[tessel.name]) {
// Updates tessels in accum by reference
accounts[tessel.name].addConnection(tessel.connection);
} else {
accounts[tessel.name] = tessel;
accum.push(tessel);
}
return accum;
}, []);
resolve(reconciled);
});
};
/*
0. using the --name flag
1. an environment variable in the terminal, set as export TESSEL=Bulbasaur
2. if there is a single tessel connected over USB, prefer that one
3. if there is only one tessel visible, use that one
4. if none of the above are found run tessel list automatically and prompt selection
*/
// Called when multiple tessels are found are we need to figure out
// Which one the user should act upon
controller.runHeuristics = function(opts, tessels) {
var NAME_OPTION_PRIORITY = 0;
var ENV_OPTION_PRIORITY = 1;
var USB_CONN_PRIORITY = 2;
var LAN_CONN_PRIORITY = 3;
// For each of the Tessels found
return Promise.resolve(tessels.reduce(function(collector, tessel) {
// Create an object to keep track of what priority this Tessel has
// The lower the priority, the more likely the user wanted this Tessel
var entry = {
tessel: tessel,
priority: undefined
};
// If a name option was provided and it matches this Tessel
if (opts.name && opts.name === tessel.name) {
// Set it to the highest priority
entry.priority = NAME_OPTION_PRIORITY;
return collector.concat([entry]);
}
// If an environment variable was set and it equals this Tessel
if (process.env.TESSEL && process.env.TESSEL === tessel.name) {
// Mark the priority level
entry.priority = ENV_OPTION_PRIORITY;
return collector.concat([entry]);
}
// If this has a USB connection
if (tessel.usbConnection) {
// Mark the priority
entry.priority = USB_CONN_PRIORITY;
return collector.concat([entry]);
}
// This is a LAN connection so give it the lowest priority
entry.priority = LAN_CONN_PRIORITY;
return collector.concat([entry]);
}, []))
.then(function selectTessel(collector) {
var usbFound = false;
var lanFound = false;
// Sort all of the entries by priority
collector.sort((a, b) => {
return a.priority > b.priority;
});
// For each entry
for (var i = 0; i < collector.length; i++) {
var collectorEntry = collector[i];
// If this is a name option or environment variable option
if (collectorEntry.priority === NAME_OPTION_PRIORITY ||
collectorEntry.priority === ENV_OPTION_PRIORITY) {
// Return the Tessel and stop searching
return collectorEntry.tessel;
}
// If this is a USB Tessel
else if (collectorEntry.priority === USB_CONN_PRIORITY) {
// And no other USB Tessels have been found yet
if (usbFound === false) {
// Mark it as found and continue
usbFound = true;
}
// We have multiple USB Tessels which is an issue
else {
// Return nothing because the user needs to be more specific
return Promise.reject(new controller.HeuristicAmbiguityError());
}
}
// If this is a LAN Tessel
else if (collectorEntry.priority === LAN_CONN_PRIORITY) {
// And we haven't found any other Tessels
if (lanFound === false) {
// Mark it as found and continue
lanFound = true;
}
// We have multiple LAN Tessels which is an issue
// If a USB connection wasn't found, we have too much ambiguity
else if (!usbFound) {
// Return nothing because the user needs to be more specific
return Promise.reject(new controller.HeuristicAmbiguityError());
}
}
}
// At this point, we know that no name option or env variable was set
// and we know that there is only one USB and/or on LAN Tessel
// We'll return the highest priority available
return collector[0].tessel;
});
};
controller.HeuristicAmbiguityError = function() {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = 'It is unclear which device should be operated upon.';
};
util.inherits(controller.HeuristicAmbiguityError, Error);
controller.provisionTessel = function(opts) {
opts = opts || {};
return new Promise(function(resolve, reject) {
if (Tessel.isProvisioned()) {
if (opts.force) {
cp.exec('rm -r ' + Tessel.LOCAL_AUTH_PATH, function(error) {
if (error) {
reject(error);
} else {
resolve();
}
});
} else {
// ~/.tessel exists with keys
resolve();
}
} else {
// There is no ~/.tessel
resolve();
}
})
.then(function executeProvision() {
// We should only be using a USB connection
opts.usb = true;
opts.authorized = false;
// Fetch a Tessel
return controller.standardTesselCommand(opts, function(tessel) {
// Provision Tessel with SSH keys
return tessel.provisionTessel(opts);
});
});
};
controller.deployScript = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
// Deploy a path to Tessel
return tessel.deployScript(opts);
});
};
controller.restartScript = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
// Tell Tessel to restart an existing script
return tessel.restartScript(opts);
});
};
controller.eraseScript = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
// Tell Tessel to erase any pushed script
return tessel.eraseScript(opts, false);
});
};
controller.renameTessel = function(opts) {
opts = opts || {};
opts.authorized = true;
// Grab the preferred tessel
return new Promise(function(resolve, reject) {
if (!opts.reset && !opts.newName) {
reject('A new name must be provided.');
} else {
if (!opts.reset && !Tessel.isValidName(opts.newName)) {
reject('Invalid name: ' + opts.newName + '. The name must be a valid hostname string. See http://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names.');
} else {
resolve();
}
}
})
.then(function executeRename() {
return controller.standardTesselCommand(opts, function(tessel) {
return tessel.rename(opts);
});
});
};
controller.printAvailableNetworks = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
// Ask Tessel what networks it finds in a scan
return tessel.findAvailableNetworks()
.then(function(networks) {
logs.info('Currently visible networks (' + networks.length + '):');
// Print out networks
networks.forEach(function(network) {
logs.info('\t', network.ssid, '(' + network.quality + ')');
});
});
});
};
controller.getWifiInfo = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
return tessel.getWifiInfo()
.then(function(network) {
// Grab inet lines, flatmap them, remove empty
// Wanted to do this with awk and cut inside commands.js
var ips = network.ips.filter(function(item) {
return /inet/.exec(item);
})
.map(function(line) {
return line.split(' ');
})
.reduce(function(a, b) {
return a.concat(b);
})
.filter(function(item) {
return /addr/.exec(item);
})
.map(function(chunk) {
return chunk.split(':')[1];
})
.filter(function(addr) {
return addr.length;
});
logs.info('Connected to "' + network.ssid + '"');
ips.forEach(function(ip) {
logs.info('IP Address: ' + ip);
});
logs.info('Signal Strength: (' + network.quality + '/' + network.quality_max + ')');
logs.info('Bitrate: ' + Math.round(network.bitrate / 1000) + 'mbps');
})
.then(function() {
return controller.closeTesselConnections([tessel]);
});
});
};
controller.connectToNetwork = function(opts) {
opts.authorized = true;
var ssid = opts.ssid;
var password = opts.password;
var security = opts.security;
var securityOptions = ['none', 'wep', 'psk', 'psk2', 'wpa', 'wpa2'];
return new Promise(function(resolve, reject) {
if (!ssid) {
return reject('Invalid credentials: must set SSID with the -n or --ssid option.');
}
if (security && !password) {
return reject('Invalid credentials: must set a password with the -p or --password option.');
}
if (security && securityOptions.indexOf(security) < 0) {
return reject(`"${security}" is not a valid security option. Please choose on of the following: ${securityOptions.join(', ')}`);
}
resolve();
})
.then(function() {
return controller.standardTesselCommand(opts, function(tessel) {
return tessel.connectToNetwork(opts);
});
});
};
controller.setWiFiState = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
return tessel.setWiFiState(opts.on);
});
};
controller.createAccessPoint = function(opts) {
opts.authorized = true;
var ssid = opts.ssid;
var password = opts.pass;
var security = opts.security;
var securityOptions = ['none', 'wep', 'psk', 'psk2', 'wpa', 'wpa2'];
return new Promise(function(resolve, reject) {
if (!ssid) {
reject('Invalid credentials. Must set ssid');
}
if (security && !password) {
reject('Invalid credentials. Must set a password with security option');
}
if (security && securityOptions.indexOf(security) < 0) {
reject(`${security} is not a valid security option. Please choose on of the following: ${securityOptions.join(', ')}`);
}
resolve();
})
.then(function() {
return controller.standardTesselCommand(opts, (tessel) => tessel.createAccessPoint(opts));
});
};
controller.enableAccessPoint = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, (tessel) => tessel.enableAccessPoint());
};
controller.disableAccessPoint = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, (tessel) => tessel.disableAccessPoint());
};
controller.getAccessPointInfo = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
return tessel.getAccessPointInfo()
.then((ap) => {
if (ap.ssid) {
logs.info(`SSID: ${ap.ssid}`);
if (ap.key || ap.encryption !== 'none') {
logs.info(`Password: ${ap.key}`);
}
logs.info(`Security: ${ap.encryption}`);
logs.info(`IP Address: ${ap.ip}`);
logs.info(`State: ${(!Number(ap.disabled) ? 'Enabled' : 'Disabled')}`);
} else {
logs.info(`${tessel.name} is not configured as an access point (run "t2 ap --help" to learn more)`);
}
})
.catch((error) => {
throw error;
});
});
};
/*
The T2 root command is used to login into the Tessel's root shell.
*/
controller.root = function(opts) {
// Only give us LAN connections
opts.lan = true;
// We must already be authorized
opts.authorized = true;
// Fetch a Tessel
return controller.standardTesselCommand(opts, function(tessel) {
logs.info('Starting SSH Session on Tessel. Type \'exit\' at the prompt to end.');
return new Promise(function(resolve, reject) {
// Spawn a new SSH process
var child = cp.spawn('ssh', ['-i',
// Use the provided key path
Tessel.LOCAL_AUTH_KEY,
// Connect to the Tessel's IP Address
'root@' + tessel.lanConnection.ip
], {
// Pipe our standard streams to the console
stdio: 'inherit'
});
// Report any errors on connection
child.once('error', function(err) {
return reject('Failed to start SSH session: ' + err.toString());
});
// Close the process when we no longer can communicate with the process
child.once('close', resolve);
child.once('disconnect', resolve);
});
});
};
controller.printAvailableUpdates = function() {
return updates.requestBuildList().then(function(builds) {
logs.info('Latest builds:');
// Reverse the list to show the latest version first
builds.reverse().slice(-10).forEach(function(build) {
logs.basic('\t Version:', build.version, '\tPublished:', new Date(build.released).toLocaleString());
});
});
};
controller.update = function(opts) {
opts.authorized = true;
opts.lanPrefer = true;
return controller.standardTesselCommand(opts, function(tessel) {
return new Promise(function updateProcess(resolve, reject) {
// If it's not connected via USB, we can't update it
if (!tessel.usbConnection) {
return reject('Must have Tessel connected over USB to complete update. Aborting update.');
}
// // If this Tessel isn't connected to the LAN
if (!tessel.lanConnection || !tessel.lanConnection.authorized) {
// Reject because USB updates are broken...
return reject('No LAN connection found. USB-only updates do not work yet. Please ensure Tessel is connected to wifi and try again');
}
return updates.requestBuildList().then(function(builds) {
var version = opts.version || 'latest';
var versionFromSHA = Promise.resolve(version);
// If we aren't forcing, we'll want to get the current SHA on Tessel
if (!opts.force) {
// Once we have the Tessel
// Over-ride the resolved Promise
versionFromSHA = new Promise(function(resolve, reject) {
// Figure out what commit SHA is running on it
return tessel.fetchCurrentBuildInfo()
// Once we have the current SHA, provide the version
.then(function(currentSHA) {
return resolve(updates.findBuild(builds, 'sha', currentSHA));
})
.catch(function(err) {
// If there was an error because the version file doesn't exist
if (err.message.search('No such file or directory') !== -1) {
// Warn the user
logs.warn('Could not find firmware version on', tessel.name);
if (opts.force !== false) {
// Force the update
opts.force = true;
// Notify the user
logs.warn('Forcefully updating...');
// Resolve instead of reject (the string isn't used anywhere)
return resolve('unknown version');
} else {
// Reject because the user specifically did not want to force
return reject(err);
}
} else {
// Reject because an unknown error occurred
return reject(err);
}
});
});
}
return versionFromSHA.then(function(currentVersionInfo) {
var build = updates.findBuild(builds, 'version', version);
var verifiedVersion;
// If the update is forced or this version was requested,
// and a valid build exists for the version provided.
if (version && build) {
// Fetch and Update with the requested version
return controller.updateTesselWithVersion(opts.force, tessel, currentVersionInfo.version, build);
} else {
// If they have requested the latest firmware
if (version === 'latest') {
build = builds[builds.length - 1];
verifiedVersion = build.version;
} else {
// They provided a valid version that matches a known build.
if (build) {
verifiedVersion = build.version;
}
}
// If we've reached this point and no verified version has not
// been identified, then we need to abord the operation and
// notify the user.
if (!verifiedVersion) {
return reject('The requested build was not found. Please see the available builds with `t2 update -l`.');
}
// Check if the current build is the same or newer if this isn't a forced update
if (!opts.force && semver.gte(currentVersionInfo.version, verifiedVersion)) {
// If it's not, close the Tessel connection and print the error message
var message = tessel.name + ' is already on the latest firmware version (' + currentVersionInfo.version + '). You can force an update with "t2 update --force".';
logs.warn(message);
return resolve();
} else {
if (!opts.force) {
// If it is a newer version, let's update...
logs.info('New firmware version found...' + verifiedVersion);
}
logs.info('Updating ' + tessel.name + ' to latest version (' + verifiedVersion + ')...');
// Fetch the requested version
return controller.updateTesselWithVersion(opts.force, tessel, currentVersionInfo.version, build);
}
}
});
})
.then(resolve)
.catch(reject);
});
});
};
controller.updateTesselWithVersion = function(force, tessel, currentVersion, build) {
// Fetch the requested build
return updates.fetchBuild(build)
.then(function startUpdate(image) {
// Update Tessel with it
return tessel.update(image)
// Log that the update completed
.then(function logCompletion() {
if (!force) {
logs.info('Updated', tessel.name, 'from ', currentVersion, ' to ', build.version);
} else {
logs.info('Force updated', tessel.name, 'to version', build.version);
}
});
});
};
controller.tesselFirmwareVerion = function(opts) {
opts.authorized = true;
return controller.standardTesselCommand(opts, function(tessel) {
// Grab the version information
return tessel.fetchCurrentBuildInfo()
.then(function(versionSha) {
return updates.requestBuildList().then(function(builds) {
// Figure out what commit SHA is running on it
var version = updates.findBuild(builds, 'sha', versionSha).version;
logs.info('Tessel [' + tessel.name + '] version: ' + version);
});
})
.catch(function() {
logs.info('Tessel [' + tessel.name + '] version: unknown');
});
});
};
/*
controller.menu({
// Custom prefix
prefix: colors.grey('INFO '),
prompt: [inquirer.prompt options],
// Custom answer -> data translation
translate: function(answer) {
// answer =>
// { [prompt.name]: ... }
return answer[prompt.name];
}
}) => Promise
*/
controller.menu = function(setup) {
var options = setup.prompt;
if (options.type === 'list') {
options.choices.push('\tExit');
}
// Enforce a customized prompt prefix
inquirer.prompt.prompts[options.type].prototype.prefix = function(str) {
// String() used to coerce an `undefined` to ''. Do not change.
return String(setup.prefix) + str;
};
return new Promise(function(resolve) {
inquirer.prompt([options], function(answer) {
if (setup.translate) {
resolve(setup.translate(answer));
} else {
resolve(answer);
}
});
});
};
module.exports = controller;
// Shared exports
module.exports.listTessels = controller.list;
module.exports.getTessel = controller.get;