1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// 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';
import 'package:path/path.dart' as path;
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.
const Duration taskTimeoutWithGracePeriod = Duration(minutes: 26);
/// 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<Map<String, dynamic>> runTask(String taskName, { bool silent = false }) 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>[
'--enable-vm-service=0', // zero causes the system to choose a free port
'--no-pause-isolates-on-exit',
taskExecutable,
]);
bool runnerFinished = false;
runner.exitCode.whenComplete(() {
runnerFinished = true;
});
final Completer<int> port = Completer<int>();
final StreamSubscription<String> stdoutSub = runner.stdout
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) {
if (!port.isCompleted) {
final int portValue = parseServicePort(line, prefix: 'Observatory listening on ');
if (portValue != null)
port.complete(portValue);
}
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');
});
String waitingFor = 'connection';
try {
final VMIsolateRef isolate = await _connectToRunnerIsolate(await port.future);
waitingFor = 'task completion';
final Map<String, dynamic> taskResult =
await isolate.invokeExtension('ext.cocoonRunTask').timeout(taskTimeoutWithGracePeriod);
waitingFor = 'task process to exit';
await runner.exitCode.timeout(const Duration(seconds: 60));
return taskResult;
} on TimeoutException catch (timeout) {
runner.kill(ProcessSignal.sigint);
return <String, dynamic>{
'success': false,
'reason': 'Timeout in runner.dart waiting for $waitingFor: ${timeout.message}',
};
} finally {
if (!runnerFinished)
runner.kill(ProcessSignal.sigkill);
await cleanupSystem();
await stdoutSub.cancel();
await stderrSub.cancel();
}
}
Future<VMIsolateRef> _connectToRunnerIsolate(int vmServicePort) async {
final String url = 'ws://localhost:$vmServicePort/ws';
final DateTime started = DateTime.now();
// 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.
await Future<void>.delayed(const Duration(milliseconds: 100));
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');
if (response != 'ready')
throw 'not ready yet';
return isolate;
} catch (error) {
const Duration connectionTimeout = Duration(seconds: 10);
if (DateTime.now().difference(started) > connectionTimeout) {
throw TimeoutException(
'Failed to connect to the task runner process',
connectionTimeout,
);
}
print('VM service not ready yet: $error');
const Duration pauseBetweenRetries = Duration(milliseconds: 200);
print('Will retry in $pauseBetweenRetries.');
await Future<void>.delayed(pauseBetweenRetries);
}
}
}
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.');
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,
);
}
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.');
}
}