flutter/dev/bots/utils.dart
Greg Price 98b3e48ed4
Fix hang on successful dev/bots/analyze.dart (#117660)
Fixes #117659

It turns out this was due to the output-suppression timer introduced
recently as part of cleaning up the output (#109206); on success, the
script would wait 10 minutes for the timeout to expire.  This didn't
affect CI because this feature doesn't apply in CI (as detected by
lack of color on stdout.)

Fix the issue by cleaning up the timer on success in the same way
as on failure.

While here, clean up the final summary messages slightly,
and also cut the trailing space that printProgress was leaving
on each line.
2023-02-08 00:04:23 +00:00

253 lines
8.8 KiB
Dart

// Copyright 2014 The Flutter 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:core' hide print;
import 'dart:io' as system show exit;
import 'dart:io' hide exit;
import 'dart:math' as math;
import 'package:meta/meta.dart';
const Duration _quietTimeout = Duration(minutes: 10); // how long the output should be hidden between calls to printProgress before just being verbose
final bool hasColor = stdout.supportsAnsiEscapes;
final String bold = hasColor ? '\x1B[1m' : ''; // shard titles
final String red = hasColor ? '\x1B[31m' : ''; // errors
final String green = hasColor ? '\x1B[32m' : ''; // section titles, commands
final String yellow = hasColor ? '\x1B[33m' : ''; // indications that a test was skipped (usually renders orange or brown)
final String cyan = hasColor ? '\x1B[36m' : ''; // paths
final String reverse = hasColor ? '\x1B[7m' : ''; // clocks
final String gray = hasColor ? '\x1B[30m' : ''; // subtle decorative items (usually renders as dark gray)
final String white = hasColor ? '\x1B[37m' : ''; // last log line (usually renders as light gray)
final String reset = hasColor ? '\x1B[0m' : '';
const int kESC = 0x1B;
const int kOpenSquareBracket = 0x5B;
const int kCSIParameterRangeStart = 0x30;
const int kCSIParameterRangeEnd = 0x3F;
const int kCSIIntermediateRangeStart = 0x20;
const int kCSIIntermediateRangeEnd = 0x2F;
const int kCSIFinalRangeStart = 0x40;
const int kCSIFinalRangeEnd = 0x7E;
String get redLine {
if (hasColor) {
return '$red${'' * stdout.terminalColumns}$reset';
}
return '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
}
String get clock {
final DateTime now = DateTime.now();
return '$reverse'
'${now.hour.toString().padLeft(2, "0")}:'
'${now.minute.toString().padLeft(2, "0")}:'
'${now.second.toString().padLeft(2, "0")}'
'$reset';
}
String prettyPrintDuration(Duration duration) {
String result = '';
final int minutes = duration.inMinutes;
if (minutes > 0) {
result += '${minutes}min ';
}
final int seconds = duration.inSeconds - minutes * 60;
final int milliseconds = duration.inMilliseconds - (seconds * 1000 + minutes * 60 * 1000);
result += '$seconds.${milliseconds.toString().padLeft(3, "0")}s';
return result;
}
typedef PrintCallback = void Function(Object? line);
typedef VoidCallback = void Function();
// Allow print() to be overridden, for tests.
//
// Files that import this library should not import `print` from dart:core
// and should not use dart:io's `stdout` or `stderr`.
//
// By default this hides log lines between `printProgress` calls unless a
// timeout expires or anything calls `foundError`.
//
// Also used to implement `--verbose` in test.dart.
PrintCallback print = _printQuietly;
// Called by foundError and used to implement `--abort-on-error` in test.dart.
VoidCallback? onError;
bool get hasError => _hasError;
bool _hasError = false;
List<List<String>> _errorMessages = <List<String>>[];
final List<String> _pendingLogs = <String>[];
Timer? _hideTimer; // When this is null, the output is verbose.
void foundError(List<String> messages) {
assert(messages.isNotEmpty);
// Make the error message easy to notice in the logs by
// wrapping it in a red box.
final int width = math.max(15, (hasColor ? stdout.terminalColumns : 80) - 1);
print('$red╔═╡${bold}ERROR$reset$red╞═${"" * (width - 9)}');
for (final String message in messages.expand((String line) => line.split('\n'))) {
print('$red$reset $message');
}
print('$red${"" * width}');
// Normally, "print" actually prints to the log. To make the errors visible,
// and to include useful context, print the entire log up to this point, and
// clear it. Subsequent messages will continue to not be logged until there is
// another error.
_pendingLogs.forEach(_printLoudly);
_pendingLogs.clear();
_errorMessages.add(messages);
_hasError = true;
if (onError != null) {
onError!();
}
}
@visibleForTesting
void resetErrorStatus() {
_hasError = false;
_errorMessages.clear();
_pendingLogs.clear();
_hideTimer?.cancel();
_hideTimer = null;
}
Never reportSuccessAndExit(String message) {
_hideTimer?.cancel();
_hideTimer = null;
print('$clock $message$reset');
system.exit(0);
}
Never reportErrorsAndExit(String message) {
_hideTimer?.cancel();
_hideTimer = null;
print('$clock $message$reset');
print(redLine);
print('${red}For your convenience, the error messages reported above are repeated here:$reset');
final bool printSeparators = _errorMessages.any((List<String> messages) => messages.length > 1);
if (printSeparators) {
print(' 🙙 🙛 ');
}
for (int index = 0; index < _errorMessages.length * 2 - 1; index += 1) {
if (index.isEven) {
_errorMessages[index ~/ 2].forEach(print);
} else if (printSeparators) {
print(' 🙙 🙛 ');
}
}
print(redLine);
system.exit(1);
}
void printProgress(String message) {
_pendingLogs.clear();
_hideTimer?.cancel();
_hideTimer = null;
print('$clock $message$reset');
if (hasColor) {
// This sets up a timer to switch to verbose mode when the tests take too long,
// so that if a test hangs we can see the logs.
// (This is only supported with a color terminal. When the terminal doesn't
// support colors, the scripts just print everything verbosely, that way in
// CI there's nothing hidden.)
_hideTimer = Timer(_quietTimeout, () {
_hideTimer = null;
_pendingLogs.forEach(_printLoudly);
_pendingLogs.clear();
});
}
}
final Pattern _lineBreak = RegExp(r'[\r\n]');
void _printQuietly(Object? message) {
// The point of this function is to avoid printing its output unless the timer
// has gone off in which case the function assumes verbose mode is active and
// prints everything. To show that progress is still happening though, rather
// than showing nothing at all, it instead shows the last line of output and
// keeps overwriting it. To do this in color mode, carefully measures the line
// of text ignoring color codes, which is what the parser below does.
if (_hideTimer != null) {
_pendingLogs.add(message.toString());
String line = '$message'.trimRight();
final int start = line.lastIndexOf(_lineBreak) + 1;
int index = start;
int length = 0;
while (index < line.length && length < stdout.terminalColumns) {
if (line.codeUnitAt(index) == kESC) { // 0x1B
index += 1;
if (index < line.length && line.codeUnitAt(index) == kOpenSquareBracket) { // 0x5B, [
// That was the start of a CSI sequence.
index += 1;
while (index < line.length && line.codeUnitAt(index) >= kCSIParameterRangeStart
&& line.codeUnitAt(index) <= kCSIParameterRangeEnd) { // 0x30..0x3F
index += 1; // ...parameter bytes...
}
while (index < line.length && line.codeUnitAt(index) >= kCSIIntermediateRangeStart
&& line.codeUnitAt(index) <= kCSIIntermediateRangeEnd) { // 0x20..0x2F
index += 1; // ...intermediate bytes...
}
if (index < line.length && line.codeUnitAt(index) >= kCSIFinalRangeStart
&& line.codeUnitAt(index) <= kCSIFinalRangeEnd) { // 0x40..0x7E
index += 1; // ...final byte.
}
}
} else {
index += 1;
length += 1;
}
}
line = line.substring(start, index);
if (line.isNotEmpty) {
stdout.write('\r\x1B[2K$white$line$reset');
}
} else {
_printLoudly('$message');
}
}
void _printLoudly(String message) {
if (hasColor) {
// Overwrite the last line written by _printQuietly.
stdout.writeln('\r\x1B[2K$reset${message.trimRight()}');
} else {
stdout.writeln(message);
}
}
// THE FOLLOWING CODE IS A VIOLATION OF OUR STYLE GUIDE
// BECAUSE IT INTRODUCES A VERY FLAKY RACE CONDITION
// https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#never-check-if-a-port-is-available-before-using-it-never-add-timeouts-and-other-race-conditions
// DO NOT USE THE FOLLOWING FUNCTIONS
// DO NOT WRITE CODE LIKE THE FOLLOWING FUNCTIONS
// https://github.com/flutter/flutter/issues/109474
int _portCounter = 8080;
/// Finds the next available local port.
Future<int> findAvailablePortAndPossiblyCauseFlakyTests() async {
while (!await _isPortAvailable(_portCounter)) {
_portCounter += 1;
}
return _portCounter++;
}
Future<bool> _isPortAvailable(int port) async {
try {
final RawSocket socket = await RawSocket.connect('localhost', port);
socket.shutdown(SocketDirection.both);
await socket.close();
return false;
} on SocketException {
return true;
}
}