logger.dart 11 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:convert' show LineSplitter;
7

8
import 'package:meta/meta.dart';
9

10
import 'io.dart';
11
import 'terminal.dart';
12
import 'utils.dart';
13

14 15
const int kDefaultStatusPadding = 59;

16
abstract class Logger {
Devon Carew's avatar
Devon Carew committed
17
  bool get isVerbose => false;
18

19 20
  bool quiet = false;

21
  bool get supportsColor => terminal.supportsColor;
22
  set supportsColor(bool value) {
23
    terminal.supportsColor = value;
24 25
  }

26 27
  /// Display an error level message to the user. Commands should use this if they
  /// fail in some way.
28
  void printError(String message, { StackTrace stackTrace, bool emphasis: false });
29 30 31

  /// Display normal output of the command. This should be used for things like
  /// progress messages, success messages, or just normal command output.
32 33 34 35
  void printStatus(
    String message,
    { bool emphasis: false, bool newline: true, String ansiAlternative, int indent }
  );
36 37 38 39

  /// Use this for verbose tracing output. Users can turn this output on in order
  /// to help diagnose issues with the toolchain or with their setup.
  void printTrace(String message);
Devon Carew's avatar
Devon Carew committed
40

41
  /// Start an indeterminate progress display.
42 43 44
  ///
  /// [message] is the message to display to the user; [progressId] provides an ID which can be
  /// used to identify this type of progress (`hot.reload`, `hot.restart`, ...).
45 46 47 48 49 50 51
  ///
  /// [progressIndicatorPadding] can optionally be used to specify spacing
  /// between the [message] and the progress indicator.
  Status startProgress(
    String message, {
    String progressId,
    bool expectSlowOperation: false,
52
    int progressIndicatorPadding: kDefaultStatusPadding,
53
  });
54 55
}

56 57
typedef void _FinishCallback();

58
class StdoutLogger extends Logger {
59

60 61
  Status _status;

62
  @override
Devon Carew's avatar
Devon Carew committed
63
  bool get isVerbose => false;
64

65
  @override
66
  void printError(String message, { StackTrace stackTrace, bool emphasis: false }) {
67 68 69
    _status?.cancel();
    _status = null;

70 71
    if (emphasis)
      message = terminal.bolden(message);
Devon Carew's avatar
Devon Carew committed
72
    stderr.writeln(message);
73
    if (stackTrace != null)
74
      stderr.writeln(stackTrace.toString());
75 76
  }

77
  @override
78 79 80 81
  void printStatus(
    String message,
    { bool emphasis: false, bool newline: true, String ansiAlternative, int indent }
  ) {
82 83
    _status?.cancel();
    _status = null;
84 85 86 87
    if (terminal.supportsColor && ansiAlternative != null)
      message = ansiAlternative;
    if (emphasis)
      message = terminal.bolden(message);
88 89
    if (indent != null && indent > 0)
      message = LineSplitter.split(message).map((String line) => ' ' * indent + line).join('\n');
90
    if (newline)
91
      message = '$message\n';
92 93 94 95 96
    writeToStdOut(message);
  }

  @protected
  void writeToStdOut(String message) {
97
    stdout.write(message);
98
  }
99

100
  @override
Devon Carew's avatar
Devon Carew committed
101
  void printTrace(String message) { }
102

103
  @override
104 105 106 107
  Status startProgress(
    String message, {
    String progressId,
    bool expectSlowOperation: false,
108
    int progressIndicatorPadding: 59,
109
  }) {
Devon Carew's avatar
Devon Carew committed
110 111
    if (_status != null) {
      // Ignore nested progresses; return a no-op status object.
112 113 114 115
      return new Status()..start();
    }
    if (terminal.supportsColor) {
      _status = new AnsiStatus(message, expectSlowOperation, () { _status = null; }, progressIndicatorPadding)..start();
Devon Carew's avatar
Devon Carew committed
116
    } else {
117 118
      printStatus(message);
      _status = new Status()..start();
119
    }
120
    return _status;
121
  }
122 123
}

124 125 126
/// A [StdoutLogger] which replaces Unicode characters that cannot be printed to
/// the Windows console with alternative symbols.
///
127 128 129 130 131
/// By default, Windows uses either "Consolas" or "Lucida Console" as fonts to
/// render text in the console. Both fonts only have a limited character set.
/// Unicode characters, that are not available in either of the two default
/// fonts, should be replaced by this class with printable symbols. Otherwise,
/// they will show up as the unrepresentable character symbol '�'.
132 133 134 135
class WindowsStdoutLogger extends StdoutLogger {

