From a3a21db385e1da046d132190ebb2faec61b200b6 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 12:52:08 -0700 Subject: [PATCH 01/13] Add chrome launching code to browser_launcher --- lib/src/chrome.dart | 241 ++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 5 +- test/chrome_test.dart | 69 ++++++++++++ 3 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 test/chrome_test.dart diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 79bf402..46d4a80 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -1,3 +1,244 @@ // 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'; +var _windowsPrefixes = [ + Platform.environment['LOCALAPPDATA'], + Platform.environment['PROGRAMFILES'], + Platform.environment['PROGRAMFILES(X86)'] +]; + +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) { + 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.'); +} + +var _currentCompleter = Completer(); + +/// A class for managing an instance of Chrome. +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 fromExisting(int port) async => + _connect(Chrome._(port, ChromeConnection('localhost', port))); + + static Future 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 startWithPort( + List 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 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 + .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 start( + List 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 _startProcess( + List 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 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 _connect(Chrome chrome) async { + if (_currentCompleter.isCompleted) { + throw ChromeError('Only one instance of chrome can be started.'); + } + // 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 close() async { + if (_currentCompleter.isCompleted) _currentCompleter = Completer(); + chromeConnection.close(); + _process?.kill(); + await _process?.exitCode; + } +} + +class ChromeError extends Error { + final String details; + ChromeError(this.details); + + @override + String toString() { + return '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 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; +} diff --git a/pubspec.yaml b/pubspec.yaml index ad8b8bb..f5cbaaf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,9 @@ environment: sdk: '>=2.2.0 <3.0.0' dependencies: + path: ^1.6.2 + webkit_inspection_protocol: ^0.4.0 -dev_dependnecies: +dev_dependencies: pedantic: ^1.5.0 + test: ^1.0.0 diff --git a/test/chrome_test.dart b/test/chrome_test.dart new file mode 100644 index 0000000..6dbc172 --- /dev/null +++ b/test/chrome_test.dart @@ -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 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 launchChrome() async { + await Chrome.start([_googleUrl]); + } + + tearDown(() async { + await chrome?.close(); + chrome = null; + }); + + test('can launch chrome', () async { + await launchChrome(); + expect(chrome, isNull); + }, skip: Platform.isWindows); + + test('can launch chrome with debug port', () async { + await launchChromeWithDebugPort(); + expect(chrome, isNotNull); + }, skip: Platform.isWindows); + + test('debugger is working', () async { + await launchChromeWithDebugPort(); + var tabs = await chrome.chromeConnection.getTabs(); + expect( + tabs, + contains(const TypeMatcher() + .having((t) => t.url, 'url', _googleUrl))); + }, skip: Platform.isWindows); + + test('uses open debug port if provided port is 0', () async { + await launchChromeWithDebugPort(port: 0); + expect(chrome.debugPort, isNot(equals(0))); + }, skip: Platform.isWindows); +} + +const _googleUrl = 'http://www.google.com/'; From 97796f47bc62bc93406c702fadf725ea3b45f467 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 12:57:04 -0700 Subject: [PATCH 02/13] remove unnecessary defaults --- lib/src/chrome.dart | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 46d4a80..da2da6c 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -65,14 +65,14 @@ class Chrome { List 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, + bool disableBackgroundTimerThrottling, + bool disableExtensions, + bool disablePopupBlocking, + bool bwsi, + bool noFirstRun, + bool noDefaultBrowserCheck, + bool disableDefaultApps, + bool disableTranslate, }) async { final port = remoteDebuggingPort == null || remoteDebuggingPort == 0 ? await findUnusedPort() @@ -115,14 +115,14 @@ class Chrome { List 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, + bool disableBackgroundTimerThrottling, + bool disableExtensions, + bool disablePopupBlocking, + bool bwsi, + bool noFirstRun, + bool noDefaultBrowserCheck, + bool disableDefaultApps, + bool disableTranslate, }) async { await _startProcess( urls, From 3ad9e11043f76bf069736ef8e07fa0657480c1ab Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 13:17:49 -0700 Subject: [PATCH 03/13] uncomment test for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c936b6f..b4e1991 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ dart: - dev dart_task: - # - test + - test - dartanalyzer: --fatal-infos --fatal-warnings . matrix: From 6c5b904ee6a34019ede0046d21f68531181d0940 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 13:53:10 -0700 Subject: [PATCH 04/13] Revert "remove unnecessary defaults" This reverts commit 97796f47bc62bc93406c702fadf725ea3b45f467. --- lib/src/chrome.dart | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index da2da6c..46d4a80 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -65,14 +65,14 @@ class Chrome { List urls, { String userDataDir, int remoteDebuggingPort, - bool disableBackgroundTimerThrottling, - bool disableExtensions, - bool disablePopupBlocking, - bool bwsi, - bool noFirstRun, - bool noDefaultBrowserCheck, - bool disableDefaultApps, - bool disableTranslate, + 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() @@ -115,14 +115,14 @@ class Chrome { List urls, { String userDataDir, int remoteDebuggingPort, - bool disableBackgroundTimerThrottling, - bool disableExtensions, - bool disablePopupBlocking, - bool bwsi, - bool noFirstRun, - bool noDefaultBrowserCheck, - bool disableDefaultApps, - bool disableTranslate, + 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, From bb3fc62baa9ddf7ab653722b8b046c73db54f285 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 14:17:01 -0700 Subject: [PATCH 05/13] kevmoo review comments. --- lib/src/chrome.dart | 14 +++++--------- test/chrome_test.dart | 10 +++++----- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 46d4a80..a1fcf2e 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -14,13 +14,11 @@ const _linuxExecutable = 'google-chrome'; const _macOSExecutable = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; const _windowsExecutable = r'Google\Chrome\Application\chrome.exe'; -var _windowsPrefixes = [ - Platform.environment['LOCALAPPDATA'], - Platform.environment['PROGRAMFILES'], - Platform.environment['PROGRAMFILES(X86)'] -]; +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]; } @@ -28,7 +26,7 @@ String get _executable { if (Platform.isMacOS) return _macOSExecutable; if (Platform.isWindows) { return p.join( - _windowsPrefixes.firstWhere((prefix) { + windowsPrefixes.firstWhere((prefix) { if (prefix == null) return false; final path = p.join(prefix, _windowsExecutable); return File(path).existsSync(); @@ -220,9 +218,7 @@ class ChromeError extends Error { ChromeError(this.details); @override - String toString() { - return 'ChromeError: $details'; - } + String toString() => 'ChromeError: $details'; } /// Returns a port that is probably, but not definitely, not in use. diff --git a/test/chrome_test.dart b/test/chrome_test.dart index 6dbc172..abd2047 100644 --- a/test/chrome_test.dart +++ b/test/chrome_test.dart @@ -44,12 +44,12 @@ void main() { test('can launch chrome', () async { await launchChrome(); expect(chrome, isNull); - }, skip: Platform.isWindows); + }, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); test('can launch chrome with debug port', () async { await launchChromeWithDebugPort(); expect(chrome, isNotNull); - }, skip: Platform.isWindows); + }, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); test('debugger is working', () async { await launchChromeWithDebugPort(); @@ -58,12 +58,12 @@ void main() { tabs, contains(const TypeMatcher() .having((t) => t.url, 'url', _googleUrl))); - }, skip: Platform.isWindows); + }, 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))); - }, skip: Platform.isWindows); + }, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); } -const _googleUrl = 'http://www.google.com/'; +const _googleUrl = 'https://www.google.com/'; From 4b69d992e185b56e4326d77cd8b111a00dfde3ed Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 14:57:50 -0700 Subject: [PATCH 06/13] review comments --- lib/src/chrome.dart | 31 ++++++++++++++++++------------- test/chrome_test.dart | 11 ++++++----- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index a1fcf2e..8a71d2d 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -14,31 +14,32 @@ 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)']; +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) { + final windowsPrefixes = + _windowsPrefixes.map((name) => Platform.environment[name]).toList(); return p.join( - windowsPrefixes.firstWhere((prefix) { - if (prefix == null) return false; - final path = p.join(prefix, _windowsExecutable); - return File(path).existsSync(); - }, orElse: () => '.'), - _windowsExecutable); + 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.'); } var _currentCompleter = Completer(); -/// A class for managing an instance of Chrome. +/// Manager for an instance of Chrome. class Chrome { Chrome._( this.debugPort, @@ -58,8 +59,9 @@ class Chrome { /// Starts Chrome with the given arguments and a specific port. /// - /// Each url in [urls] will be loaded in a separate tab. - static Future startWithPort( + /// Only one instance of Chrome can run at a time. Each url in [urls] will be + /// loaded in a separate tab. + static Future startWithDebugPort( List urls, { String userDataDir, int remoteDebuggingPort, @@ -91,6 +93,8 @@ class Chrome { ); // Wait until the DevTools are listening before trying to connect. + // TODO(kenzie): integrate changes from + // https://github.com/dart-lang/webdev/pull/341 await process.stderr .transform(utf8.decoder) .transform(const LineSplitter()) @@ -108,7 +112,8 @@ class Chrome { /// Starts Chrome with the given arguments. /// - /// Each url in [urls] will be loaded in a separate tab. + /// Only one instance of Chrome can run at a time. Each url in [urls] will be + /// loaded in a separate tab. static Future start( List urls, { String userDataDir, diff --git a/test/chrome_test.dart b/test/chrome_test.dart index abd2047..2ecd65f 100644 --- a/test/chrome_test.dart +++ b/test/chrome_test.dart @@ -2,6 +2,7 @@ // 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 'dart:io'; @@ -17,7 +18,7 @@ void main() { final dataDir = Directory(p.joinAll( [Directory.current.path, '.dart_tool', 'webdev', 'chrome_profile'])) ..createSync(recursive: true); - chrome = await Chrome.startWithPort( + chrome = await Chrome.startWithDebugPort( [_googleUrl], userDataDir: dataDir.path, remoteDebuggingPort: port, @@ -44,12 +45,12 @@ void main() { test('can launch chrome', () async { await launchChrome(); expect(chrome, isNull); - }, onPlatform: {'windows': Skip('appveyor is not setup to install Chrome')}); + }); 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(); @@ -58,12 +59,12 @@ void main() { tabs, contains(const TypeMatcher() .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/'; From b95903209855529ffef59140a43388b061e75ab6 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 15:43:34 -0700 Subject: [PATCH 07/13] Use a list for args; implement changes from dart-lang/webdev#341 --- lib/src/chrome.dart | 169 ++++++++++++------------------------------ test/chrome_test.dart | 52 +++++++++---- 2 files changed, 86 insertions(+), 135 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 8a71d2d..bebbf2f 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:async/async.dart'; import 'package:path/path.dart' as p; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; @@ -63,46 +64,53 @@ class Chrome { /// loaded in a separate tab. static Future startWithDebugPort( List 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, + int debugPort, + List chromeArgs = const [], }) 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, - ); + final dataDir = Directory(p.joinAll( + [Directory.current.path, '.dart_tool', 'webdev', 'chrome_profile'])); + final activePortFile = File(p.join(dataDir.path, 'DevToolsActivePort')); + // If we are reusing the Chrome profile we'll need to be able to read the + // DevToolsActivePort to connect the debugger. When a non-zero debugging + // port is provided Chrome will not write the DevToolsActivePort file and + // therefore we can not reuse the profile. + if (dataDir.existsSync() && !activePortFile.existsSync()) { + dataDir.deleteSync(recursive: true); + } + dataDir.createSync(recursive: true); + + int port = debugPort == null ? 0 : debugPort; + final args = chromeArgs + ..addAll([ + // 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', + ]); + + final process = await _startProcess(urls, args: args); + final output = StreamGroup.merge([ + process.stderr.transform(utf8.decoder).transform(const LineSplitter()), + process.stdout.transform(utf8.decoder).transform(const LineSplitter()) + ]); // Wait until the DevTools are listening before trying to connect. - // TODO(kenzie): integrate changes from - // https://github.com/dart-lang/webdev/pull/341 - await process.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .firstWhere((line) => line.startsWith('DevTools listening')) + await output + .firstWhere((line) => + line.startsWith('DevTools listening') || + line.startsWith('Opening in existing')) .timeout(Duration(seconds: 60), onTimeout: () => throw Exception('Unable to connect to Chrome DevTools.')); + // The DevToolsActivePort file is only written if 0 is provided. + if (port == 0) { + if (!activePortFile.existsSync()) { + throw ChromeError("Can't read DevToolsActivePort file."); + } + port = int.parse(activePortFile.readAsLinesSync().first); + } + return _connect(Chrome._( port, ChromeConnection('localhost', port), @@ -112,85 +120,20 @@ class Chrome { /// Starts Chrome with the given arguments. /// - /// Only one instance of Chrome can run at a time. Each url in [urls] will be - /// loaded in a separate tab. + /// Each url in [urls] will be loaded in a separate tab. static Future start( List 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, + List chromeArgs = const [], }) async { - await _startProcess( - urls, - userDataDir: userDataDir, - remoteDebuggingPort: remoteDebuggingPort, - disableBackgroundTimerThrottling: disableBackgroundTimerThrottling, - disableExtensions: disableExtensions, - disablePopupBlocking: disablePopupBlocking, - bwsi: bwsi, - noFirstRun: noFirstRun, - noDefaultBrowserCheck: noDefaultBrowserCheck, - disableDefaultApps: disableDefaultApps, - disableTranslate: disableTranslate, - ); + await _startProcess(urls, args: chromeArgs); } static Future _startProcess( List 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, + List args = const [], }) async { - final List 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; + final processArgs = args.toList()..addAll(urls); + return await Process.start(_executable, processArgs); } static Future _connect(Chrome chrome) async { @@ -225,21 +168,3 @@ class ChromeError extends Error { @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 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; -} diff --git a/test/chrome_test.dart b/test/chrome_test.dart index 2ecd65f..70f2cf1 100644 --- a/test/chrome_test.dart +++ b/test/chrome_test.dart @@ -18,19 +18,21 @@ void main() { final dataDir = Directory(p.joinAll( [Directory.current.path, '.dart_tool', 'webdev', 'chrome_profile'])) ..createSync(recursive: true); - chrome = await Chrome.startWithDebugPort( - [_googleUrl], - userDataDir: dataDir.path, - remoteDebuggingPort: port, - disableBackgroundTimerThrottling: true, - disableExtensions: true, - disablePopupBlocking: true, - bwsi: true, - noFirstRun: true, - noDefaultBrowserCheck: true, - disableDefaultApps: true, - disableTranslate: true, - ); + chrome = await Chrome.startWithDebugPort([_googleUrl], + debugPort: port, + chromeArgs: [ + // 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', + ]); } Future launchChrome() async { @@ -65,6 +67,30 @@ void main() { 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/'; + +/// 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 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; +} From 4220d043182a47f2600c089e960ece7fd5094eb5 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 15:45:30 -0700 Subject: [PATCH 08/13] remove unused var --- test/chrome_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/chrome_test.dart b/test/chrome_test.dart index 70f2cf1..afe327f 100644 --- a/test/chrome_test.dart +++ b/test/chrome_test.dart @@ -15,9 +15,6 @@ void main() { Chrome chrome; Future launchChromeWithDebugPort({int port}) async { - final dataDir = Directory(p.joinAll( - [Directory.current.path, '.dart_tool', 'webdev', 'chrome_profile'])) - ..createSync(recursive: true); chrome = await Chrome.startWithDebugPort([_googleUrl], debugPort: port, chromeArgs: [ From 98d79a200eeff1d4baac6ba3882c3faa9bd2a691 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Thu, 25 Apr 2019 15:50:01 -0700 Subject: [PATCH 09/13] define windows prefixes where they are used. --- lib/src/chrome.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index bebbf2f..b6694ab 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -15,7 +15,6 @@ 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 { if (Platform.environment.containsKey(_chromeEnvironment)) { @@ -24,8 +23,11 @@ String get _executable { if (Platform.isLinux) return _linuxExecutable; if (Platform.isMacOS) return _macOSExecutable; if (Platform.isWindows) { - final windowsPrefixes = - _windowsPrefixes.map((name) => Platform.environment[name]).toList(); + final windowsPrefixes = [ + Platform.environment['LOCALAPPDATA'], + Platform.environment['PROGRAMFILES'], + Platform.environment['PROGRAMFILES(X86)'] + ]; return p.join( windowsPrefixes.firstWhere((prefix) { if (prefix == null) return false; From 9267c1c9974319b2fe3bd82f74a64f41620dc2f9 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Fri, 26 Apr 2019 08:21:22 -0700 Subject: [PATCH 10/13] use temp dataDir --- lib/src/chrome.dart | 70 +++++++++++++++++++++++-------------------- test/chrome_test.dart | 18 ----------- 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index b6694ab..73cdb77 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -48,11 +48,14 @@ class Chrome { this.debugPort, this.chromeConnection, { Process process, - }) : _process = process; + Directory dataDir, + }) : _process = process, + _dataDir = dataDir; final int debugPort; - final Process _process; final ChromeConnection chromeConnection; + final Process _process; + final Directory _dataDir; /// Connects to an instance of Chrome with an open debug port. static Future fromExisting(int port) async => @@ -67,21 +70,13 @@ class Chrome { static Future startWithDebugPort( List urls, { int debugPort, + bool headless = false, List chromeArgs = const [], }) async { - final dataDir = Directory(p.joinAll( - [Directory.current.path, '.dart_tool', 'webdev', 'chrome_profile'])); - final activePortFile = File(p.join(dataDir.path, 'DevToolsActivePort')); - // If we are reusing the Chrome profile we'll need to be able to read the - // DevToolsActivePort to connect the debugger. When a non-zero debugging - // port is provided Chrome will not write the DevToolsActivePort file and - // therefore we can not reuse the profile. - if (dataDir.existsSync() && !activePortFile.existsSync()) { - dataDir.deleteSync(recursive: true); - } - dataDir.createSync(recursive: true); - - int port = debugPort == null ? 0 : debugPort; + final dataDir = Directory.systemTemp.createTempSync(); + final port = debugPort == null || debugPort == 0 + ? await findUnusedPort() + : debugPort; final args = chromeArgs ..addAll([ // Using a tmp directory ensures that a new instance of chrome launches @@ -89,34 +84,26 @@ class Chrome { '--user-data-dir=${dataDir.path}', '--remote-debugging-port=$port', ]); + if (headless) { + args.add('--headless'); + } final process = await _startProcess(urls, args: args); - final output = StreamGroup.merge([ - process.stderr.transform(utf8.decoder).transform(const LineSplitter()), - process.stdout.transform(utf8.decoder).transform(const LineSplitter()) - ]); // Wait until the DevTools are listening before trying to connect. - await output - .firstWhere((line) => - line.startsWith('DevTools listening') || - line.startsWith('Opening in existing')) + 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.')); - // The DevToolsActivePort file is only written if 0 is provided. - if (port == 0) { - if (!activePortFile.existsSync()) { - throw ChromeError("Can't read DevToolsActivePort file."); - } - port = int.parse(activePortFile.readAsLinesSync().first); - } - return _connect(Chrome._( port, ChromeConnection('localhost', port), process: process, + dataDir: dataDir, )); } @@ -158,8 +145,9 @@ class Chrome { Future close() async { if (_currentCompleter.isCompleted) _currentCompleter = Completer(); chromeConnection.close(); - _process?.kill(); + _process?.kill(ProcessSignal.sigkill); await _process?.exitCode; + await _dataDir?.delete(recursive: true); } } @@ -170,3 +158,21 @@ class ChromeError extends Error { @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 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; +} diff --git a/test/chrome_test.dart b/test/chrome_test.dart index afe327f..bbefd86 100644 --- a/test/chrome_test.dart +++ b/test/chrome_test.dart @@ -73,21 +73,3 @@ void main() { } const _googleUrl = 'https://www.google.com/'; - -/// 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 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; -} From 4e9115492e7eb55d28df1d7577093be5e007dd68 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Fri, 26 Apr 2019 08:27:09 -0700 Subject: [PATCH 11/13] add headless optional param to start method --- lib/src/chrome.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 73cdb77..4b1c716 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -112,9 +112,14 @@ class Chrome { /// Each url in [urls] will be loaded in a separate tab. static Future start( List urls, { + bool headless = false, List chromeArgs = const [], }) async { - await _startProcess(urls, args: chromeArgs); + final List args = chromeArgs; + if (headless) { + args.add('--headless'); + } + await _startProcess(urls, args: args); } static Future _startProcess( From d4d8ad2cdf36c5a51d688144c562a3337b974596 Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Fri, 26 Apr 2019 08:42:57 -0700 Subject: [PATCH 12/13] remove completer code; add default debug flags --- lib/src/chrome.dart | 42 ++++++++++++++++++------------------------ test/chrome_test.dart | 18 +----------------- 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 4b1c716..427008c 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:async/async.dart'; import 'package:path/path.dart' as p; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; @@ -40,8 +39,6 @@ String get _executable { throw StateError('Unexpected platform type.'); } -var _currentCompleter = Completer(); - /// Manager for an instance of Chrome. class Chrome { Chrome._( @@ -61,8 +58,6 @@ class Chrome { static Future fromExisting(int port) async => _connect(Chrome._(port, ChromeConnection('localhost', port))); - static Future get connectedInstance => _currentCompleter.future; - /// 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 @@ -71,19 +66,28 @@ class Chrome { List urls, { int debugPort, bool headless = false, - List chromeArgs = const [], }) async { final dataDir = Directory.systemTemp.createTempSync(); final port = debugPort == null || debugPort == 0 ? await findUnusedPort() : debugPort; - final args = chromeArgs - ..addAll([ - // 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', - ]); + 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'); } @@ -112,13 +116,8 @@ class Chrome { /// Each url in [urls] will be loaded in a separate tab. static Future start( List urls, { - bool headless = false, - List chromeArgs = const [], + List args = const [], }) async { - final List args = chromeArgs; - if (headless) { - args.add('--headless'); - } await _startProcess(urls, args: args); } @@ -131,9 +130,6 @@ class Chrome { } static Future _connect(Chrome chrome) async { - if (_currentCompleter.isCompleted) { - throw ChromeError('Only one instance of chrome can be started.'); - } // The connection is lazy. Try a simple call to make sure the provided // connection is valid. try { @@ -143,12 +139,10 @@ class Chrome { throw ChromeError( 'Unable to connect to Chrome debug port: ${chrome.debugPort}\n $e'); } - _currentCompleter.complete(chrome); return chrome; } Future close() async { - if (_currentCompleter.isCompleted) _currentCompleter = Completer(); chromeConnection.close(); _process?.kill(ProcessSignal.sigkill); await _process?.exitCode; diff --git a/test/chrome_test.dart b/test/chrome_test.dart index bbefd86..a63220f 100644 --- a/test/chrome_test.dart +++ b/test/chrome_test.dart @@ -4,10 +4,8 @@ @OnPlatform({'windows': Skip('appveyor is not setup to install Chrome')}) 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'; @@ -15,21 +13,7 @@ void main() { Chrome chrome; Future launchChromeWithDebugPort({int port}) async { - chrome = await Chrome.startWithDebugPort([_googleUrl], - debugPort: port, - chromeArgs: [ - // 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', - ]); + chrome = await Chrome.startWithDebugPort([_googleUrl], debugPort: port); } Future launchChrome() async { From 04bd948119ce26521d7d0596d3a423ee5fd2f58b Mon Sep 17 00:00:00 2001 From: kenzieschmoll Date: Fri, 26 Apr 2019 10:38:28 -0700 Subject: [PATCH 13/13] implement latest from dart-lang/webdev/pull/342 --- lib/src/chrome.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/chrome.dart b/lib/src/chrome.dart index 427008c..7f1250b 100644 --- a/lib/src/chrome.dart +++ b/lib/src/chrome.dart @@ -146,7 +146,16 @@ class Chrome { chromeConnection.close(); _process?.kill(ProcessSignal.sigkill); await _process?.exitCode; - await _dataDir?.delete(recursive: true); + 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. + } } }