blob: 8cd8d1e4057c874ee807a8554f19933acdad0962 [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. 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:async/async.dart';
import '../../backend/test_platform.dart';
import '../../util/io.dart';
import '../../utils.dart';
import '../executable_settings.dart';
import 'browser.dart';
import 'default_settings.dart';
final _observatoryRegExp =
new RegExp(r'Observatory listening (?:on|at) ([^ "]+)');
/// A class for running an instance of Dartium.
///
/// Most of the communication with the browser is expected to happen via HTTP,
/// so this exposes a bare-bones API. The browser starts as soon as the class is
/// constructed, and is killed when [close] is called.
///
/// Any errors starting or running the process are reported through [onExit].
class Dartium extends Browser {
final name = "Dartium";
final Future<Uri> observatoryUrl;
final Future<Uri> remoteDebuggerUrl;
factory Dartium(url, {ExecutableSettings settings, bool debug: false}) {
settings ??= defaultSettings[TestPlatform.dartium];
var observatoryCompleter = new Completer<Uri>.sync();
var remoteDebuggerCompleter = new Completer<Uri>.sync();
return new Dartium._(() async {
var tryPort = ([int port]) async {
var dir = createTempDir();
var args = [
"--user-data-dir=$dir",
url.toString(),
"--disable-extensions",
"--disable-popup-blocking",
"--bwsi",
"--no-first-run",
"--no-default-browser-check",
"--disable-default-apps",
"--disable-translate"
]..addAll(settings.arguments);
if (port != null) {
args.add("--remote-debugging-port=$port");
// This forces Dartium to emit logging on Windows. See sdk#28034.
args.add("--enable-logging=stderr");
// This flags causes Dartium to print a consistent line of output
// after its internal call to `bind()` has succeeded or failed. We
// wait for that output to determine whether the port we chose worked.
args.add("--vmodule=startup_browser_creator_impl=1");
}
var process = await Process.start(settings.executable, args,
environment: {"DART_FLAGS": "--checked"});
if (port != null) {
// Dartium on Windows prints all standard IO to stderr, so we need to
// look there rather than stdout for the Observatory URL.
Stream<List<int>> observatoryStream;
Stream<List<int>> logStream;
if (Platform.isWindows) {
var split = StreamSplitter.splitFrom(process.stderr);
observatoryStream = split.first;
logStream = split.last;
} else {
observatoryStream = process.stdout;
logStream = process.stderr;
}
observatoryCompleter.complete(_getObservatoryUrl(observatoryStream));
var logs = new StreamIterator(lineSplitter.bind(logStream));
// Before we can consider Dartium started successfully, we have to
// make sure the remote debugging port worked. Any errors from this
// will always come before the "Running without renderer sandbox"
// message.
while (await logs.moveNext() &&
!logs.current.contains("startup_browser_creator_impl")) {
if (logs.current.contains("bind() returned an error")) {
// If we failed to bind to the port, return null to tell
// getUnusedPort to try another one.
logs.cancel();
process.kill();
return null;
}
}
logs.cancel();
} else {
observatoryCompleter.complete(null);
}
if (port != null) {
remoteDebuggerCompleter.complete(
getRemoteDebuggerUrl(Uri.parse("http://localhost:$port")));
} else {
remoteDebuggerCompleter.complete(null);
}
process.exitCode
.then((_) => new Directory(dir).deleteSync(recursive: true));
return process;
};
if (!debug) return tryPort();
return getUnusedPort<Future<Process>>(tryPort);
}, observatoryCompleter.future, remoteDebuggerCompleter.future);
}
Dartium._(Future<Process> startBrowser(), this.observatoryUrl,
this.remoteDebuggerUrl)
: super(startBrowser);
// TODO(nweiz): simplify this when sdk#23923 is fixed.
/// Returns the Observatory URL for the Dartium executable with the given
/// [stdout] stream, or `null` if the correct one couldn't be found.
///
/// Dartium prints out three different Observatory URLs when it starts. Only
/// one of them is connected to the VM instance running the host page, and the
/// ordering isn't guaranteed, so we need to figure out which one is correct.
/// We do so by connecting to the VM service via WebSockets and looking for
/// the Observatory instance that actually contains an isolate, and returning
/// the corresponding URI.
static Future<Uri> _getObservatoryUrl(Stream<List<int>> stdout) async {
var urlQueue = new StreamQueue<Uri>(lineSplitter.bind(stdout).map((line) {
var match = _observatoryRegExp.firstMatch(line);
return match == null ? null : Uri.parse(match[1]);
}).where((line) => line != null));
var operations = [urlQueue.next, urlQueue.next, urlQueue.next]
.map((future) => _checkObservatoryUrl(future));
urlQueue.cancel();
/// Dartium will print three possible observatory URLs. For each one, we
/// check whether it's actually connected to an isolate, indicating that
/// it's the observatory for the main page. Once we find the one that is, we
/// cancel the other requests and return it.
///
/// Use try/catch rather than `defaultValue` in `firstWhere` since the
/// parameter name will change in Dart 2.0.
try {
return (await inCompletionOrder(operations)
.firstWhere((url) => url != null)) as Uri;
} on StateError catch (_) {
return null;
}
}
/// If the URL returned by [future] corresponds to the correct Observatory
/// instance, returns it. Otherwise, returns `null`.
///
/// If the returned operation is canceled before it fires, the WebSocket
/// connection with the given Observatory will be closed immediately.
static CancelableOperation<Uri> _checkObservatoryUrl(Future<Uri> future) {
var webSocket;
var canceled = false;
var completer = new CancelableCompleter<Uri>(onCancel: () {
canceled = true;
if (webSocket != null) webSocket.close();
});
// We've encountered a format we don't understand. Close the web socket and
// complete to null.
giveUp() {
webSocket.close();
if (!completer.isCompleted) completer.complete();
}
future.then((url) async {
try {
webSocket = await WebSocket
.connect(url.replace(scheme: 'ws', path: '/ws').toString());
if (canceled) {
webSocket.close();
return null;
}
webSocket.add(JSON.encode({
"jsonrpc": "2.0",
"method": "streamListen",
"params": {"streamId": "Isolate"},
"id": "0"
}));
webSocket.add(JSON.encode(
{"jsonrpc": "2.0", "method": "getVM", "params": {}, "id": "1"}));
webSocket.listen((response) {
try {
response = JSON.decode(response);
} on FormatException catch (_) {
giveUp();
return;
}
// If there's a "response" key, we're probably talking to the pre-1.0
// VM service protocol, in which case we should just give up.
if (response is! Map || response.containsKey("response")) {
giveUp();
return;
}
if (response["id"] == "0") return;
if (response["id"] == "1") {
var result = response["result"];
if (result is! Map) {
giveUp();
return;
}
var isolates = result["isolates"];
if (isolates is! List) {
giveUp();
return;
}
if (isolates.isNotEmpty) {
webSocket.close();
if (!completer.isCompleted) completer.complete(url);
}
return;
}
// The 1.0 protocol used a raw "event" key, while the 2.0 protocol
// wraps it in JSON-RPC method params.
var event;
if (response.containsKey("event")) {
event = response["event"];
} else {
var params = response["params"];
if (params is Map) event = params["event"];
}
if (event is! Map) {
giveUp();
return;
}
if (event["kind"] != "IsolateStart") return;
webSocket.close();
if (completer.isCompleted) return;
// TODO(nweiz): include the isolate ID in the URL?
completer.complete(url);
});
} on IOException catch (_) {
// IO exceptions are probably caused by connecting to an
// incorrect WebSocket that already closed.
return null;
}
}).catchError((error, stackTrace) {
if (!completer.isCompleted) completer.completeError(error, stackTrace);
});
return completer.operation;
}
}