1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | var __ = require('underscore');
|
18 | var tty = require('tty');
|
19 | var fs = require('fs');
|
20 | var util = require('util');
|
21 | var tty = require('tty');
|
22 |
|
23 |
|
24 | var child_process = require('child_process');
|
25 | var nonInteractiveMode = process.env['AZURE_NON_INTERACTIVE_MODE'];
|
26 |
|
27 |
|
28 |
|
29 | var prompt_pkg = require('prompt');
|
30 |
|
31 | var log = require('./logging');
|
32 | var utils = require('./utils');
|
33 |
|
34 | function Interactor(cli) {
|
35 | this.cli = cli;
|
36 | this.istty1 = tty.isatty(1);
|
37 |
|
38 | this._initProgressBars();
|
39 | }
|
40 |
|
41 | function 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 |
|
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 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
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 |
|
144 | self.clearProgress();
|
145 |
|
146 |
|
147 | fs.writeSync(1, '\r' + self.clearBuffer);
|
148 |
|
149 |
|
150 | self._drawAndUpdateProgress();
|
151 |
|
152 |
|
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 |
|
203 | prompt: function (msg, callback) {
|
204 | checkNonInteractiveMode(msg);
|
205 | prompt_pkg.start();
|
206 |
|
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 |
|
222 | confirm: function (msg, callback) {
|
223 | checkNonInteractiveMode(msg);
|
224 | prompt_pkg.start();
|
225 |
|
226 | prompt_pkg.message = '';
|
227 | prompt_pkg.delimiter = '';
|
228 | prompt_pkg.confirm(msg, callback);
|
229 | },
|
230 |
|
231 |
|
232 | promptPassword: function (msg, callback) {
|
233 | this.password(msg, '*', function (err, result) {
|
234 | callback(err, result);
|
235 | });
|
236 | },
|
237 |
|
238 |
|
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 |
|
248 | promptPasswordOnce: function (msg, callback) {
|
249 | this.passwordOnce(msg, '*', function (err, result) {
|
250 | callback(err, result);
|
251 | });
|
252 | },
|
253 |
|
254 |
|
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 |
|
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 |
|
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 |
|
296 | chooseIfNotGiven: function (promptString, progressString, currentValue, valueProvider, callback) {
|
297 | var self = this;
|
298 | checkNonInteractiveMode(promptString);
|
299 | if (__.isUndefined(currentValue)) {
|
300 |
|
301 |
|
302 | valueProvider(function (err, values) {
|
303 | if (err) return callback(err);
|
304 |
|
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 |
|
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 |
|
349 |
|
350 |
|
351 | passwordOnce: function (currentStr, mask, callback) {
|
352 | checkNonInteractiveMode(currentStr);
|
353 | var buf = '';
|
354 |
|
355 |
|
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 |
|
371 | character = character.toString();
|
372 | if (character === '\003') {
|
373 | console.log('%s', buf);
|
374 | process.exit();
|
375 | }
|
376 |
|
377 |
|
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 |
|
388 |
|
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');
|
394 | }
|
395 | }
|
396 |
|
397 | return;
|
398 | }
|
399 |
|
400 | character = character.split('\015')[0];
|
401 | for(var i = 0; i < character.length; ++i) {
|
402 | process.stdout.write(mask);
|
403 | }
|
404 |
|
405 | buf += character;
|
406 | });
|
407 | },
|
408 |
|
409 |
|
410 | password: function (str, mask, callback) {
|
411 | var self = this;
|
412 | checkNonInteractiveMode(str);
|
413 |
|
414 | this.passwordOnce(str, mask, function (err, pass) {
|
415 |
|
416 |
|
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 |
|
428 | module.exports = Interactor;
|