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