-
Notifications
You must be signed in to change notification settings - Fork 13
Add chrome launching code to browser_launcher #4
Changes from 5 commits
a3a21db
97796f4
3ad9e11
6c5b904
bb3fc62
4b69d99
b959032
4220d04
98d79a2
9267c1c
4e91154
d4d8ad2
04bd948
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ dart: | |
- dev | ||
|
||
dart_task: | ||
# - test | ||
- test | ||
- dartanalyzer: --fatal-infos --fatal-warnings . | ||
|
||
matrix: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,240 @@ | ||
// Copyright (c) 2019, 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:path/path.dart' as p; | ||
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; | ||
|
||
const _chromeEnvironment = 'CHROME_EXECUTABLE'; | ||
const _linuxExecutable = 'google-chrome'; | ||
const _macOSExecutable = | ||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; | ||
const _windowsExecutable = r'Google\Chrome\Application\chrome.exe'; | ||
const _windowsPrefixes = ['LOCALAPPDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)']; | ||
|
||
String get _executable { | ||
final windowsPrefixes = | ||
_windowsPrefixes.map((name) => Platform.environment[name]).toList(); | ||
if (Platform.environment.containsKey(_chromeEnvironment)) { | ||
return Platform.environment[_chromeEnvironment]; | ||
} | ||
if (Platform.isLinux) return _linuxExecutable; | ||
if (Platform.isMacOS) return _macOSExecutable; | ||
if (Platform.isWindows) { | ||
return p.join( | ||
windowsPrefixes.firstWhere((prefix) { | ||
kenzieschmoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (prefix == null) return false; | ||
final path = p.join(prefix, _windowsExecutable); | ||
return File(path).existsSync(); | ||
}, orElse: () => '.'), | ||
_windowsExecutable); | ||
} | ||
throw StateError('Unexpected platform type.'); | ||
} | ||
|
||
var _currentCompleter = Completer<Chrome>(); | ||
|
||
/// A class for managing an instance of Chrome. | ||
kenzieschmoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class Chrome { | ||
Chrome._( | ||
this.debugPort, | ||
this.chromeConnection, { | ||
Process process, | ||
}) : _process = process; | ||
|
||
final int debugPort; | ||
final Process _process; | ||
final ChromeConnection chromeConnection; | ||
|
||
/// Connects to an instance of Chrome with an open debug port. | ||
static Future<Chrome> fromExisting(int port) async => | ||
_connect(Chrome._(port, ChromeConnection('localhost', port))); | ||
|
||
static Future<Chrome> get connectedInstance => _currentCompleter.future; | ||
|
||
/// Starts Chrome with the given arguments and a specific port. | ||
/// | ||
/// Each url in [urls] will be loaded in a separate tab. | ||
static Future<Chrome> startWithPort( | ||
List<String> urls, { | ||
String userDataDir, | ||
int remoteDebuggingPort, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we want to be in the business of keeping track of all Chrome configuration. There are a ton of possible flags. We should probably make this more generic and just use an iterable of strings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made these named params per Nate's comment on the proposal doc: "if there are common args that multiple usages need to have knowledge of it would be cool to have named args or something here." @natebosch am I misunderstanding your request? Or do you think there are some that should be named and some that should not? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed it to a list of strings. If we want to make some named in the future, we can do that as needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there are args that multiple clients need to know about I'd be in favor of encoding knowledge here. @grouma what is your concern? Why do you prefer to duplicate this knowledge in other clients? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My concerns don't need to be blocking - as you mention we can always add new named args in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nate and I discussed offline. My concern was having a significant list of unused optional arguments and trying to keep that in sync with the available Chrome flags. Right now we have three potential clients of this package, We agreed that the best approach is to provide optional arguments for those two options only. We will still pass the other options and just document their use. If and when other clients need to configure more options we can easily add another optional argument in a non-breaking way. Ignore my suggestion of providing a mechanism to add arbitrary arguments. We can revisit that if necessary but it won't be likely given the low number of users for this package. Sorry for the churn! |
||
bool disableBackgroundTimerThrottling = false, | ||
bool disableExtensions = false, | ||
bool disablePopupBlocking = false, | ||
bool bwsi = false, | ||
bool noFirstRun = false, | ||
bool noDefaultBrowserCheck = false, | ||
bool disableDefaultApps = false, | ||
bool disableTranslate = false, | ||
}) async { | ||
final port = remoteDebuggingPort == null || remoteDebuggingPort == 0 | ||
? await findUnusedPort() | ||
: remoteDebuggingPort; | ||
|
||
final process = await _startProcess( | ||
urls, | ||
userDataDir: userDataDir, | ||
remoteDebuggingPort: port, | ||
disableBackgroundTimerThrottling: disableBackgroundTimerThrottling, | ||
disableExtensions: disableExtensions, | ||
disablePopupBlocking: disablePopupBlocking, | ||
bwsi: bwsi, | ||
noFirstRun: noFirstRun, | ||
noDefaultBrowserCheck: noDefaultBrowserCheck, | ||
disableDefaultApps: disableDefaultApps, | ||
disableTranslate: disableTranslate, | ||
); | ||
|
||
// Wait until the DevTools are listening before trying to connect. | ||
await process.stderr | ||
kenzieschmoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.transform(utf8.decoder) | ||
.transform(const LineSplitter()) | ||
.firstWhere((line) => line.startsWith('DevTools listening')) | ||
.timeout(Duration(seconds: 60), | ||
onTimeout: () => | ||
throw Exception('Unable to connect to Chrome DevTools.')); | ||
|
||
return _connect(Chrome._( | ||
port, | ||
ChromeConnection('localhost', port), | ||
process: process, | ||
)); | ||
} | ||
|
||
/// Starts Chrome with the given arguments. | ||
/// | ||
/// Each url in [urls] will be loaded in a separate tab. | ||
static Future<void> start( | ||
List<String> urls, { | ||
String userDataDir, | ||
int remoteDebuggingPort, | ||
bool disableBackgroundTimerThrottling = false, | ||
bool disableExtensions = false, | ||
bool disablePopupBlocking = false, | ||
bool bwsi = false, | ||
bool noFirstRun = false, | ||
bool noDefaultBrowserCheck = false, | ||
bool disableDefaultApps = false, | ||
bool disableTranslate = false, | ||
}) async { | ||
await _startProcess( | ||
urls, | ||
userDataDir: userDataDir, | ||
remoteDebuggingPort: remoteDebuggingPort, | ||
disableBackgroundTimerThrottling: disableBackgroundTimerThrottling, | ||
disableExtensions: disableExtensions, | ||
disablePopupBlocking: disablePopupBlocking, | ||
bwsi: bwsi, | ||
noFirstRun: noFirstRun, | ||
noDefaultBrowserCheck: noDefaultBrowserCheck, | ||
disableDefaultApps: disableDefaultApps, | ||
disableTranslate: disableTranslate, | ||
); | ||
} | ||
|
||
static Future<Process> _startProcess( | ||
List<String> urls, { | ||
String userDataDir, | ||
int remoteDebuggingPort, | ||
bool disableBackgroundTimerThrottling = false, | ||
bool disableExtensions = false, | ||
bool disablePopupBlocking = false, | ||
bool bwsi = false, | ||
bool noFirstRun = false, | ||
bool noDefaultBrowserCheck = false, | ||
bool disableDefaultApps = false, | ||
bool disableTranslate = false, | ||
}) async { | ||
final List<String> args = []; | ||
if (userDataDir != null) { | ||
args.add('--user-data-dir=$userDataDir'); | ||
} | ||
if (remoteDebuggingPort != null) { | ||
args.add('--remote-debugging-port=$remoteDebuggingPort'); | ||
} | ||
if (disableBackgroundTimerThrottling) { | ||
args.add('--disable-background-timer-throttling'); | ||
} | ||
if (disableExtensions) { | ||
args.add('--disable-extensions'); | ||
} | ||
if (disablePopupBlocking) { | ||
args.add('--disable-popup-blocking'); | ||
} | ||
if (bwsi) { | ||
args.add('--bwsi'); | ||
} | ||
if (noFirstRun) { | ||
args.add('--no-first-run'); | ||
} | ||
if (noDefaultBrowserCheck) { | ||
args.add('--no-default-browser-check'); | ||
} | ||
if (disableDefaultApps) { | ||
args.add('--disable-default-apps'); | ||
} | ||
if (disableTranslate) { | ||
args.add('--disable-translate'); | ||
} | ||
args..addAll(urls); | ||
|
||
final process = await Process.start(_executable, args); | ||
|
||
return process; | ||
} | ||
|
||
static Future<Chrome> _connect(Chrome chrome) async { | ||
if (_currentCompleter.isCompleted) { | ||
throw ChromeError('Only one instance of chrome can be started.'); | ||
natebosch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
// The connection is lazy. Try a simple call to make sure the provided | ||
// connection is valid. | ||
try { | ||
await chrome.chromeConnection.getTabs(); | ||
} catch (e) { | ||
await chrome.close(); | ||
throw ChromeError( | ||
'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e'); | ||
} | ||
_currentCompleter.complete(chrome); | ||
return chrome; | ||
} | ||
|
||
Future<void> close() async { | ||
if (_currentCompleter.isCompleted) _currentCompleter = Completer<Chrome>(); | ||
chromeConnection.close(); | ||
_process?.kill(); | ||
await _process?.exitCode; | ||
} | ||
} | ||
|
||
class ChromeError extends Error { | ||
final String details; | ||
ChromeError(this.details); | ||
|
||
@override | ||
String toString() => 'ChromeError: $details'; | ||
} | ||
|
||
/// Returns a port that is probably, but not definitely, not in use. | ||
/// | ||
/// This has a built-in race condition: another process may bind this port at | ||
/// any time after this call has returned. | ||
Future<int> findUnusedPort() async { | ||
int port; | ||
ServerSocket socket; | ||
try { | ||
socket = | ||
await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, v6Only: true); | ||
} on SocketException { | ||
socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); | ||
} | ||
port = socket.port; | ||
await socket.close(); | ||
return port; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// Copyright (c) 2019, 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:io'; | ||
|
||
import 'package:browser_launcher/src/chrome.dart'; | ||
import 'package:path/path.dart' as p; | ||
import 'package:test/test.dart'; | ||
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; | ||
|
||
void main() { | ||
Chrome chrome; | ||
|
||
Future<void> launchChromeWithDebugPort({int port}) async { | ||
final dataDir = Directory(p.joinAll( | ||
[Directory.current.path, '.dart_tool', 'webdev', 'chrome_profile'])) | ||
..createSync(recursive: true); | ||
chrome = await Chrome.startWithPort( | ||
[_googleUrl], | ||
userDataDir: dataDir.path, | ||
remoteDebuggingPort: port, | ||
disableBackgroundTimerThrottling: true, | ||
disableExtensions: true, | ||
disablePopupBlocking: true, | ||
bwsi: true, | ||
noFirstRun: true, | ||
noDefaultBrowserCheck: true, | ||
disableDefaultApps: true, | ||
disableTranslate: true, | ||
); | ||
} | ||
|
||
Future<void> launchChrome() async { | ||
await Chrome.start([_googleUrl]); | ||
} | ||
|
||
tearDown(() async { | ||
await chrome?.close(); | ||
chrome = null; | ||
}); | ||
|
||
test('can launch chrome', () async { | ||
await launchChrome(); | ||
expect(chrome, isNull); | ||
}, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); | ||
kenzieschmoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
test('can launch chrome with debug port', () async { | ||
await launchChromeWithDebugPort(); | ||
expect(chrome, isNotNull); | ||
}, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); | ||
|
||
test('debugger is working', () async { | ||
await launchChromeWithDebugPort(); | ||
var tabs = await chrome.chromeConnection.getTabs(); | ||
expect( | ||
tabs, | ||
contains(const TypeMatcher<ChromeTab>() | ||
.having((t) => t.url, 'url', _googleUrl))); | ||
}, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); | ||
|
||
test('uses open debug port if provided port is 0', () async { | ||
await launchChromeWithDebugPort(port: 0); | ||
expect(chrome.debugPort, isNot(equals(0))); | ||
}, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); | ||
} | ||
|
||
const _googleUrl = 'https://www.google.com/'; |
Uh oh!
There was an error while loading. Please reload this page.