UNPKG

19.3 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
17// If running from MSI installed version, don't use the
18// compile on the fly streamline files. MSI install precompiles
19// the streamline files
20if (!process.env.PRECOMPILE_STREAMLINE_FILES) {
21 require('streamline').register({ cache: true });
22}
23
24var fs = require('fs');
25var path = require('path');
26var util = require('util');
27
28var _ = require('underscore');
29var callerId = require('caller-id');
30
31var CmdLoader = require('./cmdLoader');
32var ExtendedCommand = require('./util/extendedcommand');
33var log = require('./util/logging');
34
35var utilsCore = require('./util/utilsCore');
36var Interactor = require('./util/interaction');
37
38//'genMode' is only used on generating command metadata, value: 'asm' or 'arm'
39function AzureCli(name, parent, genMode) {
40 this.parent = parent;
41 this.output = log;
42 this.interaction = new Interactor(this);
43
44 AzureCli['super_'].call(this, name);
45
46 if (parent) {
47 this._mode = parent._mode;
48 }
49 else {
50 this.initSetup();
51
52 this.enableNestedCommands(this);
53
54 // Check node.js version.
55 // Do it after changing exception handler.
56 this.checkVersion();
57
58 this._mode = genMode;
59 if (!this._mode) {
60 this._mode = utilsCore.getMode();
61 }
62 var loader = new CmdLoader(this, this._mode);
63 if (genMode) {
64 log.info('Generating command metadata file: ' + loader.cmdMetadataFile);
65 loader.harvestPlugins();
66 loader.harvestModules();
67 loader.saveCmdMetadata();
68 log.info('Done');
69 return;
70 } else if (loader.cmdMetadataExists()) {
71 loader.initFromCmdMetadata(AzureCli);
72 } else {
73 log.warn('No existing command metadata files. Command will run slow.');
74 loader.harvestPlugins();
75 loader.harvestModules();
76 }
77 }
78}
79
80util.inherits(AzureCli, ExtendedCommand);
81
82_.extend(AzureCli.prototype, {
83 initSetup: function () {
84 var self = this;
85
86 self.debug = process.env.AZURE_DEBUG === '1';
87
88 // Install global unhandled exception handler to make unexpected errors more user-friendly.
89 if (!self.debug && process.listeners('uncaughtException').length === 0) {
90 self.uncaughExceptionHandler = function (err) {
91 self.interaction.clearProgress();
92
93 // Exceptions should always be logged to the console
94 var noConsole = false;
95 if (!log['default'].transports.console) {
96 noConsole = true;
97 self.output.add(self.output.transports.Console);
98 }
99
100 var loggedFullError = false;
101 if (err.message) {
102 log.error(err.message);
103 } else if (err.Message) {
104 log.error(err.Message);
105 } else {
106 log.json('error', err);
107 loggedFullError = true;
108 }
109
110 if (!loggedFullError) {
111 if (err.stack) {
112 log.verbose('stack', err.stack);
113 }
114
115 log.json('silly', err);
116 }
117
118 self.recordError(err);
119
120 if (noConsole) {
121 self.output.remove(self.output.transports.Console);
122 }
123
124 self.exit('error', null, 1);
125 };
126
127 process.addListener('uncaughtException', self.uncaughExceptionHandler);
128 }
129 },
130
131 getErrorFile: function () {
132 return path.join(utilsCore.azureDir(), 'azure.err');
133 },
134
135 getSillyErrorFile: function () {
136 return path.join(utilsCore.azureDir(), 'azure.details.err');
137 },
138
139 recordError: function (err) {
140 if (err) {
141 var errorFile = this.getErrorFile();
142 try {
143 var writeFileFunction = process.env.AZURE_CLI_APPEND_LOGS ? fs.appendFileSync : fs.writeFileSync;
144 writeFileFunction(errorFile, (new Date().toISOString()) + ':\n' +
145 util.inspect(err) + '\n' + err.stack + '\n');
146 (log.format().json ? log.error : log.info)('Error information has been recorded to ' + errorFile);
147 } catch (err2) {
148 log.warn('Cannot save error information :' + util.inspect(err2));
149 }
150
151 log.writeCapturedSillyLogs(this.getSillyErrorFile(), process.env.AZURE_CLI_APPEND_LOGS);
152 }
153 },
154
155 exit: function (level, message, exitCode) {
156 var self = this;
157
158 self.interaction.clearProgress();
159 if (message) {
160 log.log(level, message);
161 }
162
163 if (self.uncaughtExceptionHandler) {
164 process.removeListener('uncaughtException', self.uncaughExceptionHandler);
165 }
166
167 if (log.shouldWaitForStdoutDrained) {
168 process.on('exit', function () { process.exit(exitCode); });
169 }
170 else {
171 process.exit(exitCode);
172 }
173 },
174
175 normalizeAuthorizationError: function (msg) {
176 var regex = /.*The \'Authorization\' header is not present or provided in an invalid format.*/ig;
177 if (msg.match(regex)) {
178 msg = 'Certificate based Authentication is not supported in current mode: \'' + this._mode +
179 '\'. Please authenticate using an organizational account via \'azure login\' command.';
180 }
181 return msg;
182 },
183
184 execute: function (fn) {
185 var self = this;
186
187 return self.action(function () {
188 self.setupCommandOutput();
189
190 if (log.format().json) {
191 log.verbose('Executing command ' + self.fullName().bold);
192 } else {
193 log.info('Executing command ' + self.fullName().bold);
194 }
195
196 try {
197 // Expected arguments + options + callback
198 var argsCount = fn.length <= 1 ? self.args.length + 2 : fn.length;
199 var args = new Array(argsCount);
200
201 var optionIndex = arguments.length - 1;
202 for (var i = 0; i < arguments.length; i++) {
203 if (typeof arguments[i] === 'object') {
204 optionIndex = i;
205 break;
206 }
207 }
208
209 // append with options and callback
210 var options = arguments[optionIndex].optionValues;
211
212 args[args.length - 2] = options;
213 args[args.length - 1] = callback;
214
215 // set option arguments into their positional respective places
216 var freeArguments = 0;
217 for (var j = 0; j < self.args.length; j++) {
218 var optionName = utilsCore.camelcase(self.args[j].name);
219 if (options[optionName]) {
220 args[j] = options[optionName];
221 delete options[optionName];
222 } else if (freeArguments < arguments.length) {
223 args[j] = arguments[freeArguments];
224 freeArguments++;
225 }
226 }
227
228 fn.apply(this, args);
229 } catch (err) {
230 callback(err);
231 }
232
233 function callback(err) {
234 if (err) {
235 // Exceptions should always be logged to the console unless overturned by test run
236 var noConsole = false;
237 if (!process.env.AZURE_NO_ERROR_ON_CONSOLE && !log['default'].transports.console) {
238 noConsole = true;
239 self.output.add(self.output.transports.Console);
240 }
241
242 if (err.message) {
243 log.error(err.message);
244 log.json('silly', err);
245 } else if (err.Message) {
246 if (typeof err.Message === 'object' && typeof err.Message['#'] === 'string') {
247 var innerError;
248 try {
249 innerError = JSON.parse(err.Message['#']);
250 } catch (e) {
251 // empty
252 }
253
254 if (innerError) {
255 if (noConsole) {
256 self.output.remove(self.output.transports.Console);
257 }
258
259 return callback(innerError);
260 }
261 }
262
263 err.message = self.normalizeAuthorizationError(err.message);
264 log.error(err.Message);
265 log.json('verbose', err);
266 } else {
267 log.error(err);
268 }
269
270 self.recordError(err);
271 if (err.stack) {
272 (self.debug ? log.error : log.verbose)(err.stack);
273 }
274
275 if (noConsole) {
276 self.output.remove(self.output.transports.Console);
277 }
278
279 self.exit('error', self.fullName().bold + ' command ' + 'failed\n'.red.bold, 1);
280 } else {
281 if (log.format().json) {
282 self.exit('verbose', self.fullName().bold + ' command ' + 'OK'.green.bold, 0);
283 }
284 else {
285 self.exit('info', self.fullName().bold + ' command ' + 'OK'.green.bold, 0);
286 }
287 }
288 }
289 });
290 },
291
292 /*
293 * Extends the default parseOptions to support multiple levels in commans parsing.
294 */
295 parseOptions: function (argv) {
296 var args = [];
297 var len = argv.length;
298 var literal = false;
299 var option;
300 var arg;
301
302 var unknownOptions = [];
303
304 // parse options
305 for (var i = 0; i < len; ++i) {
306 arg = argv[i];
307
308 // literal args after --
309 if ('--' == arg) {
310 literal = true;
311 continue;
312 }
313
314 if (literal) {
315 args.push(arg);
316 continue;
317 }
318
319 // find matching Option
320 option = this.optionFor(arg);
321
322 //// patch begins
323 var commandOption = null;
324
325 if (!option && arg[0] === '-') {
326 var command = this;
327 var arga = null;
328 for (var a = 0; a < args.length && command && !commandOption; ++a) {
329 arga = args[a];
330 if (command.categories && (arga in command.categories)) {
331 command = command.categories[arga];
332 commandOption = command.optionFor(arg);
333 continue;
334 }
335 break;
336 }
337 if (!commandOption && arga && command && command.commands) {
338 for (var j in command.commands) {
339 if (command.commands[j].name === arga) {
340 commandOption = command.commands[j].optionFor(arg);
341 break;
342 }
343 }
344 }
345 }
346 //// patch ends
347
348 // option is defined
349 if (option) {
350 // requires arg
351 if (option.required) {
352 arg = argv[++i];
353 if (!arg) {
354 return this.optionMissingArgument(option);
355 }
356
357 if ('-' === arg[0]) {
358 return this.optionMissingArgument(option, arg);
359 }
360
361 this.emit(option.name(), arg);
362 } else if (option.optional) {
363 // optional arg
364 arg = argv[i + 1];
365 if (!arg || '-' === arg[0]) {
366 arg = null;
367 } else {
368 ++i;
369 }
370
371 this.emit(option.name(), arg);
372 // bool
373 } else {
374 this.emit(option.name());
375 }
376 continue;
377 }
378
379 // looks like an option
380 if (arg.length > 1 && '-' == arg[0]) {
381 unknownOptions.push(arg);
382
383 // If the next argument looks like it might be
384 // an argument for this option, we pass it on.
385 //// patch: using commandOption if available to detect if the next value is an argument
386 // If it isn't, then it'll simply be ignored
387 commandOption = commandOption || { optional : 1 }; // default assumption
388 if (commandOption.required || (commandOption.optional && argv[i + 1] && '-' != argv[i + 1][0])) {
389 unknownOptions.push(argv[++i]);
390 }
391 continue;
392 }
393
394 // arg
395 args.push(arg);
396 }
397
398 return { args: args, unknown: unknownOptions };
399 },
400
401 setupCommandLogFormat: function (topMost) {
402 if (topMost) {
403 var opts = {
404 json: false,
405 level: 'info',
406 logo: 'on'
407 };
408
409 log.format(opts);
410 }
411 },
412
413 setupCommandOutput: function (raw) {
414 var self = this;
415 var verbose = 0;
416 var json = 0;
417
418 if (!raw) {
419 raw = self.normalize(self.parent.rawArgs.slice(2));
420 }
421
422 function hasOption(optionName) {
423 return self.options.some(function (o) { return o.long === optionName; });
424 }
425
426 for (var i = 0, len = raw.length; i < len; ++i) {
427 if (hasOption('--json') &&
428 raw[i] === '--json') {
429 ++json;
430 } else if (hasOption('--verbose') &&
431 (raw[i] === '-v' || raw[i] === '--verbose')) {
432 ++verbose;
433 }
434 }
435
436 var opts = {};
437 if (verbose || json) {
438 if (json) {
439 opts.json = true;
440 opts.level = 'data';
441 }
442
443 if (verbose == 1) {
444 opts.json = false;
445 opts.level = 'verbose';
446 }
447
448 if (verbose >= 2) {
449 opts.json = false;
450 opts.level = 'silly';
451 }
452 } else {
453 opts.level = 'info';
454 }
455 log.format(opts);
456 },
457
458 enableNestedCommands: function (command) {
459 if (!command.parent) {
460 command.option('-v, --version', 'output the application version');
461 }
462
463 if (!command.categories) {
464 command.categories = {};
465 }
466
467 command.category = function (name) {
468 var category = command.categories[name];
469 if (!command.categories[name] || (command.categories[name]).stub && this.executingCmd) {
470 category = command.categories[name] = new AzureCli(name, this);
471 command.categories[name].stub = false;
472 category.helpInformation = command.categoryHelpInformation;
473 command.enableNestedCommands(category);
474 }
475
476 return category;
477 };
478
479 command.on('*', function () {
480 var args = command.rawArgs.slice(0, 2);
481 var raw = command.normalize(command.rawArgs.slice(2));
482
483 var category = '*';
484 if (raw.length > 0) {
485 category = raw[0];
486 args = args.concat(raw.slice(1));
487 }
488
489 var i, index;
490 var targetCmd;
491 var cat = command.categories[category];
492 //see whether it is top level command, like 'login', 'logout', etc
493 if (!cat){
494 index = command.searchCommand(category, command.commands);
495 if (index !== -1){
496 targetCmd = require(command.commands[index].filePath);
497 targetCmd.init.apply(command, [command]);
498 //execute command by emitting event, which will be routed to the handler.
499 return this.parse(command.rawArgs);
500 }
501 }
502
503 //see whether it is a nested command
504 for (i = 2; cat && i < args.length && args[i] !== '-h' && args[i] !== '--help'; i++) {
505 index = command.searchCommand(args[i], cat.commands);
506 if (index !== -1) {
507 targetCmd = cat.commands[index];
508 break;
509 } else {
510 cat = cat.categories[args[i]];
511 }
512 }
513
514 //we have found the command, execute it.
515 if (targetCmd) {
516 //no need to load the command file, as we get help from the metadata file
517 if (i+1 < args.length && (args[i+1] === '-h' || args[i+1] === '--help')) {
518 return targetCmd.commandHelpInformation();
519 }
520 this.executingCmd = true;
521 if (!this.workaroundOnAsmSiteCommands(targetCmd, command)) {
522 targetCmd = require(targetCmd.filePath);
523 targetCmd.init(command);
524 }
525 cat = command.categories[category];
526 return cat.parse(args);
527 }
528
529 if (!cat) {
530 var toBlame = (i>2) ? args[i-1] : category;
531 log.error('\'' + toBlame + '\' is not an azure command. See \'azure help\'.');
532 } else {
533 //if we are here, then it is about display help.
534 command.categoryHelpInformation.apply(cat,[]);
535 }
536 });
537 },
538
539 //Contrary to all other commands, ASM\Site commands were written
540 //differently that loading the single file containing the command
541 //is not enough, due to cross referencing, so we load them all.
542 //For new commands, we will not approve using the style.
543 workaroundOnAsmSiteCommands: function (targetCmd, command) {
544 if (path.basename(targetCmd.filePath).indexOf('site.') !== -1) {
545 var siteCmdDir = path.dirname(targetCmd.filePath);
546 var siteCmdFiles = utilsCore.getFiles(siteCmdDir, false);
547 var filesToLoad = {};
548 var sitePlugins = [];
549
550 //It is possible that ._js and precompiled version (.js) co-exist when
551 //both are laid down by the installer. We should only load .js ones.
552 siteCmdFiles.forEach(function (f) {
553 var basename = path.basename(f);
554 if (basename.indexOf('site.') === 0) {
555 var nameWithoutExt = basename.substring(0, basename.lastIndexOf('.'));
556 var ext = path.extname(basename);
557 if (filesToLoad[nameWithoutExt]) {
558 if (ext === '.js') {
559 filesToLoad[nameWithoutExt] = f;
560 }
561 } else {
562 filesToLoad[nameWithoutExt] = f;
563 }
564 }
565 });
566 Object.keys(filesToLoad).forEach(function (f) {
567 sitePlugins.push(require(filesToLoad[f]));
568 });
569 sitePlugins.forEach(function (plugin) {
570 if (plugin.init) {
571 plugin.init(command);
572 }
573 });
574 return true;
575 } else {
576 return false;
577 }
578 },
579
580 command: function (name) {
581 var args = name.split(/ +/);
582 var cmd = new AzureCli(args.shift(), this);
583 cmd.option('-v, --verbose', 'use verbose output');
584 cmd.option('-vv', 'more verbose with debug output');
585 cmd.option('--json', 'use json output');
586
587 var caller = callerId.getData();
588 cmd.filePath = caller.filePath;
589 cmd.helpInformation = cmd.commandHelpInformation;
590 var index = this.searchCommand(cmd.name, this.commands);
591 if (index !== -1) {
592 this.commands[index] = cmd;
593 } else {
594 this.commands.push(cmd);
595 }
596 cmd.parseExpectedArgs(args);
597 return cmd;
598 },
599
600 searchCommand: function(name, commands) {
601 if ( !commands || !name ) return -1;
602 for (var i = 0; i < commands.length; i++) {
603 if (commands[i].name === name) {
604 return i;
605 }
606 }
607 return -1;
608 },
609
610 deprecatedDescription: function (text, newCommand) {
611 return this.description(util.format('%s (deprecated. This command is deprecated and will be removed in a future version. Please use \"%s\" instead', text, newCommand));
612 },
613
614 detailedDescription: function (str) {
615 if (0 === arguments.length) return this._detailedDescription;
616 this._detailedDescription = str;
617 return this;
618 },
619
620 getMode: function () {
621 return this._mode;
622 },
623
624 isAsmMode: function () {
625 return utilsCore.ignoreCaseEquals(this._mode, 'asm');
626 },
627
628 isArmMode: function () {
629 return utilsCore.ignoreCaseEquals(this._mode, 'arm');
630 },
631
632 checkVersion: function () {
633 // Uploading VHD needs 0.6.15 on Windows
634 var version = process.version;
635 var ver = version.split('.');
636 var ver1num = parseInt(ver[1], 10);
637 var ver2num = parseInt(ver[2], 10);
638 if (ver[0] === 'v0') {
639 if (ver1num < 6 || (ver1num === 6 && ver2num < 15)) {
640 throw new Error('You need node.js v0.6.15 or higher to run this code. Your version: ' +
641 version);
642 }
643 if (ver1num === 7 && ver2num <= 7) {
644 throw new Error('You need node.js v0.6.15 or higher to run this code. Your version ' +
645 version + ' won\'t work either.');
646 }
647 }
648 }
649});
650
651exports = module.exports = AzureCli;