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.content_shell; | 5 library test.runner.browser.content_shell; |
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 '../../util/io.dart'; |
10 import '../../utils.dart'; | 12 import '../../utils.dart'; |
11 import '../application_exception.dart'; | 13 import '../application_exception.dart'; |
12 import 'browser.dart'; | 14 import 'browser.dart'; |
13 | 15 |
14 final _observatoryRegExp = new RegExp(r"^Observatory listening on ([^ ]+)"); | 16 final _observatoryRegExp = new RegExp(r"^Observatory listening on ([^ ]+)"); |
15 | 17 |
16 /// A class for running an instance of the Dartium content shell. | 18 /// A class for running an instance of the Dartium content shell. |
17 /// | 19 /// |
18 /// Most of the communication with the browser is expected to happen via HTTP, | 20 /// Most of the communication with the browser is expected to happen via HTTP, |
19 /// so this exposes a bare-bones API. The browser starts as soon as the class is | 21 /// so this exposes a bare-bones API. The browser starts as soon as the class is |
20 /// constructed, and is killed when [close] is called. | 22 /// constructed, and is killed when [close] is called. |
21 /// | 23 /// |
22 /// Any errors starting or running the process are reported through [onExit]. | 24 /// Any errors starting or running the process are reported through [onExit]. |
23 class ContentShell extends Browser { | 25 class ContentShell extends Browser { |
24 final name = "Content Shell"; | 26 final name = "Content Shell"; |
25 | 27 |
26 final Future<Uri> observatoryUrl; | 28 final Future<Uri> observatoryUrl; |
27 | 29 |
| 30 final Future<Uri> remoteDebuggerUrl; |
| 31 |
28 factory ContentShell(url, {String executable, bool debug: false}) { | 32 factory ContentShell(url, {String executable, bool debug: false}) { |
29 var completer = new Completer.sync(); | 33 var observatoryCompleter = new Completer.sync(); |
30 return new ContentShell._(() async { | 34 var remoteDebuggerCompleter = new Completer.sync(); |
| 35 return new ContentShell._(() { |
31 if (executable == null) executable = _defaultExecutable(); | 36 if (executable == null) executable = _defaultExecutable(); |
32 | 37 |
33 var process = await Process.start( | 38 tryPort([port]) async { |
34 executable, ["--dump-render-tree", url.toString()], | 39 var args = ["--dump-render-tree", url.toString()]; |
35 environment: {"DART_FLAGS": "--checked"}); | 40 if (port != null) args.add("--remote-debugging-port=$port"); |
36 | 41 |
37 if (debug) { | 42 var process = await Process.start(executable, args, |
38 completer.complete(lineSplitter.bind(process.stdout).map((line) { | 43 environment: {"DART_FLAGS": "--checked"}); |
39 var match = _observatoryRegExp.firstMatch(line); | 44 |
40 if (match == null) return null; | 45 if (debug) { |
41 return Uri.parse(match[1]); | 46 observatoryCompleter.complete(lineSplitter.bind(process.stdout) |
42 }).where((uri) => uri != null).first); | 47 .map((line) { |
43 } else { | 48 var match = _observatoryRegExp.firstMatch(line); |
44 completer.complete(null); | 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; |
45 } | 93 } |
46 | 94 |
47 lineSplitter.bind(process.stderr).listen((line) { | 95 if (!debug) return tryPort(); |
48 if (line != "[dartToStderr]: Dartium build has expired") return; | 96 return getUnusedPort(tryPort); |
49 | 97 }, observatoryCompleter.future, remoteDebuggerCompleter.future); |
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); | |
62 } | 98 } |
63 | 99 |
64 ContentShell._(Future<Process> startBrowser(), this.observatoryUrl) | 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 } |
| 117 } |
| 118 |
| 119 ContentShell._(Future<Process> startBrowser(), this.observatoryUrl, |
| 120 this.remoteDebuggerUrl) |
65 : super(startBrowser); | 121 : super(startBrowser); |
66 | 122 |
67 /// Return the default executable for the current operating system. | 123 /// Return the default executable for the current operating system. |
68 static String _defaultExecutable() => | 124 static String _defaultExecutable() => |
69 Platform.isWindows ? "content_shell.exe" : "content_shell"; | 125 Platform.isWindows ? "content_shell.exe" : "content_shell"; |
70 } | 126 } |
OLD | NEW |