utils.dart 28.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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:convert';
import 'dart:io';
8
import 'dart:math' as math;
9 10

import 'package:path/path.dart' as path;
11
import 'package:process/process.dart';
12 13
import 'package:stack_trace/stack_trace.dart';

14
import 'devices.dart';
15
import 'host_agent.dart';
16
import 'task_result.dart';
17

18 19 20
/// Virtual current working directory, which affect functions, such as [exec].
String cwd = Directory.current.path;

21
/// The local engine to use for [flutter] and [evalFlutter], if any.
22 23 24
///
/// This is set as an environment variable when running the task, see runTask in runner.dart.
String? get localEngineFromEnv {
25
  const bool isDefined = bool.hasEnvironment('localEngine');
26 27
  return isDefined ? const String.fromEnvironment('localEngine') : null;
}
28

29 30 31 32 33 34 35 36
/// The local engine host to use for [flutter] and [evalFlutter], if any.
///
/// This is set as an environment variable when running the task, see runTask in runner.dart.
String? get localEngineHostFromEnv {
  const bool isDefined = bool.hasEnvironment('localEngineHost');
  return isDefined ? const String.fromEnvironment('localEngineHost') : null;
}

37 38
/// The local engine source path to use if a local engine is used for [flutter]
/// and [evalFlutter].
39 40 41
///
/// This is set as an environment variable when running the task, see runTask in runner.dart.
String? get localEngineSrcPathFromEnv {
42
  const bool isDefined = bool.hasEnvironment('localEngineSrcPath');
43 44
  return isDefined ? const String.fromEnvironment('localEngineSrcPath') : null;
}
45

46 47 48 49 50 51 52 53
/// The local Web SDK to use for [flutter] and [evalFlutter], if any.
///
/// This is set as an environment variable when running the task, see runTask in runner.dart.
String? get localWebSdkFromEnv {
  const bool isDefined = bool.hasEnvironment('localWebSdk');
  return isDefined ? const String.fromEnvironment('localWebSdk') : null;
}

54
List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
55
ProcessManager _processManager = const LocalProcessManager();
56 57 58 59

class ProcessInfo {
  ProcessInfo(this.command, this.process);

60
  final DateTime startTime = DateTime.now();
61 62 63 64 65 66
  final String command;
  final Process process;

  @override
  String toString() {
    return '''
67 68 69
  command: $command
  started: $startTime
  pid    : ${process.pid}
70 71 72 73 74 75 76 77 78 79 80
'''
        .trim();
  }
}

/// Result of a health check for a specific parameter.
class HealthCheckResult {
  HealthCheckResult.success([this.details]) : succeeded = true;
  HealthCheckResult.failure(this.details) : succeeded = false;
  HealthCheckResult.error(dynamic error, dynamic stackTrace)
      : succeeded = false,
81
        details = 'ERROR: $error${stackTrace != null ? '\n$stackTrace' : ''}';
82 83

  final bool succeeded;
84
  final String? details;
85 86 87

  @override
  String toString() {
88
    final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed');
89
    if (details != null && details!.trim().isNotEmpty) {
90 91
      buf.writeln();
      // Indent details by 4 spaces
92
      for (final String line in details!.trim().split('\n')) {
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
        buf.writeln('    $line');
      }
    }
    return '$buf';
  }
}

class BuildFailedError extends Error {
  BuildFailedError(this.message);

  final String message;

  @override
  String toString() => message;
}

void fail(String message) {
110
  throw BuildFailedError(message);
111 112
}

113 114 115 116 117 118 119 120 121 122 123 124
// Remove the given file or directory.
void rm(FileSystemEntity entity, { bool recursive = false}) {
  if (entity.existsSync()) {
    // This should not be necessary, but it turns out that
    // on Windows it's common for deletions to fail due to
    // bogus (we think) "access denied" errors.
    try {
      entity.deleteSync(recursive: recursive);
    } on FileSystemException catch (error) {
      print('Failed to delete ${entity.path}: $error');
    }
  }
125 126 127 128
}

/// Remove recursively.
void rmTree(FileSystemEntity entity) {
129
  rm(entity, recursive: true);
130 131 132 133
}

List<FileSystemEntity> ls(Directory directory) => directory.listSync();

