utils.dart 17.9 KB
Newer Older
1 2 3 4 5
// 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';
6
import 'dart:math' show Random, max;
Devon Carew's avatar
Devon Carew committed
7 8

import 'package:crypto/crypto.dart';
9
import 'package:intl/intl.dart';
Devon Carew's avatar
Devon Carew committed
10

11
import '../convert.dart';
12
import '../globals.dart';
13
import 'context.dart';
14
import 'file_system.dart';
15
import 'io.dart' as io;
16
import 'platform.dart';
17
import 'terminal.dart';
18

19
const BotDetector _kBotDetector = BotDetector();
20 21 22 23 24

class BotDetector {
  const BotDetector();

  bool get isRunningOnBot {
25 26
    return platform.environment['BOT'] != 'false'
       && (platform.environment['BOT'] == 'true'
27

28
        // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
29 30 31
        || platform.environment['TRAVIS'] == 'true'
        || platform.environment['CONTINUOUS_INTEGRATION'] == 'true'
        || platform.environment.containsKey('CI') // Travis and AppVeyor
32

33
        // https://www.appveyor.com/docs/environment-variables/
34
        || platform.environment.containsKey('APPVEYOR')
35

36 37 38 39
        // https://cirrus-ci.org/guide/writing-tasks/#environment-variables
        || platform.environment.containsKey('CIRRUS_CI')

        // https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
40
        || (platform.environment.containsKey('AWS_REGION') && platform.environment.containsKey('CODEBUILD_INITIATOR'))
41

42
        // https://wiki.jenkins.io/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-belowJenkinsSetEnvironmentVariables
43
        || platform.environment.containsKey('JENKINS_URL')
44

45
        // Properties on Flutter's Chrome Infra bots.
46 47
        || platform.environment['CHROME_HEADLESS'] == '1'
        || platform.environment.containsKey('BUILDBOT_BUILDERNAME'));
48 49
  }
}
50

51
bool get isRunningOnBot {
52
  final BotDetector botDetector = context.get<BotDetector>() ?? _kBotDetector;
53
  return botDetector.isRunningOnBot;
54 55
}

Ian Hickson's avatar
Ian Hickson committed
56
String hex(List<int> bytes) {
57
  final StringBuffer result = StringBuffer();
Ian Hickson's avatar
Ian Hickson committed
58 59 60 61 62
  for (int part in bytes)
    result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
  return result.toString();
}

Devon Carew's avatar
Devon Carew committed
63
String calculateSha(File file) {
Ian Hickson's avatar
Ian Hickson committed
64
  return hex(sha1.convert(file.readAsBytesSync()).bytes);
Devon Carew's avatar
Devon Carew committed
65
}
66

67 68 69 70 71 72 73 74 75 76 77 78
/// Convert `foo_bar` to `fooBar`.
String camelCase(String str) {
  int index = str.indexOf('_');
  while (index != -1 && index < str.length - 2) {
    str = str.substring(0, index) +
      str.substring(index + 1, index + 2).toUpperCase() +
      str.substring(index + 2);
    index = str.indexOf('_');
  }
  return str;
}

79
final RegExp _upperRegex = RegExp(r'[A-Z]');
80 81

/// Convert `fooBar` to `foo_bar`.
82
String snakeCase(String str, [ String sep = '_' ]) {
83 84 85 86
  return str.replaceAllMapped(_upperRegex,
      (Match m) => '${m.start == 0 ? '' : sep}${m[0].toLowerCase()}');
}

87 88 89 90 91 92
String toTitleCase(String str) {
  if (str.isEmpty)
    return str;
  return str.substring(0, 1).toUpperCase() + str.substring(1);
}

93 94 95
/// Return the plural of the given word (`cat(s)`).
String pluralize(String word, int count) => count == 1 ? word : word + 's';

96 97
/// Return the name of an enum item.
String getEnumName(dynamic enumItem) {
98 99
  final String name = '$enumItem';
  final int index = name.indexOf('.');
100 101 102
  return index == -1 ? name : name.substring(index + 1);
}

