• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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