134
Directory dir(String path) => Directory(path);
135

136
File file(String path) => File(path);
137

138
void copy(File sourceFile, Directory targetDirectory, {String? name}) {
139
  final File target = file(
140 141 142 143
      path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
  target.writeAsBytesSync(sourceFile.readAsBytesSync());
}

144
void recursiveCopy(Directory source, Directory target) {
145
  if (!target.existsSync()) {
146
    target.createSync();
147
  }
148

149
  for (final FileSystemEntity entity in source.listSync(followLinks: false)) {
150
    final String name = path.basename(entity.path);
151
    if (entity is Directory && !entity.path.contains('.dart_tool')) {
152
      recursiveCopy(entity, Directory(path.join(target.path, name)));
153
    } else if (entity is File) {
154
      final File dest = File(path.join(target.path, name));
155
      dest.writeAsBytesSync(entity.readAsBytesSync());
156 157
      // Preserve executable bit
      final String modes = entity.statSync().modeString();
158
      if (modes.contains('x')) {
159 160
        makeExecutable(dest);
      }
161 162 163 164
    }
  }
}

165
FileSystemEntity move(FileSystemEntity whatToMove,
166
    {required Directory to, String? name}) {
167 168 169 170
  return whatToMove
      .renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}

171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
/// Equivalent of `chmod a+x file`
void makeExecutable(File file) {
  // Windows files do not have an executable bit
  if (Platform.isWindows) {
    return;
  }
  final ProcessResult result = _processManager.runSync(<String>[
    'chmod',
    'a+x',
    file.path,
  ]);

  if (result.exitCode != 0) {
    throw FileSystemException(
      'Error making ${file.path} executable.\n'
      '${result.stderr}',
      file.path,
    );
  }
}

192 193 194 195 196 197 198 199 200 201 202 203 204
/// Equivalent of `mkdir directory`.
void mkdir(Directory directory) {
  directory.createSync();
}

/// Equivalent of `mkdir -p directory`.
void mkdirs(Directory directory) {
  directory.createSync(recursive: true);
}

bool exists(FileSystemEntity entity) => entity.existsSync();

void section(String title) {
205 206 207 208 209 210 211 212 213
  String output;
  if (Platform.isWindows) {
    // Windows doesn't cope well with characters produced for *nix systems, so
    // just output the title with no decoration.
    output = title;
  } else {
    title = '╡ ••• $title ••• ╞';
    final String line = '═' * math.max((80 - title.length) ~/ 2, 2);
    output = '$line$title$line';
214
    if (output.length == 79) {
215
      output += '═';
216
    }
217
  }
218
  print('\n\n$output\n');
219 220 221 222
}

Future<String> getDartVersion() async {
  // The Dart VM returns the version text to stderr.
223
  final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
224
  String version = (result.stderr as String).trim();
225 226 227 228 229

  // Convert:
  //   Dart VM version: 1.17.0-dev.2.0 (Tue May  3 12:14:52 2016) on "macos_x64"
  // to:
  //   1.17.0-dev.2.0
230
  if (version.contains('(')) {
231
    version = version.substring(0, version.indexOf('(')).trim();
232 233
  }
  if (version.contains(':')) {
234
    version = version.substring(version.indexOf(':') + 1).trim();
235
  }
236 237 238 239

  return version.replaceAll('"', "'");
}

240
Future<String?> getCurrentFlutterRepoCommit() {
241
  if (!dir('${flutterDirectory.path}/.git').existsSync()) {
242
    return Future<String?>.value();
243 244
  }

245
  return inDirectory<String>(flutterDirectory, () {
246 247 248 249 250 251
    return eval('git', <String>['rev-parse', 'HEAD']);
  });
}

Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
  // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
252
  return inDirectory<DateTime>(flutterDirectory, () async {
253
    final String unixTimestamp = await eval('git', <String>[
254 255 256 257 258
      'show',
      '-s',
      '--format=%at',
      commit,
    ]);
259
    final int secondsSinceEpoch = int.parse(unixTimestamp);
260
    return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
261 262 263
  });
}

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
/// Starts a subprocess.
///
/// The first argument is the full path to the executable to run.
///
/// The second argument is the list of arguments to provide on the command line.
/// This argument can be null, indicating no arguments (same as the empty list).
///
/// The `environment` argument can be provided to configure environment variables
/// that will be made available to the subprocess. The `BOT` environment variable
/// is always set and overrides any value provided in the `environment` argument.
/// The `isBot` argument controls the value of the `BOT` variable. It will either
/// be "true", if `isBot` is true (the default), or "false" if it is false.
///
/// The `BOT` variable is in particular used by the `flutter` tool to determine
/// how verbose to be and whether to enable analytics by default.
///
/// The working directory can be provided using the `workingDirectory` argument.
/// By default it will default to the current working directory (see [cwd]).
///
/// Information regarding the execution of the subprocess is printed to the
/// console.
///
/// The actual process executes asynchronously. A handle to the subprocess is
/// returned in the form of a [Future] that completes to a [Process] object.
288 289
Future<Process> startProcess(
  String executable,
290 291
  List<String>? arguments, {
  Map<String, String>? environment,
292
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
293
  String? workingDirectory,
294
}) async {
295
  final String command = '$executable ${arguments?.join(" ") ?? ""}';
296
  final String finalWorkingDirectory = workingDirectory ?? cwd;
297 298
  final Map<String, String> newEnvironment = Map<String, String>.from(environment ?? <String, String>{});
  newEnvironment['BOT'] = isBot ? 'true' : 'false';
299
  newEnvironment['LANG'] = 'en_US.UTF-8';
300
  print('Executing "$command" in "$finalWorkingDirectory" with environment $newEnvironment');
301

302
  final Process process = await _processManager.start(
303
    <String>[executable, ...?arguments],
304
    environment: newEnvironment,
305
    workingDirectory: finalWorkingDirectory,
306
  );
307
  final ProcessInfo processInfo = ProcessInfo(command, process);
308 309
  _runningProcesses.add(processInfo);

310
  unawaited(process.exitCode.then<void>((int exitCode) {
311
    _runningProcesses.remove(processInfo);
312
  }));
313

314
  return process;
315 316
}

