Skip to content
This repository was archived by the owner on Oct 28, 2024. It is now read-only.

Commit 5c179e5

Browse files
Add chrome launching code to browser_launcher (#4)
* Add chrome launching code to browser_launcher
1 parent 17c6891 commit 5c179e5

File tree

4 files changed

+247
-2
lines changed

4 files changed

+247
-2
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ dart:
55
- dev
66

77
dart_task:
8-
# - test
8+
- test
99
- dartanalyzer: --fatal-infos --fatal-warnings .
1010

1111
matrix:

lib/src/chrome.dart

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,186 @@
11
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:io';
8+
9+
import 'package:path/path.dart' as p;
10+
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
11+
12+
const _chromeEnvironment = 'CHROME_EXECUTABLE';
13+
const _linuxExecutable = 'google-chrome';
14+
const _macOSExecutable =
15+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
16+
const _windowsExecutable = r'Google\Chrome\Application\chrome.exe';
17+
18+
String get _executable {
19+
if (Platform.environment.containsKey(_chromeEnvironment)) {
20+
return Platform.environment[_chromeEnvironment];
21+
}
22+
if (Platform.isLinux) return _linuxExecutable;
23+
if (Platform.isMacOS) return _macOSExecutable;
24+
if (Platform.isWindows) {
25+
final windowsPrefixes = [
26+
Platform.environment['LOCALAPPDATA'],
27+
Platform.environment['PROGRAMFILES'],
28+
Platform.environment['PROGRAMFILES(X86)']
29+
];
30+
return p.join(
31+
windowsPrefixes.firstWhere((prefix) {
32+
if (prefix == null) return false;
33+
final path = p.join(prefix, _windowsExecutable);
34+
return File(path).existsSync();
35+
}, orElse: () => '.'),
36+
_windowsExecutable,
37+
);
38+
}
39+
throw StateError('Unexpected platform type.');
40+
}
41+
42+
/// Manager for an instance of Chrome.
43+
class Chrome {
44+
Chrome._(
45+
this.debugPort,
46+
this.chromeConnection, {
47+
Process process,
48+
Directory dataDir,
49+
}) : _process = process,
50+
_dataDir = dataDir;
51+
52+
final int debugPort;
53+
final ChromeConnection chromeConnection;
54+
final Process _process;
55+
final Directory _dataDir;
56+
57+
/// Connects to an instance of Chrome with an open debug port.
58+
static Future<Chrome> fromExisting(int port) async =>
59+
_connect(Chrome._(port, ChromeConnection('localhost', port)));
60+
61+
/// Starts Chrome with the given arguments and a specific port.
62+
///
63+
/// Only one instance of Chrome can run at a time. Each url in [urls] will be
64+
/// loaded in a separate tab.
65+
static Future<Chrome> startWithDebugPort(
66+
List<String> urls, {
67+
int debugPort,
68+
bool headless = false,
69+
}) async {
70+
final dataDir = Directory.systemTemp.createTempSync();
71+
final port = debugPort == null || debugPort == 0
72+
? await findUnusedPort()
73+
: debugPort;
74+
final args = [
75+
// Using a tmp directory ensures that a new instance of chrome launches
76+
// allowing for the remote debug port to be enabled.
77+
'--user-data-dir=${dataDir.path}',
78+
'--remote-debugging-port=$port',
79+
// When the DevTools has focus we don't want to slow down the application.
80+
'--disable-background-timer-throttling',
81+
// Since we are using a temp profile, disable features that slow the
82+
// Chrome launch.
83+
'--disable-extensions',
84+
'--disable-popup-blocking',
85+
'--bwsi',
86+
'--no-first-run',
87+
'--no-default-browser-check',
88+
'--disable-default-apps',
89+
'--disable-translate',
90+
];
91+
if (headless) {
92+
args.add('--headless');
93+
}
94+
95+
final process = await _startProcess(urls, args: args);
96+
97+
// Wait until the DevTools are listening before trying to connect.
98+
await process.stderr
99+
.transform(utf8.decoder)
100+
.transform(const LineSplitter())
101+
.firstWhere((line) => line.startsWith('DevTools listening'))
102+
.timeout(Duration(seconds: 60),
103+
onTimeout: () =>
104+
throw Exception('Unable to connect to Chrome DevTools.'));
105+
106+
return _connect(Chrome._(
107+
port,
108+
ChromeConnection('localhost', port),
109+
process: process,
110+
dataDir: dataDir,
111+
));
112+
}
113+
114+
/// Starts Chrome with the given arguments.
115+
///
116+
/// Each url in [urls] will be loaded in a separate tab.
117+
static Future<void> start(
118+
List<String> urls, {
119+
List<String> args = const [],
120+
}) async {
121+
await _startProcess(urls, args: args);
122+
}
123+
124+
static Future<Process> _startProcess(
125+
List<String> urls, {
126+
List<String> args = const [],
127+
}) async {
128+
final processArgs = args.toList()..addAll(urls);
129+
return await Process.start(_executable, processArgs);
130+
}
131+
132+
static Future<Chrome> _connect(Chrome chrome) async {
133+
// The connection is lazy. Try a simple call to make sure the provided
134+
// connection is valid.
135+
try {
136+
await chrome.chromeConnection.getTabs();
137+
} catch (e) {
138+
await chrome.close();
139+
throw ChromeError(
140+
'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e');
141+
}
142+
return chrome;
143+
}
144+
145+
Future<void> close() async {
146+
chromeConnection.close();
147+
_process?.kill(ProcessSignal.sigkill);
148+
await _process?.exitCode;
149+
try {
150+
// Chrome starts another process as soon as it dies that modifies the
151+
// profile information. Give it some time before attempting to delete
152+
// the directory.
153+
await Future.delayed(Duration(milliseconds: 500));
154+
await _dataDir?.delete(recursive: true);
155+
} catch (_) {
156+
// Silently fail if we can't clean up the profile information.
157+
// It is a system tmp directory so it should get cleaned up eventually.
158+
}
159+
}
160+
}
161+
162+
class ChromeError extends Error {
163+
final String details;
164+
ChromeError(this.details);
165+
166+
@override
167+
String toString() => 'ChromeError: $details';
168+
}
169+
170+
/// Returns a port that is probably, but not definitely, not in use.
171+
///
172+
/// This has a built-in race condition: another process may bind this port at
173+
/// any time after this call has returned.
174+
Future<int> findUnusedPort() async {
175+
int port;
176+
ServerSocket socket;
177+
try {
178+
socket =
179+
await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, v6Only: true);
180+
} on SocketException {
181+
socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
182+
}
183+
port = socket.port;
184+
await socket.close();
185+
return port;
186+
}

