utils.dart 29.2 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 343
  StringBuffer? output, // if not null, the stdout will be written here
  StringBuffer? stderr, // if not null, the stderr will be written here
344
}) async {
345 346 347 348 349 350
  return _execute(
    executable,
    arguments,
    environment: environment,
    canFail : canFail,
    workingDirectory: workingDirectory,
351 352
    output: output,
    stderr: stderr,
353 354 355 356 357 358
  );
}

Future<int> _execute(
  String executable,
  List<String> arguments, {
359
  Map<String, String>? environment,
360
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
361 362 363
  String? workingDirectory,
  StringBuffer? output, // if not null, the stdout will be written here
  StringBuffer? stderr, // if not null, the stderr will be written here
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
  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,
379
  );
380 381
  final int exitCode = await process.exitCode;

382
  if (exitCode != 0 && !canFail) {
383
    fail('Executable "$executable" failed with exit code $exitCode.');
384
  }
385 386 387

  return exitCode;
}
388

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

422 423 424 425
  return Future.wait<void>(<Future<void>>[
    stdoutDone.future,
    stderrDone.future,
  ]);
426 427 428 429
}

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

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

481 482
    if (command == 'drive' && hostAgent.dumpDirectory != null) ...<String>[
      '--screenshot',
483
      hostAgent.dumpDirectory!.path,
484
    ],
485
    if (localEngine != null) ...<String>['--local-engine', localEngine],
486
    if (localEngineHost != null) ...<String>['--local-engine-host', localEngineHost],
487
    if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
488
    if (localWebSdk != null) ...<String>['--local-web-sdk', localWebSdk],
489
    ...options,
490 491 492
    // 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.
493 494 495
    if (!pubOrPackagesCommand) '--ci',
    if (!pubOrPackagesCommand && hostAgent.dumpDirectory != null)
      '--debug-logs-dir=${hostAgent.dumpDirectory!.path}'
496
  ];
497 498
}

499 500
/// Runs the flutter `command`, and returns the exit code.
/// If `canFail` is `false`, the future completes with an error.
501 502 503
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.
504
  Map<String, String>? environment,
505
  String? workingDirectory,
506
}) async {
507
  final List<String> args = _flutterCommandArgs(command, options);
508
  final int exitCode = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
509
    canFail: canFail, environment: environment, workingDirectory: workingDirectory);
510 511 512 513 514

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

517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
/// 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.
539 540 541
Future<Process> startFlutter(String command, {
  List<String> options = const <String>[],
  Map<String, String> environment = const <String, String>{},
542
  bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
543 544 545
  String? workingDirectory,
}) async {
  final List<String> args = _flutterCommandArgs(command, options);
546
  final Process process = await startProcess(
547 548 549
    path.join(flutterDirectory.path, 'bin', 'flutter'),
    args,
    environment: environment,
550
    isBot: isBot,
551
    workingDirectory: workingDirectory,
552
  );
553 554 555 556 557 558 559

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

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

575 576
Future<ProcessResult> executeFlutter(String command, {
  List<String> options = const <String>[],
577
  bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
578
}) async {
579
  final List<String> args = _flutterCommandArgs(command, options);
580
  final ProcessResult processResult = await _processManager.run(
581 582 583
    <String>[path.join(flutterDirectory.path, 'bin', 'flutter'), ...args],
    workingDirectory: cwd,
  );
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 617 618 619 620 621 622 623

  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');
  }
624 625
}

626 627 628
String get dartBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');

629 630 631
String get pubBin =>
    path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');

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

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

655
Future<T> inDirectory<T>(dynamic directory, Future<T> Function() action) async {
656
  final String previousCwd = cwd;
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
  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 {
674
    throw FileSystemException('Unsupported directory type ${directory.runtimeType}', directory.toString());
675 676
  }

677
  if (!d.existsSync()) {
678
    throw FileSystemException('Cannot cd into directory that does not exist', d.toString());
679
  }
680 681
}

682
Directory get flutterDirectory => Directory.current.parent.parent;
683

684 685
Directory get openpayDirectory => Directory(requireEnvVar('OPENPAY_CHECKOUT_PATH'));

686
String requireEnvVar(String name) {
687
  final String? value = Platform.environment[name];
688

689
  if (value == null) {
690
    fail('$name environment variable is missing. Quitting.');
691
  }
692

693
  return value!;
694 695
}