317
Future<void> forceQuitRunningProcesses() async {
318
  if (_runningProcesses.isEmpty) {
319
    return;
320
  }
321 322

  // Give normally quitting processes a chance to report their exit code.
323
  await Future<void>.delayed(const Duration(seconds: 1));
324 325

  // Whatever's left, kill it.
326
  for (final ProcessInfo p in _runningProcesses) {
327
    print('Force-quitting process:\n$p');
328
    if (!p.process.kill()) {
329
      print('Failed to force quit process.');
330 331 332 333 334 335
    }
  }
  _runningProcesses.clear();
}

/// Executes a command and returns its exit code.
336 337 338
Future<int> exec(
  String executable,
  List<String> arguments, {
339
  Map<String, String>? environment,
340
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
341
  String? workingDirectory,
342
}) async {
343 344 345 346 347 348 349 350 351 352 353 354
  return _execute(
    executable,
    arguments,
    environment: environment,
    canFail : canFail,
    workingDirectory: workingDirectory,
  );
}

Future<int> _execute(
  String executable,
  List<String> arguments, {
355
  Map<String, String>? environment,
356
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
357 358 359
  String? workingDirectory,
  StringBuffer? output, // if not null, the stdout will be written here
  StringBuffer? stderr, // if not null, the stderr will be written here
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
  bool printStdout = true,
  bool printStderr = true,
}) async {
  final Process process = await startProcess(
    executable,
    arguments,
    environment: environment,
    workingDirectory: workingDirectory,
  );
  await forwardStandardStreams(
    process,
    output: output,
    stderr: stderr,
    printStdout: printStdout,
    printStderr: printStderr,
375
  );
376 377
  final int exitCode = await process.exitCode;

378
  if (exitCode != 0 && !canFail) {
379
    fail('Executable "$executable" failed with exit code $exitCode.');
380
  }
381 382 383

  return exitCode;
}
384