Devon Carew's avatar
Devon Carew committed
103
File getUniqueFile(Directory dir, String baseName, String ext) {
104
  final FileSystem fs = dir.fileSystem;
Devon Carew's avatar
Devon Carew committed
105 106 107
  int i = 1;

  while (true) {
108 109
    final String name = '${baseName}_${i.toString().padLeft(2, '0')}.$ext';
    final File file = fs.file(fs.path.join(dir.path, name));
Devon Carew's avatar
Devon Carew committed
110 111 112 113 114 115
    if (!file.existsSync())
      return file;
    i++;
  }
}

116
String toPrettyJson(Object jsonable) {
117
  return const JsonEncoder.withIndent('  ').convert(jsonable) + '\n';
118 119
}

120 121 122 123 124
/// Return a String - with units - for the size in MB of the given number of bytes.
String getSizeAsMB(int bytesLength) {
  return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB';
}

125 126
final NumberFormat kSecondsFormat = NumberFormat('0.0');
final NumberFormat kMillisecondsFormat = NumberFormat.decimalPattern();
127 128

String getElapsedAsSeconds(Duration duration) {
129
  final double seconds = duration.inMilliseconds / Duration.millisecondsPerSecond;
130 131 132 133 134 135
  return '${kSecondsFormat.format(seconds)}s';
}

String getElapsedAsMilliseconds(Duration duration) {
  return '${kMillisecondsFormat.format(duration.inMilliseconds)}ms';
}
136

137 138 139
/// Return a relative path if [fullPath] is contained by the cwd, else return an
/// absolute path.
String getDisplayPath(String fullPath) {
140
  final String cwd = fs.currentDirectory.path + fs.path.separator;
141
  return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
142 143
}

144 145 146 147 148
/// A class to maintain a list of items, fire events when items are added or
/// removed, and calculate a diff of changes when a new list of items is
/// available.
class ItemListNotifier<T> {
  ItemListNotifier() {
149
    _items = <T>{};
150 151 152
  }

  ItemListNotifier.from(List<T> items) {
153
    _items = Set<T>.from(items);
154 155 156 157
  }

  Set<T> _items;

158 159
  final StreamController<T> _addedController = StreamController<T>.broadcast();
  final StreamController<T> _removedController = StreamController<T>.broadcast();
160 161 162 163 164 165 166

  Stream<T> get onAdded => _addedController.stream;
  Stream<T> get onRemoved => _removedController.stream;

  List<T> get items => _items.toList();

  void updateWithNewList(List<T> updatedList) {
167
    final Set<T> updatedSet = Set<T>.from(updatedList);
168

169 170
    final Set<T> addedItems = updatedSet.difference(_items);
    final Set<T> removedItems = _items.difference(updatedSet);
171 172 173

    _items = updatedSet;

174 175
    addedItems.forEach(_addedController.add);
    removedItems.forEach(_removedController.add);
176 177 178 179 180 181 182 183
  }

  /// Close the streams.
  void dispose() {
    _addedController.close();
    _removedController.close();
  }
}
184 185

class SettingsFile {
186 187
  SettingsFile();

188 189 190 191 192
  SettingsFile.parse(String contents) {
    for (String line in contents.split('\n')) {
      line = line.trim();
      if (line.startsWith('#') || line.isEmpty)
        continue;
193
      final int index = line.indexOf('=');
194 195 196 197 198 199
      if (index != -1)
        values[line.substring(0, index)] = line.substring(index + 1);
    }
  }

  factory SettingsFile.parseFromFile(File file) {
200
    return SettingsFile.parse(file.readAsStringSync());
201 202 203 204 205
  }

  final Map<String, String> values = <String, String>{};

  void writeContents(File file) {
206
    file.parent.createSync(recursive: true);
207
    file.writeAsStringSync(values.keys.map<String>((String key) {
208 209 210 211
      return '$key=${values[key]}';
    }).join('\n'));
  }
}
212 213 214 215 216

