1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:meta/meta.dart'; 8 9import '../base/context.dart'; 10import 'io.dart'; 11import 'platform.dart'; 12import 'terminal.dart'; 13import 'utils.dart'; 14 15const int kDefaultStatusPadding = 59; 16const Duration _kFastOperation = Duration(seconds: 2); 17const Duration _kSlowOperation = Duration(minutes: 2); 18 19/// The [TimeoutConfiguration] instance. 20/// 21/// If not provided via injection, a default instance is provided. 22TimeoutConfiguration get timeoutConfiguration => context.get<TimeoutConfiguration>() ?? const TimeoutConfiguration(); 23 24class TimeoutConfiguration { 25 const TimeoutConfiguration(); 26 27 /// The expected time that various "slow" operations take, such as running 28 /// the analyzer. 29 /// 30 /// Defaults to 2 minutes. 31 Duration get slowOperation => _kSlowOperation; 32 33 /// The expected time that various "fast" operations take, such as a hot 34 /// reload. 35 /// 36 /// Defaults to 2 seconds. 37 Duration get fastOperation => _kFastOperation; 38} 39 40typedef VoidCallback = void Function(); 41 42abstract class Logger { 43 bool get isVerbose => false; 44 45 bool quiet = false; 46 47 bool get supportsColor => terminal.supportsColor; 48 49 bool get hasTerminal => stdio.hasTerminal; 50 51 /// Display an error `message` to the user. Commands should use this if they 52 /// fail in some way. 53 /// 54 /// The `message` argument is printed to the stderr in red by default. 55 /// 56 /// The `stackTrace` argument is the stack trace that will be printed if 57 /// supplied. 58 /// 59 /// The `emphasis` argument will cause the output message be printed in bold text. 60 /// 61 /// The `color` argument will print the message in the supplied color instead 62 /// of the default of red. Colors will not be printed if the output terminal 63 /// doesn't support them. 64 /// 65 /// The `indent` argument specifies the number of spaces to indent the overall 66 /// message. If wrapping is enabled in [outputPreferences], then the wrapped 67 /// lines will be indented as well. 68 /// 69 /// If `hangingIndent` is specified, then any wrapped lines will be indented 70 /// by this much more than the first line, if wrapping is enabled in 71 /// [outputPreferences]. 72 /// 73 /// If `wrap` is specified, then it overrides the 74 /// `outputPreferences.wrapText` setting. 75 void printError( 76 String message, { 77 StackTrace stackTrace, 78 bool emphasis, 79 TerminalColor color, 80 int indent, 81 int hangingIndent, 82 bool wrap, 83 }); 84 85 /// Display normal output of the command. This should be used for things like 86 /// progress messages, success messages, or just normal command output. 87 /// 88 /// The `message` argument is printed to the stderr in red by default. 89 /// 90 /// The `stackTrace` argument is the stack trace that will be printed if 91 /// supplied. 92 /// 93 /// If the `emphasis` argument is true, it will cause the output message be 94 /// printed in bold text. Defaults to false. 95 /// 96 /// The `color` argument will print the message in the supplied color instead 97 /// of the default of red. Colors will not be printed if the output terminal 98 /// doesn't support them. 99 /// 100 /// If `newline` is true, then a newline will be added after printing the 101 /// status. Defaults to true. 102 /// 103 /// The `indent` argument specifies the number of spaces to indent the overall 104 /// message. If wrapping is enabled in [outputPreferences], then the wrapped 105 /// lines will be indented as well. 106 /// 107 /// If `hangingIndent` is specified, then any wrapped lines will be indented 108 /// by this much more than the first line, if wrapping is enabled in 109 /// [outputPreferences]. 110 /// 111 /// If `wrap` is specified, then it overrides the 112 /// `outputPreferences.wrapText` setting. 113 void printStatus( 114 String message, { 115 bool emphasis, 116 TerminalColor color, 117 bool newline, 118 int indent, 119 int hangingIndent, 120 bool wrap, 121 }); 122 123 /// Use this for verbose tracing output. Users can turn this output on in order 124 /// to help diagnose issues with the toolchain or with their setup. 125 void printTrace(String message); 126 127 /// Start an indeterminate progress display. 128 /// 129 /// The `message` argument is the message to display to the user. 130 /// 131 /// The `timeout` argument sets a duration after which an additional message 132 /// may be shown saying that the operation is taking a long time. (Not all 133 /// [Status] subclasses show such a message.) Set this to null if the 134 /// operation can legitimately take an arbitrary amount of time (e.g. waiting 135 /// for the user). 136 /// 137 /// The `progressId` argument provides an ID that can be used to identify 138 /// this type of progress (e.g. `hot.reload`, `hot.restart`). 139 /// 140 /// The `progressIndicatorPadding` can optionally be used to specify spacing 141 /// between the `message` and the progress indicator, if any. 142 Status startProgress( 143 String message, { 144 @required Duration timeout, 145 String progressId, 146 bool multilineOutput = false, 147 int progressIndicatorPadding = kDefaultStatusPadding, 148 }); 149} 150 151class StdoutLogger extends Logger { 152 Status _status; 153 154 @override 155 bool get isVerbose => false; 156 157 @override 158 void printError( 159 String message, { 160 StackTrace stackTrace, 161 bool emphasis, 162 TerminalColor color, 163 int indent, 164 int hangingIndent, 165 bool wrap, 166 }) { 167 _status?.pause(); 168 message ??= ''; 169 message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap); 170 if (emphasis == true) 171 message = terminal.bolden(message); 172 message = terminal.color(message, color ?? TerminalColor.red); 173 stderr.writeln(message); 174 if (stackTrace != null) 175 stderr.writeln(stackTrace.toString()); 176 _status?.resume(); 177 } 178 179 @override 180 void printStatus( 181 String message, { 182 bool emphasis, 183 TerminalColor color, 184 bool newline, 185 int indent, 186 int hangingIndent, 187 bool wrap, 188 }) { 189 _status?.pause(); 190 message ??= ''; 191 message = wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap); 192 if (emphasis == true) 193 message = terminal.bolden(message); 194 if (color != null) 195 message = terminal.color(message, color); 196 if (newline != false) 197 message = '$message\n'; 198 writeToStdOut(message); 199 _status?.resume(); 200 } 201 202 @protected 203 void writeToStdOut(String message) { 204 stdout.write(message); 205 } 206 207 @override 208 void printTrace(String message) { } 209 210 @override 211 Status startProgress( 212 String message, { 213 @required Duration timeout, 214 String progressId, 215 bool multilineOutput = false, 216 int progressIndicatorPadding = kDefaultStatusPadding, 217 }) { 218 assert(progressIndicatorPadding != null); 219 if (_status != null) { 220 // Ignore nested progresses; return a no-op status object. 221 return SilentStatus( 222 timeout: timeout, 223 onFinish: _clearStatus, 224 )..start(); 225 } 226 if (terminal.supportsColor) { 227 _status = AnsiStatus( 228 message: message, 229 timeout: timeout, 230 multilineOutput: multilineOutput, 231 padding: progressIndicatorPadding, 232 onFinish: _clearStatus, 233 )..start(); 234 } else { 235 _status = SummaryStatus( 236 message: message, 237 timeout: timeout, 238 padding: progressIndicatorPadding, 239 onFinish: _clearStatus, 240 )..start(); 241 } 242 return _status; 243 } 244 245 void _clearStatus() { 246 _status = null; 247 } 248} 249 250/// A [StdoutLogger] which replaces Unicode characters that cannot be printed to 251/// the Windows console with alternative symbols. 252/// 253/// By default, Windows uses either "Consolas" or "Lucida Console" as fonts to 254/// render text in the console. Both fonts only have a limited character set. 255/// Unicode characters, that are not available in either of the two default 256/// fonts, should be replaced by this class with printable symbols. Otherwise, 257/// they will show up as the unrepresentable character symbol '�'. 258class WindowsStdoutLogger extends StdoutLogger { 259 @override 260 void writeToStdOut(String message) { 261 // TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio]. 262 stdout.write(message 263 .replaceAll('✗', 'X') 264 .replaceAll('✓', '√') 265 ); 266 } 267} 268 269class BufferLogger extends Logger { 270 @override 271 bool get isVerbose => false; 272 273 final StringBuffer _error = StringBuffer(); 274 final StringBuffer _status = StringBuffer(); 275 final StringBuffer _trace = StringBuffer(); 276 277 String get errorText => _error.toString(); 278 String get statusText => _status.toString(); 279 String get traceText => _trace.toString(); 280 281 @override 282 void printError( 283 String message, { 284 StackTrace stackTrace, 285 bool emphasis, 286 TerminalColor color, 287 int indent, 288 int hangingIndent, 289 bool wrap, 290 }) { 291 _error.writeln(terminal.color( 292 wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap), 293 color ?? TerminalColor.red, 294 )); 295 } 296 297 @override 298 void printStatus( 299 String message, { 300 bool emphasis, 301 TerminalColor color, 302 bool newline, 303 int indent, 304 int hangingIndent, 305 bool wrap, 306 }) { 307 if (newline != false) 308 _status.writeln(wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap)); 309 else 310 _status.write(wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap)); 311 } 312 313 @override 314 void printTrace(String message) => _trace.writeln(message); 315 316 @override 317 Status startProgress( 318 String message, { 319 @required Duration timeout, 320 String progressId, 321 bool multilineOutput = false, 322 int progressIndicatorPadding = kDefaultStatusPadding, 323 }) { 324 assert(progressIndicatorPadding != null); 325 printStatus(message); 326 return SilentStatus(timeout: timeout)..start(); 327 } 328 329 /// Clears all buffers. 330 void clear() { 331 _error.clear(); 332 _status.clear(); 333 _trace.clear(); 334 } 335} 336 337class VerboseLogger extends Logger { 338 VerboseLogger(this.parent) : assert(terminal != null) { 339 stopwatch.start(); 340 } 341 342 final Logger parent; 343 344 Stopwatch stopwatch = Stopwatch(); 345 346 @override 347 bool get isVerbose => true; 348 349 @override 350 void printError( 351 String message, { 352 StackTrace stackTrace, 353 bool emphasis, 354 TerminalColor color, 355 int indent, 356 int hangingIndent, 357 bool wrap, 358 }) { 359 _emit( 360 _LogType.error, 361 wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap), 362 stackTrace, 363 ); 364 } 365 366 @override 367 void printStatus( 368 String message, { 369 bool emphasis, 370 TerminalColor color, 371 bool newline, 372 int indent, 373 int hangingIndent, 374 bool wrap, 375 }) { 376 _emit(_LogType.status, wrapText(message, indent: indent, hangingIndent: hangingIndent, shouldWrap: wrap)); 377 } 378 379 @override 380 void printTrace(String message) { 381 _emit(_LogType.trace, message); 382 } 383 384 @override 385 Status startProgress( 386 String message, { 387 @required Duration timeout, 388 String progressId, 389 bool multilineOutput = false, 390 int progressIndicatorPadding = kDefaultStatusPadding, 391 }) { 392 assert(progressIndicatorPadding != null); 393 printStatus(message); 394 final Stopwatch timer = Stopwatch()..start(); 395 return SilentStatus( 396 timeout: timeout, 397 onFinish: () { 398 String time; 399 if (timeout == null || timeout > timeoutConfiguration.fastOperation) { 400 time = getElapsedAsSeconds(timer.elapsed); 401 } else { 402 time = getElapsedAsMilliseconds(timer.elapsed); 403 } 404 if (timeout != null && timer.elapsed > timeout) { 405 printTrace('$message (completed in $time, longer than expected)'); 406 } else { 407 printTrace('$message (completed in $time)'); 408 } 409 }, 410 )..start(); 411 } 412 413 void _emit(_LogType type, String message, [ StackTrace stackTrace ]) { 414 if (message.trim().isEmpty) 415 return; 416 417 final int millis = stopwatch.elapsedMilliseconds; 418 stopwatch.reset(); 419 420 String prefix; 421 const int prefixWidth = 8; 422 if (millis == 0) { 423 prefix = ''.padLeft(prefixWidth); 424 } else { 425 prefix = '+$millis ms'.padLeft(prefixWidth); 426 if (millis >= 100) 427 prefix = terminal.bolden(prefix); 428 } 429 prefix = '[$prefix] '; 430 431 final String indent = ''.padLeft(prefix.length); 432 final String indentMessage = message.replaceAll('\n', '\n$indent'); 433 434 if (type == _LogType.error) { 435 parent.printError(prefix + terminal.bolden(indentMessage)); 436 if (stackTrace != null) 437 parent.printError(indent + stackTrace.toString().replaceAll('\n', '\n$indent')); 438 } else if (type == _LogType.status) { 439 parent.printStatus(prefix + terminal.bolden(indentMessage)); 440 } else { 441 parent.printStatus(prefix + indentMessage); 442 } 443 } 444} 445 446enum _LogType { error, status, trace } 447 448typedef SlowWarningCallback = String Function(); 449 450/// A [Status] class begins when start is called, and may produce progress 451/// information asynchronously. 452/// 453/// Some subclasses change output once [timeout] has expired, to indicate that 454/// something is taking longer than expected. 455/// 456/// The [SilentStatus] class never has any output. 457/// 458/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single 459/// space character when stopped or canceled. 460/// 461/// The [AnsiStatus] subclass shows a spinner, and replaces it with timing 462/// information when stopped. When canceled, the information isn't shown. In 463/// either case, a newline is printed. 464/// 465/// The [SummaryStatus] subclass shows only a static message (without an 466/// indicator), then updates it when the operation ends. 467/// 468/// Generally, consider `logger.startProgress` instead of directly creating 469/// a [Status] or one of its subclasses. 470abstract class Status { 471 Status({ @required this.timeout, this.onFinish }); 472 473 /// A [SilentStatus] or an [AnsiSpinner] (depending on whether the 474 /// terminal is fancy enough), already started. 475 factory Status.withSpinner({ 476 @required Duration timeout, 477 VoidCallback onFinish, 478 SlowWarningCallback slowWarningCallback, 479 }) { 480 if (terminal.supportsColor) 481 return AnsiSpinner(timeout: timeout, onFinish: onFinish, slowWarningCallback: slowWarningCallback)..start(); 482 return SilentStatus(timeout: timeout, onFinish: onFinish)..start(); 483 } 484 485 final Duration timeout; 486 final VoidCallback onFinish; 487 488 @protected 489 final Stopwatch _stopwatch = context.get<Stopwatch>() ?? Stopwatch(); 490 491 @protected 492 @visibleForTesting 493 bool get seemsSlow => timeout != null && _stopwatch.elapsed > timeout; 494 495 @protected 496 String get elapsedTime { 497 if (timeout == null || timeout > timeoutConfiguration.fastOperation) 498 return getElapsedAsSeconds(_stopwatch.elapsed); 499 return getElapsedAsMilliseconds(_stopwatch.elapsed); 500 } 501 502 /// Call to start spinning. 503 void start() { 504 assert(!_stopwatch.isRunning); 505 _stopwatch.start(); 506 } 507 508 /// Call to stop spinning after success. 509 void stop() { 510 finish(); 511 } 512 513 /// Call to cancel the spinner after failure or cancellation. 514 void cancel() { 515 finish(); 516 } 517 518 /// Call to clear the current line but not end the progress. 519 void pause() { } 520 521 /// Call to resume after a pause. 522 void resume() { } 523 524 @protected 525 void finish() { 526 assert(_stopwatch.isRunning); 527 _stopwatch.stop(); 528 if (onFinish != null) 529 onFinish(); 530 } 531} 532 533/// A [SilentStatus] shows nothing. 534class SilentStatus extends Status { 535 SilentStatus({ 536 @required Duration timeout, 537 VoidCallback onFinish, 538 }) : super(timeout: timeout, onFinish: onFinish); 539} 540 541/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call 542/// [onFinish]. On [stop], will additionally print out summary information. 543class SummaryStatus extends Status { 544 SummaryStatus({ 545 this.message = '', 546 @required Duration timeout, 547 this.padding = kDefaultStatusPadding, 548 VoidCallback onFinish, 549 }) : assert(message != null), 550 assert(padding != null), 551 super(timeout: timeout, onFinish: onFinish); 552 553 final String message; 554 final int padding; 555 556 bool _messageShowingOnCurrentLine = false; 557 558 @override 559 void start() { 560 _printMessage(); 561 super.start(); 562 } 563 564 void _printMessage() { 565 assert(!_messageShowingOnCurrentLine); 566 stdout.write('${message.padRight(padding)} '); 567 _messageShowingOnCurrentLine = true; 568 } 569 570 @override 571 void stop() { 572 if (!_messageShowingOnCurrentLine) 573 _printMessage(); 574 super.stop(); 575 writeSummaryInformation(); 576 stdout.write('\n'); 577 } 578 579 @override 580 void cancel() { 581 super.cancel(); 582 if (_messageShowingOnCurrentLine) 583 stdout.write('\n'); 584 } 585 586 /// Prints a (minimum) 8 character padded time. 587 /// 588 /// If [timeout] is less than or equal to [kFastOperation], the time is in 589 /// seconds; otherwise, milliseconds. If the time is longer than [timeout], 590 /// appends "(!)" to the time. 591 /// 592 /// Examples: ` 0.5s`, ` 150ms`, ` 1,600ms`, ` 3.1s (!)` 593 void writeSummaryInformation() { 594 assert(_messageShowingOnCurrentLine); 595 stdout.write(elapsedTime.padLeft(_kTimePadding)); 596 if (seemsSlow) 597 stdout.write(' (!)'); 598 } 599 600 @override 601 void pause() { 602 super.pause(); 603 stdout.write('\n'); 604 _messageShowingOnCurrentLine = false; 605 } 606} 607 608/// An [AnsiSpinner] is a simple animation that does nothing but implement a 609/// terminal spinner. When stopped or canceled, the animation erases itself. 610/// 611/// If the timeout expires, a customizable warning is shown (but the spinner 612/// continues otherwise unabated). 613class AnsiSpinner extends Status { 614 AnsiSpinner({ 615 @required Duration timeout, 616 VoidCallback onFinish, 617 this.slowWarningCallback, 618 }) : super(timeout: timeout, onFinish: onFinish); 619 620 final String _backspaceChar = '\b'; 621 final String _clearChar = ' '; 622 623 bool timedOut = false; 624 625 int ticks = 0; 626 Timer timer; 627 628 // Windows console font has a limited set of Unicode characters. 629 List<String> get _animation => platform.isWindows 630 ? <String>[r'-', r'\', r'|', r'/'] 631 : <String>['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷']; 632 633 static const String _defaultSlowWarning = '(This is taking an unexpectedly long time.)'; 634 final SlowWarningCallback slowWarningCallback; 635 636 String _slowWarning = ''; 637 638 String get _currentAnimationFrame => _animation[ticks % _animation.length]; 639 int get _currentLength => _currentAnimationFrame.length + _slowWarning.length; 640 String get _backspace => _backspaceChar * (spinnerIndent + _currentLength); 641 String get _clear => _clearChar * (spinnerIndent + _currentLength); 642 643 @protected 644 int get spinnerIndent => 0; 645 646 @override 647 void start() { 648 super.start(); 649 assert(timer == null); 650 _startSpinner(); 651 } 652 653 void _startSpinner() { 654 stdout.write(_clear); // for _callback to backspace over 655 timer = Timer.periodic(const Duration(milliseconds: 100), _callback); 656 _callback(timer); 657 } 658 659 void _callback(Timer timer) { 660 assert(this.timer == timer); 661 assert(timer != null); 662 assert(timer.isActive); 663 stdout.write(_backspace); 664 ticks += 1; 665 if (seemsSlow) { 666 if (!timedOut) { 667 timedOut = true; 668 stdout.write('$_clear\n'); 669 } 670 if (slowWarningCallback != null) { 671 _slowWarning = slowWarningCallback(); 672 } else { 673 _slowWarning = _defaultSlowWarning; 674 } 675 stdout.write(_slowWarning); 676 } 677 stdout.write('${_clearChar * spinnerIndent}$_currentAnimationFrame'); 678 } 679 680 @override 681 void finish() { 682 assert(timer != null); 683 assert(timer.isActive); 684 timer.cancel(); 685 timer = null; 686 _clearSpinner(); 687 super.finish(); 688 } 689 690 void _clearSpinner() { 691 stdout.write('$_backspace$_clear$_backspace'); 692 } 693 694 @override 695 void pause() { 696 assert(timer != null); 697 assert(timer.isActive); 698 _clearSpinner(); 699 timer.cancel(); 700 } 701 702 @override 703 void resume() { 704 assert(timer != null); 705 assert(!timer.isActive); 706 _startSpinner(); 707 } 708} 709 710const int _kTimePadding = 8; // should fit "99,999ms" 711 712/// Constructor writes [message] to [stdout] with padding, then starts an 713/// indeterminate progress indicator animation (it's a subclass of 714/// [AnsiSpinner]). 715/// 716/// On [cancel] or [stop], will call [onFinish]. On [stop], will 717/// additionally print out summary information. 718class AnsiStatus extends AnsiSpinner { 719 AnsiStatus({ 720 this.message = '', 721 @required Duration timeout, 722 this.multilineOutput = false, 723 this.padding = kDefaultStatusPadding, 724 VoidCallback onFinish, 725 }) : assert(message != null), 726 assert(multilineOutput != null), 727 assert(padding != null), 728 super(timeout: timeout, onFinish: onFinish); 729 730 final String message; 731 final bool multilineOutput; 732 final int padding; 733 734 static const String _margin = ' '; 735 736 @override 737 int get spinnerIndent => _kTimePadding - 1; 738 739 int _totalMessageLength; 740 741 @override 742 void start() { 743 _startStatus(); 744 super.start(); 745 } 746 747 void _startStatus() { 748 final String line = '${message.padRight(padding)}$_margin'; 749 _totalMessageLength = line.length; 750 stdout.write(line); 751 } 752 753 @override 754 void stop() { 755 super.stop(); 756 writeSummaryInformation(); 757 stdout.write('\n'); 758 } 759 760 @override 761 void cancel() { 762 super.cancel(); 763 stdout.write('\n'); 764 } 765 766 /// Print summary information when a task is done. 767 /// 768 /// If [multilineOutput] is false, replaces the spinner with the summary message. 769 /// 770 /// If [multilineOutput] is true, then it prints the message again on a new 771 /// line before writing the elapsed time. 772 void writeSummaryInformation() { 773 if (multilineOutput) 774 stdout.write('\n${'$message Done'.padRight(padding)}$_margin'); 775 stdout.write(elapsedTime.padLeft(_kTimePadding)); 776 if (seemsSlow) 777 stdout.write(' (!)'); 778 } 779 780 void _clearStatus() { 781 stdout.write('${_backspaceChar * _totalMessageLength}${_clearChar * _totalMessageLength}${_backspaceChar * _totalMessageLength}'); 782 } 783 784 @override 785 void pause() { 786 super.pause(); 787 _clearStatus(); 788 } 789 790 @override 791 void resume() { 792 _startStatus(); 793 super.resume(); 794 } 795} 796