385
/// Forwards standard out and standard error from [process] to this process'
386 387
/// respective outputs. Also writes stdout to [output] and stderr to [stderr]
/// if they are not null.
388 389
///
/// Returns a future that completes when both out and error streams a closed.
390 391
Future<void> forwardStandardStreams(
  Process process, {
392 393
  StringBuffer? output,
  StringBuffer? stderr,
394 395 396
  bool printStdout = true,
  bool printStderr = true,
  }) {
397 398
  final Completer<void> stdoutDone = Completer<void>();
  final Completer<void> stderrDone = Completer<void>();
399
  process.stdout
400 401 402 403 404 405 406 407
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
    .listen((String line) {
      if (printStdout) {
        print('stdout: $line');
      }
      output?.writeln(line);
    }, onDone: () { stdoutDone.complete(); });
408
  process.stderr
409 410 411 412 413 414 415 416
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
    .listen((String line) {
      if (printStderr) {
        print('stderr: $line');
      }
      stderr?.writeln(line);
    }, onDone: () { stderrDone.complete(); });
417

418 419 420 421
  return Future.wait<void>(<Future<void>>[
    stdoutDone.future,
    stderrDone.future,
  ]);
422 423 424 425
}

/// Executes a command and returns its standard output as a String.
///
426
/// For logging purposes, the command's output is also printed out by default.
427 428 429
Future<String> eval(
  String executable,
  List<String> arguments, {
430
  Map<String, String>? environment,
431
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
432
  String? workingDirectory,
433
  StringBuffer? stdout, // if not null, the stdout will be written here
434
  StringBuffer? stderr, // if not null, the stderr will be written here
435 436
  bool printStdout = true,
  bool printStderr = true,
437
}) async {
438
  final StringBuffer output = stdout ?? StringBuffer();
439 440 441 442 443 444 445 446 447 448 449
  await _execute(
    executable,
    arguments,
    environment: environment,
    canFail: canFail,
    workingDirectory: workingDirectory,
    output: output,
    stderr: stderr,
    printStdout: printStdout,
    printStderr: printStderr,
  );
450
  return output.toString().trimRight();
451 452
}

453
List<String> _flutterCommandArgs(String command, List<String> options) {
454
  // Commands support the --device-timeout flag.
455
  final Set<String> supportedDeviceTimeoutCommands = <String>{
456 457 458 459 460 461 462
    'attach',
    'devices',
    'drive',
    'install',
    'logs',
    'run',
    'screenshot',
463
  };
464
  final String? localEngine = localEngineFromEnv;
465
  final String? localEngineHost = localEngineHostFromEnv;
466
  final String? localEngineSrcPath = localEngineSrcPathFromEnv;
467
  final String? localWebSdk = localWebSdkFromEnv;
468
  return <String>[
469
    command,
470 471 472
    if (deviceOperatingSystem == DeviceOperatingSystem.ios && supportedDeviceTimeoutCommands.contains(command))
      ...<String>[
        '--device-timeout',
473
        '5',
474
      ],
475

476 477
    if (command == 'drive' && hostAgent.dumpDirectory != null) ...<String>[
      '--screenshot',
478
      hostAgent.dumpDirectory!.path,
479
    ],
480
    if (localEngine != null) ...<String>['--local-engine', localEngine],
481
    if (localEngineHost != null) ...<String>['--local-engine-host', localEngineHost],
482
    if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
483
    if (localWebSdk != null) ...<String>['--local-web-sdk', localWebSdk],
484
    ...options,
485 486 487 488
    // Use CI flag when running devicelab tests, except for `packages`/`pub` commands.
    // `packages`/`pub` commands effectively runs the `pub` tool, which does not have
    // the same allowed args.
    if (!command.startsWith('packages') && !command.startsWith('pub')) '--ci',
489
  ];
490 491
}

492 493
/// Runs the flutter `command`, and returns the exit code.
/// If `canFail` is `false`, the future completes with an error.
494 495 496
Future<int> flutter(String command, {
  List<String> options = const <String>[],
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
497
  Map<String, String>? environment,
498
  String? workingDirectory,
499
}) async {
500
  final List<String> args = _flutterCommandArgs(command, options);
501
  final int exitCode = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
502
    canFail: canFail, environment: environment, workingDirectory: workingDirectory);
503 504 505 506 507

  if (exitCode != 0 && !canFail) {
    await _flutterScreenshot(workingDirectory: workingDirectory);
  }
  return exitCode;
508 509
}