/// A UUID generator. This will generate unique IDs in the format:
///
///     f47ac10b-58cc-4372-a567-0e02b2c3d479
///
217
/// The generated UUIDs are 128 bit numbers encoded in a specific string format.
218 219 220 221
///
/// For more information, see
/// http://en.wikipedia.org/wiki/Universally_unique_identifier.
class Uuid {
222
  final Random _random = Random();
223

224 225
  /// Generate a version 4 (random) UUID. This is a UUID scheme that only uses
  /// random numbers as the source of the generated UUID.
226 227
  String generateV4() {
    // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
228
    final int special = 8 + _random.nextInt(4);
229 230 231 232 233

    return
      '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
          '${_bitsDigits(16, 4)}-'
          '4${_bitsDigits(12, 3)}-'
234
          '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
235 236 237 238 239 240 241 242 243 244 245
          '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
  }

  String _bitsDigits(int bitCount, int digitCount) =>
      _printDigits(_generateBits(bitCount), digitCount);

  int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);

  String _printDigits(int value, int count) =>
      value.toRadixString(16).padLeft(count, '0');
}
246

247 248 249 250 251 252 253
/// Given a data structure which is a Map of String to dynamic values, return
/// the same structure (`Map<String, dynamic>`) with the correct runtime types.
Map<String, dynamic> castStringKeyedMap(dynamic untyped) {
  final Map<dynamic, dynamic> map = untyped;
  return map.cast<String, dynamic>();
}

254
typedef AsyncCallback = Future<void> Function();
255 256 257 258 259

/// A [Timer] inspired class that:
///   - has a different initial value for the first callback delay
///   - waits for a callback to be complete before it starts the next timer
class Poller {
260
  Poller(this.callback, this.pollingInterval, { this.initialDelay = Duration.zero }) {
261
    Future<void>.delayed(initialDelay, _handleCallback);
262 263 264 265 266 267
  }

  final AsyncCallback callback;
  final Duration initialDelay;
  final Duration pollingInterval;

268
  bool _canceled = false;
269 270
  Timer _timer;

271
  Future<void> _handleCallback() async {
272
    if (_canceled)
273 274 275 276 277 278 279 280
      return;

    try {
      await callback();
    } catch (error) {
      printTrace('Error from poller: $error');
    }

281
    if (!_canceled)
282
      _timer = Timer(pollingInterval, _handleCallback);
283 284 285 286
  }

  /// Cancels the poller.
  void cancel() {
287
    _canceled = true;
288 289 290 291
    _timer?.cancel();
    _timer = null;
  }
}
292 293 294 295 296 297 298 299 300 301 302

/// Returns a [Future] that completes when all given [Future]s complete.
///
/// Uses [Future.wait] but removes null elements from the provided
/// `futures` iterable first.
///
/// The returned [Future<List>] will be shorter than the given `futures` if
/// it contains nulls.
Future<List<T>> waitGroup<T>(Iterable<Future<T>> futures) {
  return Future.wait<T>(futures.where((Future<T> future) => future != null));
}
303 304 305 306 307 308 309 310 311 312 313
/// The terminal width used by the [wrapText] function if there is no terminal
/// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified.
const int kDefaultTerminalColumns = 100;

/// Smallest column that will be used for text wrapping. If the requested column
/// width is smaller than this, then this is what will be used.
const int kMinColumnWidth = 10;

