UNPKG

12.2 kBJavaScriptView Raw
1//
2// Copyright (c) Microsoft and contributors. All rights reserved.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17var __ = require('underscore');
18var tty = require('tty');
19var fs = require('fs');
20var util = require('util');
21var tty = require('tty');
22
23/*jshint camelcase:false*/
24var child_process = require('child_process');
25var nonInteractiveMode = process.env['AZURE_NON_INTERACTIVE_MODE'];
26
27//this replaces 'command' package's 'prompt'/'confirm'/'choose', which don't
28//work well with winston's async logging.
29var prompt_pkg = require('prompt');
30
31var log = require('./logging');
32var utils = require('./utils');
33
34function Interactor(cli) {
35 this.cli = cli;
36 this.istty1 = tty.isatty(1);
37
38 this._initProgressBars();
39}
40
41function checkNonInteractiveMode(requiredVar) {
42 if (nonInteractiveMode) {
43 throw new Error(util.format('Currently, the CLI is being run in \'Non Interactive Mode\'. ' +
44 'For the current command, \'%s\' is a required parameter (see the help). ' +
45 'Please provide it while executing the command. If you wish '+
46 'to be in \'Interactive Mode\' so that the CLI can prompt you for ' +
47 'missing required parameters, please unset the environment variable '+
48 '\'AZURE_NON_INTERACTIVE_MODE\'.', requiredVar));
49 }
50}
51
52__.extend(Interactor.prototype, {
53
54 _initProgressBars: function() {
55 var self = this;
56 self.progressChars = ['-', '\\', '|', '/'];
57 self.progressIndex = 0;
58
59 self.clearBuffer = new Buffer(79);
60 self.clearBuffer.fill(' ');
61 self.clearBuffer = self.clearBuffer.toString();
62 },
63
64 _drawAndUpdateProgress: function() {
65 var self = this;
66 if (nonInteractiveMode) {
67 return;
68 }
69
70 fs.writeSync(1, '\r');
71 process.stdout.write(self.progressChars[self.progressIndex].cyan);
72
73 self.progressIndex++;
74 if (self.progressIndex === self.progressChars.length) {
75 self.progressIndex = 0;
76 }
77 },
78
79 clearProgress: function () {
80 var self = this;
81 // do not output '+' if there is no progress
82 if (self.currentProgress) {
83 if (self.activeProgressTimer) {
84 clearInterval(self.activeProgressTimer);
85 self.activeProgressTimer = null;
86 }
87 if (!nonInteractiveMode) {
88 fs.writeSync(1, '\r+\n');
89 }
90 self.currentProgress = undefined;
91 }
92 },
93
94 //Not used
95 //writeDuringProgress: function(level, message) {
96 // if (this.currentProgress) {
97 // fs.writeSync(1, '\r' + this.clearBuffer + '\r');
98 // log[level](message);
99 // this._drawAndUpdateProgress();
100 // }
101 //},
102
103 _pauseProgress: function () {
104 if (nonInteractiveMode) {
105 return;
106 }
107
108 if (this.currentProgress) {
109 fs.writeSync(1, '\r' + this.clearBuffer + '\r');
110 }
111 },
112
113 _restartProgress: function (label) {
114 if (nonInteractiveMode) {
115 return;
116 }
117
118 if (this.currentProgress) {
119 this._drawAndUpdateProgress();
120 if (label) {
121 fs.writeSync(1, ' ' + label);
122 }
123 }
124 },
125
126 progress: function(label, log) {
127 var self = this;
128 if (!log && self.cli) {
129 log = self.cli.output;
130 }
131
132 var verbose = log && (log.format().json || log.format().level === 'verbose' || log.format().level === 'silly');
133 if (!self.istty1 || verbose) {
134 (verbose ? log.verbose : log.info)(label);
135 return {
136 write: function (logAction) {
137 logAction();
138 },
139 end: function() {}
140 };
141 }
142
143 // clear any previous progress
144 self.clearProgress();
145
146 // Clear the console
147 fs.writeSync(1, '\r' + self.clearBuffer);
148
149 // Draw initial progress
150 self._drawAndUpdateProgress();
151
152 // Draw label
153 if (label) {
154 fs.writeSync(1, ' ' + label);
155 }
156
157 self.activeProgressTimer = setInterval(function() {
158 self._drawAndUpdateProgress();
159 }, 200);
160
161 self.currentProgress = {
162 write: function (logAction, newLabel) {
163 newLabel = newLabel || label;
164 self._pauseProgress();
165 logAction();
166 self._restartProgress(newLabel);
167 },
168 end: function() {
169 self.clearProgress();
170 }
171 };
172
173 return self.currentProgress;
174 },
175
176 withProgress: function (label, action, callback) {
177 var self = this;
178 var p = this.progress(label);
179 var logMsgs = [];
180 var logger = {
181 error: function (message) {
182 logMsgs.push(function () { self.cli.output.error(message); });
183 },
184 info: function (message) {
185 logMsgs.push(function () { self.cli.output.info(message); });
186 },
187 data: function (message) {
188 logMsgs.push(function () { self.cli.output.data(message); });
189 },
190 warn: function (message) {
191 logMsgs.push(function () { self.cli.output.warn(message); });
192 }
193 };
194
195 action.call(p, logger, function () {
196 p.end();
197 logMsgs.forEach(function (lf) { lf(); });
198 callback.apply(null, arguments);
199 });
200 },
201
202 //behavior verified
203 prompt: function (msg, callback) {
204 checkNonInteractiveMode(msg);
205 prompt_pkg.start();
206 //surpress the default prompt message
207 prompt_pkg.message = '';
208 prompt_pkg.delimiter = '';
209 prompt_pkg.get([{
210 name: msg
211 }], function (err, result) {
212 if (err) return callback(err);
213 if (utils.stringIsNullOrEmpty(result[msg])) {
214 return callback(new Error(util.format('Please provide a non empty ' +
215 'value for \'%s\'. You provided - \'%s\'.', msg, result[msg])));
216 }
217 callback(null, result[msg]);
218 });
219 },
220
221 //behavior verified
222 confirm: function (msg, callback) {
223 checkNonInteractiveMode(msg);
224 prompt_pkg.start();
225 //surpress the default prompt message
226 prompt_pkg.message = '';
227 prompt_pkg.delimiter = '';
228 prompt_pkg.confirm(msg, callback);
229 },
230
231 //behavior verified
232 promptPassword: function (msg, callback) {
233 this.password(msg, '*', function (err, result) {
234 callback(err, result);
235 });
236 },
237
238 //behavior verified, "vm quick-create" uses it
239 promptPasswordIfNotGiven: function (promptString, currentValue, callback) {
240 if (__.isUndefined(currentValue)) {
241 return this.promptPassword(promptString, callback);
242 } else {
243 return callback(null, currentValue);
244 }
245 },
246
247 //behavior verified, 'promptPasswordOnceIfNotGiven' below uses it.
248 promptPasswordOnce: function (msg, callback) {
249 this.passwordOnce(msg, '*', function (err, result) {
250 callback(err, result);
251 });
252 },
253
254 //behavior verified, 'login' uses this
255 promptPasswordOnceIfNotGiven: function (promptString, currentValue, callback) {
256 if (__.isUndefined(currentValue)) {
257 this.promptPasswordOnce(promptString, function (err, result) {
258 return callback(err, result);
259 });
260 } else {
261 return callback(null, currentValue);
262 }
263 },
264
265 //behavior verified
266 promptIfNotGiven: function (promptString, currentValue, callback) {
267 if (__.isUndefined(currentValue)) {
268 return this.prompt(promptString, callback);
269 } else {
270 return callback(null, currentValue);
271 }
272 },
273
274 //behavior verified
275 choose: function (values, callback) {
276 var self = this;
277 var displays = values.map(function (v, index) {
278 return util.format(' %d) %s', index + 1, v);
279 });
280 var msg = displays.join('\n') + '\n:';
281 function again() {
282 self.prompt(msg, function (err, result) {
283 if (err) return callback(err);
284 var selection = parseInt(result, 10) - 1;
285 if (!(values[selection])) {
286 again();
287 } else {
288 callback(null, selection);
289 }
290 });
291 }
292 again();
293 },
294
295 //behavior verified
296 chooseIfNotGiven: function (promptString, progressString, currentValue, valueProvider, callback) {
297 var self = this;
298 checkNonInteractiveMode(promptString);
299 if (__.isUndefined(currentValue)) {
300 //comment out the progress usage, as it interferes winton's async logging
301 //var progress = self.cli.interaction.progress(progressString);
302 valueProvider(function (err, values) {
303 if (err) return callback(err);
304 //progress.end();
305 self.cli.output.help(promptString);
306 self.choose(values, function (err, selection) {
307 return callback(err, values[selection]);
308 });
309 });
310 } else {
311 return callback(null, currentValue);
312 }
313 },
314
315 formatOutput: function (outputData, humanOutputGenerator) {
316 this.cli.output.json('silly', outputData);
317 if(this.cli.output.format().json) {
318 this.cli.output.json(outputData);
319 } else {
320 humanOutputGenerator(outputData);
321 }
322 },
323
324 logEachData: function (title, data) {
325 for (var property in data) {
326 if (data.hasOwnProperty(property)) {
327 if (data[property]) {
328 this.cli.output.data(title + ' ' + property, data[property]);
329 } else {
330 this.cli.output.data(title + ' ' + property, '');
331 }
332 }
333 }
334 },
335
336 launchBrowser: function (url, callback) {
337 log.info('Launching browser to', url);
338 if (process.env.OS !== undefined) {
339 // escape & characters for start cmd
340 var cmd = util.format('start %s', url).replace(/&/g, '^&');
341 child_process.exec(cmd, callback);
342 } else {
343 child_process.spawn('open', [url]);
344 callback();
345 }
346 },
347
348 //the reason of reinventing the wheel, rather use the npm 'prompt' package
349 //is to display the mask of '*' for each character. No idea why we prefered
350 //this behavior, but it is what it is.
351 passwordOnce: function (currentStr, mask, callback) {
352 checkNonInteractiveMode(currentStr);
353 var buf = '';
354
355 // default mask
356 if ('function' === typeof mask) {
357 callback = mask;
358 mask = '';
359 }
360
361 if (!process.stdin.setRawMode) {
362 process.stdin.setRawMode = tty.setRawMode;
363 }
364
365 process.stdin.resume();
366 process.stdin.setRawMode(true);
367 fs.writeSync(this.istty1 ? 1 : 2, currentStr);
368
369 process.stdin.on('data', function (character) {
370 // Exit on Ctrl+C keypress
371 character = character.toString();
372 if (character === '\003') {
373 console.log('%s', buf);
374 process.exit();
375 }
376
377 // Return password in the buffer on enter key press
378 if (character === '\015') {
379 process.stdin.pause();
380 process.stdin.removeAllListeners('data');
381 process.stdout.write('\n');
382 process.stdin.setRawMode(false);
383
384 return callback(null, buf);
385 }
386
387 // Backspace handling
388 // Windows usually sends '\b' (^H) while Linux sends '\x7f'
389 if (character === '\b' || character === '\x7f') {
390 if (buf) {
391 buf = buf.slice(0, -1);
392 for (var j = 0; j < mask.length; ++j) {
393 process.stdout.write('\b \b'); // space the last character out
394 }
395 }
396
397 return;
398 }
399
400 character = character.split('\015')[0]; // only use the first line if many (for paste)
401 for(var i = 0; i < character.length; ++i) {
402 process.stdout.write(mask); // output several chars (for paste)
403 }
404
405 buf += character;
406 });
407 },
408
409 // Allow cli.password to accept empty passwords
410 password: function (str, mask, callback) {
411 var self = this;
412 checkNonInteractiveMode(str);
413 // Prompt first time
414 this.passwordOnce(str, mask, function (err, pass) {
415 //till today, *err* is always null, so we skip the check.
416 // Prompt for confirmation
417 self.passwordOnce('Confirm password: ', mask, function (err2, pass2) {
418 if (pass === pass2) {
419 return callback(null, pass);
420 } else {
421 throw new Error('Passwords do not match.');
422 }
423 });
424 });
425 }
426});
427
428module.exports = Interactor;