696
T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
697
  if (!map.containsKey(propertyName)) {
698
    fail('Configuration property not found: $propertyName');
699
  }
700
  final T result = map[propertyName] as T;
701
  return result;
702 703 704
}

String jsonEncode(dynamic data) {
705 706
  final String jsonValue = const JsonEncoder.withIndent('  ').convert(data);
  return '$jsonValue\n';
707 708 709
}

/// Splits [from] into lines and selects those that contain [pattern].
710
Iterable<String> grep(Pattern pattern, {required String from}) {
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
  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) {
///
///     }
728
Future<void> runAndCaptureAsyncStacks(Future<void> Function() callback) {
729
  final Completer<void> completer = Completer<void>();
730 731 732
  Chain.capture(() async {
    await callback();
    completer.complete();
733
  }, onError: completer.completeError);
734 735
  return completer.future;
}
736

737
bool canRun(String path) => _processManager.canRun(path);
738

739
final RegExp _obsRegExp =
740
  RegExp('A Dart VM Service .* is available at: ');
741
final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
742
final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
743

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

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

780 781 782
/// Checks that the file exists, otherwise throws a [FileSystemException].
void checkFileExists(String file) {
  if (!exists(File(file))) {
783
    throw FileSystemException('Expected file to exist.', file);
784 785
  }
}
786

xster's avatar
xster committed
787 788 789
/// Checks that the file does not exists, otherwise throws a [FileSystemException].
void checkFileNotExists(String file) {
  if (exists(File(file))) {
790
    throw FileSystemException('Expected file to not exist.', file);
xster's avatar
xster committed
791 792 793
  }
}

794 795 796 797 798 799 800
/// Checks that the directory exists, otherwise throws a [FileSystemException].
void checkDirectoryExists(String directory) {
  if (!exists(Directory(directory))) {
    throw FileSystemException('Expected directory to exist.', directory);
  }
}

801 802 803 804 805 806 807
/// 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);
  }
}

808 809 810 811 812 813 814
/// Checks that the symlink exists, otherwise throws a [FileSystemException].
void checkSymlinkExists(String file) {
  if (!exists(Link(file))) {
    throw FileSystemException('Expected symlink to exist.', file);
  }
}

815 816
/// Check that `collection` contains all entries in `values`.
void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
817
  for (final T value in values) {
818
    if (!collection.contains(value)) {
819
      throw TaskResult.failure('Expected to find `$value` in `$collection`.');
820
    }
821 822 823
  }
}

824 825
/// Check that `collection` does not contain any entries in `values`
void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
826
  for (final T value in values) {
827 828 829 830
    if (collection.contains(value)) {
      throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
    }
  }
831 832
}

833 834 835 836
/// 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();
837
  for (final Pattern pattern in patterns) {
838 839 840 841 842 843 844
    if (!fileContent.contains(pattern)) {
      throw TaskResult.failure(
        'Expected to find `$pattern` in `$filePath` '
        'instead it found:\n$fileContent'
      );
    }
  }
845
}
846 847 848 849 850

/// Clones a git repository.
///
/// Removes the directory [path], then clones the git repository
/// specified by [repo] to the directory [path].
851
Future<int> gitClone({required String path, required String repo}) async {
852 853 854 855
  rmTree(Directory(path));

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

856
  return inDirectory<int>(
857 858 859 860
    path,
        () => exec('git', <String>['clone', repo]),
  );
}
861 862 863 864 865 866 867 868 869 870 871

/// 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, {
872
  FutureOr<bool> Function(Exception)? retryIf,
873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891
  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);
  }
}
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918

Future<void> createFfiPackage(String name, Directory parent) async {
  await inDirectory(parent, () async {
    await flutter(
      'create',
      options: <String>[
        '--no-pub',
        '--org',
        'io.flutter.devicelab',
        '--template=package_ffi',
        name,
      ],
    );
    await _pinDependencies(
      File(path.join(parent.path, name, 'pubspec.yaml')),
    );
    await _pinDependencies(
      File(path.join(parent.path, name, 'example', 'pubspec.yaml')),
    );
  });
}

Future<void> _pinDependencies(File pubspecFile) async {
  final String oldPubspec = await pubspecFile.readAsString();
  final String newPubspec = oldPubspec.replaceAll(': ^', ': ');
  await pubspecFile.writeAsString(newPubspec);
}