510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
/// Starts a Flutter subprocess.
///
/// The first argument is the flutter command to run.
///
/// The second argument is the list of arguments to provide on the command line.
/// This argument can be null, indicating no arguments (same as the empty list).
///
/// The `environment` argument can be provided to configure environment variables
/// that will be made available to the subprocess. The `BOT` environment variable
/// is always set and overrides any value provided in the `environment` argument.
/// The `isBot` argument controls the value of the `BOT` variable. It will either
/// be "true", if `isBot` is true (the default), or "false" if it is false.
///
/// The `isBot` argument controls whether the `BOT` environment variable is set
/// to `true` or `false` and is used by the `flutter` tool to determine how
/// verbose to be and whether to enable analytics by default.
///
/// Information regarding the execution of the subprocess is printed to the
/// console.
///
/// The actual process executes asynchronously. A handle to the subprocess is
/// returned in the form of a [Future] that completes to a [Process] object.
532 533 534
Future<Process> startFlutter(String command, {
  List<String> options = const <String>[],
  Map<String, String> environment = const <String, String>{},
535
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
536 537 538
  String? workingDirectory,
}) async {
  final List<String> args = _flutterCommandArgs(command, options);
539
  final Process process = await startProcess(
540 541 542
    path.join(flutterDirectory.path, 'bin', 'flutter'),
    args,
    environment: environment,
543
    isBot: isBot,
544
    workingDirectory: workingDirectory,
545
  );
546 547 548 549 550 551 552

  unawaited(process.exitCode.then<void>((int exitCode) async {
    if (exitCode != 0) {
      await _flutterScreenshot(workingDirectory: workingDirectory);
    }
  }));
  return process;
553 554
}

555
/// Runs a `flutter` command and returns the standard output as a string.
556
Future<String> evalFlutter(String command, {
557
  List<String> options = const <String>[],
558
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
559 560
  Map<String, String>? environment,
  StringBuffer? stderr, // if not null, the stderr will be written here.
561
  String? workingDirectory,
562
}) {
563
  final List<String> args = _flutterCommandArgs(command, options);
564
  return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
565
      canFail: canFail, environment: environment, stderr: stderr, workingDirectory: workingDirectory);
566 567
}

568 569
Future<ProcessResult> executeFlutter(String command, {
  List<String> options = const <String>[],
570
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
571
}) async {
572
  final List<String> args = _flutterCommandArgs(command, options);
573
  final ProcessResult processResult = await _processManager.run(
574 575 576
    <String>[path.join(flutterDirectory.path, 'bin', 'flutter'), ...args],
    workingDirectory: cwd,
  );
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616

  if (processResult.exitCode != 0 && !canFail) {
    await _flutterScreenshot();
  }
  return processResult;
}

Future<void> _flutterScreenshot({ String? workingDirectory }) async {
  try {
    final Directory? dumpDirectory = hostAgent.dumpDirectory;
    if (dumpDirectory == null) {
      return;
    }
    // On command failure try uploading screenshot of failing command.
    final String screenshotPath = path.join(
      dumpDirectory.path,
      'device-screenshot-${DateTime.now().toLocal().toIso8601String()}.png',
    );

    final String deviceId = (await devices.workingDevice).deviceId;
    print('Taking screenshot of working device $deviceId at $screenshotPath');
    final List<String> args = _flutterCommandArgs(
      'screenshot',
      <String>[
        '--out',
        screenshotPath,
        '-d', deviceId,
      ],
    );
    final ProcessResult screenshot = await _processManager.run(
      <String>[path.join(flutterDirectory.path, 'bin', 'flutter'), ...args],
      workingDirectory: workingDirectory ?? cwd,
    );

    if (screenshot.exitCode != 0) {
      print('Failed to take screenshot. Continuing.');
    }
  } catch (exception) {
    print('Failed to take screenshot. Continuing.\n$exception');
  }
617 618
}

619 620 621
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

622 623 624
String get pubBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');

625
Future<int> dart(List<String> args) => exec(dartBin, <String>['--disable-dart-dev', ...args]);
626

627 628
/// Returns a future that completes with a path suitable for JAVA_HOME
/// or with null, if Java cannot be found.
629
Future<String?> findJavaHome() async {
630 631 632 633 634
  if (_javaHome == null) {
    final Iterable<String> hits = grep(
      'Java binary at: ',
      from: await evalFlutter('doctor', options: <String>['-v']),
    );
635
    if (hits.isEmpty) {
636
      return null;
637
    }
638 639 640 641 642 643 644
    final String javaBinary = hits.first
        .split(': ')
        .last;
    // javaBinary == /some/path/to/java/home/bin/java
    _javaHome = path.dirname(path.dirname(javaBinary));
  }
  return _javaHome;
645
}
646
String? _javaHome;
647

