mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Disallow calling stop() or cancel() multiple times. This means that when you use startProgress you have to more carefully think about what exactly is going on. Properly cancel startProgress in non-ANSI situations, so that back-to-back startProgress calls all render to the console.
429 lines
11 KiB
Dart
429 lines
11 KiB
Dart
// 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.
|
||
|
||
import 'dart:async';
|
||
import 'dart:convert' show LineSplitter;
|
||
|
||
import 'package:meta/meta.dart';
|
||
|
||
import 'io.dart';
|
||
import 'terminal.dart';
|
||
import 'utils.dart';
|
||
|
||
const int kDefaultStatusPadding = 59;
|
||
|
||
typedef void VoidCallback();
|
||
|
||
abstract class Logger {
|
||
bool get isVerbose => false;
|
||
|
||
bool quiet = false;
|
||
|
||
bool get supportsColor => terminal.supportsColor;
|
||
set supportsColor(bool value) {
|
||
terminal.supportsColor = value;
|
||
}
|
||
|
||
/// Display an error level message to the user. Commands should use this if they
|
||
/// fail in some way.
|
||
void printError(String message, { StackTrace stackTrace, bool emphasis = false });
|
||
|
||
/// Display normal output of the command. This should be used for things like
|
||
/// progress messages, success messages, or just normal command output.
|
||
void printStatus(
|
||
String message,
|
||
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
|
||
);
|
||
|
||
/// 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);
|
||
|
||
/// Start an indeterminate progress display.
|
||
///
|
||
/// [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`, ...).
|
||
///
|
||
/// [progressIndicatorPadding] can optionally be used to specify spacing
|
||
/// between the [message] and the progress indicator.
|
||
Status startProgress(
|
||
String message, {
|
||
String progressId,
|
||
bool expectSlowOperation = false,
|
||
int progressIndicatorPadding = kDefaultStatusPadding,
|
||
});
|
||
}
|
||
|
||
class StdoutLogger extends Logger {
|
||
|
||
Status _status;
|
||
|
||
@override
|
||
bool get isVerbose => false;
|
||
|
||
@override
|
||
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
|
||
_status?.cancel();
|
||
_status = null;
|
||
if (emphasis)
|
||
message = terminal.bolden(message);
|
||
stderr.writeln(message);
|
||
if (stackTrace != null)
|
||
stderr.writeln(stackTrace.toString());
|
||
}
|
||
|
||
@override
|
||
void printStatus(
|
||
String message,
|
||
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
|
||
) {
|
||
_status?.cancel();
|
||
_status = null;
|
||
if (terminal.supportsColor && ansiAlternative != null)
|
||
message = ansiAlternative;
|
||
if (emphasis)
|
||
message = terminal.bolden(message);
|
||
if (indent != null && indent > 0)
|
||
message = LineSplitter.split(message).map((String line) => ' ' * indent + line).join('\n');
|
||
if (newline)
|
||
message = '$message\n';
|
||
writeToStdOut(message);
|
||
}
|
||
|
||
@protected
|
||
void writeToStdOut(String message) {
|
||
stdout.write(message);
|
||
}
|
||
|
||
@override
|
||
void printTrace(String message) { }
|
||
|
||
@override
|
||
Status startProgress(
|
||
String message, {
|
||
String progressId,
|
||
bool expectSlowOperation = false,
|
||
int progressIndicatorPadding = 59,
|
||
}) {
|
||
if (_status != null) {
|
||
// Ignore nested progresses; return a no-op status object.
|
||
return new Status(onFinish: _clearStatus)..start();
|
||
}
|
||
if (terminal.supportsColor) {
|
||
_status = new AnsiStatus(
|
||
message: message,
|
||
expectSlowOperation: expectSlowOperation,
|
||
padding: progressIndicatorPadding,
|
||
onFinish: _clearStatus,
|
||
)..start();
|
||
} else {
|
||
printStatus(message);
|
||
_status = new Status(onFinish: _clearStatus)..start();
|
||
}
|
||
return _status;
|
||
}
|
||
|
||
void _clearStatus() {
|
||
_status = null;
|
||
}
|
||
}
|
||
|
||
/// A [StdoutLogger] which replaces Unicode characters that cannot be printed to
|
||
/// the Windows console with alternative symbols.
|
||
///
|
||
/// 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 '<27>'.
|
||
class WindowsStdoutLogger extends StdoutLogger {
|
||
|
||
@override
|
||
void writeToStdOut(String message) {
|
||
// TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio].
|
||
stdout.write(message
|
||
.replaceAll('✗', 'X')
|
||
.replaceAll('✓', '√')
|
||
);
|
||
}
|
||
}
|
||
|
||
class BufferLogger extends Logger {
|
||
@override
|
||
bool get isVerbose => false;
|
||
|
||
final StringBuffer _error = new StringBuffer();
|
||
final StringBuffer _status = new StringBuffer();
|
||
final StringBuffer _trace = new StringBuffer();
|
||
|
||
String get errorText => _error.toString();
|
||
String get statusText => _status.toString();
|
||
String get traceText => _trace.toString();
|
||
|
||
@override
|
||
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
|
||
_error.writeln(message);
|
||
}
|
||
|
||
@override
|
||
void printStatus(
|
||
String message,
|
||
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
|
||
) {
|
||
if (newline)
|
||
_status.writeln(message);
|
||
else
|
||
_status.write(message);
|
||
}
|
||
|
||
@override
|
||
void printTrace(String message) => _trace.writeln(message);
|
||
|
||
@override
|
||
Status startProgress(
|
||
String message, {
|
||
String progressId,
|
||
bool expectSlowOperation = false,
|
||
int progressIndicatorPadding = kDefaultStatusPadding,
|
||
}) {
|
||
printStatus(message);
|
||
return new Status()..start();
|
||
}
|
||
|
||
/// Clears all buffers.
|
||
void clear() {
|
||
_error.clear();
|
||
_status.clear();
|
||
_trace.clear();
|
||
}
|
||
}
|
||
|
||
class VerboseLogger extends Logger {
|
||
VerboseLogger(this.parent)
|
||
: assert(terminal != null) {
|
||
stopwatch.start();
|
||
}
|
||
|
||
final Logger parent;
|
||
|
||
Stopwatch stopwatch = new Stopwatch();
|
||
|
||
@override
|
||
bool get isVerbose => true;
|
||
|
||
@override
|
||
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
|
||
_emit(_LogType.error, message, stackTrace);
|
||
}
|
||
|
||
@override
|
||
void printStatus(
|
||
String message,
|
||
{ bool emphasis = false, bool newline = true, String ansiAlternative, int indent }
|
||
) {
|
||
_emit(_LogType.status, message);
|
||
}
|
||
|
||
@override
|
||
void printTrace(String message) {
|
||
_emit(_LogType.trace, message);
|
||
}
|
||
|
||
@override
|
||
Status startProgress(
|
||
String message, {
|
||
String progressId,
|
||
bool expectSlowOperation = false,
|
||
int progressIndicatorPadding = kDefaultStatusPadding,
|
||
}) {
|
||
printStatus(message);
|
||
return new Status(onFinish: () {
|
||
printTrace('$message (completed)');
|
||
})..start();
|
||
}
|
||
|
||
void _emit(_LogType type, String message, [StackTrace stackTrace]) {
|
||
if (message.trim().isEmpty)
|
||
return;
|
||
|
||
final int millis = stopwatch.elapsedMilliseconds;
|
||
stopwatch.reset();
|
||
|
||
String prefix;
|
||
const int prefixWidth = 8;
|
||
if (millis == 0) {
|
||
prefix = ''.padLeft(prefixWidth);
|
||
} else {
|
||
prefix = '+$millis ms'.padLeft(prefixWidth);
|
||
if (millis >= 100)
|
||
prefix = terminal.bolden(prefix);
|
||
}
|
||
prefix = '[$prefix] ';
|
||
|
||
final String indent = ''.padLeft(prefix.length);
|
||
final String indentMessage = message.replaceAll('\n', '\n$indent');
|
||
|
||
if (type == _LogType.error) {
|
||
parent.printError(prefix + terminal.bolden(indentMessage));
|
||
if (stackTrace != null)
|
||
parent.printError(indent + stackTrace.toString().replaceAll('\n', '\n$indent'));
|
||
} else if (type == _LogType.status) {
|
||
parent.printStatus(prefix + terminal.bolden(indentMessage));
|
||
} else {
|
||
parent.printStatus(prefix + indentMessage);
|
||
}
|
||
}
|
||
}
|
||
|
||
enum _LogType {
|
||
error,
|
||
status,
|
||
trace
|
||
}
|
||
|
||
/// A [Status] class begins when start is called, and may produce progress
|
||
/// information asynchronously.
|
||
///
|
||
/// The [Status] class itself never has any output.
|
||
///
|
||
/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single
|
||
/// space character when stopped or canceled.
|
||
///
|
||
/// The [AnsiStatus] subclass shows a spinner, and replaces it with timing
|
||
/// information when stopped. When canceled, the information isn't shown. In
|
||
/// either case, a newline is printed.
|
||
///
|
||
/// Generally, consider `logger.startProgress` instead of directly creating
|
||
/// a [Status] or one of its subclasses.
|
||
class Status {
|
||
Status({ this.onFinish });
|
||
|
||
/// A straight [Status] or an [AnsiSpinner] (depending on whether the
|
||
/// terminal is fancy enough), already started.
|
||
factory Status.withSpinner({ VoidCallback onFinish }) {
|
||
if (terminal.supportsColor)
|
||
return new AnsiSpinner(onFinish: onFinish)..start();
|
||
return new Status(onFinish: onFinish)..start();
|
||
}
|
||
|
||
final VoidCallback onFinish;
|
||
|
||
bool _isStarted = false;
|
||
|
||
/// Call to start spinning.
|
||
void start() {
|
||
assert(!_isStarted);
|
||
_isStarted = true;
|
||
}
|
||
|
||
/// Call to stop spinning after success.
|
||
void stop() {
|
||
assert(_isStarted);
|
||
_isStarted = false;
|
||
if (onFinish != null)
|
||
onFinish();
|
||
}
|
||
|
||
/// Call to cancel the spinner after failure or cancelation.
|
||
void cancel() {
|
||
assert(_isStarted);
|
||
_isStarted = false;
|
||
if (onFinish != null)
|
||
onFinish();
|
||
}
|
||
}
|
||
|
||
/// 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 {
|
||
AnsiSpinner({ VoidCallback onFinish }) : super(onFinish: onFinish);
|
||
|
||
int ticks = 0;
|
||
Timer timer;
|
||
|
||
static final List<String> _progress = <String>[r'-', r'\', r'|', r'/'];
|
||
|
||
void _callback(Timer timer) {
|
||
stdout.write('\b${_progress[ticks++ % _progress.length]}');
|
||
}
|
||
|
||
@override
|
||
void start() {
|
||
super.start();
|
||
assert(timer == null);
|
||
stdout.write(' ');
|
||
timer = new Timer.periodic(const Duration(milliseconds: 100), _callback);
|
||
_callback(timer);
|
||
}
|
||
|
||
@override
|
||
void stop() {
|
||
assert(timer.isActive);
|
||
timer.cancel();
|
||
stdout.write('\b \b');
|
||
super.stop();
|
||
}
|
||
|
||
@override
|
||
void cancel() {
|
||
assert(timer.isActive);
|
||
timer.cancel();
|
||
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.padding,
|
||
VoidCallback onFinish,
|
||
}) : super(onFinish: onFinish);
|
||
|
||
final String message;
|
||
final bool expectSlowOperation;
|
||
final int padding;
|
||
|
||
Stopwatch stopwatch;
|
||
|
||
@override
|
||
void start() {
|
||
stopwatch = new Stopwatch()..start();
|
||
stdout.write('${message.padRight(padding)} ');
|
||
super.start();
|
||
}
|
||
|
||
@override
|
||
void stop() {
|
||
super.stop();
|
||
writeSummaryInformation();
|
||
stdout.write('\n');
|
||
}
|
||
|
||
@override
|
||
void cancel() {
|
||
super.cancel();
|
||
stdout.write('\n');
|
||
}
|
||
|
||
/// 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 writeSummaryInformation() {
|
||
if (expectSlowOperation) {
|
||
stdout.write('\b\b\b\b${getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)}');
|
||
} else {
|
||
stdout.write('\b\b\b\b${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}');
|
||
}
|
||
}
|
||
}
|