pubspec.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ environment:
1010
sdk: '>=2.2.0 <3.0.0'
1111

1212
dependencies:
13+
path: ^1.6.2
14+
webkit_inspection_protocol: ^0.4.0
1315

14-
dev_dependnecies:
16+
dev_dependencies:
1517
pedantic: ^1.5.0
18+
test: ^1.0.0

test/chrome_test.dart

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2019, 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+
@OnPlatform({'windows': Skip('appveyor is not setup to install Chrome')})
6+
import 'dart:async';
7+
8+
import 'package:browser_launcher/src/chrome.dart';
9+
import 'package:test/test.dart';
10+
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
11+
12+
void main() {
13+
Chrome chrome;
14+
15+
Future<void> launchChromeWithDebugPort({int port}) async {
16+
chrome = await Chrome.startWithDebugPort([_googleUrl], debugPort: port);
17+
}
18+
19+
Future<void> launchChrome() async {
20+
await Chrome.start([_googleUrl]);
21+
}
22+
23+
tearDown(() async {
24+
await chrome?.close();
25+
chrome = null;
26+
});
27+
28+
test('can launch chrome', () async {
29+
await launchChrome();
30+
expect(chrome, isNull);
31+
});
32+
33+
test('can launch chrome with debug port', () async {
34+
await launchChromeWithDebugPort();
35+
expect(chrome, isNotNull);
36+
});
37+
38+
test('debugger is working', () async {
39+
await launchChromeWithDebugPort();
40+
var tabs = await chrome.chromeConnection.getTabs();
41+
expect(
42+
tabs,
43+
contains(const TypeMatcher<ChromeTab>()
44+
.having((t) => t.url, 'url', _googleUrl)));
45+
});
46+
47+
test('uses open debug port if provided port is 0', () async {
48+
await launchChromeWithDebugPort(port: 0);
49+
expect(chrome.debugPort, isNot(equals(0)));
50+
});
51+
52+
test('can provide a specific debug port', () async {
53+
var port = await findUnusedPort();
54+
await launchChromeWithDebugPort(port: port);
55+
expect(chrome.debugPort, port);
56+
});
57+
}
58+
59+
const _googleUrl = 'https://www.google.com/';

0 commit comments

Comments
 (0)