runner.dart 6.28 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 8
// 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';

9
import 'package:path/path.dart' as path;
10 11 12
import 'package:vm_service_client/vm_service_client.dart';

import 'package:flutter_devicelab/framework/utils.dart';
13
import 'package:flutter_devicelab/framework/adb.dart';
14

15 16
import 'task_result.dart';

17 18 19 20 21
/// 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`.
22 23 24
///
/// Running the task in [silent] mode will suppress standard output from task
/// processes and only print standard errors.
25
Future<TaskResult> runTask(
26 27 28 29
  String taskName, {
  bool silent = false,
  String localEngine,
  String localEngineSrcPath,
30
  String deviceId,
31
}) async {
32
  final String taskExecutable = 'bin/tasks/$taskName.dart';
33 34 35 36

  if (!file(taskExecutable).existsSync())
    throw 'Executable Dart file not found: $taskExecutable';

37 38 39
  final Process runner = await startProcess(
    dartBin,
    <String>[
40
      '--disable-dart-dev',
41 42 43 44 45 46 47 48 49 50 51
      '--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,
    },
  );
52 53 54

  bool runnerFinished = false;

55
  runner.exitCode.whenComplete(() {
56 57 58
    runnerFinished = true;
  });

59 60
  final Completer<Uri> uri = Completer<Uri>();

61
  final StreamSubscription<String> stdoutSub = runner.stdout
62 63
      .transform<String>(const Utf8Decoder())
      .transform<String>(const LineSplitter())
64
      .listen((String line) {
65
    if (!uri.isCompleted) {
66 67
      final Uri serviceUri = parseServiceUri(line, prefix: 'Observatory listening on ');
      if (serviceUri != null)
68
        uri.complete(serviceUri);
69
    }
70 71 72
    if (!silent) {
      stdout.writeln('[$taskName] [STDOUT] $line');
    }
73 74
  });

75
  final StreamSubscription<String> stderrSub = runner.stderr
76 77
      .transform<String>(const Utf8Decoder())
      .transform<String>(const LineSplitter())
78 79 80 81 82
      .listen((String line) {
    stderr.writeln('[$taskName] [STDERR] $line');
  });

  try {
83
    final VMIsolateRef isolate = await _connectToRunnerIsolate(await uri.future);
84 85
    final Map<String, dynamic> taskResultJson = await isolate.invokeExtension('ext.cocoonRunTask') as Map<String, dynamic>;
    final TaskResult taskResult = TaskResult.fromJson(taskResultJson);
86
    await runner.exitCode;
87 88 89
    return taskResult;
  } finally {
    if (!runnerFinished)
90
      runner.kill(ProcessSignal.sigkill);
91
    await cleanupSystem();
92 93 94 95 96
    await stdoutSub.cancel();
    await stderrSub.cancel();
  }
}

97
Future<VMIsolateRef> _connectToRunnerIsolate(Uri vmServiceUri) async {
98
  final List<String> pathSegments = <String>[
99
    // Add authentication code.
100 101 102
    if (vmServiceUri.pathSegments.isNotEmpty) vmServiceUri.pathSegments[0],
    'ws',
  ];
103 104
  final String url = vmServiceUri.replace(scheme: 'ws', pathSegments:
      pathSegments).toString();
105
  final Stopwatch stopwatch = Stopwatch()..start();
106

107
  while (true) {
108 109 110 111 112
    try {
      // Make sure VM server is up by successfully opening and closing a socket.
      await (await WebSocket.connect(url)).close();

      // Look up the isolate.
113
      final VMServiceClient client = VMServiceClient.connect(url);
114
      final VM vm = await client.getVM();
115
      final VMIsolateRef isolate = vm.isolates.single;
116
      final String response = await isolate.invokeExtension('ext.cocoonRunnerReady') as String;
117
      if (response != 'ready')
118
        throw 'not ready yet';
119
      return isolate;
120
    } catch (error) {
121 122 123
      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));
124 125 126
    }
  }
}
127 128

Future<void> cleanupSystem() async {
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
  if (deviceOperatingSystem == null || deviceOperatingSystem == DeviceOperatingSystem.android) {
    print('\n\nCleaning up system after task...');
    final String javaHome = await findJavaHome();
    if (javaHome != null) {
      // To shut gradle down, we have to call "gradlew --stop".
      // To call gradlew, we need to have a gradle-wrapper.properties file along
      // with a shell script, a .jar file, etc. We get these from various places
      // as you see in the code below, and we save them all into a temporary dir
      // which we can then delete after.
      // All the steps below are somewhat tolerant of errors, because it doesn't
      // really matter if this succeeds every time or not.
      print('\nTelling Gradle to shut down (JAVA_HOME=$javaHome)');
      final String gradlewBinaryName = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
      final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_shutdown_gradle.');
      recursiveCopy(Directory(path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper')), tempDir);
      copy(File(path.join(path.join(flutterDirectory.path, 'packages', 'flutter_tools'), 'templates', 'app', 'android.tmpl', 'gradle', 'wrapper', 'gradle-wrapper.properties')), Directory(path.join(tempDir.path, 'gradle', 'wrapper')));
      if (!Platform.isWindows) {
        await exec(
          'chmod',
          <String>['a+x', path.join(tempDir.path, gradlewBinaryName)],
          canFail: true,
        );
      }
152
      await exec(
153 154 155 156
        path.join(tempDir.path, gradlewBinaryName),
        <String>['--stop'],
        environment: <String, String>{ 'JAVA_HOME': javaHome},
        workingDirectory: tempDir.path,
157 158
        canFail: true,
      );
159 160 161 162
      rmTree(tempDir);
      print('\n');
    } else {
      print('Could not determine JAVA_HOME; not shutting down Gradle.');
163
    }
164 165 166 167 168
    // Removes the .gradle directory because sometimes gradle fails in downloading
    // a new version and leaves a corrupted zip archive, which could cause the
    // next devicelab task to fail.
    // https://github.com/flutter/flutter/issues/65277
    rmTree(dir('${Platform.environment['HOME']}/.gradle'));
169
  }
170
}