1// Copyright 2014 The Flutter 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:convert';
7import 'dart:io';
8import 'dart:math' as math;
9
10import 'package:path/path.dart' as path;
11import 'package:process/process.dart';
12import 'package:stack_trace/stack_trace.dart';
13
14import 'devices.dart';
15import 'host_agent.dart';
16import 'task_result.dart';
17
18/// Virtual current working directory, which affect functions, such as [exec].
19String cwd = Directory.current.path;
20
21/// The local engine to use for [flutter] and [evalFlutter], if any.
22///
23/// This is set as an environment variable when running the task, see runTask in runner.dart.
24String? get localEngineFromEnv {
25 const bool isDefined = bool.hasEnvironment('localEngine');
26 return isDefined ? const String.fromEnvironment('localEngine') : null;
27}
28
29/// The local engine host to use for [flutter] and [evalFlutter], if any.
30///
31/// This is set as an environment variable when running the task, see runTask in runner.dart.
32String? get localEngineHostFromEnv {
33 const bool isDefined = bool.hasEnvironment('localEngineHost');
34 return isDefined ? const String.fromEnvironment('localEngineHost') : null;
35}
36
37/// The local engine source path to use if a local engine is used for [flutter]
38/// and [evalFlutter].
39///
40/// This is set as an environment variable when running the task, see runTask in runner.dart.
41String? get localEngineSrcPathFromEnv {
42 const bool isDefined = bool.hasEnvironment('localEngineSrcPath');
43 return isDefined ? const String.fromEnvironment('localEngineSrcPath') : null;
44}
45
46/// The local Web SDK to use for [flutter] and [evalFlutter], if any.
47///
48/// This is set as an environment variable when running the task, see runTask in runner.dart.
49String? get localWebSdkFromEnv {
50 const bool isDefined = bool.hasEnvironment('localWebSdk');
51 return isDefined ? const String.fromEnvironment('localWebSdk') : null;
52}
53
54List<ProcessInfo> _runningProcesses = <ProcessInfo>[];
55ProcessManager _processManager = const LocalProcessManager();
56
57class ProcessInfo {
58 ProcessInfo(this.command, this.process);
59
60 final DateTime startTime = DateTime.now();
61 final String command;
62 final Process process;
63
64 @override
65 String toString() {
66 return '''
67 command: $command
68 started: $startTime
69 pid : ${process.pid}
70'''
71 .trim();
72 }
73}
74
75/// Result of a health check for a specific parameter.
76class HealthCheckResult {
77 HealthCheckResult.success([this.details]) : succeeded = true;
78 HealthCheckResult.failure(this.details) : succeeded = false;
79 HealthCheckResult.error(dynamic error, dynamic stackTrace)
80 : succeeded = false,
81 details = 'ERROR: $error${stackTrace != null ? '\n$stackTrace' : ''}';
82
83 final bool succeeded;
84 final String? details;
85
86 @override
87 String toString() {
88 final StringBuffer buf = StringBuffer(succeeded ? 'succeeded' : 'failed');
89 if (details != null && details!.trim().isNotEmpty) {
90 buf.writeln();
91 // Indent details by 4 spaces
92 for (final String line in details!.trim().split('\n')) {
93 buf.writeln(' $line');
94 }
95 }
96 return '$buf';
97 }
98}
99
100class BuildFailedError extends Error {
101 BuildFailedError(this.message);
102
103 final String message;
104
105 @override
106 String toString() => message;
107}
108
109void fail(String message) {
110 throw BuildFailedError(message);
111}
112
113// Remove the given file or directory.
114void rm(FileSystemEntity entity, {bool recursive = false}) {
115 if (entity.existsSync()) {
116 // This should not be necessary, but it turns out that
117 // on Windows it's common for deletions to fail due to
118 // bogus (we think) "access denied" errors.
119 try {
120 entity.deleteSync(recursive: recursive);
121 } on FileSystemException catch (error) {
122 print('Failed to delete ${entity.path}: $error');
123 }
124 }
125}
126
127/// Remove recursively.
128void rmTree(FileSystemEntity entity) {
129 rm(entity, recursive: true);
130}
131
132List<FileSystemEntity> ls(Directory directory) => directory.listSync();
133
134Directory dir(String path) => Directory(path);
135
136File file(String path) => File(path);
137
138void copy(File sourceFile, Directory targetDirectory, {String? name}) {
139 final File target = file(path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
140 target.writeAsBytesSync(sourceFile.readAsBytesSync());
141}
142
143void recursiveCopy(Directory source, Directory target) {
144 if (!target.existsSync()) {
145 target.createSync();
146 }
147
148 for (final FileSystemEntity entity in source.listSync(followLinks: false)) {
149 final String name = path.basename(entity.path);
150 if (entity is Directory && !entity.path.contains('.dart_tool')) {
151 recursiveCopy(entity, Directory(path.join(target.path, name)));
152 } else if (entity is File) {
153 final File dest = File(path.join(target.path, name));
154 dest.writeAsBytesSync(entity.readAsBytesSync());
155 // Preserve executable bit
156 final String modes = entity.statSync().modeString();
157 if (modes.contains('x')) {
158 makeExecutable(dest);
159 }
160 }
161 }
162}
163
164FileSystemEntity move(FileSystemEntity whatToMove, {required Directory to, String? name}) {
165 return whatToMove.renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
166}
167
168/// Equivalent of `chmod a+x file`
169void makeExecutable(File file) {
170 // Windows files do not have an executable bit
171 if (Platform.isWindows) {
172 return;
173 }
174 final ProcessResult result = _processManager.runSync(<String>['chmod', 'a+x', file.path]);
175
176 if (result.exitCode != 0) {
177 throw FileSystemException(
178 'Error making ${file.path} executable.\n'
179 '${result.stderr}',
180 file.path,
181 );
182 }
183}
184
185/// Equivalent of `mkdir directory`.
186void mkdir(Directory directory) {
187 directory.createSync();
188}
189
190/// Equivalent of `mkdir -p directory`.
191void mkdirs(Directory directory) {
192 directory.createSync(recursive: true);
193}
194
195bool exists(FileSystemEntity entity) => entity.existsSync();
196
197void section(String title) {
198 String output;
199 if (Platform.isWindows) {
200 // Windows doesn't cope well with characters produced for *nix systems, so
201 // just output the title with no decoration.
202 output = title;
203 } else {
204 title = '╡ ••• $title ••• ╞';
205 final String line = '═' * math.max((80 - title.length) ~/ 2, 2);
206 output = '$line$title$line';
207 if (output.length == 79) {
208 output += '═';
209 }
210 }
211 print('\n\n$output\n');
212}
213
214Future<String> getDartVersion() async {
215 // The Dart VM returns the version text to stderr.
216 final ProcessResult result = _processManager.runSync(<String>[dartBin, '--version']);
217 String version = (result.stderr as String).trim();
218
219 // Convert:
220 // Dart VM version: 1.17.0-dev.2.0 (Tue May 3 12:14:52 2016) on "macos_x64"
221 // to:
222 // 1.17.0-dev.2.0
223 if (version.contains('(')) {
224 version = version.substring(0, version.indexOf('(')).trim();
225 }
226 if (version.contains(':')) {
227 version = version.substring(version.indexOf(':') + 1).trim();
228 }
229
230 return version.replaceAll('"', "'");
231}
232
233Future<String?> getCurrentFlutterRepoCommit() {
234 if (!dir('${flutterDirectory.path}/.git').existsSync()) {
235 return Future<String?>.value();
236 }
237
238 return inDirectory<String>(flutterDirectory, () {
239 return eval('git', <String>['rev-parse', 'HEAD']);
240 });
241}
242
243Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
244 // git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
245 return inDirectory<DateTime>(flutterDirectory, () async {
246 final String unixTimestamp = await eval('git', <String>['show', '-s', '--format=%at', commit]);
247 final int secondsSinceEpoch = int.parse(unixTimestamp);
248 return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
249 });
250}
251
252/// Starts a subprocess.
253///
254/// The first argument is the full path to the executable to run.
255///
256/// The second argument is the list of arguments to provide on the command line.
257/// This argument can be null, indicating no arguments (same as the empty list).
258///
259/// The `environment` argument can be provided to configure environment variables
260/// that will be made available to the subprocess. The `BOT` environment variable
261/// is always set and overrides any value provided in the `environment` argument.
262/// The `isBot` argument controls the value of the `BOT` variable. It will either
263/// be "true", if `isBot` is true (the default), or "false" if it is false.
264///
265/// The `BOT` variable is in particular used by the `flutter` tool to determine
266/// how verbose to be and whether to enable analytics by default.
267///
268/// The working directory can be provided using the `workingDirectory` argument.
269/// By default it will default to the current working directory (see [cwd]).
270///
271/// Information regarding the execution of the subprocess is printed to the
272/// console.
273///
274/// The actual process executes asynchronously. A handle to the subprocess is
275/// returned in the form of a [Future] that completes to a [Process] object.
276Future<Process> startProcess(
277 String executable,
278 List<String>? arguments, {
279 Map<String, String>? environment,
280 bool isBot =
281 true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
282 String? workingDirectory,
283}) async {
284 final String command = '$executable ${arguments?.join(" ") ?? ""}';
285 final String finalWorkingDirectory = workingDirectory ?? cwd;
286 final Map<String, String> newEnvironment = Map<String, String>.from(
287 environment ?? <String, String>{},
288 );
289 newEnvironment['BOT'] = isBot ? 'true' : 'false';
290 newEnvironment['LANG'] = 'en_US.UTF-8';
291 print('Executing "$command" in "$finalWorkingDirectory" with environment $newEnvironment');
292
293 final Process process = await _processManager.start(
294 <String>[executable, ...?arguments],
295 environment: newEnvironment,
296 workingDirectory: finalWorkingDirectory,
297 );
298 final ProcessInfo processInfo = ProcessInfo(command, process);
299 _runningProcesses.add(processInfo);
300
301 unawaited(
302 process.exitCode.then<void>((int exitCode) {
303 _runningProcesses.remove(processInfo);
304 }),
305 );
306
307 return process;
308}
309
310Future<void> forceQuitRunningProcesses() async {
311 if (_runningProcesses.isEmpty) {
312 return;
313 }
314
315 // Give normally quitting processes a chance to report their exit code.
316 await Future<void>.delayed(const Duration(seconds: 1));
317
318 // Whatever's left, kill it.
319 for (final ProcessInfo p in _runningProcesses) {
320 print('Force-quitting process:\n$p');
321 if (!p.process.kill()) {
322 print('Failed to force quit process.');
323 }
324 }
325 _runningProcesses.clear();
326}
327
328/// Executes a command and returns its exit code.
329Future<int> exec(
330 String executable,
331 List<String> arguments, {
332 Map<String, String>? environment,
333 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
334 String? workingDirectory,
335 StringBuffer? output, // if not null, the stdout will be written here
336 StringBuffer? stderr, // if not null, the stderr will be written here
337}) async {
338 return _execute(
339 executable,
340 arguments,
341 environment: environment,
342 canFail: canFail,
343 workingDirectory: workingDirectory,
344 output: output,
345 stderr: stderr,
346 );
347}
348
349Future<int> _execute(
350 String executable,
351 List<String> arguments, {
352 Map<String, String>? environment,
353 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
354 String? workingDirectory,
355 StringBuffer? output, // if not null, the stdout will be written here
356 StringBuffer? stderr, // if not null, the stderr will be written here
357 bool printStdout = true,
358 bool printStderr = true,
359}) async {
360 final Process process = await startProcess(
361 executable,
362 arguments,
363 environment: environment,
364 workingDirectory: workingDirectory,
365 );
366 await forwardStandardStreams(
367 process,
368 output: output,
369 stderr: stderr,
370 printStdout: printStdout,
371 printStderr: printStderr,
372 );
373 final int exitCode = await process.exitCode;
374
375 if (exitCode != 0 && !canFail) {
376 fail('Executable "$executable" failed with exit code $exitCode.');
377 }
378
379 return exitCode;
380}
381
382/// Forwards standard out and standard error from [process] to this process'
383/// respective outputs. Also writes stdout to [output] and stderr to [stderr]
384/// if they are not null.
385///
386/// Returns a future that completes when both out and error streams a closed.
387Future<void> forwardStandardStreams(
388 Process process, {
389 StringBuffer? output,
390 StringBuffer? stderr,
391 bool printStdout = true,
392 bool printStderr = true,
393}) {
394 final Completer<void> stdoutDone = Completer<void>();
395 final Completer<void> stderrDone = Completer<void>();
396 process.stdout
397 .transform<String>(utf8.decoder)
398 .transform<String>(const LineSplitter())
399 .listen(
400 (String line) {
401 if (printStdout) {
402 print('stdout: $line');
403 }
404 output?.writeln(line);
405 },
406 onDone: () {
407 stdoutDone.complete();
408 },
409 );
410 process.stderr
411 .transform<String>(utf8.decoder)
412 .transform<String>(const LineSplitter())
413 .listen(
414 (String line) {
415 if (printStderr) {
416 print('stderr: $line');
417 }
418 stderr?.writeln(line);
419 },
420 onDone: () {
421 stderrDone.complete();
422 },
423 );
424
425 return Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
426}
427
428/// Executes a command and returns its standard output as a String.
429///
430/// For logging purposes, the command's output is also printed out by default.
431Future<String> eval(
432 String executable,
433 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 bool printStdout = true,
440 bool printStderr = true,
441}) async {
442 final StringBuffer output = stdout ?? StringBuffer();
443 await _execute(
444 executable,
445 arguments,
446 environment: environment,
447 canFail: canFail,
448 workingDirectory: workingDirectory,
449 output: output,
450 stderr: stderr,
451 printStdout: printStdout,
452 printStderr: printStderr,
453 );
454 return output.toString().trimRight();
455}
456
457List<String> _flutterCommandArgs(
458 String command,
459 List<String> options, {
460 bool driveWithDds = false,
461}) {
462 // Commands support the --device-timeout flag.
463 final Set<String> supportedDeviceTimeoutCommands = <String>{
464 'attach',
465 'devices',
466 'drive',
467 'install',
468 'logs',
469 'run',
470 'screenshot',
471 };
472 final String? localEngine = localEngineFromEnv;
473 final String? localEngineHost = localEngineHostFromEnv;
474 final String? localEngineSrcPath = localEngineSrcPathFromEnv;
475 final String? localWebSdk = localWebSdkFromEnv;
476 final bool pubOrPackagesCommand = command.startsWith('packages') || command.startsWith('pub');
477 return <String>[
478 command,
479 if (deviceOperatingSystem == DeviceOperatingSystem.ios &&
480 supportedDeviceTimeoutCommands.contains(command)) ...<String>['--device-timeout', '5'],
481
482 // DDS should generally be disabled for flutter drive in CI.
483 // See https://github.com/flutter/flutter/issues/152684.
484 if (command == 'drive' && !driveWithDds) '--no-dds',
485
486 if (command == 'drive' && hostAgent.dumpDirectory != null) ...<String>[
487 '--screenshot',
488 hostAgent.dumpDirectory!.path,
489 ],
490 if (localEngine != null) ...<String>['--local-engine', localEngine],
491 if (localEngineHost != null) ...<String>['--local-engine-host', localEngineHost],
492 if (localEngineSrcPath != null) ...<String>['--local-engine-src-path', localEngineSrcPath],
493 if (localWebSdk != null) ...<String>['--local-web-sdk', localWebSdk],
494 ...options,
495 // Use CI flag when running devicelab tests, except for `packages`/`pub` commands.
496 // `packages`/`pub` commands effectively runs the `pub` tool, which does not have
497 // the same allowed args.
498 if (!pubOrPackagesCommand) '--ci',
499 if (!pubOrPackagesCommand && hostAgent.dumpDirectory != null)
500 '--debug-logs-dir=${hostAgent.dumpDirectory!.path}',
501 ];
502}
503
504/// Runs the flutter `command`, and returns the exit code.
505/// If `canFail` is `false`, the future completes with an error.
506Future<int> flutter(
507 String command, {
508 List<String> options = const <String>[],
509 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
510 bool driveWithDds = false, // `flutter drive` tests should generally have dds disabled.
511 // The exception is tests that also exercise DevTools, such as
512 // DevToolsMemoryTest in perf_tests.dart.
513 Map<String, String>? environment,
514 String? workingDirectory,
515 StringBuffer? output, // if not null, the stdout will be written here
516 StringBuffer? stderr, // if not null, the stderr will be written here
517}) async {
518 final List<String> args = _flutterCommandArgs(command, options, driveWithDds: driveWithDds);
519 final int exitCode = await exec(
520 path.join(flutterDirectory.path, 'bin', 'flutter'),
521 args,
522 canFail: canFail,
523 environment: environment,
524 workingDirectory: workingDirectory,
525 output: output,
526 stderr: stderr,
527 );
528
529 if (exitCode != 0 && !canFail) {
530 await _flutterScreenshot(workingDirectory: workingDirectory);
531 }
532 return exitCode;
533}
534
535/// Starts a Flutter subprocess.
536///
537/// The first argument is the flutter command to run.
538///
539/// The second argument is the list of arguments to provide on the command line.
540/// This argument can be null, indicating no arguments (same as the empty list).
541///
542/// The `environment` argument can be provided to configure environment variables
543/// that will be made available to the subprocess. The `BOT` environment variable
544/// is always set and overrides any value provided in the `environment` argument.
545/// The `isBot` argument controls the value of the `BOT` variable. It will either
546/// be "true", if `isBot` is true (the default), or "false" if it is false.
547///
548/// The `isBot` argument controls whether the `BOT` environment variable is set
549/// to `true` or `false` and is used by the `flutter` tool to determine how
550/// verbose to be and whether to enable analytics by default.
551///
552/// Information regarding the execution of the subprocess is printed to the
553/// console.
554///
555/// The actual process executes asynchronously. A handle to the subprocess is
556/// returned in the form of a [Future] that completes to a [Process] object.
557Future<Process> startFlutter(
558 String command, {
559 List<String> options = const <String>[],
560 Map<String, String> environment = const <String, String>{},
561 bool isBot =
562 true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
563 String? workingDirectory,
564}) async {
565 final List<String> args = _flutterCommandArgs(command, options);
566 final Process process = await startProcess(
567 path.join(flutterDirectory.path, 'bin', 'flutter'),
568 args,
569 environment: environment,
570 isBot: isBot,
571 workingDirectory: workingDirectory,
572 );
573
574 unawaited(
575 process.exitCode.then<void>((int exitCode) async {
576 if (exitCode != 0) {
577 await _flutterScreenshot(workingDirectory: workingDirectory);
578 }
579 }),
580 );
581 return process;
582}
583
584/// Runs a `flutter` command and returns the standard output as a string.
585Future<String> evalFlutter(
586 String command, {
587 List<String> options = const <String>[],
588 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
589 Map<String, String>? environment,
590 StringBuffer? stderr, // if not null, the stderr will be written here.
591 String? workingDirectory,
592}) {
593 final List<String> args = _flutterCommandArgs(command, options);
594 return eval(
595 path.join(flutterDirectory.path, 'bin', 'flutter'),
596 args,
597 canFail: canFail,
598 environment: environment,
599 stderr: stderr,
600 workingDirectory: workingDirectory,
601 );
602}
603
604Future<ProcessResult> executeFlutter(
605 String command, {
606 List<String> options = const <String>[],
607 bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
608}) async {
609 final List<String> args = _flutterCommandArgs(command, options);
610 final ProcessResult processResult = await _processManager.run(<String>[
611 path.join(flutterDirectory.path, 'bin', 'flutter'),
612 ...args,
613 ], workingDirectory: cwd);
614
615 if (processResult.exitCode != 0 && !canFail) {
616 await _flutterScreenshot();
617 }
618 return processResult;
619}
620
621Future<void> _flutterScreenshot({String? workingDirectory}) async {
622 try {
623 final Directory? dumpDirectory = hostAgent.dumpDirectory;
624 if (dumpDirectory == null) {
625 return;
626 }
627 // On command failure try uploading screenshot of failing command.
628 final String screenshotPath = path.join(
629 dumpDirectory.path,
630 'device-screenshot-${DateTime.now().toLocal().toIso8601String()}.png',
631 );
632
633 final String deviceId = (await devices.workingDevice).deviceId;
634 print('Taking screenshot of working device $deviceId at $screenshotPath');
635 final List<String> args = _flutterCommandArgs('screenshot', <String>[
636 '--out',
637 screenshotPath,
638 '-d',
639 deviceId,
640 ]);
641 final ProcessResult screenshot = await _processManager.run(<String>[
642 path.join(flutterDirectory.path, 'bin', 'flutter'),
643 ...args,
644 ], workingDirectory: workingDirectory ?? cwd);
645
646 if (screenshot.exitCode != 0) {
647 print('Failed to take screenshot. Continuing.');
648 }
649 } catch (exception) {
650 print('Failed to take screenshot. Continuing.\n$exception');
651 }
652}
653
654String get dartBin => path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');
655
656String get pubBin => path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');
657
658Future<int> dart(List<String> args) => exec(dartBin, args);
659
660/// Returns a future that completes with a path suitable for JAVA_HOME
661/// or with null, if Java cannot be found.
662Future<String?> findJavaHome() async {
663 if (_javaHome == null) {
664 final Iterable<String> hits = grep(
665 'Java binary at: ',
666 from: await evalFlutter('doctor', options: <String>['-v']),
667 );
668 if (hits.isEmpty) {
669 return null;
670 }
671 final String javaBinary = hits.first.split(': ').last;
672 // javaBinary == /some/path/to/java/home/bin/java
673 _javaHome = path.dirname(path.dirname(javaBinary));
674 }
675 return _javaHome;
676}
677
678String? _javaHome;
679
680Future<T> inDirectory<T>(dynamic directory, Future<T> Function() action) async {
681 final String previousCwd = cwd;
682 try {
683 cd(directory);
684 return await action();
685 } finally {
686 cd(previousCwd);
687 }
688}
689
690void cd(dynamic directory) {
691 Directory d;
692 if (directory is String) {
693 cwd = directory;
694 d = dir(directory);
695 } else if (directory is Directory) {
696 cwd = directory.path;
697 d = directory;
698 } else {
699 throw FileSystemException(
700 'Unsupported directory type ${directory.runtimeType}',
701 directory.toString(),
702 );
703 }
704
705 if (!d.existsSync()) {
706 throw FileSystemException('Cannot cd into directory that does not exist', d.toString());
707 }
708}
709
710Directory get flutterDirectory => Directory.current.parent.parent;
711
712Directory get openpayDirectory => Directory(requireEnvVar('OPENPAY_CHECKOUT_PATH'));
713
714String requireEnvVar(String name) {
715 final String? value = Platform.environment[name];
716
717 if (value == null) {
718 fail('$name environment variable is missing. Quitting.');
719 }
720
721 return value!;
722}
723
724T requireConfigProperty<T>(Map<String, dynamic> map, String propertyName) {
725 if (!map.containsKey(propertyName)) {
726 fail('Configuration property not found: $propertyName');
727 }
728 final T result = map[propertyName] as T;
729 return result;
730}
731
732String jsonEncode(dynamic data) {
733 final String jsonValue = const JsonEncoder.withIndent(' ').convert(data);
734 return '$jsonValue\n';
735}
736
737/// Splits [from] into lines and selects those that contain [pattern].
738Iterable<String> grep(Pattern pattern, {required String from}) {
739 return from.split('\n').where((String line) {
740 return line.contains(pattern);
741 });
742}
743
744/// Captures asynchronous stack traces thrown by [callback].
745///
746/// This is a convenience wrapper around [Chain] optimized for use with
747/// `async`/`await`.
748///
749/// Example:
750///
751/// try {
752/// await captureAsyncStacks(() { /* async things */ });
753/// } catch (error, chain) {
754///
755/// }
756Future<void> runAndCaptureAsyncStacks(Future<void> Function() callback) {
757 final Completer<void> completer = Completer<void>();
758 Chain.capture(() async {
759 await callback();
760 completer.complete();
761 }, onError: completer.completeError);
762 return completer.future;
763}
764
765bool canRun(String path) => _processManager.canRun(path);
766
767final RegExp _obsRegExp = RegExp('A Dart VM Service .* is available at: ');
768final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
769final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
770
771/// Tries to extract a port 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: `.
775int? parseServicePort(String line, {Pattern? prefix}) {
776 prefix ??= _obsRegExp;
777 final Iterable<Match> matchesIter = prefix.allMatches(line);
778 if (matchesIter.isEmpty) {
779 return null;
780 }
781 final Match prefixMatch = matchesIter.first;
782 final List<Match> matches = _obsPortRegExp.allMatches(line, prefixMatch.end).toList();
783 return matches.isEmpty ? null : int.parse(matches[0].group(2)!);
784}
785
786/// Tries to extract a URL from the string.
787///
788/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
789/// `prefix` defaults to the RegExp: `A Dart VM Service .* is available at: `.
790Uri? parseServiceUri(String line, {Pattern? prefix}) {
791 prefix ??= _obsRegExp;
792 final Iterable<Match> matchesIter = prefix.allMatches(line);
793 if (matchesIter.isEmpty) {
794 return null;
795 }
796 final Match prefixMatch = matchesIter.first;
797 final List<Match> matches = _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
798 return matches.isEmpty ? null : Uri.parse(matches[0].group(0)!);
799}
800
801/// Checks that the file exists, otherwise throws a [FileSystemException].
802void checkFileExists(String file) {
803 if (!exists(File(file))) {
804 throw FileSystemException('Expected file to exist.', file);
805 }
806}
807
808/// Checks that the file does not exists, otherwise throws a [FileSystemException].
809void checkFileNotExists(String file) {
810 if (exists(File(file))) {
811 throw FileSystemException('Expected file to not exist.', file);
812 }
813}
814
815/// Checks that the directory exists, otherwise throws a [FileSystemException].
816void checkDirectoryExists(String directory) {
817 if (!exists(Directory(directory))) {
818 throw FileSystemException('Expected directory to exist.', directory);
819 }
820}
821
822/// Checks that the directory does not exist, otherwise throws a [FileSystemException].
823void checkDirectoryNotExists(String directory) {
824 if (exists(Directory(directory))) {
825 throw FileSystemException('Expected directory to not exist.', directory);
826 }
827}
828
829/// Checks that the symlink exists, otherwise throws a [FileSystemException].
830void checkSymlinkExists(String file) {
831 if (!exists(Link(file))) {
832 throw FileSystemException('Expected symlink to exist.', file);
833 }
834}
835
836/// Check that `collection` contains all entries in `values`.
837void checkCollectionContains<T>(Iterable<T> values, Iterable<T> collection) {
838 for (final T value in values) {
839 if (!collection.contains(value)) {
840 throw TaskResult.failure('Expected to find `$value` in `$collection`.');
841 }
842 }
843}
844
845/// Check that `collection` does not contain any entries in `values`
846void checkCollectionDoesNotContain<T>(Iterable<T> values, Iterable<T> collection) {
847 for (final T value in values) {
848 if (collection.contains(value)) {
849 throw TaskResult.failure('Did not expect to find `$value` in `$collection`.');
850 }
851 }
852}
853
854/// Checks that the contents of a [File] at `filePath` contains the specified
855/// [Pattern]s, otherwise throws a [TaskResult].
856void checkFileContains(List<Pattern> patterns, String filePath) {
857 final String fileContent = File(filePath).readAsStringSync();
858 for (final Pattern pattern in patterns) {
859 if (!fileContent.contains(pattern)) {
860 throw TaskResult.failure(
861 'Expected to find `$pattern` in `$filePath` '
862 'instead it found:\n$fileContent',
863 );
864 }
865 }
866}
867
868/// Clones a git repository.
869///
870/// Removes the directory [path], then clones the git repository
871/// specified by [repo] to the directory [path].
872Future<int> gitClone({required String path, required String repo}) async {
873 rmTree(Directory(path));
874
875 await Directory(path).create(recursive: true);
876
877 return inDirectory<int>(path, () => exec('git', <String>['clone', repo]));
878}
879
880/// Call [fn] retrying so long as [retryIf] return `true` for the exception
881/// thrown and [maxAttempts] has not been reached.
882///
883/// If no [retryIf] function is given this will retry any for any [Exception]
884/// thrown. To retry on an [Error], the error must be caught and _rethrown_
885/// as an [Exception].
886///
887/// Waits a constant duration of [delayDuration] between every retry attempt.
888Future<T> retry<T>(
889 FutureOr<T> Function() fn, {
890 FutureOr<bool> Function(Exception)? retryIf,
891 int maxAttempts = 5,
892 Duration delayDuration = const Duration(seconds: 3),
893}) async {
894 int attempt = 0;
895 while (true) {
896 attempt++; // first invocation is the first attempt
897 try {
898 return await fn();
899 } on Exception catch (e) {
900 if (attempt >= maxAttempts || (retryIf != null && !(await retryIf(e)))) {
901 rethrow;
902 }
903 }
904
905 // Sleep for a delay
906 await Future<void>.delayed(delayDuration);
907 }
908}
909
910Future<void> createFfiPackage(String name, Directory parent) async {
911 await inDirectory(parent, () async {
912 await flutter(
913 'create',
914 options: <String>[
915 '--no-pub',
916 '--org',
917 'io.flutter.devicelab',
918 '--template=package_ffi',
919 name,
920 ],
921 );
922 await _pinDependencies(File(path.join(parent.path, name, 'pubspec.yaml')));
923 await _pinDependencies(File(path.join(parent.path, name, 'example', 'pubspec.yaml')));
924 });
925}
926
927Future<void> _pinDependencies(File pubspecFile) async {
928 final String oldPubspec = await pubspecFile.readAsString();
929 final String newPubspec = oldPubspec.replaceAll(': ^', ': ');
930 await pubspecFile.writeAsString(newPubspec);
931}
932