  @override
  void writeToStdOut(String message) {
136
    // TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio].
137 138
    stdout.write(message
        .replaceAll('✗', 'X')
139
        .replaceAll('✓', '√')
140 141 142 143
    );
  }
}

144
class BufferLogger extends Logger {
145
  @override
Devon Carew's avatar
Devon Carew committed
146 147
  bool get isVerbose => false;

148 149 150
  final StringBuffer _error = new StringBuffer();
  final StringBuffer _status = new StringBuffer();
  final StringBuffer _trace = new StringBuffer();
151 152 153 154 155

  String get errorText => _error.toString();
  String get statusText => _status.toString();
  String get traceText => _trace.toString();

156
  @override
157 158 159 160
  void printError(String message, { StackTrace stackTrace, bool emphasis: false }) {
    _error.writeln(message);
  }

161
  @override
162 163 164 165
  void printStatus(
    String message,
    { bool emphasis: false, bool newline: true, String ansiAlternative, int indent }
  ) {
166 167 168 169 170
    if (newline)
      _status.writeln(message);
    else
      _status.write(message);
  }
171 172

  @override
173
  void printTrace(String message) => _trace.writeln(message);
Devon Carew's avatar
Devon Carew committed
174

175
  @override
176 177 178 179
  Status startProgress(
    String message, {
    String progressId,
    bool expectSlowOperation: false,
180
    int progressIndicatorPadding: kDefaultStatusPadding,
181
  }) {
182 183 184
    printStatus(message);
    return new Status();
  }
185 186 187 188 189 190 191

  /// Clears all buffers.
  void clear() {
    _error.clear();
    _status.clear();
    _trace.clear();
  }
Devon Carew's avatar
Devon Carew committed
192 193
}

194
class VerboseLogger extends Logger {
195 196
  VerboseLogger(this.parent)
    : assert(terminal != null) {
197 198
    stopwatch.start();
  }
Devon Carew's avatar
Devon Carew committed
199

200 201 202 203
  final Logger parent;

  Stopwatch stopwatch = new Stopwatch();

204
  @override
Devon Carew's avatar
Devon Carew committed
205 206
  bool get isVerbose => true;

207
  @override
208
  void printError(String message, { StackTrace stackTrace, bool emphasis: false }) {
209
    _emit(_LogType.error, message, stackTrace);
Devon Carew's avatar
Devon Carew committed
210 211
  }

212
  @override
213 214 215 216
  void printStatus(
    String message,
    { bool emphasis: false, bool newline: true, String ansiAlternative, int indent }
  ) {
217
    _emit(_LogType.status, message);
Devon Carew's avatar
Devon Carew committed
218 219
  }

220
  @override
Devon Carew's avatar
Devon Carew committed
221
  void printTrace(String message) {
222
    _emit(_LogType.trace, message);
Devon Carew's avatar
Devon Carew committed
223 224
  }

225
  @override
226 227 228 229
  Status startProgress(
    String message, {
    String progressId,
    bool expectSlowOperation: false,
230
    int progressIndicatorPadding: kDefaultStatusPadding,
231
  }) {
232 233 234 235
    printStatus(message);
    return new Status();
  }

236 237 238
  void _emit(_LogType type, String message, [StackTrace stackTrace]) {
    if (message.trim().isEmpty)
      return;
Devon Carew's avatar
Devon Carew committed
239

240
    final int millis = stopwatch.elapsedMilliseconds;
241
    stopwatch.reset();
Devon Carew's avatar
Devon Carew committed
242

243
    String prefix;
244
    const int prefixWidth = 8;
245 246 247 248 249
    if (millis == 0) {
      prefix = ''.padLeft(prefixWidth);
    } else {
      prefix = '+$millis ms'.padLeft(prefixWidth);
      if (millis >= 100)
250
        prefix = terminal.bolden(prefix);
251 252
    }
    prefix = '[$prefix] ';
Devon Carew's avatar
Devon Carew committed
253

254 255
    final String indent = ''.padLeft(prefix.length);
    final String indentMessage = message.replaceAll('\n', '\n$indent');
Devon Carew's avatar
Devon Carew committed
256 257

    if (type == _LogType.error) {
258
      parent.printError(prefix + terminal.bolden(indentMessage));
Devon Carew's avatar
Devon Carew committed
259
      if (stackTrace != null)
260
        parent.printError(indent + stackTrace.toString().replaceAll('\n', '\n$indent'));
Devon Carew's avatar
Devon Carew committed
261
    } else if (type == _LogType.status) {
262
      parent.printStatus(prefix + terminal.bolden(indentMessage));
Devon Carew's avatar
Devon Carew committed
263
    } else {
264
      parent.printStatus(prefix + indentMessage);
Devon Carew's avatar
Devon Carew committed
265 266 267 268
    }
  }
}