/// Wraps a block of text into lines no longer than [columnWidth].
///
/// Tries to split at whitespace, but if that's not good enough to keep it
314 315
/// under the limit, then it splits in the middle of a word. If [columnWidth] is
/// smaller than 10 columns, will wrap at 10 columns.
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
///
/// Preserves indentation (leading whitespace) for each line (delimited by '\n')
/// in the input, and will indent wrapped lines that same amount, adding
/// [indent] spaces in addition to any existing indent.
///
/// If [hangingIndent] is supplied, then that many additional spaces will be
/// added to each line, except for the first line. The [hangingIndent] is added
/// to the specified [indent], if any. This is useful for wrapping
/// text with a heading prefix (e.g. "Usage: "):
///
/// ```dart
/// String prefix = "Usage: ";
/// print(prefix + wrapText(invocation, indent: 2, hangingIndent: prefix.length, columnWidth: 40));
/// ```
///
/// yields:
/// ```
///   Usage: app main_command <subcommand>
///          [arguments]
/// ```
///
/// If [columnWidth] is not specified, then the column width will be the
/// [outputPreferences.wrapColumn], which is set with the --wrap-column option.
///
/// If [outputPreferences.wrapText] is false, then the text will be returned
341 342
/// unchanged. If [shouldWrap] is specified, then it overrides the
/// [outputPreferences.wrapText] setting.
343 344 345
///
/// The [indent] and [hangingIndent] must be smaller than [columnWidth] when
/// added together.
346
String wrapText(String text, { int columnWidth, int hangingIndent, int indent, bool shouldWrap }) {
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
  if (text == null || text.isEmpty) {
    return '';
  }
  indent ??= 0;
  columnWidth ??= outputPreferences.wrapColumn;
  columnWidth -= indent;
  assert(columnWidth >= 0);

  hangingIndent ??= 0;
  final List<String> splitText = text.split('\n');
  final List<String> result = <String>[];
  for (String line in splitText) {
    String trimmedText = line.trimLeft();
    final String leadingWhitespace = line.substring(0, line.length - trimmedText.length);
    List<String> notIndented;
    if (hangingIndent != 0) {
      // When we have a hanging indent, we want to wrap the first line at one
      // width, and the rest at another (offset by hangingIndent), so we wrap
      // them twice and recombine.
      final List<String> firstLineWrap = _wrapTextAsLines(
        trimmedText,
        columnWidth: columnWidth - leadingWhitespace.length,
369
        shouldWrap: shouldWrap,
370 371 372 373 374 375 376
      );
      notIndented = <String>[firstLineWrap.removeAt(0)];
      trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
      if (firstLineWrap.isNotEmpty) {
        notIndented.addAll(_wrapTextAsLines(
          trimmedText,
          columnWidth: columnWidth - leadingWhitespace.length - hangingIndent,
377
          shouldWrap: shouldWrap,
378 379 380 381 382 383
        ));
      }
    } else {
      notIndented = _wrapTextAsLines(
        trimmedText,
        columnWidth: columnWidth - leadingWhitespace.length,
384
        shouldWrap: shouldWrap,
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
      );
    }
    String hangingIndentString;
    final String indentString = ' ' * indent;
    result.addAll(notIndented.map(
      (String line) {
        // Don't return any lines with just whitespace on them.
        if (line.isEmpty) {
          return '';
        }
        final String result = '$indentString${hangingIndentString ?? ''}$leadingWhitespace$line';
        hangingIndentString ??= ' ' * hangingIndent;
        return result;
      },
    ));
  }
  return result.join('\n');
}

404 405 406 407 408 409 410
void writePidFile(String pidFile) {
  if (pidFile != null) {
    // Write our pid to the file.
    fs.file(pidFile).writeAsStringSync(io.pid.toString());
  }
}

411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
// Used to represent a run of ANSI control sequences next to a visible
// character.
class _AnsiRun {
  _AnsiRun(this.original, this.character);

  String original;
  String character;
}