648
Future<T> inDirectory<T>(dynamic directory, Future<T> Function() action) async {
649
  final String previousCwd = cwd;
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666
  try {
    cd(directory);
    return await action();
  } finally {
    cd(previousCwd);
  }
}

void cd(dynamic directory) {
  Directory d;
  if (directory is String) {
    cwd = directory;
    d = dir(directory);
  } else if (directory is Directory) {
    cwd = directory.path;
    d = directory;
  } else {
667
    throw FileSystemException('Unsupported directory type ${directory.runtimeType}', directory.toString());
668 669
  }

670
  if (!d.existsSync()) {
671
    throw FileSystemException('Cannot cd into directory that does not exist', d.toString());
672
  }
673 674
}

675
Directory get flutterDirectory => Directory.current.parent.parent;
676

677 678
Directory get openpayDirectory => Directory(requireEnvVar('OPENPAY_CHECKOUT_PATH'));

679
String requireEnvVar(String name) {
680
  final String? value = Platform.environment[name];
681

682
  if (value == null) {
683
    fail('$name environment variable is missing. Quitting.');
684
  }
685

686
  return value!;
687 688
}

689
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
690
  if (!map.containsKey(propertyName)) {
691
    fail('Configuration property not found: $propertyName');
692
  }
693
  final T result = map[propertyName] as T;
694
  return result;
695 696 697
}

String jsonEncode(dynamic data) {
698 699
  final String jsonValue = const JsonEncoder.withIndent('  ').convert(data);
  return '$jsonValue\n';
700 701
}

702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
Future<void> getNewGallery(String revision, Directory galleryDir) async {
  section('Get New Flutter Gallery!');

  if (exists(galleryDir)) {
    galleryDir.deleteSync(recursive: true);
  }

  await inDirectory<void>(galleryDir.parent, () async {
    await exec('git', <String>['clone', 'https://github.com/flutter/gallery.git']);
  });

  await inDirectory<void>(galleryDir, () async {
    await exec('git', <String>['checkout', revision]);
  });
}

718
/// Splits [from] into lines and selects those that contain [pattern].
719
Iterable<String> grep(Pattern pattern, {required String from}) {
720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
  return from.split('\n').where((String line) {
    return line.contains(pattern);
  });
}

/// Captures asynchronous stack traces thrown by [callback].
///
/// This is a convenience wrapper around [Chain] optimized for use with
/// `async`/`await`.
///
/// Example:
///
///     try {
///       await captureAsyncStacks(() { /* async things */ });
///     } catch (error, chain) {
///
///     }
737
Future<void> runAndCaptureAsyncStacks(Future<void> Function() callback) {
738
  final Completer<void> completer = Completer<void>();
739 740 741
  Chain.capture(() async {
    await callback();
    completer.complete();
742
  }, onError: completer.completeError);
743 744
  return completer.future;
}
745

746
bool canRun(String path) => _processManager.canRun(path);
747

748
final RegExp _obsRegExp =
749
  RegExp('A Dart VM Service .* is available at: ');
750
final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
751
final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
752

753 754 755
/// Tries to extract a port from the string.
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
756
/// `prefix` defaults to the RegExp: `A Dart VM Service .* is available at: `.
757 758
int? parseServicePort(String line, {
  Pattern? prefix,
759
}) {
760
  prefix ??= _obsRegExp;
761 762
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
763 764
    return null;
  }
765
  final Match prefixMatch = matchesIter.first;
766 767
  final List<Match> matches =
    _obsPortRegExp.allMatches(line, prefixMatch.end).toList();
768
  return matches.isEmpty ? null : int.parse(matches[0].group(2)!);
769 770
}

