• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:math' show Random, max;
7
8import 'package:intl/intl.dart';
9
10import '../convert.dart';
11import '../globals.dart';
12import 'context.dart';
13import 'file_system.dart';
14import 'io.dart' as io;
15import 'platform.dart';
16import 'terminal.dart';
17
18const BotDetector _kBotDetector = BotDetector();
19
20class BotDetector {
21  const BotDetector();
22
23  bool get isRunningOnBot {
24    if (
25        // Explicitly stated to not be a bot.
26        platform.environment['BOT'] == 'false'
27
28        // Set by the IDEs to the IDE name, so a strong signal that this is not a bot.
29        || platform.environment.containsKey('FLUTTER_HOST')
30        // When set, GA logs to a local file (normally for tests) so we don't need to filter.
31        || platform.environment.containsKey('FLUTTER_ANALYTICS_LOG_FILE')
32    ) {
33      return false;
34    }
35
36    return platform.environment['BOT'] == 'true'
37
38        // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
39        || platform.environment['TRAVIS'] == 'true'
40        || platform.environment['CONTINUOUS_INTEGRATION'] == 'true'
41        || platform.environment.containsKey('CI') // Travis and AppVeyor
42
43        // https://www.appveyor.com/docs/environment-variables/
44        || platform.environment.containsKey('APPVEYOR')
45
46        // https://cirrus-ci.org/guide/writing-tasks/#environment-variables
47        || platform.environment.containsKey('CIRRUS_CI')
48
49        // https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
50        || (platform.environment.containsKey('AWS_REGION') &&
51            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        || platform.environment.containsKey('BUILDBOT_BUILDERNAME')
59        || platform.environment.containsKey('SWARMING_TASK_ID');
60  }
61}
62
63bool get isRunningOnBot {
64  final BotDetector botDetector = context.get<BotDetector>() ?? _kBotDetector;
65  return botDetector.isRunningOnBot;
66}
67
68/// Convert `foo_bar` to `fooBar`.
69String camelCase(String str) {
70  int index = str.indexOf('_');
71  while (index != -1 && index < str.length - 2) {
72    str = str.substring(0, index) +
73      str.substring(index + 1, index + 2).toUpperCase() +
74      str.substring(index + 2);
75    index = str.indexOf('_');
76  }
77  return str;
78}
79
80final RegExp _upperRegex = RegExp(r'[A-Z]');
81
82/// Convert `fooBar` to `foo_bar`.
83String snakeCase(String str, [ String sep = '_' ]) {
84  return str.replaceAllMapped(_upperRegex,
85      (Match m) => '${m.start == 0 ? '' : sep}${m[0].toLowerCase()}');
86}
87
88String toTitleCase(String str) {
89  if (str.isEmpty)
90    return str;
91  return str.substring(0, 1).toUpperCase() + str.substring(1);
92}
93
94/// Return the plural of the given word (`cat(s)`).
95String pluralize(String word, int count) => count == 1 ? word : word + 's';
96
97/// Return the name of an enum item.
98String getEnumName(dynamic enumItem) {
99  final String name = '$enumItem';
100  final int index = name.indexOf('.');
101  return index == -1 ? name : name.substring(index + 1);
102}
103
104File getUniqueFile(Directory dir, String baseName, String ext) {
105  final FileSystem fs = dir.fileSystem;
106  int i = 1;
107
108  while (true) {
109    final String name = '${baseName}_${i.toString().padLeft(2, '0')}.$ext';
110    final File file = fs.file(fs.path.join(dir.path, name));
111    if (!file.existsSync())
112      return file;
113    i++;
114  }
115}
116
117String toPrettyJson(Object jsonable) {
118  return const JsonEncoder.withIndent('  ').convert(jsonable) + '\n';
119}
120
121/// Return a String - with units - for the size in MB of the given number of bytes.
122String getSizeAsMB(int bytesLength) {
123  return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB';
124}
125
126final NumberFormat kSecondsFormat = NumberFormat('0.0');
127final NumberFormat kMillisecondsFormat = NumberFormat.decimalPattern();
128
129String getElapsedAsSeconds(Duration duration) {
130  final double seconds = duration.inMilliseconds / Duration.millisecondsPerSecond;
131  return '${kSecondsFormat.format(seconds)}s';
132}
133
134String getElapsedAsMilliseconds(Duration duration) {
135  return '${kMillisecondsFormat.format(duration.inMilliseconds)}ms';
136}
137
138/// Return a relative path if [fullPath] is contained by the cwd, else return an
139/// absolute path.
140String 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/// A class to maintain a list of items, fire events when items are added or
146/// removed, and calculate a diff of changes when a new list of items is
147/// available.
148class ItemListNotifier<T> {
149  ItemListNotifier() {
150    _items = <T>{};
151  }
152
153  ItemListNotifier.from(List<T> items) {
154    _items = Set<T>.from(items);
155  }
156
157  Set<T> _items;
158
159  final StreamController<T> _addedController = StreamController<T>.broadcast();
160  final StreamController<T> _removedController = StreamController<T>.broadcast();
161
162  Stream<T> get onAdded => _addedController.stream;
163  Stream<T> get onRemoved => _removedController.stream;
164
165  List<T> get items => _items.toList();
166
167  void updateWithNewList(List<T> updatedList) {
168    final Set<T> updatedSet = Set<T>.from(updatedList);
169
170    final Set<T> addedItems = updatedSet.difference(_items);
171    final Set<T> removedItems = _items.difference(updatedSet);
172
173    _items = updatedSet;
174
175    addedItems.forEach(_addedController.add);
176    removedItems.forEach(_removedController.add);
177  }
178
179  /// Close the streams.
180  void dispose() {
181    _addedController.close();
182    _removedController.close();
183  }
184}
185
186class SettingsFile {
187  SettingsFile();
188
189  SettingsFile.parse(String contents) {
190    for (String line in contents.split('\n')) {
191      line = line.trim();
192      if (line.startsWith('#') || line.isEmpty)
193        continue;
194      final int index = line.indexOf('=');
195      if (index != -1)
196        values[line.substring(0, index)] = line.substring(index + 1);
197    }
198  }
199
200  factory SettingsFile.parseFromFile(File file) {
201    return SettingsFile.parse(file.readAsStringSync());
202  }
203
204  final Map<String, String> values = <String, String>{};
205
206  void writeContents(File file) {
207    file.parent.createSync(recursive: true);
208    file.writeAsStringSync(values.keys.map<String>((String key) {
209      return '$key=${values[key]}';
210    }).join('\n'));
211  }
212}
213
214/// A UUID generator. This will generate unique IDs in the format:
215///
216///     f47ac10b-58cc-4372-a567-0e02b2c3d479
217///
218/// The generated UUIDs are 128 bit numbers encoded in a specific string format.
219///
220/// For more information, see
221/// http://en.wikipedia.org/wiki/Universally_unique_identifier.
222class Uuid {
223  final Random _random = Random();
224
225  /// Generate a version 4 (random) UUID. This is a UUID scheme that only uses
226  /// random numbers as the source of the generated UUID.
227  String generateV4() {
228    // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
229    final int special = 8 + _random.nextInt(4);
230
231    return
232      '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
233          '${_bitsDigits(16, 4)}-'
234          '4${_bitsDigits(12, 3)}-'
235          '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
236          '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
237  }
238
239  String _bitsDigits(int bitCount, int digitCount) =>
240      _printDigits(_generateBits(bitCount), digitCount);
241
242  int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);
243
244  String _printDigits(int value, int count) =>
245      value.toRadixString(16).padLeft(count, '0');
246}
247
248/// Given a data structure which is a Map of String to dynamic values, return
249/// the same structure (`Map<String, dynamic>`) with the correct runtime types.
250Map<String, dynamic> castStringKeyedMap(dynamic untyped) {
251  final Map<dynamic, dynamic> map = untyped;
252  return map.cast<String, dynamic>();
253}
254
255typedef AsyncCallback = Future<void> Function();
256
257/// A [Timer] inspired class that:
258///   - has a different initial value for the first callback delay
259///   - waits for a callback to be complete before it starts the next timer
260class Poller {
261  Poller(this.callback, this.pollingInterval, { this.initialDelay = Duration.zero }) {
262    Future<void>.delayed(initialDelay, _handleCallback);
263  }
264
265  final AsyncCallback callback;
266  final Duration initialDelay;
267  final Duration pollingInterval;
268
269  bool _canceled = false;
270  Timer _timer;
271
272  Future<void> _handleCallback() async {
273    if (_canceled)
274      return;
275
276    try {
277      await callback();
278    } catch (error) {
279      printTrace('Error from poller: $error');
280    }
281
282    if (!_canceled)
283      _timer = Timer(pollingInterval, _handleCallback);
284  }
285
286  /// Cancels the poller.
287  void cancel() {
288    _canceled = true;
289    _timer?.cancel();
290    _timer = null;
291  }
292}
293
294/// Returns a [Future] that completes when all given [Future]s complete.
295///
296/// Uses [Future.wait] but removes null elements from the provided
297/// `futures` iterable first.
298///
299/// The returned [Future<List>] will be shorter than the given `futures` if
300/// it contains nulls.
301Future<List<T>> waitGroup<T>(Iterable<Future<T>> futures) {
302  return Future.wait<T>(futures.where((Future<T> future) => future != null));
303}
304/// The terminal width used by the [wrapText] function if there is no terminal
305/// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified.
306const int kDefaultTerminalColumns = 100;
307
308/// Smallest column that will be used for text wrapping. If the requested column
309/// width is smaller than this, then this is what will be used.
310const int kMinColumnWidth = 10;
311
312/// Wraps a block of text into lines no longer than [columnWidth].
313///
314/// Tries to split at whitespace, but if that's not good enough to keep it
315/// under the limit, then it splits in the middle of a word. If [columnWidth] is
316/// smaller than 10 columns, will wrap at 10 columns.
317///
318/// Preserves indentation (leading whitespace) for each line (delimited by '\n')
319/// in the input, and will indent wrapped lines that same amount, adding
320/// [indent] spaces in addition to any existing indent.
321///
322/// If [hangingIndent] is supplied, then that many additional spaces will be
323/// added to each line, except for the first line. The [hangingIndent] is added
324/// to the specified [indent], if any. This is useful for wrapping
325/// text with a heading prefix (e.g. "Usage: "):
326///
327/// ```dart
328/// String prefix = "Usage: ";
329/// print(prefix + wrapText(invocation, indent: 2, hangingIndent: prefix.length, columnWidth: 40));
330/// ```
331///
332/// yields:
333/// ```
334///   Usage: app main_command <subcommand>
335///          [arguments]
336/// ```
337///
338/// If [columnWidth] is not specified, then the column width will be the
339/// [outputPreferences.wrapColumn], which is set with the --wrap-column option.
340///
341/// If [outputPreferences.wrapText] is false, then the text will be returned
342/// unchanged. If [shouldWrap] is specified, then it overrides the
343/// [outputPreferences.wrapText] setting.
344///
345/// The [indent] and [hangingIndent] must be smaller than [columnWidth] when
346/// added together.
347String wrapText(String text, { int columnWidth, int hangingIndent, int indent, bool shouldWrap }) {
348  if (text == null || text.isEmpty) {
349    return '';
350  }
351  indent ??= 0;
352  columnWidth ??= outputPreferences.wrapColumn;
353  columnWidth -= indent;
354  assert(columnWidth >= 0);
355
356  hangingIndent ??= 0;
357  final List<String> splitText = text.split('\n');
358  final List<String> result = <String>[];
359  for (String line in splitText) {
360    String trimmedText = line.trimLeft();
361    final String leadingWhitespace = line.substring(0, line.length - trimmedText.length);
362    List<String> notIndented;
363    if (hangingIndent != 0) {
364      // When we have a hanging indent, we want to wrap the first line at one
365      // width, and the rest at another (offset by hangingIndent), so we wrap
366      // them twice and recombine.
367      final List<String> firstLineWrap = _wrapTextAsLines(
368        trimmedText,
369        columnWidth: columnWidth - leadingWhitespace.length,
370        shouldWrap: shouldWrap,
371      );
372      notIndented = <String>[firstLineWrap.removeAt(0)];
373      trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
374      if (firstLineWrap.isNotEmpty) {
375        notIndented.addAll(_wrapTextAsLines(
376          trimmedText,
377          columnWidth: columnWidth - leadingWhitespace.length - hangingIndent,
378          shouldWrap: shouldWrap,
379        ));
380      }
381    } else {
382      notIndented = _wrapTextAsLines(
383        trimmedText,
384        columnWidth: columnWidth - leadingWhitespace.length,
385        shouldWrap: shouldWrap,
386      );
387    }
388    String hangingIndentString;
389    final String indentString = ' ' * indent;
390    result.addAll(notIndented.map(
391      (String line) {
392        // Don't return any lines with just whitespace on them.
393        if (line.isEmpty) {
394          return '';
395        }
396        final String result = '$indentString${hangingIndentString ?? ''}$leadingWhitespace$line';
397        hangingIndentString ??= ' ' * hangingIndent;
398        return result;
399      },
400    ));
401  }
402  return result.join('\n');
403}
404
405void writePidFile(String pidFile) {
406  if (pidFile != null) {
407    // Write our pid to the file.
408    fs.file(pidFile).writeAsStringSync(io.pid.toString());
409  }
410}
411
412// Used to represent a run of ANSI control sequences next to a visible
413// character.
414class _AnsiRun {
415  _AnsiRun(this.original, this.character);
416
417  String original;
418  String character;
419}
420
421/// Wraps a block of text into lines no longer than [columnWidth], starting at the
422/// [start] column, and returning the result as a list of strings.
423///
424/// Tries to split at whitespace, but if that's not good enough to keep it
425/// under the limit, then splits in the middle of a word. Preserves embedded
426/// newlines, but not indentation (it trims whitespace from each line).
427///
428/// If [columnWidth] is not specified, then the column width will be the width of the
429/// terminal window by default. If the stdout is not a terminal window, then the
430/// default will be [outputPreferences.wrapColumn].
431///
432/// If [outputPreferences.wrapText] is false, then the text will be returned
433/// simply split at the newlines, but not wrapped. If [shouldWrap] is specified,
434/// then it overrides the [outputPreferences.wrapText] setting.
435List<String> _wrapTextAsLines(String text, { int start = 0, int columnWidth, bool shouldWrap }) {
436  if (text == null || text.isEmpty) {
437    return <String>[''];
438  }
439  assert(columnWidth != null);
440  assert(columnWidth >= 0);
441  assert(start >= 0);
442  shouldWrap ??= outputPreferences.wrapText;
443
444  /// Returns true if the code unit at [index] in [text] is a whitespace
445  /// character.
446  ///
447  /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
448  bool isWhitespace(_AnsiRun run) {
449    final int rune = run.character.isNotEmpty ? run.character.codeUnitAt(0) : 0x0;
450    return rune >= 0x0009 && rune <= 0x000D ||
451        rune == 0x0020 ||
452        rune == 0x0085 ||
453        rune == 0x1680 ||
454        rune == 0x180E ||
455        rune >= 0x2000 && rune <= 0x200A ||
456        rune == 0x2028 ||
457        rune == 0x2029 ||
458        rune == 0x202F ||
459        rune == 0x205F ||
460        rune == 0x3000 ||
461        rune == 0xFEFF;
462  }
463
464  // Splits a string so that the resulting list has the same number of elements
465  // as there are visible characters in the string, but elements may include one
466  // or more adjacent ANSI sequences. Joining the list elements again will
467  // reconstitute the original string. This is useful for manipulating "visible"
468  // characters in the presence of ANSI control codes.
469  List<_AnsiRun> splitWithCodes(String input) {
470    final RegExp characterOrCode = RegExp('(\u001b\[[0-9;]*m|.)', multiLine: true);
471    List<_AnsiRun> result = <_AnsiRun>[];
472    final StringBuffer current = StringBuffer();
473    for (Match match in characterOrCode.allMatches(input)) {
474      current.write(match[0]);
475      if (match[0].length < 4) {
476        // This is a regular character, write it out.
477        result.add(_AnsiRun(current.toString(), match[0]));
478        current.clear();
479      }
480    }
481    // If there's something accumulated, then it must be an ANSI sequence, so
482    // add it to the end of the last entry so that we don't lose it.
483    if (current.isNotEmpty) {
484      if (result.isNotEmpty) {
485        result.last.original += current.toString();
486      } else {
487        // If there is nothing in the string besides control codes, then just
488        // return them as the only entry.
489        result = <_AnsiRun>[_AnsiRun(current.toString(), '')];
490      }
491    }
492    return result;
493  }
494
495  String joinRun(List<_AnsiRun> list, int start, [ int end ]) {
496    return list.sublist(start, end).map<String>((_AnsiRun run) => run.original).join().trim();
497  }
498
499  final List<String> result = <String>[];
500  final int effectiveLength = max(columnWidth - start, kMinColumnWidth);
501  for (String line in text.split('\n')) {
502    // If the line is short enough, even with ANSI codes, then we can just add
503    // add it and move on.
504    if (line.length <= effectiveLength || !shouldWrap) {
505      result.add(line);
506      continue;
507    }
508    final List<_AnsiRun> splitLine = splitWithCodes(line);
509    if (splitLine.length <= effectiveLength) {
510      result.add(line);
511      continue;
512    }
513
514    int currentLineStart = 0;
515    int lastWhitespace;
516    // Find the start of the current line.
517    for (int index = 0; index < splitLine.length; ++index) {
518      if (splitLine[index].character.isNotEmpty && isWhitespace(splitLine[index])) {
519        lastWhitespace = index;
520      }
521
522      if (index - currentLineStart >= effectiveLength) {
523        // Back up to the last whitespace, unless there wasn't any, in which
524        // case we just split where we are.
525        if (lastWhitespace != null) {
526          index = lastWhitespace;
527        }
528
529        result.add(joinRun(splitLine, currentLineStart, index));
530
531        // Skip any intervening whitespace.
532        while (index < splitLine.length && isWhitespace(splitLine[index])) {
533          index++;
534        }
535
536        currentLineStart = index;
537        lastWhitespace = null;
538      }
539    }
540    result.add(joinRun(splitLine, currentLineStart));
541  }
542  return result;
543}
544