runner.dart 6.35 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2016 The Chromium 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';

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

import 'package:flutter_devicelab/framework/utils.dart';

/// Slightly longer than task timeout that gives the task runner a chance to
/// clean-up before forcefully quitting it.
16
const Duration taskTimeoutWithGracePeriod = Duration(minutes: 26);
17 18 19 20 21 22

/// 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`.
23 24 25
///
/// Running the task in [silent] mode will suppress standard output from task
/// processes and only print standard errors.
26
Future<Map<String, dynamic>> runTask(String taskName, { bool silent = false }) async {
27
  final String taskExecutable = 'bin/tasks/$taskName.dart';
28 29 30 31

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

32
  final Process runner = await startProcess(dartBin, <String>[
33
    '--enable-vm-service=0', // zero causes the system to choose a free port
34 35 36 37 38 39
    '--no-pause-isolates-on-exit',
    taskExecutable,
  ]);

  bool runnerFinished = false;

40
  runner.exitCode.whenComplete(() {
41 42 43
    runnerFinished = true;
  });

44
  final Completer<int> port = Completer<int>();
45

46
  final StreamSubscription<String> stdoutSub = runner.stdout
47 48
      .transform<String>(const Utf8Decoder())
      .transform<String>(const LineSplitter())
49
      .listen((String line) {
50 51 52 53 54
    if (!port.isCompleted) {
      final int portValue = parseServicePort(line, prefix: 'Observatory listening on ');
      if (portValue != null)
        port.complete(portValue);
    }
55 56 57
    if (!silent) {
      stdout.writeln('[$taskName] [STDOUT] $line');
    }
58 59
  });

60
  final StreamSubscription<String> stderrSub = runner.stderr
61 62
      .transform<String>(const Utf8Decoder())
      .transform<String>(const LineSplitter())
63 64 65 66 67 68
      .listen((String line) {
    stderr.writeln('[$taskName] [STDERR] $line');
  });

  String waitingFor = 'connection';
  try {
69
    final VMIsolateRef isolate = await _connectToRunnerIsolate(await port.future);
70
    waitingFor = 'task completion';
71
    final Map<String, dynamic> taskResult =
72 73
        await isolate.invokeExtension('ext.cocoonRunTask').timeout(taskTimeoutWithGracePeriod);
    waitingFor = 'task process to exit';
74
    await runner.exitCode.timeout(const Duration(seconds: 60));
75 76
    return taskResult;
  } on TimeoutException catch (timeout) {
77
    runner.kill(ProcessSignal.sigint);
78 79
    return <String, dynamic>{
      'success': false,
80
      'reason': 'Timeout in runner.dart waiting for $waitingFor: ${timeout.message}',
81 82 83
    };
  } finally {
    if (!runnerFinished)
84
      runner.kill(ProcessSignal.sigkill);
85
    await cleanupSystem();
86 87 88 89 90
    await stdoutSub.cancel();
    await stderrSub.cancel();
  }
}

91
Future<VMIsolateRef> _connectToRunnerIsolate(int vmServicePort) async {
92
  final String url = 'ws://localhost:$vmServicePort/ws';
93
  final DateTime started = DateTime.now();
94 95 96 97 98 99

  // TODO(yjbanov): due to lack of imagination at the moment the handshake with
  //                the task process is very rudimentary and requires this small
  //                delay to let the task process open up the VM service port.
  //                Otherwise we almost always hit the non-ready case first and
  //                wait a whole 1 second, which is annoying.
100
  await Future<void>.delayed(const Duration(milliseconds: 100));
101 102 103 104 105 106 107

  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.
108
      final VMServiceClient client = VMServiceClient.connect(url);
109 110 111
      final VM vm = await client.getVM();
      final VMIsolateRef isolate = vm.isolates.single;
      final String response = await isolate.invokeExtension('ext.cocoonRunnerReady');
112 113
      if (response != 'ready')
        throw 'not ready yet';
114 115
      return isolate;
    } catch (error) {
116
      const Duration connectionTimeout = Duration(seconds: 10);
117 118
      if (DateTime.now().difference(started) > connectionTimeout) {
        throw TimeoutException(
119 120 121 122 123
          'Failed to connect to the task runner process',
          connectionTimeout,
        );
      }
      print('VM service not ready yet: $error');
124
      const Duration pauseBetweenRetries = Duration(milliseconds: 200);
125
      print('Will retry in $pauseBetweenRetries.');
126
      await Future<void>.delayed(pauseBetweenRetries);
127 128 129
    }
  }
}
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144

Future<void> cleanupSystem() async {
  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.');
145
    recursiveCopy(Directory(path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper')), tempDir);
146
    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')));
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    if (!Platform.isWindows) {
      await exec(
        'chmod',
        <String>['a+x', path.join(tempDir.path, gradlewBinaryName)],
        canFail: true,
      );
    }
    await exec(
      path.join(tempDir.path, gradlewBinaryName),
      <String>['--stop'],
      environment: <String, String>{ 'JAVA_HOME': javaHome },
      workingDirectory: tempDir.path,
      canFail: true,
    );
    rmTree(tempDir);
    print('\n');
  } else {
    print('Could not determine JAVA_HOME; not shutting down Gradle.');
  }
}