269 270 271 272 273 274
enum _LogType {
  error,
  status,
  trace
}

275 276 277 278 279 280 281 282
/// A [Status] class begins when start is called, and may produce progress
/// information asynchronously.
///
/// When stop is called, summary information supported by this class is printed.
/// If cancel is called, no summary information is displayed.
/// The base class displays nothing at all.
class Status {
  Status();
283

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  bool _isStarted = false;

  factory Status.withSpinner() {
    if (terminal.supportsColor)
      return new AnsiSpinner()..start();
    return new Status()..start();
  }

  /// Display summary information for this spinner; called by [stop].
  void summaryInformation() {}

  /// Call to start spinning.  Call this method via super at the beginning
  /// of a subclass [start] method.
  void start() {
    _isStarted = true;
  }

  /// Call to stop spinning and delete the spinner.  Print summary information,
  /// if applicable to the spinner.
  void stop() {
    if (_isStarted) {
      cancel();
      summaryInformation();
    }
  }

  /// Call to cancel the spinner without printing any summary output.  Call
  /// this method via super at the end of a subclass [cancel] method.
  void cancel() {
    _isStarted = false;
  }
}

/// An [AnsiSpinner] is a simple animation that does nothing but implement an
/// ASCII spinner.  When stopped or canceled, the animation erases itself.
class AnsiSpinner extends Status {
  int ticks = 0;
  Timer timer;

  static final List<String> _progress = <String>['-', r'\', '|', r'/'];
324

325 326 327 328 329 330 331 332 333
  void _callback(Timer _) {
    stdout.write('\b${_progress[ticks++ % _progress.length]}');
  }

  @override
  void start() {
    super.start();
    stdout.write(' ');
    _callback(null);
334
    timer = new Timer.periodic(const Duration(milliseconds: 100), _callback);
335 336
  }

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
  @override
  /// Clears the spinner.  After cancel, the cursor will be one space right
  /// of where it was when [start] was called (assuming no other input).
  void cancel() {
    if (timer?.isActive == true) {
      timer.cancel();
      // Many terminals do not interpret backspace as deleting a character,
      // but rather just moving the cursor back one.
      stdout.write('\b \b');
    }
    super.cancel();
  }
}

/// Constructor writes [message] to [stdout] with padding, then starts as an
/// [AnsiSpinner].  On [cancel] or [stop], will call [onFinish].
/// On [stop], will additionally print out summary information in
/// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
class AnsiStatus extends AnsiSpinner {
  AnsiStatus(this.message, this.expectSlowOperation, this.onFinish, this.padding);
357

358
  final String message;
359
  final bool expectSlowOperation;
360
  final _FinishCallback onFinish;
361 362
  final int padding;

363
  Stopwatch stopwatch;
364
  bool _finished = false;
365

366 367 368 369 370 371 372
  @override
  /// Writes [message] to [stdout] with padding, then begins spinning.
  void start() {
    stopwatch = new Stopwatch()..start();
    stdout.write('${message.padRight(padding)}     ');
    assert(!_finished);
    super.start();
373
  }
374 375

  @override
376
  /// Calls onFinish.
377
  void stop() {
378 379 380 381 382 383 384
    if (!_finished) {
      onFinish();
      _finished = true;
      super.cancel();
      summaryInformation();
    }
  }
385

386 387 388 389 390 391 392 393
  @override
  /// Backs up 4 characters and prints a (minimum) 5 character padded time.  If
  /// [expectSlowOperation] is true, the time is in seconds; otherwise,
  /// milliseconds.  Only backs up 4 characters because [super.cancel] backs
  /// up one.
  ///
  /// Example: '\b\b\b\b 0.5s', '\b\b\b\b150ms', '\b\b\b\b1600ms'
  void summaryInformation() {
394
    if (expectSlowOperation) {
395
      stdout.writeln('\b\b\b\b${getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)}');
396
    } else {
397
      stdout.writeln('\b\b\b\b${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}');
398 399 400 401
    }
  }

  @override
402
  /// Calls [onFinish].
403
  void cancel() {
404 405 406 407 408 409
    if (!_finished) {
      onFinish();
      _finished = true;
      super.cancel();
      stdout.write('\n');
    }
410 411
  }
}