| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // 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'; |
| |
| import 'package:vm_service_client/vm_service_client.dart'; |
| |
| import 'package:flutter_devicelab/framework/utils.dart'; |
| import 'package:flutter_devicelab/framework/adb.dart'; |
| |
| import 'task_result.dart'; |
| |
| /// Runs a task in a separate Dart VM and collects the result using the VM |
| /// service protocol. |
| /// |
| /// [taskName] is the name of the task. The corresponding task executable is |
| /// expected to be found under `bin/tasks`. |
| /// |
| /// Running the task in [silent] mode will suppress standard output from task |
| /// processes and only print standard errors. |
| Future<TaskResult> runTask( |
| String taskName, { |
| bool silent = false, |
| String localEngine, |
| String localEngineSrcPath, |
| String deviceId, |
| }) async { |
| final String taskExecutable = 'bin/tasks/$taskName.dart'; |
| |
| if (!file(taskExecutable).existsSync()) |
| throw 'Executable Dart file not found: $taskExecutable'; |
| |
| final Process runner = await startProcess( |
| dartBin, |
| <String>[ |
| '--disable-dart-dev', |
| '--enable-vm-service=0', // zero causes the system to choose a free port |
| '--no-pause-isolates-on-exit', |
| if (localEngine != null) '-DlocalEngine=$localEngine', |
| if (localEngineSrcPath != null) '-DlocalEngineSrcPath=$localEngineSrcPath', |
| taskExecutable, |
| ], |
| environment: <String, String>{ |
| if (deviceId != null) |
| DeviceIdEnvName: deviceId, |
| }, |
| ); |
| |
| bool runnerFinished = false; |
| |
| runner.exitCode.whenComplete(() { |
| runnerFinished = true; |
| }); |
| |
| final Completer<Uri> uri = Completer<Uri>(); |
| |
| final StreamSubscription<String> stdoutSub = runner.stdout |
| .transform<String>(const Utf8Decoder()) |
| .transform<String>(const LineSplitter()) |
| .listen((String line) { |
| if (!uri.isCompleted) { |
| final Uri serviceUri = parseServiceUri(line, prefix: 'Observatory listening on '); |
| if (serviceUri != null) |
| uri.complete(serviceUri); |
| } |
| if (!silent) { |
| stdout.writeln('[$taskName] [STDOUT] $line'); |
| } |
| }); |
| |
| final StreamSubscription<String> stderrSub = runner.stderr |
| .transform<String>(const Utf8Decoder()) |
| .transform<String>(const LineSplitter()) |
| .listen((String line) { |
| stderr.writeln('[$taskName] [STDERR] $line'); |
| }); |
| |
| try { |
| final VMIsolateRef isolate = await _connectToRunnerIsolate(await uri.future); |
| final Map<String, dynamic> taskResultJson = await isolate.invokeExtension('ext.cocoonRunTask') as Map<String, dynamic>; |
| final TaskResult taskResult = TaskResult.fromJson(taskResultJson); |
| await runner.exitCode; |
| return taskResult; |
| } finally { |
| if (!runnerFinished) |
| runner.kill(ProcessSignal.sigkill); |
| await stdoutSub.cancel(); |
| await stderrSub.cancel(); |
| } |
| } |
| |
| Future<VMIsolateRef> _connectToRunnerIsolate(Uri vmServiceUri) async { |
| final List<String> pathSegments = <String>[ |
| // Add authentication code. |
| if (vmServiceUri.pathSegments.isNotEmpty) vmServiceUri.pathSegments[0], |
| 'ws', |
| ]; |
| final String url = vmServiceUri.replace(scheme: 'ws', pathSegments: |
| pathSegments).toString(); |
| final Stopwatch stopwatch = Stopwatch()..start(); |
| |
| while (true) { |
| try { |
| // Make sure VM server is up by successfully opening and closing a socket. |
| await (await WebSocket.connect(url)).close(); |
| |
| // Look up the isolate. |
| final VMServiceClient client = VMServiceClient.connect(url); |
| final VM vm = await client.getVM(); |
| final VMIsolateRef isolate = vm.isolates.single; |
| final String response = await isolate.invokeExtension('ext.cocoonRunnerReady') as String; |
| if (response != 'ready') |
| throw 'not ready yet'; |
| return isolate; |
| } catch (error) { |
| if (stopwatch.elapsed > const Duration(seconds: 10)) |
| print('VM service still not ready after ${stopwatch.elapsed}: $error\nContinuing to retry...'); |
| await Future<void>.delayed(const Duration(milliseconds: 50)); |
| } |
| } |
| } |