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