771
/// Tries to extract a URL from the string.
772 773
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
774
/// `prefix` defaults to the RegExp: `A Dart VM Service .* is available at: `.
775 776
Uri? parseServiceUri(String line, {
  Pattern? prefix,
777 778
}) {
  prefix ??= _obsRegExp;
779 780
  final Iterable<Match> matchesIter = prefix.allMatches(line);
  if (matchesIter.isEmpty) {
781 782
    return null;
  }
783
  final Match prefixMatch = matchesIter.first;
784 785
  final List<Match> matches =
    _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
786
  return matches.isEmpty ? null : Uri.parse(matches[0].group(0)!);
787
}
788

789 790 791
/// Checks that the file exists, otherwise throws a [FileSystemException].
void checkFileExists(String file) {
  if (!exists(File(file))) {
792
    throw FileSystemException('Expected file to exist.', file);
793 794
  }
}
795

xster's avatar
xster committed
796 797 798
/// Checks that the file does not exists, otherwise throws a [FileSystemException].
void checkFileNotExists(String file) {
  if (exists(File(file))) {
799
    throw FileSystemException('Expected file to not exist.', file);
xster's avatar
xster committed
800 801 802
  }
}

803 804 805 806 807 808 809
/// Checks that the directory exists, otherwise throws a [FileSystemException].
void checkDirectoryExists(String directory) {
  if (!exists(Directory(directory))) {
    throw FileSystemException('Expected directory to exist.', directory);
  }
}

810 811 812 813 814 815 816
/// Checks that the directory does not exist, otherwise throws a [FileSystemException].
void checkDirectoryNotExists(String directory) {
  if (exists(Directory(directory))) {
    throw FileSystemException('Expected directory to not exist.', directory);
  }
}

817 818 819 820 821 822 823
/// Checks that the symlink exists, otherwise throws a [FileSystemException].
void checkSymlinkExists(String file) {
  if (!exists(Link(file))) {
    throw FileSystemException('Expected symlink to exist.', file);
  }
}

824 825
/// Check that `collection` contains all entries in `values`.
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
826
  for (final T value in values) {
827
    if (!collection.contains(value)) {
828
      throw TaskResult.failure('Expected to find `$value` in `$collection`.');
829
    }
830 831 832
  }
}

833 834
/// Check that `collection` does not contain any entries in `values`
void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
835
  for (final T value in values) {
836 837 838 839
    if (collection.contains(value)) {
      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
    }
  }
840 841
}

842 843 844 845
/// Checks that the contents of a [File] at `filePath` contains the specified
/// [Pattern]s, otherwise throws a [TaskResult].
void checkFileContains(List<Pattern> patterns, String filePath) {
  final String fileContent = File(filePath).readAsStringSync();
846
  for (final Pattern pattern in patterns) {
847 848 849 850 851 852 853
    if (!fileContent.contains(pattern)) {
      throw TaskResult.failure(
        'Expected to find `$pattern` in `$filePath` '
        'instead it found:\n$fileContent'
      );
    }
  }
854
}
855 856 857 858 859

/// Clones a git repository.
///
/// Removes the directory [path], then clones the git repository
/// specified by [repo] to the directory [path].
860
Future<int> gitClone({required String path, required String repo}) async {
861 862 863 864
  rmTree(Directory(path));

  await Directory(path).create(recursive: true);

865
  return inDirectory<int>(
866 867 868 869
    path,
        () => exec('git', <String>['clone', repo]),
  );
}
870 871 872 873 874 875 876 877 878 879 880

/// Call [fn] retrying so long as [retryIf] return `true` for the exception
/// thrown and [maxAttempts] has not been reached.
///
/// If no [retryIf] function is given this will retry any for any [Exception]
/// thrown. To retry on an [Error], the error must be caught and _rethrown_
/// as an [Exception].
///
/// Waits a constant duration of [delayDuration] between every retry attempt.
Future<T> retry<T>(
  FutureOr<T> Function() fn, {
881
  FutureOr<bool> Function(Exception)? retryIf,
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
  int maxAttempts = 5,
  Duration delayDuration = const Duration(seconds: 3),
}) async {
  int attempt = 0;
  while (true) {
    attempt++; // first invocation is the first attempt
    try {
      return await fn();
    } on Exception catch (e) {
      if (attempt >= maxAttempts ||
          (retryIf != null && !(await retryIf(e)))) {
        rethrow;
      }
    }

    // Sleep for a delay
    await Future<void>.delayed(delayDuration);
  }
}