/// Wraps a block of text into lines no longer than [columnWidth], starting at the
/// [start] column, and returning the result as a list of strings.
///
/// Tries to split at whitespace, but if that's not good enough to keep it
/// under the limit, then splits in the middle of a word. Preserves embedded
/// newlines, but not indentation (it trims whitespace from each line).
///
/// If [columnWidth] is not specified, then the column width will be the width of the
/// terminal window by default. If the stdout is not a terminal window, then the
/// default will be [outputPreferences.wrapColumn].
///
/// If [outputPreferences.wrapText] is false, then the text will be returned
432 433
/// simply split at the newlines, but not wrapped. If [shouldWrap] is specified,
/// then it overrides the [outputPreferences.wrapText] setting.
434
List<String> _wrapTextAsLines(String text, { int start = 0, int columnWidth, bool shouldWrap }) {
435 436 437 438 439 440
  if (text == null || text.isEmpty) {
    return <String>[''];
  }
  assert(columnWidth != null);
  assert(columnWidth >= 0);
  assert(start >= 0);
441
  shouldWrap ??= outputPreferences.wrapText;
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493

  /// Returns true if the code unit at [index] in [text] is a whitespace
  /// character.
  ///
  /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
  bool isWhitespace(_AnsiRun run) {
    final int rune = run.character.isNotEmpty ? run.character.codeUnitAt(0) : 0x0;
    return rune >= 0x0009 && rune <= 0x000D ||
        rune == 0x0020 ||
        rune == 0x0085 ||
        rune == 0x1680 ||
        rune == 0x180E ||
        rune >= 0x2000 && rune <= 0x200A ||
        rune == 0x2028 ||
        rune == 0x2029 ||
        rune == 0x202F ||
        rune == 0x205F ||
        rune == 0x3000 ||
        rune == 0xFEFF;
  }

  // Splits a string so that the resulting list has the same number of elements
  // as there are visible characters in the string, but elements may include one
  // or more adjacent ANSI sequences. Joining the list elements again will
  // reconstitute the original string. This is useful for manipulating "visible"
  // characters in the presence of ANSI control codes.
  List<_AnsiRun> splitWithCodes(String input) {
    final RegExp characterOrCode = RegExp('(\u001b\[[0-9;]*m|.)', multiLine: true);
    List<_AnsiRun> result = <_AnsiRun>[];
    final StringBuffer current = StringBuffer();
    for (Match match in characterOrCode.allMatches(input)) {
      current.write(match[0]);
      if (match[0].length < 4) {
        // This is a regular character, write it out.
        result.add(_AnsiRun(current.toString(), match[0]));
        current.clear();
      }
    }
    // If there's something accumulated, then it must be an ANSI sequence, so
    // add it to the end of the last entry so that we don't lose it.
    if (current.isNotEmpty) {
      if (result.isNotEmpty) {
        result.last.original += current.toString();
      } else {
        // If there is nothing in the string besides control codes, then just
        // return them as the only entry.
        result = <_AnsiRun>[_AnsiRun(current.toString(), '')];
      }
    }
    return result;
  }

494
  String joinRun(List<_AnsiRun> list, int start, [ int end ]) {
495 496 497 498 499 500 501 502
    return list.sublist(start, end).map<String>((_AnsiRun run) => run.original).join().trim();
  }

  final List<String> result = <String>[];
  final int effectiveLength = max(columnWidth - start, kMinColumnWidth);
  for (String line in text.split('\n')) {
    // If the line is short enough, even with ANSI codes, then we can just add
    // add it and move on.
503
    if (line.length <= effectiveLength || !shouldWrap) {
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
      result.add(line);
      continue;
    }
    final List<_AnsiRun> splitLine = splitWithCodes(line);
    if (splitLine.length <= effectiveLength) {
      result.add(line);
      continue;
    }

    int currentLineStart = 0;
    int lastWhitespace;
    // Find the start of the current line.
    for (int index = 0; index < splitLine.length; ++index) {
      if (splitLine[index].character.isNotEmpty && isWhitespace(splitLine[index])) {
        lastWhitespace = index;
      }

      if (index - currentLineStart >= effectiveLength) {
        // Back up to the last whitespace, unless there wasn't any, in which
        // case we just split where we are.
        if (lastWhitespace != null) {
          index = lastWhitespace;
        }

        result.add(joinRun(splitLine, currentLineStart, index));

        // Skip any intervening whitespace.
531
        while (index < splitLine.length && isWhitespace(splitLine[index])) {
532 533 534 535 536 537 538 539 540 541 542
          index++;
        }

        currentLineStart = index;
        lastWhitespace = null;
      }
    }
    result.add(joinRun(splitLine, currentLineStart));
  }
  return result;
}