UNPKG

azure-cli

Version:

Microsoft Azure Cross Platform Command Line tool

428 lines (372 loc) 12.1 kB
// // Copyright (c) Microsoft and contributors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // var __ = require('underscore'); var tty = require('tty'); var fs = require('fs'); var util = require('util'); /*jshint camelcase:false*/ var child_process = require('child_process'); var nonInteractiveMode = process.env['AZURE_NON_INTERACTIVE_MODE']; //this replaces 'command' package's 'prompt'/'confirm'/'choose', which don't //work well with winston's async logging. var prompt_pkg = require('prompt'); var log = require('./logging'); var utils = require('./utils'); function Interactor(cli) { this.cli = cli; this.istty1 = tty.isatty(1); this._initProgressBars(); } function checkNonInteractiveMode(requiredVar) { if (nonInteractiveMode) { throw new Error(util.format('Currently, the CLI is being run in \'Non Interactive Mode\'. ' + 'For the current command, \'%s\' is a required parameter (see the help). ' + 'Please provide it while executing the command. If you wish '+ 'to be in \'Interactive Mode\' so that the CLI can prompt you for ' + 'missing required parameters, please unset the environment variable '+ '\'AZURE_NON_INTERACTIVE_MODE\'.', requiredVar)); } } __.extend(Interactor.prototype, { _initProgressBars: function() { var self = this; self.progressChars = ['-', '\\', '|', '/']; self.progressIndex = 0; self.clearBuffer = new Buffer(79); self.clearBuffer.fill(' '); self.clearBuffer = self.clearBuffer.toString(); }, _drawAndUpdateProgress: function() { var self = this; if (nonInteractiveMode) { return; } fs.writeSync(1, '\r'); process.stdout.write(self.progressChars[self.progressIndex].cyan); self.progressIndex++; if (self.progressIndex === self.progressChars.length) { self.progressIndex = 0; } }, clearProgress: function () { var self = this; // do not output '+' if there is no progress if (self.currentProgress) { if (self.activeProgressTimer) { clearInterval(self.activeProgressTimer); self.activeProgressTimer = null; } if (!nonInteractiveMode) { fs.writeSync(1, '\r+\n'); } self.currentProgress = undefined; } }, //Not used //writeDuringProgress: function(level, message) { // if (this.currentProgress) { // fs.writeSync(1, '\r' + this.clearBuffer + '\r'); // log[level](message); // this._drawAndUpdateProgress(); // } //}, _pauseProgress: function () { if (nonInteractiveMode) { return; } if (this.currentProgress) { fs.writeSync(1, '\r' + this.clearBuffer + '\r'); } }, _restartProgress: function (label) { if (nonInteractiveMode) { return; } if (this.currentProgress) { this._drawAndUpdateProgress(); if (label) { fs.writeSync(1, ' ' + label); } } }, progress: function(label, log) { var self = this; if (!log && self.cli) { log = self.cli.output; } var verbose = log && (log.format().json || log.format().level === 'verbose' || log.format().level === 'silly'); if (!self.istty1 || verbose) { (verbose ? log.verbose : log.info)(label); return { write: function (logAction) { logAction(); }, end: function() {} }; } // clear any previous progress self.clearProgress(); // Clear the console fs.writeSync(1, '\r' + self.clearBuffer); // Draw initial progress self._drawAndUpdateProgress(); // Draw label if (label) { fs.writeSync(1, ' ' + label); } self.activeProgressTimer = setInterval(function() { self._drawAndUpdateProgress(); }, 200); self.currentProgress = { write: function (logAction, newLabel) { newLabel = newLabel || label; self._pauseProgress(); logAction(); self._restartProgress(newLabel); }, end: function() { self.clearProgress(); } }; return self.currentProgress; }, withProgress: function (label, action, callback) { var self = this; var p = this.progress(label); var logMsgs = []; var logger = { error: function (message) { logMsgs.push(function () { self.cli.output.error(message); }); }, info: function (message) { logMsgs.push(function () { self.cli.output.info(message); }); }, data: function (message) { logMsgs.push(function () { self.cli.output.data(message); }); }, warn: function (message) { logMsgs.push(function () { self.cli.output.warn(message); }); } }; action.call(p, logger, function () { p.end(); logMsgs.forEach(function (lf) { lf(); }); callback.apply(null, arguments); }); }, //behavior verified prompt: function (msg, callback) { checkNonInteractiveMode(msg); prompt_pkg.start(); //surpress the default prompt message prompt_pkg.message = ''; prompt_pkg.delimiter = ''; prompt_pkg.get([{ name: msg }], function (err, result) { if (err) return callback(err); if (utils.stringIsNullOrEmpty(result[msg])) { return callback(new Error(util.format('Please provide a non empty ' + 'value for \'%s\'. You provided - \'%s\'.', msg, result[msg]))); } callback(null, result[msg]); }); }, //behavior verified confirm: function (msg, callback) { checkNonInteractiveMode(msg); prompt_pkg.start(); //surpress the default prompt message prompt_pkg.message = ''; prompt_pkg.delimiter = ''; prompt_pkg.confirm(msg, callback); }, //behavior verified promptPassword: function (msg, callback) { this.password(msg, '*', function (err, result) { callback(err, result); }); }, //behavior verified, "vm quick-create" uses it promptPasswordIfNotGiven: function (promptString, currentValue, callback) { if (__.isUndefined(currentValue)) { return this.promptPassword(promptString, callback); } else { return callback(null, currentValue); } }, //behavior verified, 'promptPasswordOnceIfNotGiven' below uses it. promptPasswordOnce: function (msg, callback) { this.passwordOnce(msg, '*', function (err, result) { callback(err, result); }); }, //behavior verified, 'login' uses this promptPasswordOnceIfNotGiven: function (promptString, currentValue, callback) { if (__.isUndefined(currentValue)) { this.promptPasswordOnce(promptString, function (err, result) { return callback(err, result); }); } else { return callback(null, currentValue); } }, //behavior verified promptIfNotGiven: function (promptString, currentValue, callback) { if (__.isUndefined(currentValue)) { return this.prompt(promptString, callback); } else { return callback(null, currentValue); } }, //behavior verified choose: function (values, callback) { var self = this; var displays = values.map(function (v, index) { return util.format(' %d) %s', index + 1, v); }); var msg = displays.join('\n') + '\n:'; function again() { self.prompt(msg, function (err, result) { if (err) return callback(err); var selection = parseInt(result, 10) - 1; if (!(values[selection])) { again(); } else { callback(null, selection); } }); } again(); }, //behavior verified chooseIfNotGiven: function (promptString, progressString, currentValue, valueProvider, callback) { var self = this; checkNonInteractiveMode(promptString); if (__.isUndefined(currentValue)) { //comment out the progress usage, as it interferes winton's async logging //var progress = self.cli.interaction.progress(progressString); valueProvider(function (err, values) { if (err) return callback(err); //progress.end(); self.cli.output.help(promptString); self.choose(values, function (err, selection) { return callback(err, values[selection]); }); }); } else { return callback(null, currentValue); } }, formatOutput: function (outputData, humanOutputGenerator) { this.cli.output.json('silly', outputData); if(this.cli.output.format().json) { this.cli.output.json(outputData); } else { humanOutputGenerator(outputData); } }, logEachData: function (title, data) { for (var property in data) { if (data.hasOwnProperty(property)) { if (data[property]) { this.cli.output.data(title + ' ' + property, data[property]); } else { this.cli.output.data(title + ' ' + property, ''); } } } }, launchBrowser: function (url, callback) { log.info('Launching browser to', url); if (process.env.OS !== undefined) { // escape & characters for start cmd var cmd = util.format('start %s', url).replace(/&/g, '^&'); child_process.exec(cmd, callback); } else { child_process.spawn('open', [url]); callback(); } }, //the reason of reinventing the wheel, rather use the npm 'prompt' package //is to display the mask of '*' for each character. No idea why we prefered //this behavior, but it is what it is. passwordOnce: function (currentStr, mask, callback) { checkNonInteractiveMode(currentStr); var buf = ''; // default mask if ('function' === typeof mask) { callback = mask; mask = ''; } if (!process.stdin.setRawMode) { process.stdin.setRawMode = tty.setRawMode; } process.stdin.resume(); process.stdin.setRawMode(true); fs.writeSync(this.istty1 ? 1 : 2, currentStr); process.stdin.on('data', function (character) { // Exit on Ctrl+C keypress character = character.toString(); if (character === '\003') { console.log('%s', buf); process.exit(); } // Return password in the buffer on enter key press if (character === '\015') { process.stdin.pause(); process.stdin.removeAllListeners('data'); process.stdout.write('\n'); process.stdin.setRawMode(false); return callback(null, buf); } // Backspace handling // Windows usually sends '\b' (^H) while Linux sends '\x7f' if (character === '\b' || character === '\x7f') { if (buf) { buf = buf.slice(0, -1); for (var j = 0; j < mask.length; ++j) { process.stdout.write('\b \b'); // space the last character out } } return; } character = character.split('\015')[0]; // only use the first line if many (for paste) for(var i = 0; i < character.length; ++i) { process.stdout.write(mask); // output several chars (for paste) } buf += character; }); }, // Allow cli.password to accept empty passwords password: function (str, mask, callback) { var self = this; checkNonInteractiveMode(str); // Prompt first time this.passwordOnce(str, mask, function (err, pass) { //till today, *err* is always null, so we skip the check. // Prompt for confirmation self.passwordOnce('Confirm password: ', mask, function (err2, pass2) { if (pass === pass2) { return callback(null, pass); } else { throw new Error('Passwords do not match.'); } }); }); } }); module.exports = Interactor;