Skip to content

Commit bdda5ca

Browse files
committed
Expose content shell's remote debugging URL.
Closes #297 [email protected] Review URL: https://codereview.chromium.org//1257953008 .
1 parent 4c04b8c commit bdda5ca

File tree

7 files changed

+145
-32
lines changed

7 files changed

+145
-32
lines changed

lib/src/runner.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,25 @@ class Runner {
234234
}
235235
}
236236

237+
if (suite.platform == TestPlatform.contentShell) {
238+
var url = suite.environment.remoteDebuggerUrl;
239+
if (url == null) {
240+
print("${yellow}Remote debugger URL not found.$noColor");
241+
} else {
242+
print("Remote debugger URL: $bold$url$noColor");
243+
}
244+
}
245+
237246
var buffer = new StringBuffer(
238247
"${bold}The test runner is paused.${noColor} ");
239248
if (!suite.platform.isHeadless) {
240249
buffer.write("Open the dev console in ${suite.platform} ");
241250
if (suite.platform.isDartVM) buffer.write("or ");
242251
} else {
243252
buffer.write("Open ");
253+
if (suite.platform == TestPlatform.contentShell) {
254+
buffer.write("the remote debugger or ");
255+
}
244256
}
245257
if (suite.platform.isDartVM) buffer.write("the Observatory ");
246258

