OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library test.runner.browser.dartium; | 5 library test.runner.browser.dartium; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:convert'; |
8 import 'dart:io'; | 9 import 'dart:io'; |
9 | 10 |
| 11 import 'package:async/async.dart'; |
10 import 'package:path/path.dart' as p; | 12 import 'package:path/path.dart' as p; |
11 | 13 |
| 14 import '../../util/cancelable_future.dart'; |
12 import '../../util/io.dart'; | 15 import '../../util/io.dart'; |
| 16 import '../../utils.dart'; |
13 import 'browser.dart'; | 17 import 'browser.dart'; |
14 | 18 |
| 19 final _observatoryRegExp = new RegExp(r"^Observatory listening on ([^ ]+)"); |
| 20 |
15 /// A class for running an instance of Dartium. | 21 /// A class for running an instance of Dartium. |
16 /// | 22 /// |
17 /// Most of the communication with the browser is expected to happen via HTTP, | 23 /// Most of the communication with the browser is expected to happen via HTTP, |
18 /// so this exposes a bare-bones API. The browser starts as soon as the class is | 24 /// so this exposes a bare-bones API. The browser starts as soon as the class is |
19 /// constructed, and is killed when [close] is called. | 25 /// constructed, and is killed when [close] is called. |
20 /// | 26 /// |
21 /// Any errors starting or running the process are reported through [onExit]. | 27 /// Any errors starting or running the process are reported through [onExit]. |
22 class Dartium extends Browser { | 28 class Dartium extends Browser { |
23 final name = "Dartium"; | 29 final name = "Dartium"; |
24 | 30 |
25 Dartium(url, {String executable}) | 31 final Future<Uri> observatoryUrl; |
26 : super(() => _startBrowser(url, executable)); | 32 |
| 33 factory Dartium(url, {String executable}) { |
| 34 var completer = new Completer.sync(); |
| 35 return new Dartium._(() async { |
| 36 if (executable == null) executable = _defaultExecutable(); |
| 37 |
| 38 var dir = createTempDir(); |
| 39 var process = await Process.start(executable, [ |
| 40 "--user-data-dir=$dir", |
| 41 url.toString(), |
| 42 "--disable-extensions", |
| 43 "--disable-popup-blocking", |
| 44 "--bwsi", |
| 45 "--no-first-run", |
| 46 "--no-default-browser-check", |
| 47 "--disable-default-apps", |
| 48 "--disable-translate" |
| 49 ], environment: {"DART_FLAGS": "--checked"}); |
| 50 |
| 51 // The first observatory URL emitted is for the empty start page; the |
| 52 // second is actually for the host page. |
| 53 completer.complete(_getObservatoryUrl(process.stdout)); |
| 54 |
| 55 process.exitCode |
| 56 .then((_) => new Directory(dir).deleteSync(recursive: true)); |
| 57 |
| 58 return process; |
| 59 }, completer.future); |
| 60 } |
| 61 |
| 62 Dartium._(Future<Process> startBrowser(), this.observatoryUrl) |
| 63 : super(startBrowser); |
27 | 64 |
28 /// Starts a new instance of Dartium open to the given [url], which may be a | 65 /// Starts a new instance of Dartium open to the given [url], which may be a |
29 /// [Uri] or a [String]. | 66 /// [Uri] or a [String]. |
30 /// | 67 /// |
31 /// If [executable] is passed, it's used as the Dartium executable. Otherwise | 68 /// If [executable] is passed, it's used as the Dartium executable. Otherwise |
32 /// the default executable name for the current OS will be used. | 69 /// the default executable name for the current OS will be used. |
33 static Future<Process> _startBrowser(url, [String executable]) async { | |
34 if (executable == null) executable = _defaultExecutable(); | |
35 | |
36 var dir = createTempDir(); | |
37 var process = await Process.start(executable, [ | |
38 "--user-data-dir=$dir", | |
39 url.toString(), | |
40 "--disable-extensions", | |
41 "--disable-popup-blocking", | |
42 "--bwsi", | |
43 "--no-first-run", | |
44 "--no-default-browser-check", | |
45 "--disable-default-apps", | |
46 "--disable-translate" | |
47 ], environment: {"DART_FLAGS": "--checked"}); | |
48 | |
49 process.exitCode | |
50 .then((_) => new Directory(dir).deleteSync(recursive: true)); | |
51 | |
52 return process; | |
53 } | |
54 | 70 |
55 /// Return the default executable for the current operating system. | 71 /// Return the default executable for the current operating system. |
56 static String _defaultExecutable() { | 72 static String _defaultExecutable() { |
57 var dartium = _executableInEditor(); | 73 var dartium = _executableInEditor(); |
58 if (dartium != null) return dartium; | 74 if (dartium != null) return dartium; |
59 return Platform.isWindows ? "dartium.exe" : "dartium"; | 75 return Platform.isWindows ? "dartium.exe" : "dartium"; |
60 } | 76 } |
61 | 77 |
62 static String _executableInEditor() { | 78 static String _executableInEditor() { |
63 var dir = p.dirname(sdkDir); | 79 var dir = p.dirname(sdkDir); |
(...skipping 15 matching lines...) Expand all Loading... |
79 dir, "chromium/Chromium.app/Contents/MacOS/Chromium"); | 95 dir, "chromium/Chromium.app/Contents/MacOS/Chromium"); |
80 return new File(dartium).existsSync() ? dartium : null; | 96 return new File(dartium).existsSync() ? dartium : null; |
81 } | 97 } |
82 | 98 |
83 assert(Platform.isLinux); | 99 assert(Platform.isLinux); |
84 if (!new File(p.join(dir, "DartEditor")).existsSync()) return null; | 100 if (!new File(p.join(dir, "DartEditor")).existsSync()) return null; |
85 | 101 |
86 var dartium = p.join(dir, "chromium", "chrome"); | 102 var dartium = p.join(dir, "chromium", "chrome"); |
87 return new File(dartium).existsSync() ? dartium : null; | 103 return new File(dartium).existsSync() ? dartium : null; |
88 } | 104 } |
| 105 |
| 106 // TODO(nweiz): simplify this when sdk#23923 is fixed. |
| 107 /// Returns the Observatory URL for the Dartium executable with the given |
| 108 /// [stdout] stream, or `null` if the correct one couldn't be found. |
| 109 /// |
| 110 /// Dartium prints out three different Observatory URLs when it starts. Only |
| 111 /// one of them is connected to the VM instance running the host page, and the |
| 112 /// ordering isn't guaranteed, so we need to figure out which one is correct. |
| 113 /// We do so by connecting to the VM service via WebSockets and looking for |
| 114 /// the Observatory instance that actually contains an isolate, and returning |
| 115 /// the corresponding URI. |
| 116 static Future<Uri> _getObservatoryUrl(Stream<List<int>> stdout) async { |
| 117 var urlQueue = new StreamQueue(lineSplitter.bind(stdout).map((line) { |
| 118 var match = _observatoryRegExp.firstMatch(line); |
| 119 return match == null ? null : Uri.parse(match[1]); |
| 120 }).where((line) => line != null)); |
| 121 |
| 122 var futures = [ |
| 123 urlQueue.next, |
| 124 urlQueue.next, |
| 125 urlQueue.next |
| 126 ].map(_checkObservatoryUrl); |
| 127 |
| 128 urlQueue.cancel(); |
| 129 |
| 130 /// Dartium will print three possible observatory URLs. For each one, we |
| 131 /// check whether it's actually connected to an isolate, indicating that |
| 132 /// it's the observatory for the main page. Once we find the one that is, we |
| 133 /// cancel the other requests and return it. |
| 134 return inCompletionOrder(futures) |
| 135 .firstWhere((url) => url != null, defaultValue: () => null); |
| 136 } |
| 137 |
| 138 /// If the URL returned by [future] corresponds to the correct Observatory |
| 139 /// instance, returns it. Otherwise, returns `null`. |
| 140 /// |
| 141 /// If the returned future is canceled before it fires, the WebSocket |
| 142 /// connection with the given Observatory will be closed immediately. |
| 143 static CancelableFuture<Uri> _checkObservatoryUrl(Future<Uri> future) { |
| 144 var webSocket; |
| 145 var canceled = false; |
| 146 var completer = new CancelableCompleter(() { |
| 147 canceled = true; |
| 148 if (webSocket != null) webSocket.close(); |
| 149 }); |
| 150 |
| 151 // We've encountered a format we don't understand. Close the web socket and |
| 152 // complete to null. |
| 153 giveUp() { |
| 154 webSocket.close(); |
| 155 if (!completer.isCompleted) completer.complete(); |
| 156 } |
| 157 |
| 158 future.then((url) async { |
| 159 try { |
| 160 webSocket = await WebSocket.connect( |
| 161 url.replace(scheme: 'ws', path: '/ws').toString()); |
| 162 if (canceled) { |
| 163 webSocket.close(); |
| 164 return null; |
| 165 } |
| 166 |
| 167 webSocket.add(JSON.encode({ |
| 168 "jsonrpc": "2.0", |
| 169 "method": "streamListen", |
| 170 "params": {"streamId": "Isolate"}, |
| 171 "id": "0" |
| 172 })); |
| 173 |
| 174 webSocket.add(JSON.encode({ |
| 175 "jsonrpc": "2.0", |
| 176 "method": "getVM", |
| 177 "params": {}, |
| 178 "id": "1" |
| 179 })); |
| 180 |
| 181 webSocket.listen((response) { |
| 182 try { |
| 183 response = JSON.decode(response); |
| 184 } on FormatException catch (_) { |
| 185 giveUp(); |
| 186 return; |
| 187 } |
| 188 |
| 189 // If there's a "response" key, we're probably talking to the pre-1.0 |
| 190 // VM service protocol, in which case we should just give up. |
| 191 if (response is! Map || response.containsKey("response")) { |
| 192 giveUp(); |
| 193 return; |
| 194 } |
| 195 |
| 196 if (response["id"] == "0") return; |
| 197 |
| 198 if (response["id"] == "1") { |
| 199 var result = response["result"]; |
| 200 if (result is! Map) { |
| 201 giveUp(); |
| 202 return; |
| 203 } |
| 204 |
| 205 var isolates = result["isolates"]; |
| 206 if (isolates is! List) { |
| 207 giveUp(); |
| 208 return; |
| 209 } |
| 210 |
| 211 if (isolates.isNotEmpty) { |
| 212 webSocket.close(); |
| 213 if (!completer.isCompleted) completer.complete(url); |
| 214 } |
| 215 return; |
| 216 } |
| 217 |
| 218 // The 1.0 protocol used a raw "event" key, while the 2.0 protocol |
| 219 // wraps it in JSON-RPC method params. |
| 220 var event; |
| 221 if (response.containsKey("event")) { |
| 222 event = response["event"]; |
| 223 } else { |
| 224 var params = response["params"]; |
| 225 if (params is Map) event = params["event"]; |
| 226 } |
| 227 |
| 228 if (event is! Map) { |
| 229 giveUp(); |
| 230 return; |
| 231 } |
| 232 |
| 233 if (event["kind"] != "IsolateStart") return; |
| 234 webSocket.close(); |
| 235 if (completer.isCompleted) return; |
| 236 |
| 237 // TODO(nweiz): include the isolate ID in the URL? |
| 238 completer.complete(url); |
| 239 }); |
| 240 } on IOException catch (_) { |
| 241 // IO exceptions are probably caused by connecting to an |
| 242 // incorrect WebSocket that already closed. |
| 243 return null; |
| 244 } |
| 245 }).catchError((error, stackTrace) { |
| 246 if (!completer.isCompleted) completer.completeError(error, stackTrace); |
| 247 }); |
| 248 |
| 249 return completer.future; |
| 250 } |
89 } | 251 } |
OLD | NEW |