utils.dart 18 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:intl/intl.dart';
Devon Carew's avatar
Devon Carew committed
9

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

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

class BotDetector {
  const BotDetector();

  bool get isRunningOnBot {
24 25 26 27 28 29 30 31 32 33 34 35 36 37
    if (
        // Explicitly stated to not be a bot.
        platform.environment['BOT'] == 'false'

        // Set by the IDEs to the IDE name, so a strong signal that this is not a bot.
        || platform.environment.containsKey('FLUTTER_HOST')
    ) {
      return false;
    }

    return platform.environment['BOT'] == 'true'

        // Non-interactive terminals are assumed to be bots.
        || !io.stdout.hasTerminal
38

39
        // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
40 41 42
        || platform.environment['TRAVIS'] == 'true'
        || platform.environment['CONTINUOUS_INTEGRATION'] == 'true'
        || platform.environment.containsKey('CI') // Travis and AppVeyor
43

44
        // https://www.appveyor.com/docs/environment-variables/
45
        || platform.environment.containsKey('APPVEYOR')
46

47 48 49 50
        // 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
51
        || (platform.environment.containsKey('AWS_REGION') && platform.environment.containsKey('CODEBUILD_INITIATOR'))
52

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

56
        // Properties on Flutter's Chrome Infra bots.
57
        || platform.environment['CHROME_HEADLESS'] == '1'
58 59
        || platform.environment.containsKey('BUILDBOT_BUILDERNAME')
        || platform.environment.containsKey('SWARMING_TASK_ID');
60 61
  }
}
62

63
bool get isRunningOnBot {
64
  final BotDetector botDetector = context.get<BotDetector>() ?? _kBotDetector;
65
  return botDetector.isRunningOnBot;
66 67
}

68 69 70 71 72 73 74 75 76 77 78 79
/// 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;
}

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

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

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

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

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

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

  while (true) {
109 110
    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
111 112 113 114 115 116
    if (!file.existsSync())
      return file;
    i++;
  }
}

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

121 122 123 124 125
/// 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';
}

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

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

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

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

145 146 147 148 149
/// 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() {
150
    _items = <T>{};
151 152 153
  }

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

  Set<T> _items;

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

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

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

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

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

    _items = updatedSet;

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

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

class SettingsFile {
187 188
  SettingsFile();

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

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

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

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

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

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

    return
      '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
          '${_bitsDigits(16, 4)}-'
          '4${_bitsDigits(12, 3)}-'
235
          '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
236 237 238 239 240 241 242 243 244 245 246
          '${_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');
}
247

248 249 250 251 252 253 254
/// 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>();
}

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

/// 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 {
261
  Poller(this.callback, this.pollingInterval, { this.initialDelay = Duration.zero }) {
262
    Future<void>.delayed(initialDelay, _handleCallback);
263 264 265 266 267 268
  }

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

269
  bool _canceled = false;
270 271
  Timer _timer;

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

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

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

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

/// 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));
}
304 305 306 307 308 309 310 311 312 313 314
/// 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
315 316
/// under the limit, then it splits in the middle of a word. If [columnWidth] is
/// smaller than 10 columns, will wrap at 10 columns.
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
///
/// 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
342 343
/// unchanged. If [shouldWrap] is specified, then it overrides the
/// [outputPreferences.wrapText] setting.
344 345 346
///
/// The [indent] and [hangingIndent] must be smaller than [columnWidth] when
/// added together.
347
String wrapText(String text, { int columnWidth, int hangingIndent, int indent, bool shouldWrap }) {
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
  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,
370
        shouldWrap: shouldWrap,
371 372 373 374 375 376 377
      );
      notIndented = <String>[firstLineWrap.removeAt(0)];
      trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
      if (firstLineWrap.isNotEmpty) {
        notIndented.addAll(_wrapTextAsLines(
          trimmedText,
          columnWidth: columnWidth - leadingWhitespace.length - hangingIndent,
378
          shouldWrap: shouldWrap,
379 380 381 382 383 384
        ));
      }
    } else {
      notIndented = _wrapTextAsLines(
        trimmedText,
        columnWidth: columnWidth - leadingWhitespace.length,
385
        shouldWrap: shouldWrap,
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
      );
    }
    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');
}

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

412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
// 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
433 434
/// simply split at the newlines, but not wrapped. If [shouldWrap] is specified,
/// then it overrides the [outputPreferences.wrapText] setting.
435
List<String> _wrapTextAsLines(String text, { int start = 0, int columnWidth, bool shouldWrap }) {
436 437 438 439 440 441
  if (text == null || text.isEmpty) {
    return <String>[''];
  }
  assert(columnWidth != null);
  assert(columnWidth >= 0);
  assert(start >= 0);
442
  shouldWrap ??= outputPreferences.wrapText;
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 494

  /// 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;
  }

495
  String joinRun(List<_AnsiRun> list, int start, [ int end ]) {
496 497 498 499 500 501 502 503
    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.
504
    if (line.length <= effectiveLength || !shouldWrap) {
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 531
      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.
532
        while (index < splitLine.length && isWhitespace(splitLine[index])) {
533 534 535 536 537 538 539 540 541 542 543
          index++;
        }

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