lib/src/runner/browser/browser.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ abstract class Browser {
3232
Future<Uri> get observatoryUrl =>
3333
throw new UnsupportedError("$name doesn't support Observatory.");
3434

35+
/// The remote debugger URL for this browser.
36+
///
37+
/// This will throw an [UnsupportedError] for browsers that don't support
38+
/// remote debugging, and return `null` if the remote debugging URL can't be
39+
/// found.
40+
Future<Uri> get remoteDebuggerUrl =>
41+
throw new UnsupportedError("$name doesn't support remote debugging.");
42+
3543
/// The underlying process.
3644
///
3745
/// This will fire once the process has started successfully.

lib/src/runner/browser/browser_manager.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,13 @@ class BrowserManager {
151151
Future<_BrowserEnvironment> _loadBrowserEnvironment() async {
152152
var observatoryUrl;
153153
if (_platform.isDartVM) observatoryUrl = await _browser.observatoryUrl;
154-
return new _BrowserEnvironment(this, observatoryUrl);
154+
155+
var remoteDebuggerUrl;
156+
if (_platform == TestPlatform.contentShell) {
157+
remoteDebuggerUrl = await _browser.remoteDebuggerUrl;
158+
}
159+
160+
return new _BrowserEnvironment(this, observatoryUrl, remoteDebuggerUrl);
155161
}
156162

157163
/// Tells the browser the load a test suite from the URL [url].
@@ -285,7 +291,10 @@ class _BrowserEnvironment implements Environment {
285291

286292
final Uri observatoryUrl;
287293

288-
_BrowserEnvironment(this._manager, this.observatoryUrl);
294+
final Uri remoteDebuggerUrl;
295+
296+
_BrowserEnvironment(this._manager, this.observatoryUrl,
297+
this.remoteDebuggerUrl);
289298

290299
CancelableFuture displayPause() => _manager._displayPause();
291300
}

lib/src/runner/browser/content_shell.dart

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
library test.runner.browser.content_shell;
66

77
import 'dart:async';
8+
import 'dart:convert';
89
import 'dart:io';
910

11+
import '../../util/io.dart';
1012
import '../../utils.dart';
1113
import '../application_exception.dart';
1214
import 'browser.dart';
@@ -25,43 +27,97 @@ class ContentShell extends Browser {
2527

2628
final Future<Uri> observatoryUrl;
2729

30+
final Future<Uri> remoteDebuggerUrl;
31+
2832
factory ContentShell(url, {String executable, bool debug: false}) {
29-
var completer = new Completer.sync();
30-
return new ContentShell._(() async {
33+
var observatoryCompleter = new Completer.sync();
34+
var remoteDebuggerCompleter = new Completer.sync();
35+
return new ContentShell._(() {
3136
if (executable == null) executable = _defaultExecutable();
3237

33-
var process = await Process.start(
34-
executable, ["--dump-render-tree", url.toString()],
35-
environment: {"DART_FLAGS": "--checked"});
36-
37-
if (debug) {
38-
completer.complete(lineSplitter.bind(process.stdout).map((line) {
39-
var match = _observatoryRegExp.firstMatch(line);
40-
if (match == null) return null;
41-
return Uri.parse(match[1]);
42-
}).where((uri) => uri != null).first);
43-
} else {
44-
completer.complete(null);
38+
tryPort([port]) async {
39+
var args = ["--dump-render-tree", url.toString()];
40+
if (port != null) args.add("--remote-debugging-port=$port");
41+
42+
var process = await Process.start(executable, args,
43+
environment: {"DART_FLAGS": "--checked"});
44+
45+
if (debug) {
46+
observatoryCompleter.complete(lineSplitter.bind(process.stdout)
47+
.map((line) {
48+
var match = _observatoryRegExp.firstMatch(line);
49+
if (match == null) return null;
50+
return Uri.parse(match[1]);
51+
}).where((uri) => uri != null).first);
52+
} else {
53+
observatoryCompleter.complete(null);
54+
}
55+
56+
var stderr = new StreamIterator(lineSplitter.bind(process.stderr));
57+
58+
// Before we can consider content_shell started successfully, we have to
59+
// make sure it's not expired and that the remote debugging port worked.
60+
// Any errors from this will always come before the "Running without
61+
// renderer sanxbox" message.
62+
while (await stderr.moveNext() &&
63+
!stderr.current.endsWith("Running without renderer sandbox")) {
64+
if (stderr.current == "[dartToStderr]: Dartium build has expired") {
65+
stderr.cancel();
66+
process.kill();
67+
// TODO(nweiz): link to dartlang.org once it has download links for
68+
// content shell
69+
// (https://github.com/dart-lang/www.dartlang.org/issues/1164).
70+
throw new ApplicationException(
71+
"You're using an expired content_shell. Upgrade to the latest "
72+
"version:\n"
73+
"http://gsdview.appspot.com/dart-archive/channels/stable/"
74+
"release/latest/dartium/");
75+
} else if (stderr.current.contains("bind() returned an error")) {
76+
// If we failed to bind to the port, return null to tell
77+
// getUnusedPort to try another one.
78+
stderr.cancel();
79+
process.kill();
80+
return null;
81+
}
82+
}
83+
84+
if (port != null) {
85+
remoteDebuggerCompleter.complete(
86+
_getRemoteDebuggerUrl(Uri.parse("http://localhost:$port")));
87+
} else {
88+
remoteDebuggerCompleter.complete(null);
89+
}
90+
91+
stderr.cancel();
92+
return process;
4593
}
4694

47-
lineSplitter.bind(process.stderr).listen((line) {
48-
if (line != "[dartToStderr]: Dartium build has expired") return;
49-
50-
// TODO(nweiz): link to dartlang.org once it has download links for
51-
// content shell
52-
// (https://github.com/dart-lang/www.dartlang.org/issues/1164).
53-
throw new ApplicationException(
54-
"You're using an expired content_shell. Upgrade to the latest "
55-
"version:\n"
56-
"http://gsdview.appspot.com/dart-archive/channels/stable/release/"
57-
"latest/dartium/");
58-
});
59-
60-
return process;
61-
}, completer.future);
95+
if (!debug) return tryPort();
96+
return getUnusedPort(tryPort);
97+
}, observatoryCompleter.future, remoteDebuggerCompleter.future);
98+
}
99+
100+
/// Returns the full URL of the remote debugger for the host page.
101+
///
102+
/// This takes the base remote debugger URL (which points to a browser-wide
103+
/// page) and uses its JSON API to find the resolved URL for debugging the
104+
/// host page.
105+
static Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
106+
try {
107+
var client = new HttpClient();
108+
var request = await client.getUrl(base.resolve("/json/list"));
109+
var response = await request.close();
110+
var json = await JSON.fuse(UTF8).decoder.bind(response).single;
111+
return base.resolve(json.first["devtoolsFrontendUrl"]);
112+
} catch (_) {
113+
// If we fail to talk to the remote debugger protocol, give up and return
114+
// the raw URL rather than crashing.
115+
return base;
116+
}
62117
}
63118

64-
ContentShell._(Future<Process> startBrowser(), this.observatoryUrl)
119+
ContentShell._(Future<Process> startBrowser(), this.observatoryUrl,
120+
this.remoteDebuggerUrl)
65121
: super(startBrowser);
66122

67123
/// Return the default executable for the current operating system.

lib/src/runner/environment.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ abstract class Environment {
1313
/// environment doesn't run the Dart VM or the URL couldn't be detected.
1414
Uri get observatoryUrl;
1515

16+
/// The URL of the remote debugger for this environment, or `null` if it isn't
17+
/// enabled.
18+
Uri get remoteDebuggerUrl;
19+
1620
/// Displays information indicating that the test runner is paused.
1721
///
1822
/// The returned future will complete when the user takes action within the

lib/src/runner/vm/environment.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class VMEnvironment implements Environment {
1414
Uri get observatoryUrl => throw new UnsupportedError(
1515
"VMEnvironment.observatoryUrl is not currently supported.");
1616

17+
Uri get remoteDebuggerUrl => throw new UnsupportedError(
18+
"VMEnvironment.observatoryUrl is not supported.");
19+
1720
CancelableFuture displayPause() =>
1821
throw new UnsupportedError(
1922
"The VM doesn't yet support Environment.displayPause.");

lib/src/util/io.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,24 @@ String libraryPath(Symbol libraryName, {String packageRoot}) {
195195
if (packageRoot == null) packageRoot = p.absolute('packages');
196196
return p.join(packageRoot, p.fromUri(lib.uri.path));
197197
}
198+
199+
/// Repeatedly finds a probably-unused port on localhost and passes it to
200+
/// [tryPort] until it binds successfully.
201+
///
202+
/// [tryPort] should return a non-`null` value or a Future completing to a
203+
/// non-`null` value once it binds successfully. This value will be returned
204+
/// by [getUnusedPort] in turn.
205+
///
206+
/// This is necessary for ensuring that our port binding isn't flaky for
207+
/// applications that don't print out the bound port.
208+
Future getUnusedPort(tryPort(int port)) {
209+
var value;
210+
return Future.doWhile(() async {
211+
var socket = await RawServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 0);
212+
var port = socket.port;
213+
await socket.close();
214+
215+
value = await tryPort(port);
216+
return value == null;
217+
}).then((_) => value);
218+
}

0 commit comments

Comments
 (0)