This repository was archived by the owner on Oct 28, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
Add chrome launching code to browser_launcher #4
Merged
kenzieschmoll
merged 13 commits into
dart-archive:master
from
kenzieschmoll:launchChrome
Apr 26, 2019
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
a3a21db
Add chrome launching code to browser_launcher
kenzieschmoll 97796f4
remove unnecessary defaults
kenzieschmoll 3ad9e11
uncomment test for travis
kenzieschmoll 6c5b904
Revert "remove unnecessary defaults"
kenzieschmoll bb3fc62
kevmoo review comments.
kenzieschmoll 4b69d99
review comments
kenzieschmoll b959032
Use a list for args; implement changes from dart-lang/webdev#341
kenzieschmoll 4220d04
remove unused var
kenzieschmoll 98d79a2
define windows prefixes where they are used.
kenzieschmoll 9267c1c
use temp dataDir
kenzieschmoll 4e91154
add headless optional param to start method
kenzieschmoll d4d8ad2
remove completer code; add default debug flags
kenzieschmoll 04bd948
implement latest from dart-lang/webdev/pull/342
kenzieschmoll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,186 @@ | ||
// 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'; | ||
|
||
String get _executable { | ||
if (Platform.environment.containsKey(_chromeEnvironment)) { | ||
return Platform.environment[_chromeEnvironment]; | ||
} | ||
if (Platform.isLinux) return _linuxExecutable; | ||
if (Platform.isMacOS) return _macOSExecutable; | ||
if (Platform.isWindows) { | ||
final windowsPrefixes = [ | ||
Platform.environment['LOCALAPPDATA'], | ||
Platform.environment['PROGRAMFILES'], | ||
Platform.environment['PROGRAMFILES(X86)'] | ||
]; | ||
return p.join( | ||
windowsPrefixes.firstWhere((prefix) { | ||
if (prefix == null) return false; | ||
final path = p.join(prefix, _windowsExecutable); | ||
return File(path).existsSync(); | ||
}, orElse: () => '.'), | ||
_windowsExecutable, | ||
); | ||
} | ||
throw StateError('Unexpected platform type.'); | ||
} | ||
|
||
/// Manager for an instance of Chrome. | ||
class Chrome { | ||
Chrome._( | ||
this.debugPort, | ||
this.chromeConnection, { | ||
Process process, | ||
Directory dataDir, | ||
}) : _process = process, | ||
_dataDir = dataDir; | ||
|
||
final int debugPort; | ||
final ChromeConnection chromeConnection; | ||
final Process _process; | ||
final Directory _dataDir; | ||
|
||
/// Connects to an instance of Chrome with an open debug port. | ||
static Future<Chrome> fromExisting(int port) async => | ||
_connect(Chrome._(port, ChromeConnection('localhost', port))); | ||
|
||
/// Starts Chrome with the given arguments and a specific port. | ||
/// | ||
/// Only one instance of Chrome can run at a time. Each url in [urls] will be | ||
/// loaded in a separate tab. | ||
static Future<Chrome> startWithDebugPort( | ||
List<String> urls, { | ||
int debugPort, | ||
bool headless = false, | ||
}) async { | ||
final dataDir = Directory.systemTemp.createTempSync(); | ||
final port = debugPort == null || debugPort == 0 | ||
? await findUnusedPort() | ||
: debugPort; | ||
final args = [ | ||
// Using a tmp directory ensures that a new instance of chrome launches | ||
// allowing for the remote debug port to be enabled. | ||
'--user-data-dir=${dataDir.path}', | ||
'--remote-debugging-port=$port', | ||
// When the DevTools has focus we don't want to slow down the application. | ||
'--disable-background-timer-throttling', | ||
// Since we are using a temp profile, disable features that slow the | ||
// Chrome launch. | ||
'--disable-extensions', | ||
'--disable-popup-blocking', | ||
'--bwsi', | ||
'--no-first-run', | ||
'--no-default-browser-check', | ||
'--disable-default-apps', | ||
'--disable-translate', | ||
]; | ||
if (headless) { | ||
args.add('--headless'); | ||
} | ||
|
||
final process = await _startProcess(urls, args: args); | ||
|
||
// Wait until the DevTools are listening before trying to connect. | ||
await process.stderr | ||
.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, | ||
dataDir: dataDir, | ||
)); | ||
} | ||
|
||
/// Starts Chrome with the given arguments. | ||
/// | ||
/// Each url in [urls] will be loaded in a separate tab. | ||
static Future<void> start( | ||
List<String> urls, { | ||
List<String> args = const [], | ||
}) async { | ||
await _startProcess(urls, args: args); | ||
} | ||
|
||
static Future<Process> _startProcess( | ||
List<String> urls, { | ||
List<String> args = const [], | ||
}) async { | ||
final processArgs = args.toList()..addAll(urls); | ||
return await Process.start(_executable, processArgs); | ||
} | ||
|
||
static Future<Chrome> _connect(Chrome chrome) async { | ||
// 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'); | ||
} | ||
return chrome; | ||
} | ||
|
||
Future<void> close() async { | ||
chromeConnection.close(); | ||
_process?.kill(ProcessSignal.sigkill); | ||
await _process?.exitCode; | ||
try { | ||
// Chrome starts another process as soon as it dies that modifies the | ||
// profile information. Give it some time before attempting to delete | ||
// the directory. | ||
await Future.delayed(Duration(milliseconds: 500)); | ||
await _dataDir?.delete(recursive: true); | ||
} catch (_) { | ||
// Silently fail if we can't clean up the profile information. | ||
// It is a system tmp directory so it should get cleaned up eventually. | ||
} | ||
} | ||
} | ||
|
||
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; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// 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. | ||
|
||
@OnPlatform({'windows': Skip('appveyor is not setup to install Chrome')}) | ||
import 'dart:async'; | ||
|
||
import 'package:browser_launcher/src/chrome.dart'; | ||
import 'package:test/test.dart'; | ||
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; | ||
|
||
void main() { | ||
Chrome chrome; | ||
|
||
Future<void> launchChromeWithDebugPort({int port}) async { | ||
chrome = await Chrome.startWithDebugPort([_googleUrl], debugPort: port); | ||
} | ||
|
||
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); | ||
}); | ||
|
||
test('can launch chrome with debug port', () async { | ||
await launchChromeWithDebugPort(); | ||
expect(chrome, isNotNull); | ||
}); | ||
|
||
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))); | ||
}); | ||
|
||
test('uses open debug port if provided port is 0', () async { | ||
await launchChromeWithDebugPort(port: 0); | ||
expect(chrome.debugPort, isNot(equals(0))); | ||
}); | ||
|
||
test('can provide a specific debug port', () async { | ||
var port = await findUnusedPort(); | ||
await launchChromeWithDebugPort(port: port); | ||
expect(chrome.debugPort, port); | ||
}); | ||
} | ||
|
||
const _googleUrl = 'https://www.google.com/'; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.